feat(cost-center): 成本中心与商品映射开发草案 (含文档与订单页调整 WIP)

This commit is contained in:
yml2213
2026-10-20 10:01:52 +08:00
parent 7542d547bf
commit 111bc76357
32 changed files with 7088 additions and 156 deletions
+2
View File
@@ -21,6 +21,7 @@ import WorkerLayout from '@/layouts/WorkerLayout'
const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage'))
const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage'))
const AdminCostCenterPage = lazy(() => import('@/pages/admin/AdminCostCenterPage'))
const AdminFundPage = lazy(() => import('@/pages/admin/AdminFundPage'))
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage'))
@@ -145,6 +146,7 @@ function AdminApplication() {
</Route>
<Route element={<RequireRole roles={['admin', 'operator', 'finance']} />}>
<Route path="finance" element={<AdminFinancePage />} />
<Route path="cost-center" element={<AdminCostCenterPage />} />
</Route>
<Route element={<RequireRole roles={['admin', 'operator', 'support', 'finance']} />}>
<Route path="fund" element={<AdminFundPage />} />
+11 -1
View File
@@ -1,6 +1,7 @@
import {
AuditOutlined,
AccountBookOutlined,
ApartmentOutlined,
BellOutlined,
BugOutlined,
DashboardOutlined,
@@ -131,6 +132,7 @@ export default function AdminLayout() {
role === 'finance'
? [
{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '财务' },
{ key: '/admin/cost-center', icon: <ApartmentOutlined />, label: '商品与成本' },
{ key: '/admin/fund', icon: <WalletOutlined />, label: '备用金' },
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '资金审核' },
]
@@ -144,7 +146,14 @@ export default function AdminLayout() {
},
...(role === 'support'
? []
: [{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '财务' }]),
: [
{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '财务' },
{
key: '/admin/cost-center',
icon: <ApartmentOutlined />,
label: '商品与成本',
},
]),
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '接单平台' },
{
@@ -747,6 +756,7 @@ function playNotificationSound(kind: NotificationAlertKind) {
function resolveSelectedKey(pathname: string) {
if (pathname.startsWith('/admin/orders')) return '/admin/orders'
if (pathname.startsWith('/admin/finance')) return '/admin/finance'
if (pathname.startsWith('/admin/cost-center')) return '/admin/cost-center'
if (pathname.startsWith('/admin/tasks')) return '/admin/tasks'
if (pathname.startsWith('/admin/worker-platform')) return '/admin/worker-platform'
if (pathname.startsWith('/admin/kuaishou-industry')) return '/admin/kuaishou-industry'
File diff suppressed because it is too large Load Diff
@@ -55,15 +55,6 @@ type OrderFilterForm = {
dateRange?: unknown
}
/** 默认展示近 7 天(含今天)。 */
function resolveDefaultOrderDateRange() {
const today = dayjs()
return {
dateFrom: today.subtract(6, 'day').format('YYYY-MM-DD'),
dateTo: today.format('YYYY-MM-DD'),
}
}
export default function AdminOrdersPage() {
const navigate = useNavigate()
const { message } = App.useApp()
@@ -72,20 +63,6 @@ export default function AdminOrdersPage() {
const page = Number(searchParams.get('page') || 1) || 1
const pageSize = resolveAdminPageSize(searchParams.get('pageSize'), ADMIN_DEFAULT_PAGE_SIZE)
// 首次进入(仅有分页参数)时写入默认近 7 天,避免一把拉出全库。
useEffect(() => {
const meaningfulKeys = [...searchParams.keys()].filter(
(key) => key !== 'page' && key !== 'pageSize',
)
if (meaningfulKeys.length > 0) {
return
}
const { dateFrom, dateTo } = resolveDefaultOrderDateRange()
setSearchParams(cleanParams({ page: 1, pageSize, dateFrom, dateTo }), { replace: true })
// 仅首屏引导默认日期,避免与后续筛选互相打架。
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const filters = {
keyword: searchParams.get('keyword') || '',
provider: searchParams.get('provider') || '',
@@ -111,17 +88,9 @@ export default function AdminOrdersPage() {
],
)
// 等待默认日期写入 URL 后再请求,避免首屏无日期打出全库。
const hasBootstrappedFilters =
Boolean(filters.dateFrom && filters.dateTo) ||
Boolean(filters.keyword) ||
searchParams.get('allTime') === '1' ||
Boolean(filters.provider || filters.executorKey || filters.payStatus || filters.taskStatus)
const query = useQuery({
queryKey: ['admin-orders', queryParams],
queryFn: () => fetchAdminOrders(queryParams),
enabled: hasBootstrappedFilters,
})
const data = query.data?.data
@@ -410,7 +379,6 @@ export default function AdminOrdersPage() {
// 有关键词时不强制日期(后端也会忽略日期);清空日期且无关键词则查全部时间。
const dateFrom = rawFrom || ''
const dateTo = rawTo || ''
const allTime = !dateFrom && !dateTo && !keyword ? '1' : ''
setSearchParams(
cleanParams({
@@ -418,7 +386,6 @@ export default function AdminOrdersPage() {
keyword,
dateFrom,
dateTo,
allTime,
page: 1,
pageSize,
}),
@@ -426,16 +393,15 @@ export default function AdminOrdersPage() {
}
function resetFilters() {
const { dateFrom, dateTo } = resolveDefaultOrderDateRange()
form.setFieldsValue({
keyword: undefined,
provider: undefined,
executorKey: undefined,
payStatus: undefined,
taskStatus: undefined,
dateRange: [dayjs(dateFrom), dayjs(dateTo)],
dateRange: undefined,
})
setSearchParams(cleanParams({ page: 1, pageSize, dateFrom, dateTo }))
setSearchParams(cleanParams({ page: 1, pageSize }))
}
const currentSegmentedProvider = filters.provider || ''
@@ -537,7 +503,7 @@ export default function AdminOrdersPage() {
<Card>
<Table<AdminOrderListItem>
rowKey="orderId"
loading={query.isLoading || query.isFetching || !hasBootstrappedFilters}
loading={query.isLoading || query.isFetching}
columns={columns}
dataSource={data?.items || []}
scroll={{ x: 960 }}
@@ -549,7 +515,6 @@ export default function AdminOrdersPage() {
setSearchParams(
cleanParams({
...filters,
allTime: searchParams.get('allTime') || '',
page: nextPage,
pageSize: nextPageSize,
}),
@@ -0,0 +1,144 @@
import { apiGet, apiPatch, apiPost } from '@/lib/http'
import type {
ChannelProductMapping,
ChannelProductMappingListResult,
CostCenterOptions,
CostCenterOverview,
CostException,
CostExceptionListResult,
CostItem,
CostItemListResult,
CostPreview,
CostVersion,
CostVersionListResult,
ProductCatalogItem,
ProductCatalogListResult,
TemplateProductBootstrapPreview,
TemplateProductBootstrapResult,
} from '@/types/admin'
export function fetchAdminCostCenterOverview() {
return apiGet<CostCenterOverview>('/api/v1/admin/cost-center/overview')
}
export function fetchAdminCostCenterOptions() {
return apiGet<CostCenterOptions>('/api/v1/admin/cost-center/options')
}
export function previewAdminTemplateProductBootstrap() {
return apiGet<TemplateProductBootstrapPreview>(
'/api/v1/admin/cost-center/template-bootstrap/preview',
)
}
export function applyAdminTemplateProductBootstrap() {
return apiPost<TemplateProductBootstrapResult>(
'/api/v1/admin/cost-center/template-bootstrap/apply',
{ repair: true },
)
}
export function fetchAdminProductCatalog(params?: Record<string, unknown>) {
return apiGet<ProductCatalogListResult>('/api/v1/admin/cost-center/products', params)
}
export function createAdminProductCatalog(payload: Record<string, unknown>) {
return apiPost<{ product: ProductCatalogItem }>('/api/v1/admin/cost-center/products', payload)
}
export function updateAdminProductCatalog(productId: number, payload: Record<string, unknown>) {
return apiPatch<{ product: ProductCatalogItem }>(
`/api/v1/admin/cost-center/products/${productId}`,
payload,
)
}
export function fetchAdminChannelProductMappings(params?: Record<string, unknown>) {
return apiGet<ChannelProductMappingListResult>('/api/v1/admin/cost-center/mappings', params)
}
export function createAdminChannelProductMapping(payload: Record<string, unknown>) {
return apiPost<{ mapping: ChannelProductMapping }>('/api/v1/admin/cost-center/mappings', payload)
}
export function updateAdminChannelProductMapping(
mappingId: number,
payload: Record<string, unknown>,
) {
return apiPatch<{ mapping: ChannelProductMapping }>(
`/api/v1/admin/cost-center/mappings/${mappingId}`,
payload,
)
}
export function backfillAdminProductMappings(payload: Record<string, unknown>) {
return apiPost<{
updatedCount: number
limit: number
batches: number
remainingCount: number
}>('/api/v1/admin/cost-center/mappings/backfill', payload)
}
export function backfillAdminNormalizedQuantities(payload: Record<string, unknown>) {
return apiPost<{
updatedCount: number
limit: number
batches: number
remainingCount: number
}>('/api/v1/admin/cost-center/normalized-quantities/backfill', payload)
}
export function fetchAdminCostItems(params?: Record<string, unknown>) {
return apiGet<CostItemListResult>('/api/v1/admin/cost-center/cost-items', params)
}
export function createAdminCostItem(payload: Record<string, unknown>) {
return apiPost<{ costItem: CostItem }>('/api/v1/admin/cost-center/cost-items', payload)
}
export function updateAdminCostItem(costItemId: number, payload: Record<string, unknown>) {
return apiPatch<{ costItem: CostItem }>(
`/api/v1/admin/cost-center/cost-items/${costItemId}`,
payload,
)
}
export function fetchAdminCostVersions(params?: Record<string, unknown>) {
return apiGet<CostVersionListResult>('/api/v1/admin/cost-center/cost-versions', params)
}
export function createAdminCostVersion(payload: Record<string, unknown>) {
return apiPost<{ version: CostVersion }>('/api/v1/admin/cost-center/cost-versions', payload)
}
export function retireAdminCostVersion(versionId: number, payload: Record<string, unknown>) {
return apiPatch<{ version: CostVersion }>(
`/api/v1/admin/cost-center/cost-versions/${versionId}/retire`,
payload,
)
}
export function fetchAdminCostExceptions(params?: Record<string, unknown>) {
return apiGet<CostExceptionListResult>('/api/v1/admin/cost-center/exceptions', params)
}
export function resolveAdminCostException(exceptionId: number, payload: Record<string, unknown>) {
return apiPatch<{ exception: CostException }>(
`/api/v1/admin/cost-center/exceptions/${exceptionId}`,
payload,
)
}
export function previewAdminCost(payload: Record<string, unknown>) {
return apiPost<CostPreview>('/api/v1/admin/cost-center/preview', payload)
}
export function createAdminCostSnapshot(payload: Record<string, unknown>) {
return apiPost<{
preview: CostPreview
snapshots: Array<Record<string, unknown>>
createdCount: number
exceptionCreatedCount: number
}>('/api/v1/admin/cost-center/snapshots', payload)
}
@@ -1,6 +1,7 @@
export * from './auth'
export * from './dashboard'
export * from './finance'
export * from './cost-center'
export * from './fund'
export * from './users'
export * from './audit-logs'
@@ -0,0 +1,280 @@
export interface CostCenterOverview {
productCount: number
mappingCount: number
costItemCount: number
activeVersionCount: number
openExceptionCount: number
unmappedOrderItemCount: number
}
export interface ProductCatalogItem {
id: number
canonicalSku: string
canonicalName: string
category: string
rootCategoryId: number | null
rootCategoryName: string
subCategoryId: number | null
subCategoryName: string
unitName: string
status: 'active' | 'archived'
attributes: Record<string, unknown>
mappingCount: number
costVersionCount: number
createdAt: string
updatedAt: string
}
export interface ChannelProductMapping {
id: number
provider: string
shopId: string
platformSkuCode: string
platformSkuName: string
platformSkuNameKey: string
matchType: 'sku_code' | 'sku_code_name' | 'sku_code_series' | 'sku_name_series'
canonicalProductId: number
canonicalSku: string
canonicalName: string
quantityMultiplier: number
confidence: 'exact' | 'rule' | 'manual'
source: 'manual' | 'import' | 'rule'
enabled: boolean
note: string
createdAt: string
updatedAt: string
}
export interface CostItem {
id: number
code: string
name: string
costType:
| 'auto_purchase'
| 'work_order_reward'
| 'manual_service'
| 'platform_fee'
| 'payment_fee'
| 'after_sales_loss'
| 'shipping_other'
| 'other'
carrierType: 'affiliate_dash' | 'work_order' | 'manual' | 'shared'
pricingMode: 'per_item' | 'per_order' | 'per_hour' | 'percent_of_revenue' | 'fixed_daily'
unitName: string
currency: string
status: 'active' | 'archived'
note: string
versionCount: number
createdAt: string
updatedAt: string
}
export interface CostVersion {
id: number
costItemId: number
costItemCode: string
costItemName: string
pricingMode: CostItem['pricingMode']
scopeType: 'global' | 'provider' | 'shop' | 'product' | 'rule'
provider: string
shopId: string
canonicalProductId: number | null
canonicalSku: string
canonicalName: string
workProductRuleId: number | null
amountFen: number
rateBps: number
effectiveFrom: string
effectiveTo: string | null
source: 'manual' | 'upstream_sync' | 'import'
status: 'draft' | 'active' | 'retired'
changeReason: string
createdByAdminUserId: number | null
approvedByAdminUserId: number | null
createdAt: string
updatedAt: string
}
export interface CostException {
id: number
subjectType: 'order_item' | 'work_order' | 'manual_case'
subjectId: number
orderItemId: number | null
workOrderId: number | null
carrierType: string
reason: string
status: 'open' | 'resolved' | 'ignored'
detail: Record<string, unknown>
createdAt: string
resolvedAt: string | null
resolvedByAdminUserId: number | null
}
export interface CostCenterOptions {
providers: string[]
products: Array<{
id: number
canonicalSku: string
canonicalName: string
unitName: string
}>
costItems: Array<{
id: number
code: string
name: string
pricingMode: CostItem['pricingMode']
carrierType: CostItem['carrierType']
}>
rules: Array<{
id: number
ruleKey: string
productName: string
skuCode: string
provider: string
}>
}
export interface TemplateProductBootstrapPreview {
templateCount: number
productExistingCount: number
productCreateCount: number
mappingCandidateCount: number
mappingExistingCount: number
mappingCreateCount: number
mappingCreateByMatchType: Record<
'sku_code' | 'sku_code_name' | 'sku_code_series' | 'sku_name_series',
number
>
evidenceItemCount: number
orderItemCoverCount: number
mappingConflictCount: number
mappingConflictItemCount: number
multiRuleKeyCount: number
seriesFallbackConflictCount: number
templatesWithoutWorkOrders: number
samples: Array<{
ruleKey: string
productName: string
provider: string
shopId: string
skuCode: string
skuName: string
matchType: ChannelProductMapping['matchType']
itemCount: number
}>
conflicts: Array<{
provider: string
shopId: string
skuCode: string
skuName: string
matchType: 'sku_code_name'
itemCount: number
ruleKeys: string[]
}>
}
export interface TemplateProductBootstrapResult {
templateCount: number
createdProductCount: number
reusedProductCount: number
createdMappingCount: number
skippedMappingCount: number
mappedOrderItemCount: number
deletedImportedMappingCount: number
resetOrderItemCount: number
mappingConflictCount: number
mappingConflictItemCount: number
}
export interface CostLine {
costItemId: number
costItemCode: string
costItemName: string
costType: string
carrierType: string
pricingMode: string
costVersionId: number
scopeType: string
amountFen: number
rateBps: number
quantity: number
unitAmountFen: number
totalAmountFen: number
}
export interface CostPreview {
context: {
orderItemId: number | null
orderId: number | null
provider: string
shopId: string
skuCode: string
skuName: string
carrierType: string
quantity: number
normalizedQuantity: number
revenueFen: number
workOrderId: number | null
workProductRuleId: number | null
}
mapping: ChannelProductMapping | null
product: ProductCatalogItem | null
lines: CostLine[]
missingCostItems: Array<{
costItemId: number
costItemCode: string
costItemName: string
carrierType: string
}>
totalCostFen: number
exceptions: Array<{ reason: string; message: string; detail: Record<string, unknown> }>
}
export interface ProductCategoryNode {
categoryId: number
name: string
total: number
directTotal: number
children: Array<{
categoryId: number
name: string
total: number
}>
}
export interface ProductCatalogListResult {
items: ProductCatalogItem[]
categoryTree: ProductCategoryNode[]
uncategorizedTotal: number
pagination: { page: number; pageSize: number; total: number }
}
export interface ChannelProductMappingGroup {
canonicalProductId: number
canonicalSku: string
canonicalName: string
category: string
unitName: string
status: 'active' | 'archived'
mappings: ChannelProductMapping[]
}
export interface ChannelProductMappingListResult {
groups: ChannelProductMappingGroup[]
pagination: { page: number; pageSize: number; total: number; mappingTotal: number }
}
export interface CostItemListResult {
items: CostItem[]
pagination: { page: number; pageSize: number; total: number }
}
export interface CostVersionListResult {
items: CostVersion[]
pagination: { page: number; pageSize: number; total: number }
}
export interface CostExceptionListResult {
items: CostException[]
pagination: { page: number; pageSize: number; total: number }
}
+2
View File
@@ -122,3 +122,5 @@ export type {
AdminAffiliateDashProductListResult,
AdminAffiliateDashWallet,
} from './platform-config'
export type * from './cost-center'