feat(cost-center): 成本项编码改系统自动生成,补自动发货价格导入脚本
- 新建成本项目不再手填编码:后端按成本类型生成基础编码(auto_purchase / work_order_reward / manual_service / platform_fee / payment_fee / after_sales_loss / shipping_other / other_cost),重复时追加 _2、_3; 接口显式传 code 时仍优先使用,兼容脚本导入 - 前端移除编码输入框,改为说明提示,创建成功提示展示生成的编码 - 新增 scripts/sync-affiliate-dash-prices.ts + scripts/data/affiliate-dash-prices.json: 按上游 38 条自动发货价格补建 aff:<sku> 商品,并按 cost_amount(分)生成 scope=product 的按件成本版本;价格变化时停用旧版本再插新版本 - 自动发货商品同步与价格导入的 attributes_json 改为合并,避免互相覆盖
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
[
|
||||
{ "sku": "rock_blue_sportswear", "name": "蓝色运动服上衣", "category": "服饰", "costAmountFen": 2500 },
|
||||
{ "sku": "rock_black_tshirt", "name": "黑色ROCK长袖T恤", "category": "服饰", "costAmountFen": 2500 },
|
||||
{ "sku": "suit_jester_face", "name": "套装-花面小丑", "category": "套装", "costAmountFen": 20000 },
|
||||
{ "sku": "suit_dune_song", "name": "套装-沙丘之歌", "category": "套装", "costAmountFen": 10200 },
|
||||
{ "sku": "pack_dragon_palace", "name": "龙宫之主大礼包", "category": "礼包", "costAmountFen": 80000 },
|
||||
{ "sku": "lucky_coin_x90", "name": "幸运币90个", "category": "道具", "costAmountFen": 9000 },
|
||||
{ "sku": "lucky_coin_x30", "name": "幸运币30个", "category": "道具", "costAmountFen": 3000 },
|
||||
{ "sku": "lucky_coin_x2", "name": "幸运币2个", "category": "道具", "costAmountFen": 200 },
|
||||
{ "sku": "honor_medal_x90", "name": "荣耀勋章90个", "category": "道具", "costAmountFen": 9000 },
|
||||
{ "sku": "honor_medal_x30", "name": "荣耀勋章30个", "category": "道具", "costAmountFen": 3000 },
|
||||
{ "sku": "honor_medal_x2", "name": "荣耀勋章2个", "category": "道具", "costAmountFen": 200 },
|
||||
{ "sku": "suit_cloud_bear", "name": "套装-绵云熊熊", "category": "套装", "costAmountFen": 13480 },
|
||||
{ "sku": "pack_star_roam_weapon", "name": "星际漫游枪械礼包", "category": "礼包", "costAmountFen": 51600 },
|
||||
{ "sku": "pack_star_roam_outfit", "name": "星际漫游服装礼包", "category": "礼包", "costAmountFen": 83600 },
|
||||
{ "sku": "suit_sand_dancer", "name": "套装-狂沙舞者", "category": "套装", "costAmountFen": 8400 },
|
||||
{ "sku": "pack_lava_ranger", "name": "熔岩游骑兵礼包", "category": "礼包", "costAmountFen": 9600 },
|
||||
{ "sku": "suit_panda_tuan", "name": "套装-胖达团团", "category": "套装", "costAmountFen": 23400 },
|
||||
{ "sku": "suit_panda_round", "name": "套装-胖达圆圆", "category": "套装", "costAmountFen": 23400 },
|
||||
{ "sku": "helmet_panda_treasure", "name": "国宝胖达头盔", "category": "头盔", "costAmountFen": 23400 },
|
||||
{ "sku": "suit_western_mystery", "name": "套装-西部谜踪", "category": "套装", "costAmountFen": 11500 },
|
||||
{ "sku": "helmet_hamster_gray", "name": "仓鼠灰灰头盔", "category": "头盔", "costAmountFen": 16800 },
|
||||
{ "sku": "bag_hamster_gray", "name": "仓鼠灰灰背包", "category": "背包", "costAmountFen": 16800 },
|
||||
{ "sku": "helmet_pink_sheep", "name": "糯粉咩咩头盔", "category": "头盔", "costAmountFen": 16800 },
|
||||
{ "sku": "bag_pink_sheep", "name": "糯粉咩咩背包", "category": "背包", "costAmountFen": 16800 },
|
||||
{ "sku": "suit_cute_bear", "name": "套装-萌熊伴侣", "category": "套装", "costAmountFen": 16800 },
|
||||
{ "sku": "suit_hamster_gray", "name": "套装-仓鼠灰灰", "category": "套装", "costAmountFen": 12800 },
|
||||
{ "sku": "frag_pink_sheep", "name": "破片手榴弹-糯粉咩咩", "category": "投掷物", "costAmountFen": 4880 },
|
||||
{ "sku": "smoke_pink_sheep", "name": "烟雾弹-糯粉咩咩", "category": "投掷物", "costAmountFen": 4880 },
|
||||
{ "sku": "pack_western_cowboy", "name": "西部牛仔大礼包", "category": "礼包", "costAmountFen": 32000 },
|
||||
{ "sku": "suit_romantic_destiny", "name": "套装-浪漫天命", "category": "套装", "costAmountFen": 3600 },
|
||||
{ "sku": "suit_first_peach", "name": "套装-恋恋初桃", "category": "套装", "costAmountFen": 9880 },
|
||||
{ "sku": "suit_pink_sheep", "name": "套装-糯粉咩咩", "category": "套装", "costAmountFen": 18800 },
|
||||
{ "sku": "suit_dual_fluffy", "name": "套装-双彩绵绵", "category": "套装", "costAmountFen": 11240 },
|
||||
{ "sku": "bag_cute_bear", "name": "萌熊伴侣背包", "category": "背包", "costAmountFen": 28800 },
|
||||
{ "sku": "m416_hamster_gray", "name": "M416-仓鼠灰灰", "category": "枪械", "costAmountFen": 16800 },
|
||||
{ "sku": "top_black_elite_trainer", "name": "黑色高级特训官上衣", "category": "上衣", "costAmountFen": 3600 },
|
||||
{ "sku": "suit_shadow_gothic", "name": "套装-暗影哥特", "category": "套装", "costAmountFen": 8880 },
|
||||
{ "sku": "suit_alan_walker", "name": "套装-Alan Walker", "category": "套装", "costAmountFen": 3600 }
|
||||
]
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 按上游自动发货商品价格,批量生成「自动发货采购成本」成本版本。
|
||||
*
|
||||
* 用法:
|
||||
* npx tsx scripts/sync-affiliate-dash-prices.ts [价格 JSON 路径] [--cost-item-id=5] [--dry-run]
|
||||
*
|
||||
* 规则:
|
||||
* - 标准商品编码固定为 `aff:<sku>`,载体 affiliate_dash;
|
||||
* - 成本版本范围 = 标准商品,金额 = 上游 cost_amount(分);
|
||||
* - 同一商品已有相同金额的生效版本时跳过;
|
||||
* - 金额变化时先停用旧版本,再插入新版本(历史快照不变)。
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
import { closeDb, query } from '../src/db/client.js'
|
||||
|
||||
type PriceRow = {
|
||||
sku: string
|
||||
name: string
|
||||
category: string
|
||||
costAmountFen: number
|
||||
}
|
||||
|
||||
type CostItemRow = {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
pricing_mode: string
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]) {
|
||||
let filePath = ''
|
||||
let costItemId: number | null = null
|
||||
let dryRun = false
|
||||
for (const arg of argv) {
|
||||
if (arg === '--dry-run') {
|
||||
dryRun = true
|
||||
continue
|
||||
}
|
||||
if (arg.startsWith('--cost-item-id=')) {
|
||||
const parsed = Number(arg.slice('--cost-item-id='.length))
|
||||
if (Number.isFinite(parsed) && parsed > 0) costItemId = parsed
|
||||
continue
|
||||
}
|
||||
if (!arg.startsWith('--') && !filePath) filePath = arg
|
||||
}
|
||||
return {
|
||||
filePath: path.resolve(filePath || 'scripts/data/affiliate-dash-prices.json'),
|
||||
costItemId,
|
||||
dryRun,
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveCostItem(costItemId: number | null): Promise<CostItemRow> {
|
||||
if (costItemId) {
|
||||
const result = await query<CostItemRow>('SELECT * FROM cost_items WHERE id = $1', [costItemId])
|
||||
const row = result.rows[0]
|
||||
if (!row) throw new Error(`成本项不存在: ${costItemId}`)
|
||||
if (row.pricing_mode !== 'per_item') {
|
||||
throw new Error(`成本项 ${row.code} 的计价方式不是按件,不能导入按件采购价`)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
const existing = await query<CostItemRow>(
|
||||
`SELECT * FROM cost_items
|
||||
WHERE carrier_type = 'affiliate_dash' AND cost_type = 'auto_purchase' AND status = 'active'
|
||||
ORDER BY id LIMIT 1`,
|
||||
)
|
||||
if (existing.rows[0]) return existing.rows[0]
|
||||
|
||||
const now = new Date().toISOString()
|
||||
const inserted = await query<CostItemRow>(
|
||||
`INSERT INTO cost_items (
|
||||
code, name, cost_type, carrier_type, pricing_mode, unit_name, currency, status, note, created_at, updated_at
|
||||
) VALUES ('auto_purchase', '自动发货采购成本', 'auto_purchase', 'affiliate_dash', 'per_item', '件', 'CNY', 'active', '上游自动发货价格同步', $1, $1)
|
||||
ON CONFLICT (code) DO NOTHING
|
||||
RETURNING *`,
|
||||
[now],
|
||||
)
|
||||
if (inserted.rows[0]) return inserted.rows[0]
|
||||
|
||||
const fallback = await query<CostItemRow>(
|
||||
"SELECT * FROM cost_items WHERE code = 'auto_purchase' LIMIT 1",
|
||||
)
|
||||
if (!fallback.rows[0]) throw new Error('无法创建自动发货采购成本项')
|
||||
return fallback.rows[0]
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { filePath, costItemId, dryRun } = parseArgs(process.argv.slice(2))
|
||||
const rows = JSON.parse(fs.readFileSync(filePath, 'utf8')) as PriceRow[]
|
||||
|
||||
const costItem = await resolveCostItem(costItemId)
|
||||
console.log(
|
||||
`[affiliate-price] 成本项 #${costItem.id} ${costItem.name}(${costItem.code});价格条目 ${rows.length} 条${dryRun ? '(dry-run)' : ''}`,
|
||||
)
|
||||
|
||||
let createdProductCount = 0
|
||||
let updatedProductCount = 0
|
||||
let createdVersionCount = 0
|
||||
let retiredVersionCount = 0
|
||||
let skippedCount = 0
|
||||
|
||||
for (const row of rows) {
|
||||
const now = new Date().toISOString()
|
||||
const canonicalSku = `aff:${row.sku}`
|
||||
const unitName = row.name.includes('个') ? '个' : '件'
|
||||
|
||||
if (dryRun) {
|
||||
const existingProduct = await query<{ id: number }>(
|
||||
'SELECT id FROM product_catalog WHERE canonical_sku = $1',
|
||||
[canonicalSku],
|
||||
)
|
||||
const existingId = existingProduct.rows[0]?.id
|
||||
if (!existingId) {
|
||||
createdProductCount += 1
|
||||
createdVersionCount += 1
|
||||
continue
|
||||
}
|
||||
updatedProductCount += 1
|
||||
const current = await query<{ amount_fen: number }>(
|
||||
`SELECT amount_fen FROM cost_versions
|
||||
WHERE cost_item_id = $1 AND scope_type = 'product' AND canonical_product_id = $2 AND status = 'active'
|
||||
ORDER BY effective_from DESC, id DESC LIMIT 1`,
|
||||
[costItem.id, existingId],
|
||||
)
|
||||
if (current.rows[0] && Number(current.rows[0].amount_fen) === row.costAmountFen) {
|
||||
skippedCount += 1
|
||||
} else {
|
||||
createdVersionCount += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const productResult = await query<{ id: number; inserted: boolean }>(
|
||||
`INSERT INTO product_catalog (
|
||||
canonical_sku, canonical_name, category, unit_name, carrier_type, status,
|
||||
attributes_json, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, 'affiliate_dash', 'active', $5::jsonb, $6, $6)
|
||||
ON CONFLICT (canonical_sku) DO UPDATE
|
||||
SET canonical_name = EXCLUDED.canonical_name,
|
||||
category = EXCLUDED.category,
|
||||
unit_name = EXCLUDED.unit_name,
|
||||
carrier_type = 'affiliate_dash',
|
||||
attributes_json = product_catalog.attributes_json || EXCLUDED.attributes_json,
|
||||
updated_at = $6
|
||||
RETURNING id, (xmax = 0) AS inserted`,
|
||||
[
|
||||
canonicalSku,
|
||||
row.name,
|
||||
row.category,
|
||||
unitName,
|
||||
JSON.stringify({
|
||||
source: 'affiliate_dash_price',
|
||||
skuCode: row.sku,
|
||||
priceFen: row.costAmountFen,
|
||||
}),
|
||||
now,
|
||||
],
|
||||
)
|
||||
const product = productResult.rows[0]
|
||||
if (!product) throw new Error(`商品写入失败: ${canonicalSku}`)
|
||||
if (product.inserted) createdProductCount += 1
|
||||
else updatedProductCount += 1
|
||||
|
||||
const currentResult = await query<{ id: number; amount_fen: number }>(
|
||||
`SELECT id, amount_fen FROM cost_versions
|
||||
WHERE cost_item_id = $1 AND scope_type = 'product' AND canonical_product_id = $2 AND status = 'active'
|
||||
ORDER BY effective_from DESC, id DESC LIMIT 1`,
|
||||
[costItem.id, product.id],
|
||||
)
|
||||
const current = currentResult.rows[0]
|
||||
if (current && Number(current.amount_fen) === row.costAmountFen) {
|
||||
skippedCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (current) {
|
||||
const retired = await query(
|
||||
`UPDATE cost_versions
|
||||
SET status = 'retired', change_reason = '上游价格更新', updated_at = $1
|
||||
WHERE cost_item_id = $2 AND canonical_product_id = $3 AND status = 'active'`,
|
||||
[now, costItem.id, product.id],
|
||||
)
|
||||
retiredVersionCount += retired.rowCount || 0
|
||||
}
|
||||
|
||||
await query(
|
||||
`INSERT INTO cost_versions (
|
||||
cost_item_id, scope_type, provider, shop_id, canonical_product_id, work_product_rule_id,
|
||||
amount_fen, rate_bps, effective_from, effective_to, source, status, change_reason,
|
||||
created_by_admin_user_id, approved_by_admin_user_id, created_at, updated_at
|
||||
) VALUES ($1, 'product', '', '', $2, NULL, $3, 0, $4, NULL, 'upstream_sync', 'active',
|
||||
'上游自动发货价格同步', NULL, NULL, $4, $4)`,
|
||||
[costItem.id, product.id, row.costAmountFen, now],
|
||||
)
|
||||
createdVersionCount += 1
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[affiliate-price] 完成:新增商品 ${createdProductCount}、更新商品 ${updatedProductCount}、` +
|
||||
`新增版本 ${createdVersionCount}、停用旧版本 ${retiredVersionCount}、金额未变跳过 ${skippedCount}`,
|
||||
)
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await closeDb()
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error(error)
|
||||
await closeDb()
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -25,6 +25,17 @@ const COST_TYPES = new Set([
|
||||
'shipping_other',
|
||||
'other',
|
||||
])
|
||||
/** 成本项自动编码的基础名:按成本类型生成,重复时追加 _2、_3。 */
|
||||
const COST_ITEM_CODE_BASE: Record<string, string> = {
|
||||
auto_purchase: 'auto_purchase',
|
||||
work_order_reward: 'work_order_reward',
|
||||
manual_service: 'manual_service',
|
||||
platform_fee: 'platform_fee',
|
||||
payment_fee: 'payment_fee',
|
||||
after_sales_loss: 'after_sales_loss',
|
||||
shipping_other: 'shipping_other',
|
||||
other: 'other_cost',
|
||||
}
|
||||
const CARRIER_TYPES = new Set(['affiliate_dash', 'work_order', 'manual', 'shared'])
|
||||
const PRICING_MODES = new Set([
|
||||
'per_item',
|
||||
@@ -1498,7 +1509,7 @@ export async function syncAdminAffiliateDashProducts() {
|
||||
SET carrier_type = 'affiliate_dash',
|
||||
canonical_name = EXCLUDED.canonical_name,
|
||||
unit_name = EXCLUDED.unit_name,
|
||||
attributes_json = EXCLUDED.attributes_json,
|
||||
attributes_json = product_catalog.attributes_json || EXCLUDED.attributes_json,
|
||||
updated_at = $1
|
||||
RETURNING (xmax = 0) AS inserted
|
||||
`,
|
||||
@@ -2161,11 +2172,12 @@ export async function listAdminCostItems(payload: JsonObject = {}) {
|
||||
}
|
||||
|
||||
export async function createAdminCostItem(payload: JsonObject, _session: AdminSession) {
|
||||
const code = normalizeRequiredText(payload.code, '成本项编码', 64)
|
||||
const name = normalizeRequiredText(payload.name, '成本项名称', 120)
|
||||
const costType = normalizeStatus(payload.costType, COST_TYPES, null, '成本类型无效')
|
||||
const carrierType = normalizeStatus(payload.carrierType, CARRIER_TYPES, null, '成本载体无效')
|
||||
const pricingMode = normalizeStatus(payload.pricingMode, PRICING_MODES, null, '计价方式无效')
|
||||
// 编码由系统生成(按成本类型 + 冲突序号);显式传入时仍优先使用,兼容脚本导入。
|
||||
const code = normalizeOptionalText(payload.code, 64) || (await generateCostItemCode(costType))
|
||||
const unitName = normalizeOptionalText(payload.unitName, 16) || '件'
|
||||
const currency = normalizeOptionalText(payload.currency, 8) || 'CNY'
|
||||
const status = normalizeStatus(payload.status, PRODUCT_STATUSES, 'active', '成本项状态无效')
|
||||
@@ -2193,6 +2205,22 @@ export async function createAdminCostItem(payload: JsonObject, _session: AdminSe
|
||||
return { costItem: mapCostItem(inserted) }
|
||||
}
|
||||
|
||||
async function generateCostItemCode(costType: string) {
|
||||
const base = COST_ITEM_CODE_BASE[costType] || 'cost_item'
|
||||
let candidate = base
|
||||
let index = 2
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
const existing = await query<{ id: number }>(
|
||||
'SELECT id FROM cost_items WHERE code = $1 LIMIT 1',
|
||||
[candidate],
|
||||
)
|
||||
if (!existing.rows[0]) return candidate
|
||||
candidate = `${base}_${index}`
|
||||
index += 1
|
||||
}
|
||||
return `${base}_${Date.now()}`
|
||||
}
|
||||
|
||||
export async function updateAdminCostItem(costItemId: number | string, payload: JsonObject) {
|
||||
const id = normalizeId(costItemId, '成本项 ID')
|
||||
const current = await getCostItemRow(id)
|
||||
|
||||
@@ -110,7 +110,6 @@ type MappingFormValues = {
|
||||
}
|
||||
|
||||
type CostItemFormValues = {
|
||||
code: string
|
||||
name: string
|
||||
costType: CostItem['costType']
|
||||
carrierType: CostItem['carrierType']
|
||||
@@ -208,7 +207,6 @@ function tabKeysForCarrier(carrier: CarrierScope) {
|
||||
type CostItemPreset = {
|
||||
key: string
|
||||
label: string
|
||||
code: string
|
||||
name: string
|
||||
costType: CostItem['costType']
|
||||
carrierType: CostCarrierType
|
||||
@@ -221,7 +219,6 @@ const COST_ITEM_PRESETS: CostItemPreset[] = [
|
||||
{
|
||||
key: 'auto_purchase',
|
||||
label: '自动发货采购成本',
|
||||
code: 'auto_purchase',
|
||||
name: '自动发货采购成本',
|
||||
costType: 'auto_purchase',
|
||||
carrierType: 'affiliate_dash',
|
||||
@@ -231,7 +228,6 @@ const COST_ITEM_PRESETS: CostItemPreset[] = [
|
||||
{
|
||||
key: 'work_order_reward',
|
||||
label: '接单平台打手报酬',
|
||||
code: 'work_order_reward',
|
||||
name: '接单平台打手报酬',
|
||||
costType: 'work_order_reward',
|
||||
carrierType: 'work_order',
|
||||
@@ -241,7 +237,6 @@ const COST_ITEM_PRESETS: CostItemPreset[] = [
|
||||
{
|
||||
key: 'manual_service',
|
||||
label: '人工客服成本',
|
||||
code: 'manual_service',
|
||||
name: '人工客服成本',
|
||||
costType: 'manual_service',
|
||||
carrierType: 'manual',
|
||||
@@ -251,7 +246,6 @@ const COST_ITEM_PRESETS: CostItemPreset[] = [
|
||||
{
|
||||
key: 'platform_fee',
|
||||
label: '平台服务费',
|
||||
code: 'platform_fee',
|
||||
name: '平台服务费',
|
||||
costType: 'platform_fee',
|
||||
carrierType: 'shared',
|
||||
@@ -261,7 +255,6 @@ const COST_ITEM_PRESETS: CostItemPreset[] = [
|
||||
{
|
||||
key: 'payment_fee',
|
||||
label: '支付通道费',
|
||||
code: 'payment_fee',
|
||||
name: '支付通道费',
|
||||
costType: 'payment_fee',
|
||||
carrierType: 'shared',
|
||||
@@ -271,7 +264,6 @@ const COST_ITEM_PRESETS: CostItemPreset[] = [
|
||||
{
|
||||
key: 'after_sales_loss',
|
||||
label: '售后损失',
|
||||
code: 'after_sales_loss',
|
||||
name: '售后损失',
|
||||
costType: 'after_sales_loss',
|
||||
carrierType: 'shared',
|
||||
@@ -781,7 +773,6 @@ export default function AdminCostCenterPage() {
|
||||
costItemForm.resetFields()
|
||||
if (preset) {
|
||||
costItemForm.setFieldsValue({
|
||||
code: preset.code,
|
||||
name: preset.name,
|
||||
costType: preset.costType,
|
||||
carrierType: preset.carrierType,
|
||||
@@ -815,7 +806,6 @@ export default function AdminCostCenterPage() {
|
||||
function openEditCostItem(item: CostItem) {
|
||||
setEditingCostItem(item)
|
||||
costItemForm.setFieldsValue({
|
||||
code: item.code,
|
||||
name: item.name,
|
||||
costType: item.costType,
|
||||
carrierType: item.carrierType,
|
||||
@@ -841,8 +831,8 @@ export default function AdminCostCenterPage() {
|
||||
})
|
||||
message.success('成本项目已更新')
|
||||
} else {
|
||||
await createAdminCostItem(values)
|
||||
message.success('成本项目已创建')
|
||||
const created = await createAdminCostItem(values)
|
||||
message.success(`成本项目已创建(编码 ${created.data.costItem.code})`)
|
||||
}
|
||||
setCostItemModalOpen(false)
|
||||
refreshAll()
|
||||
@@ -2596,14 +2586,12 @@ export default function AdminCostCenterPage() {
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={costItemForm} layout="vertical" onFinish={submitCostItem}>
|
||||
<Form.Item
|
||||
name="code"
|
||||
label="编码"
|
||||
rules={[{ required: true, message: '请输入编码' }]}
|
||||
extra="建议英文小写 + 下划线,例如 auto_purchase、work_order_reward;创建后不可修改。"
|
||||
>
|
||||
<Input placeholder="例如:work_order_reward" disabled={Boolean(editingCostItem)} />
|
||||
</Form.Item>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message="编码由系统自动生成(按成本类型 + 序号),无需填写;创建后不可修改。"
|
||||
/>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="例如:接单平台打手报酬" />
|
||||
</Form.Item>
|
||||
|
||||
@@ -139,6 +139,8 @@ product_catalog
|
||||
|
||||
`carrier_type`(迁移 `107_product_catalog_carrier.sql`)是后台「商品与成本中心」的分区维度:自动发货 / 接单平台 / 人工 / 共用。回填口径(2026-10-22 修正):只把**真正走 affiliate-dash 执行器**的商品算作 `affiliate_dash`——以 `fulfillment_tasks.executor_key='affiliate_dash'` 的履约证据为准,按 `context_json.skuCode` 建标准商品(`aff:<skuCode>`,生产快照 41,676 条任务 / 28 个 SKU);接单模板初始化出来的 `tpl:*` 商品全部是 `work_order`(662 个)。**渠道映射只代表平台 SKU 能归一到标准商品,不代表自动发货**。人工/共用由后台按需手工调整;新增自动发货 SKU 可在页面点「同步自动发货商品」补建。
|
||||
|
||||
上游自动发货商品价格用 `apps/backend/scripts/sync-affiliate-dash-prices.ts` 批量导入(价格清单见 `apps/backend/scripts/data/affiliate-dash-prices.json`):按 `aff:<skuCode>` 补建/更新商品,并给指定成本项生成 `scope=product` 的按件版本;金额取上游 `cost_amount`(分),价格变化时先停用旧版本再插入新版本,历史快照不变。示例:`npx tsx scripts/sync-affiliate-dash-prices.ts --cost-item-id=5`。
|
||||
|
||||
### 3.2 渠道 SKU 映射
|
||||
|
||||
```text
|
||||
|
||||
Reference in New Issue
Block a user