feat(worker-platform): 分类标识改为系统自动生成并在前端UI隐藏

This commit is contained in:
yml2213
2026-09-13 14:25:51 +08:00
parent abccecf8ed
commit aba508f4f6
4 changed files with 151 additions and 29 deletions
@@ -33,6 +33,66 @@ export async function listAllWorkCategories(): Promise<WorkCategoryRow[]> {
return result.rows
}
export async function createWorkCategory(input: {
categoryKey: string
name: string
parentId?: number | null
iconUrl?: string | null
sortOrder: number
status: string
now: string
}): Promise<WorkCategoryRow | null> {
const result = await query<WorkCategoryRow>(
`
INSERT INTO work_categories (category_key, name, parent_id, icon_url, sort_order, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
`,
[
input.categoryKey,
input.name,
input.parentId ?? null,
input.iconUrl ?? null,
input.sortOrder,
input.status,
input.now,
input.now,
],
)
return result.rows[0] || null
}
export async function updateWorkCategoryById(
id: number,
input: {
name: string
parentId?: number | null
iconUrl?: string | null
sortOrder: number
status: string
now: string
},
): Promise<WorkCategoryRow | null> {
const result = await query<WorkCategoryRow>(
`
UPDATE work_categories
SET name = $1, parent_id = $2, icon_url = $3, sort_order = $4, status = $5, updated_at = $6
WHERE id = $7
RETURNING *
`,
[
input.name,
input.parentId ?? null,
input.iconUrl ?? null,
input.sortOrder,
input.status,
input.now,
id,
],
)
return result.rows[0] || null
}
export async function upsertWorkCategory(input: {
categoryKey: string
name: string
@@ -1,9 +1,11 @@
import {
countWorkCategoryUsages,
createWorkCategory,
deleteWorkCategory,
getWorkCategoryById,
getWorkCategoryByKey,
listAllWorkCategories,
updateWorkCategoryById,
upsertWorkCategory,
} from '../../repositories/worker-platform/work-category-repo.js'
import {
@@ -69,12 +71,25 @@ export async function listAdminWorkCategories() {
return { items: (await listAllWorkCategories()).map(mapWorkCategory) }
}
async function generateUniqueCategoryKey(): Promise<string> {
for (let i = 0; i < 10; i++) {
const key = `cat_${Date.now().toString(36)}_${randomId('', 3)}`
const existing = await getWorkCategoryByKey(key)
if (!existing) {
return key
}
}
return `cat_${Date.now().toString(36)}_${randomId('', 6)}`
}
export async function saveAdminWorkCategory(payload: JsonObject = {}) {
const now = nowIso()
const categoryKey = normalizeSlugKey(
payload.categoryKey || payload.category_key || payload.name,
DEFAULT_CATEGORY_KEY,
)
const rawCategoryId = payload.categoryId ?? payload.category_id ?? payload.id
const categoryId =
rawCategoryId !== undefined && rawCategoryId !== null && String(rawCategoryId).trim() !== ''
? Number(rawCategoryId)
: null
const name = String(payload.name || '').trim() || '默认分类'
const rawParentId = payload.parentId ?? payload.parent_id
const parentId =
@@ -87,6 +102,8 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) {
: payload.icon_url !== undefined
? String(payload.icon_url || '').trim() || null
: null
const sortOrder = normalizeInteger(payload.sortOrder ?? payload.sort_order, 100)
const status = normalizeEnabledStatus(payload.status)
if (parentId) {
const parent = await getWorkCategoryById(parentId)
@@ -96,7 +113,7 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) {
errorCode: 'parent_category_not_found',
})
}
if (parent.category_key === categoryKey) {
if (categoryId && parent.id === categoryId) {
throw createHttpError('上级分类不能是当前分类自身', {
statusCode: 400,
errorCode: 'invalid_parent_category',
@@ -110,26 +127,80 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) {
}
}
const existing = await getWorkCategoryByKey(categoryKey)
if (existing && parentId) {
const usages = await countWorkCategoryUsages(existing.id)
if (usages.childCategoryCount > 0) {
throw createHttpError('该分类下已有子分类,无法再作为二级分类', {
statusCode: 400,
errorCode: 'category_has_children',
// 编辑已有分类
if (categoryId) {
const existing = await getWorkCategoryById(categoryId)
if (!existing) {
throw createHttpError('分类不存在', {
statusCode: 404,
errorCode: 'work_category_not_found',
})
}
if (parentId) {
const usages = await countWorkCategoryUsages(existing.id)
if (usages.childCategoryCount > 0) {
throw createHttpError('该分类下已有子分类,无法再作为二级分类', {
statusCode: 400,
errorCode: 'category_has_children',
})
}
}
const category = await updateWorkCategoryById(categoryId, {
name,
parentId,
iconUrl,
sortOrder,
status,
now,
})
if (!category) {
throw createHttpError('分类更新失败', {
statusCode: 500,
errorCode: 'category_update_failed',
})
}
return { category: mapWorkCategory(category) }
}
// 创建新分类:系统自动生成全局唯一 categoryKey,无需用户手动填写
let categoryKey = ''
const providedKey = String(payload.categoryKey || payload.category_key || '').trim()
if (providedKey) {
const normalizedKey = normalizeSlugKey(providedKey, '')
if (normalizedKey) {
const existingWithKey = await getWorkCategoryByKey(normalizedKey)
if (existingWithKey) {
throw createHttpError('分类标识已存在,请勿重复创建', {
statusCode: 400,
errorCode: 'category_key_conflict',
})
}
categoryKey = normalizedKey
}
}
const category = await upsertWorkCategory({
if (!categoryKey) {
categoryKey = await generateUniqueCategoryKey()
}
const category = await createWorkCategory({
categoryKey,
name,
parentId,
iconUrl,
sortOrder: normalizeInteger(payload.sortOrder ?? payload.sort_order, 100),
status: normalizeEnabledStatus(payload.status),
sortOrder,
status,
now,
})
if (!category) {
throw createHttpError('创建分类失败', {
statusCode: 500,
errorCode: 'category_create_failed',
})
}
return { category: mapWorkCategory(category) }
}
@@ -28,7 +28,6 @@ import { uploadFile } from '@/services/files'
import type { WorkCategory } from '@/types/worker-platform'
type CategoryFormValues = {
categoryKey: string
name: string
parentId?: number
iconUrl?: string
@@ -63,7 +62,7 @@ export default function CategoriesPanel() {
{ value: 0, label: '无(作为一级分类)' },
...level1Categories.map((c) => ({
value: c.categoryId,
label: `${c.name} (${c.categoryKey})`,
label: c.name,
})),
]
}, [rawCategories, editingCategory])
@@ -98,7 +97,6 @@ export default function CategoriesPanel() {
function startEdit(category: WorkCategory) {
setEditingCategory(category)
form.setFieldsValue({
categoryKey: category.categoryKey,
name: category.name,
parentId: category.parentId || 0,
iconUrl: category.iconUrl || '',
@@ -129,7 +127,7 @@ export default function CategoriesPanel() {
async function saveCategory(values: CategoryFormValues) {
try {
await saveAdminWorkCategory({
categoryKey: values.categoryKey,
categoryId: editingCategory ? editingCategory.categoryId : undefined,
name: values.name,
parentId: values.parentId ? Number(values.parentId) : null,
iconUrl: values.iconUrl ? String(values.iconUrl).trim() : null,
@@ -147,7 +145,7 @@ export default function CategoriesPanel() {
async function toggleCategory(category: WorkCategory) {
try {
await saveAdminWorkCategory({
categoryKey: category.categoryKey,
categoryId: category.categoryId,
name: category.name,
parentId: category.parentId || null,
iconUrl: category.iconUrl || null,
@@ -219,7 +217,6 @@ export default function CategoriesPanel() {
return <Tag color="geekblue">{parent ? parent.name : `ID: ${row.parentId}`}</Tag>
},
},
{ title: '标识', dataIndex: 'categoryKey', width: 150 },
{ title: '排序', dataIndex: 'sortOrder', width: 90 },
{
title: '状态',
@@ -275,13 +272,6 @@ export default function CategoriesPanel() {
>
<Select options={parentOptions} />
</Form.Item>
<Form.Item
label="标识"
name="categoryKey"
rules={[{ required: true, message: '请输入标识' }]}
>
<Input placeholder="如 hpjy、dtts" />
</Form.Item>
<Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="如 和平精英、地铁装备" />
</Form.Item>
@@ -147,7 +147,8 @@ export function fetchAdminWorkCategories() {
}
export function saveAdminWorkCategory(payload: {
categoryKey: string
categoryId?: number
categoryKey?: string
name: string
parentId?: number | null
iconUrl?: string | null