feat(admin): country drag-sort + always-available member detail sync
- Country 加 sort_order(默认 0 保持 id 序);PATCH /countries/sort 批量保存 顺序(对齐 /tags/sort 模式);findAll 与公开 /public/countries 均按 sortOrder 排序 - CountriesView 表格改为可拖拽行列表:拖动松开即全量保存新顺序,失败回滚 - 商品编辑弹窗成员展开面板:同步详情按钮常驻(已同步显示 重新同步详情), 不再只在未同步态出现 - goods.service.spec 的 FamilyRecomputeService mock 补齐 syncFamilyTags 等 方法(全量并行时其他套件的扫名归族会把本套件夹具收进族,create/update 会调用到,mock 缺方法导致偶发 TypeError) - api 164/164、admin typecheck+22/22+构建全绿
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- 国家自定义排序:默认 0 保持既有 id 序,后台可拖拽调整
|
||||
ALTER TABLE "countries" ADD COLUMN "sort_order" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -143,6 +143,7 @@ model Country {
|
||||
id BigInt @id @default(autoincrement()) @map("country_id")
|
||||
countryName String @unique @map("country_name")
|
||||
countryIcon String? @map("country_icon")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { CountriesService } from './countries.service';
|
||||
import { CreateCountryDto } from './dto/create-country.dto';
|
||||
import { UpdateCountryDto } from './dto/update-country.dto';
|
||||
import { ReorderCountriesDto } from './dto/reorder-countries.dto';
|
||||
|
||||
@ApiTags('countries')
|
||||
@ApiBearerAuth()
|
||||
@@ -32,6 +33,13 @@ export class CountriesController {
|
||||
return this.service.findAll();
|
||||
}
|
||||
|
||||
// 注意:'sort' 静态路由必须声明在 ':id' 之前,否则会被参数路由吞掉
|
||||
@Patch('sort')
|
||||
@ApiOperation({ summary: 'Batch update country sort order (drag reorder)' })
|
||||
reorder(@Body() dto: ReorderCountriesDto) {
|
||||
return this.service.reorder(dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get one country' })
|
||||
findOne(@Param('id', ParseIntPipe) id: string) {
|
||||
|
||||
@@ -112,4 +112,30 @@ describe('CountriesService', () => {
|
||||
const idx = created.indexOf(`${name}-v2`);
|
||||
if (idx !== -1) created.splice(idx, 1);
|
||||
});
|
||||
|
||||
it('reorder persists sortOrder and findAll honors it', async () => {
|
||||
const stamp = Date.now();
|
||||
const a = await service.create({ countryName: `Sort A ${stamp}` });
|
||||
const b = await service.create({ countryName: `Sort B ${stamp}` });
|
||||
const c = await service.create({ countryName: `Sort C ${stamp}` });
|
||||
created.push(a.countryName, b.countryName, c.countryName);
|
||||
|
||||
// 新顺序 C, A, B(id 序与 sortOrder 无关时也能稳定验证)
|
||||
const reordered = await service.reorder({
|
||||
items: [
|
||||
{ id: Number(c.id), sortOrder: 0 },
|
||||
{ id: Number(a.id), sortOrder: 1 },
|
||||
{ id: Number(b.id), sortOrder: 2 },
|
||||
],
|
||||
});
|
||||
const ours = reordered.filter((r) =>
|
||||
[a.id, b.id, c.id].some((id) => id.toString() === r.id.toString()),
|
||||
);
|
||||
expect(ours.map((r) => r.id.toString())).toEqual([c.id.toString(), a.id.toString(), b.id.toString()]);
|
||||
|
||||
const list = await service.findAll();
|
||||
const pos = (id: bigint) => list.findIndex((r) => r.id.toString() === id.toString());
|
||||
expect(pos(c.id)).toBeLessThan(pos(a.id));
|
||||
expect(pos(a.id)).toBeLessThan(pos(b.id));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,13 +8,29 @@ import {
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateCountryDto } from './dto/create-country.dto';
|
||||
import { UpdateCountryDto } from './dto/update-country.dto';
|
||||
import { ReorderCountriesDto } from './dto/reorder-countries.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CountriesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
findAll() {
|
||||
return this.prisma.country.findMany({ orderBy: { id: 'asc' } });
|
||||
return this.prisma.country.findMany({
|
||||
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量保存拖拽后的顺序(全量提交,sortOrder = 目标下标) */
|
||||
async reorder(dto: ReorderCountriesDto) {
|
||||
await this.prisma.$transaction(
|
||||
dto.items.map((item) =>
|
||||
this.prisma.country.update({
|
||||
where: { id: BigInt(item.id) },
|
||||
data: { sortOrder: item.sortOrder },
|
||||
}),
|
||||
),
|
||||
);
|
||||
return this.findAll();
|
||||
}
|
||||
|
||||
async findOne(id: bigint) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsInt,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CountryOrderItem {
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
id!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
sortOrder!: number;
|
||||
}
|
||||
|
||||
export class ReorderCountriesDto {
|
||||
@ApiProperty({ type: [CountryOrderItem] })
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CountryOrderItem)
|
||||
items!: CountryOrderItem[];
|
||||
}
|
||||
@@ -33,7 +33,15 @@ describe('GoodsService', () => {
|
||||
},
|
||||
{
|
||||
provide: FamilyRecomputeService,
|
||||
useValue: { enqueue: jest.fn() },
|
||||
// 全量并行跑时其他套件的扫名归族可能把本套件夹具链接收进族,
|
||||
// create/update 会调用 syncFamilyTags —— mock 必须覆盖全部被调方法
|
||||
useValue: {
|
||||
enqueue: jest.fn(),
|
||||
recomputeFamily: jest.fn().mockResolvedValue(undefined),
|
||||
syncFamilyTags: jest.fn().mockResolvedValue({ goodsUpdated: 0, linksUpdated: 0 }),
|
||||
refreshLinkTags: jest.fn().mockResolvedValue(undefined),
|
||||
mirrorLinkTagsToGoods: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -111,7 +111,7 @@ export class PublicService {
|
||||
async getCountries(): Promise<PublicCountryDto[]> {
|
||||
const rows = await this.prisma.country.findMany({
|
||||
where: { goods: { some: { familyId: { not: null }, originGood: { delisted: false } } } },
|
||||
orderBy: { id: 'asc' },
|
||||
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
return rows.map(PublicCountryDto.from);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user