feat(category): 类目树、属性、属性字典维护,新增系统管理员管理类目数据
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { Database } from '@atlas/database';
|
||||
import { ApiError } from '../common/api-error.js';
|
||||
import { DATABASE } from '../database/database.module.js';
|
||||
|
||||
@Injectable()
|
||||
export class PlatformCatalogAdminReads {
|
||||
public constructor(@Inject(DATABASE) private readonly database: Database) {}
|
||||
|
||||
public async platforms() {
|
||||
const rows = await this.database.query<{
|
||||
id: string; code: string; name: string; status: string;
|
||||
client_id: string | null; source_updated_at: Date | null;
|
||||
category_count: string; last_tree_sync_at: Date | null; last_error: string | null;
|
||||
}>(
|
||||
`SELECT p.id, p.code, p.name, p.status,
|
||||
source.client_id, source.updated_at AS source_updated_at,
|
||||
(SELECT count(*) FROM atlas.platform_product_categories category
|
||||
WHERE category.platform_id = p.id AND category.is_active)::text AS category_count,
|
||||
(SELECT max(completed_at) FROM atlas.platform_catalog_sync_runs run
|
||||
WHERE run.platform_id = p.id AND run.scope = 'tree' AND run.status = 'succeeded') AS last_tree_sync_at,
|
||||
(SELECT CASE WHEN run.status = 'failed' THEN run.error_message ELSE NULL END
|
||||
FROM atlas.platform_catalog_sync_runs run
|
||||
WHERE run.platform_id = p.id AND run.scope = 'tree'
|
||||
ORDER BY run.created_at DESC LIMIT 1) AS last_error
|
||||
FROM atlas.platforms p
|
||||
LEFT JOIN atlas.platform_catalog_sources source ON source.platform_id = p.id
|
||||
ORDER BY CASE WHEN upper(p.code) = 'OZON' THEN 0 ELSE 1 END, p.name`,
|
||||
);
|
||||
return rows.rows.map((row) => ({
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
status: row.status,
|
||||
source: {
|
||||
configured: row.client_id !== null,
|
||||
client_id_hint: row.client_id ? `••••${row.client_id.slice(-4)}` : null,
|
||||
updated_at: row.source_updated_at,
|
||||
},
|
||||
summary: {
|
||||
category_count: Number(row.category_count),
|
||||
last_tree_sync_at: row.last_tree_sync_at,
|
||||
last_error: row.last_error,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
public async categories(platformId: string) {
|
||||
await this.requirePlatform(platformId);
|
||||
const result = await this.database.query(
|
||||
`SELECT id, parent_id, external_category_id, external_type_id, name, path,
|
||||
is_active, synced_at
|
||||
FROM atlas.platform_product_categories
|
||||
WHERE platform_id = $1 ORDER BY path, id LIMIT 20000`,
|
||||
[platformId],
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
public async attributes(platformId: string, categoryId: string) {
|
||||
await this.requireCategory(platformId, categoryId);
|
||||
const result = await this.database.query(
|
||||
`SELECT attribute.id, attribute.platform_attribute_id, attribute.complex_id,
|
||||
attribute.name, attribute.description, attribute.group_id, attribute.group_name,
|
||||
attribute.value_type, attribute.is_required, attribute.is_aspect,
|
||||
attribute.is_collection, attribute.max_value_count, attribute.dictionary_id,
|
||||
attribute.category_dependent, attribute.is_active, attribute.synced_at,
|
||||
(SELECT count(*)::int FROM atlas.platform_catalog_dictionary_values value
|
||||
WHERE value.attribute_id = attribute.id AND value.is_active) AS dictionary_value_count
|
||||
FROM atlas.platform_product_category_attributes attribute
|
||||
WHERE attribute.platform_id = $1 AND attribute.platform_product_category_id = $2
|
||||
ORDER BY attribute.is_required DESC, attribute.group_name NULLS LAST,
|
||||
attribute.name, attribute.platform_attribute_id`,
|
||||
[platformId, categoryId],
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
public async values(
|
||||
platformId: string,
|
||||
attributeId: string,
|
||||
keyword: string | undefined,
|
||||
page: number,
|
||||
) {
|
||||
const attribute = await this.database.query<{ dictionary_id: string | null }>(
|
||||
`SELECT dictionary_id FROM atlas.platform_product_category_attributes
|
||||
WHERE id = $1 AND platform_id = $2 AND is_active`,
|
||||
[attributeId, platformId],
|
||||
);
|
||||
if (!attribute.rows[0]) throw new ApiError(404, 'ATTRIBUTE_NOT_FOUND', '属性不存在');
|
||||
if (!attribute.rows[0].dictionary_id) {
|
||||
throw new ApiError(422, 'ATTRIBUTE_DICTIONARY_UNAVAILABLE', '该属性没有字典候选值');
|
||||
}
|
||||
const q = keyword?.trim() ?? '';
|
||||
const [items, total, lastRun] = await Promise.all([
|
||||
this.database.query(
|
||||
`SELECT id, external_value_id, value, info, picture
|
||||
FROM atlas.platform_catalog_dictionary_values
|
||||
WHERE attribute_id = $1 AND is_active
|
||||
AND ($2 = '' OR value ILIKE '%' || $2 || '%' OR external_value_id = $2)
|
||||
ORDER BY value, external_value_id LIMIT 100 OFFSET $3`,
|
||||
[attributeId, q, (page - 1) * 100],
|
||||
),
|
||||
this.database.query<{ count: string }>(
|
||||
`SELECT count(*)::text AS count FROM atlas.platform_catalog_dictionary_values
|
||||
WHERE attribute_id = $1 AND is_active
|
||||
AND ($2 = '' OR value ILIKE '%' || $2 || '%' OR external_value_id = $2)`,
|
||||
[attributeId, q],
|
||||
),
|
||||
this.database.query<{ is_complete: boolean }>(
|
||||
`SELECT COALESCE((result->>'is_complete')::boolean, false) AS is_complete
|
||||
FROM atlas.platform_catalog_sync_runs
|
||||
WHERE platform_id = $1 AND scope_key = $2 AND status = 'succeeded'
|
||||
ORDER BY completed_at DESC LIMIT 1`,
|
||||
[platformId, `dictionary:${attributeId}`],
|
||||
),
|
||||
]);
|
||||
return {
|
||||
data: items.rows,
|
||||
meta: {
|
||||
page,
|
||||
page_size: 100,
|
||||
total: Number(total.rows[0]?.count ?? 0),
|
||||
is_complete: lastRun.rows[0]?.is_complete ?? false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async runs(platformId: string, limit: number) {
|
||||
await this.requirePlatform(platformId);
|
||||
const result = await this.database.query(
|
||||
`SELECT id, platform_id, scope, scope_key, category_id, attribute_id,
|
||||
trigger_kind AS trigger, status, result, error_code, error_message,
|
||||
attempt_count, created_at, started_at, completed_at
|
||||
FROM atlas.platform_catalog_sync_runs
|
||||
WHERE platform_id = $1 ORDER BY created_at DESC LIMIT $2`,
|
||||
[platformId, limit],
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
public async requirePlatform(platformId: string) {
|
||||
const result = await this.database.query<{ code: string }>(
|
||||
'SELECT code FROM atlas.platforms WHERE id = $1',
|
||||
[platformId],
|
||||
);
|
||||
if (!result.rows[0]) throw new ApiError(404, 'PLATFORM_NOT_FOUND', '平台不存在');
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
public async requireCategory(platformId: string, categoryId: string) {
|
||||
const result = await this.database.query(
|
||||
`SELECT 1 FROM atlas.platform_product_categories WHERE id = $1 AND platform_id = $2`,
|
||||
[categoryId, platformId],
|
||||
);
|
||||
if (!result.rows[0]) throw new ApiError(404, 'CATEGORY_NOT_FOUND', '类目不属于当前平台');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user