feat(category): 接单平台支持二级分类目录与接单大厅联动展示及限额优化
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
countWorkCategoryUsages,
|
||||
deleteWorkCategory,
|
||||
getWorkCategoryById,
|
||||
getWorkCategoryByKey,
|
||||
listAllWorkCategories,
|
||||
upsertWorkCategory,
|
||||
} from '../../repositories/worker-platform/work-category-repo.js'
|
||||
@@ -75,10 +76,57 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) {
|
||||
DEFAULT_CATEGORY_KEY,
|
||||
)
|
||||
const name = String(payload.name || '').trim() || '默认分类'
|
||||
const rawParentId = payload.parentId ?? payload.parent_id
|
||||
const parentId =
|
||||
rawParentId !== undefined && rawParentId !== null && String(rawParentId).trim() !== ''
|
||||
? Number(rawParentId)
|
||||
: null
|
||||
const iconUrl =
|
||||
payload.iconUrl !== undefined
|
||||
? String(payload.iconUrl || '').trim() || null
|
||||
: payload.icon_url !== undefined
|
||||
? String(payload.icon_url || '').trim() || null
|
||||
: null
|
||||
|
||||
if (parentId) {
|
||||
const parent = await getWorkCategoryById(parentId)
|
||||
if (!parent) {
|
||||
throw createHttpError('所选上级分类不存在', {
|
||||
statusCode: 400,
|
||||
errorCode: 'parent_category_not_found',
|
||||
})
|
||||
}
|
||||
if (parent.category_key === categoryKey) {
|
||||
throw createHttpError('上级分类不能是当前分类自身', {
|
||||
statusCode: 400,
|
||||
errorCode: 'invalid_parent_category',
|
||||
})
|
||||
}
|
||||
if (parent.parent_id) {
|
||||
throw createHttpError('暂不支持三级及以上分类,上级分类必须是一级分类', {
|
||||
statusCode: 400,
|
||||
errorCode: 'parent_cannot_be_subcategory',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const category = await upsertWorkCategory({
|
||||
categoryKey,
|
||||
name,
|
||||
sortOrder: normalizeInteger(payload.sortOrder, 100),
|
||||
parentId,
|
||||
iconUrl,
|
||||
sortOrder: normalizeInteger(payload.sortOrder ?? payload.sort_order, 100),
|
||||
status: normalizeEnabledStatus(payload.status),
|
||||
now,
|
||||
})
|
||||
@@ -94,14 +142,15 @@ export async function deleteAdminWorkCategory(categoryId: number | string) {
|
||||
})
|
||||
}
|
||||
const usages = await countWorkCategoryUsages(Number(categoryId))
|
||||
if (usages.workOrderCount > 0 || usages.productRuleCount > 0) {
|
||||
throw createHttpError(
|
||||
`该分类已被 ${usages.workOrderCount} 个工单和 ${usages.productRuleCount} 条物品规则使用,无法删除,请先停用`,
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'work_category_in_use',
|
||||
},
|
||||
)
|
||||
if (usages.workOrderCount > 0 || usages.productRuleCount > 0 || usages.childCategoryCount > 0) {
|
||||
const reasons: string[] = []
|
||||
if (usages.workOrderCount > 0) reasons.push(`${usages.workOrderCount} 个工单`)
|
||||
if (usages.productRuleCount > 0) reasons.push(`${usages.productRuleCount} 条物品规则`)
|
||||
if (usages.childCategoryCount > 0) reasons.push(`${usages.childCategoryCount} 个子分类`)
|
||||
throw createHttpError(`该分类已被 ${reasons.join('、')} 引用,无法删除,请先处理关联数据`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_category_in_use',
|
||||
})
|
||||
}
|
||||
const { deleted } = await deleteWorkCategory(Number(categoryId))
|
||||
return { deleted }
|
||||
|
||||
@@ -33,6 +33,8 @@ export function mapWorkCategory(category: WorkCategoryRow | null | undefined) {
|
||||
categoryId: Number(category.id),
|
||||
categoryKey: category.category_key,
|
||||
name: category.name,
|
||||
parentId: category.parent_id ? Number(category.parent_id) : null,
|
||||
iconUrl: category.icon_url || null,
|
||||
sortOrder: Number(category.sort_order || 100),
|
||||
status: category.status,
|
||||
createdAt: category.created_at,
|
||||
|
||||
@@ -9,29 +9,36 @@ import {
|
||||
test('大厅配置默认展示 50 个订单', () => {
|
||||
assert.deepEqual(createDefaultWorkerHallConfig(), {
|
||||
maxVisibleOrders: 50,
|
||||
showSubcategories: true,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({}), {
|
||||
maxVisibleOrders: 50,
|
||||
showSubcategories: true,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.equal(normalizeWorkerHallConfig({ showSubcategories: false }).showSubcategories, false)
|
||||
})
|
||||
|
||||
test('大厅配置只接受 1 到 500 的整数', () => {
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 88 }), {
|
||||
maxVisibleOrders: 88,
|
||||
showSubcategories: true,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ max_visible_orders: 999 }), {
|
||||
maxVisibleOrders: 500,
|
||||
showSubcategories: true,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 0 }), {
|
||||
maxVisibleOrders: 1,
|
||||
showSubcategories: true,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 1.5 }), {
|
||||
maxVisibleOrders: 50,
|
||||
showSubcategories: true,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-sto
|
||||
|
||||
export type WorkerHallConfig = {
|
||||
maxVisibleOrders: number
|
||||
showSubcategories: boolean
|
||||
categoryMaxVisibleOrders: Record<string, number>
|
||||
}
|
||||
|
||||
@@ -24,14 +25,21 @@ export async function saveWorkerHallConfig(rawValue: unknown): Promise<WorkerHal
|
||||
}
|
||||
|
||||
export function createDefaultWorkerHallConfig(): WorkerHallConfig {
|
||||
return { maxVisibleOrders: 50, categoryMaxVisibleOrders: {} }
|
||||
return { maxVisibleOrders: 50, showSubcategories: true, categoryMaxVisibleOrders: {} }
|
||||
}
|
||||
|
||||
export function normalizeWorkerHallConfig(rawValue: unknown): WorkerHallConfig {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
const value = Number(source.maxVisibleOrders ?? source.max_visible_orders)
|
||||
const showSubcategories =
|
||||
source.showSubcategories !== undefined
|
||||
? Boolean(source.showSubcategories)
|
||||
: source.show_subcategories !== undefined
|
||||
? Boolean(source.show_subcategories)
|
||||
: true
|
||||
return {
|
||||
maxVisibleOrders: Number.isInteger(value) ? Math.min(500, Math.max(1, value)) : 50,
|
||||
showSubcategories,
|
||||
categoryMaxVisibleOrders: normalizeCategoryMaxVisibleOrders(
|
||||
source.categoryMaxVisibleOrders ?? source.category_max_visible_orders,
|
||||
),
|
||||
|
||||
@@ -90,6 +90,7 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
|
||||
}),
|
||||
pagination: { page, pageSize, total },
|
||||
categories,
|
||||
showSubcategories: hallConfig.showSubcategories,
|
||||
visibleDelaySeconds,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user