feat(cost-center): 商品初始化/回填增加时间范围,默认最近 30 天
- 接单模板初始化预览/执行支持 dateFrom/dateTo,只使用范围内工单证据; 范围外模板不再先建商品 - 按工单回填商品支持时间范围;人工商品归一 UI 传时间范围 - 模板初始化重置只清理范围内导入映射/订单项,避免误伤范围外数据 - 初始化/回填默认最近 30 天,自动发货仍固定 30 天 - 文档更新初始化口径与页面操作
This commit is contained in:
@@ -47,7 +47,7 @@ const router = Router()
|
||||
router.get(
|
||||
'/cost-center/template-bootstrap/preview',
|
||||
requireAdminPermission('finance.view'),
|
||||
createJsonHandler(() => previewAdminTemplateProductBootstrap(), {
|
||||
createJsonHandler((req) => previewAdminTemplateProductBootstrap(req.query as JsonObject), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取接单模板初始化预览失败',
|
||||
scope: '[admin/cost-center/template-bootstrap/preview]',
|
||||
@@ -156,7 +156,7 @@ router.post(
|
||||
router.post(
|
||||
'/cost-center/work-order-products/backfill',
|
||||
requireAdminPermission('finance.config'),
|
||||
createJsonHandler(() => backfillAdminWorkOrderProductSkus(), {
|
||||
createJsonHandler((req) => backfillAdminWorkOrderProductSkus(bodyOf(req)), {
|
||||
successMessage: '接单平台商品已回填',
|
||||
errorMessage: '回填接单平台商品失败',
|
||||
scope: '[admin/cost-center/work-order-products/backfill]',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { query } from '../../db/client.js'
|
||||
import type { AdminSession } from '../admin/admin-auth-service.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { resolveFinanceDateRange } from '../../utils/finance-date-range.js'
|
||||
import { formatShanghaiDate, shiftDate } from '../../utils/finance-date-range.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { normalizeMatchText, normalizeSkuSeriesEquivalence } from '../product-match/normalize.js'
|
||||
@@ -978,6 +979,11 @@ export async function backfillAdminProductMappings(payload: JsonObject = {}) {
|
||||
const limit = Math.min(20000, Math.max(1, Math.trunc(Number(payload.limit) || 5000)))
|
||||
const all = payload.all === true || payload.all === 'true'
|
||||
const maxBatches = all ? 40 : 1
|
||||
const range = resolveOptionalDateRange(payload)
|
||||
const dateSql = range
|
||||
? `AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date BETWEEN $4::date AND $5::date`
|
||||
: ''
|
||||
const rangeParams = range ? [range.fromDate, range.toDate] : []
|
||||
let updatedCount = 0
|
||||
let batches = 0
|
||||
let lastId = 0
|
||||
@@ -1015,10 +1021,11 @@ export async function backfillAdminProductMappings(payload: JsonObject = {}) {
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND oi.id > $1
|
||||
AND ($2 = '' OR o.provider = $2)
|
||||
${dateSql}
|
||||
ORDER BY oi.id
|
||||
LIMIT $3
|
||||
`,
|
||||
[lastId, provider, limit],
|
||||
[lastId, provider, limit, ...rangeParams],
|
||||
)
|
||||
if (itemsResult.rows.length === 0) break
|
||||
|
||||
@@ -1078,8 +1085,9 @@ export async function backfillAdminProductMappings(payload: JsonObject = {}) {
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND ($1 = '' OR o.provider = $1)
|
||||
${dateSql.replace('$4', '$2').replace('$5', '$3')}
|
||||
`,
|
||||
[provider],
|
||||
[provider, ...rangeParams],
|
||||
)
|
||||
return {
|
||||
updatedCount,
|
||||
@@ -1155,6 +1163,11 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {}
|
||||
const limit = Math.min(50000, Math.max(1, Math.trunc(Number(payload.limit) || 20000)))
|
||||
const all = payload.all === true || payload.all === 'true'
|
||||
const maxBatches = all ? 40 : 1
|
||||
const range = resolveOptionalDateRange(payload)
|
||||
const dateSql = range
|
||||
? `AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date BETWEEN $2::date AND $3::date`
|
||||
: ''
|
||||
const rangeParams = range ? [range.fromDate, range.toDate] : []
|
||||
let updatedCount = 0
|
||||
let batches = 0
|
||||
|
||||
@@ -1168,16 +1181,18 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {}
|
||||
normalized_quantity: number | null
|
||||
}>(
|
||||
`
|
||||
SELECT id, sku_name, sku_code, normalized_sku_name, normalized_sku_series,
|
||||
normalized_quantity
|
||||
FROM order_items
|
||||
WHERE normalized_quantity IS NULL
|
||||
OR normalized_sku_name IS NULL
|
||||
OR normalized_sku_series IS NULL
|
||||
ORDER BY id
|
||||
SELECT oi.id, oi.sku_name, oi.sku_code, oi.normalized_sku_name,
|
||||
oi.normalized_sku_series, oi.normalized_quantity
|
||||
FROM order_items oi
|
||||
${range ? 'JOIN orders o ON o.id = oi.order_id' : ''}
|
||||
WHERE (oi.normalized_quantity IS NULL
|
||||
OR oi.normalized_sku_name IS NULL
|
||||
OR oi.normalized_sku_series IS NULL)
|
||||
${dateSql.replaceAll('oi.', 'oi.')}
|
||||
ORDER BY oi.id
|
||||
LIMIT $1
|
||||
`,
|
||||
[limit],
|
||||
[limit, ...rangeParams],
|
||||
)
|
||||
if (result.rows.length === 0) break
|
||||
|
||||
@@ -1218,10 +1233,13 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {}
|
||||
|
||||
const remainingResult = await query<{ total: number }>(
|
||||
`SELECT COUNT(*)::int AS total
|
||||
FROM order_items
|
||||
WHERE normalized_quantity IS NULL
|
||||
OR normalized_sku_name IS NULL
|
||||
OR normalized_sku_series IS NULL`,
|
||||
FROM order_items oi
|
||||
${range ? 'JOIN orders o ON o.id = oi.order_id' : ''}
|
||||
WHERE (oi.normalized_quantity IS NULL
|
||||
OR oi.normalized_sku_name IS NULL
|
||||
OR oi.normalized_sku_series IS NULL)
|
||||
${dateSql.replace('$2', '$1').replace('$3', '$2')}`,
|
||||
rangeParams,
|
||||
)
|
||||
return {
|
||||
updatedCount,
|
||||
@@ -1231,6 +1249,16 @@ export async function backfillAdminNormalizedQuantities(payload: JsonObject = {}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveOptionalDateRange(payload: JsonObject) {
|
||||
const rawFrom = payload.dateFrom ?? payload.date_from
|
||||
const rawTo = payload.dateTo ?? payload.date_to
|
||||
if (!rawFrom && !rawTo) return null
|
||||
return resolveFinanceDateRange(
|
||||
{ dateFrom: rawFrom, dateTo: rawTo },
|
||||
{ defaultDays: 30, maxDays: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
export async function getProductRow(id: number) {
|
||||
const result = await query<ProductRow>('SELECT * FROM product_catalog WHERE id = $1 LIMIT 1', [
|
||||
id,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { resolveFinanceDateRange } from '../../utils/finance-date-range.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import type { AdminSession } from '../admin/admin-auth-service.js'
|
||||
import { normalizeMatchText, normalizeSkuSeriesEquivalence } from '../product-match/normalize.js'
|
||||
@@ -19,8 +20,9 @@ import {
|
||||
} from './product-catalog-service.js'
|
||||
import { syncAdminWorkOrderTemplateCostVersions } from './work-order-product-service.js'
|
||||
|
||||
export async function previewAdminTemplateProductBootstrap() {
|
||||
const data = await loadTemplateProductBootstrapData()
|
||||
export async function previewAdminTemplateProductBootstrap(payload: JsonObject = {}) {
|
||||
const range = resolveBootstrapDateRange(payload)
|
||||
const data = await loadTemplateProductBootstrapData(range)
|
||||
const plan = buildTemplateMappingPlan(data.evidenceRows)
|
||||
const templateRuleKeys = new Set(data.templates.map((row) => row.rule_key))
|
||||
const candidates = plan.candidates.filter(
|
||||
@@ -33,6 +35,8 @@ export async function previewAdminTemplateProductBootstrap() {
|
||||
const usedRuleIds = new Set(data.evidenceRows.map((row) => Number(row.product_rule_id)))
|
||||
const candidatesByMatchType = countMappingCandidates(candidates)
|
||||
return {
|
||||
dateFrom: range.fromDate,
|
||||
dateTo: range.toDate,
|
||||
templateCount: data.templates.length,
|
||||
productExistingCount: data.templates.length - data.productsToCreate.length,
|
||||
productCreateCount: data.productsToCreate.length,
|
||||
@@ -75,10 +79,11 @@ export async function applyAdminTemplateProductBootstrap(
|
||||
_session?: AdminSession,
|
||||
) {
|
||||
const repair = payload.repair !== false
|
||||
const range = resolveBootstrapDateRange(payload)
|
||||
const resetResult = repair
|
||||
? await resetImportedTemplateProductBootstrap()
|
||||
? await resetImportedTemplateProductBootstrap(range)
|
||||
: { deletedMappingCount: 0, resetOrderItemCount: 0 }
|
||||
const data = await loadTemplateProductBootstrapData()
|
||||
const data = await loadTemplateProductBootstrapData(range)
|
||||
if (data.templates.length === 0) {
|
||||
throw createHttpError('没有可导入的接单模板', {
|
||||
statusCode: 400,
|
||||
@@ -185,12 +190,24 @@ export async function applyAdminTemplateProductBootstrap(
|
||||
})
|
||||
|
||||
// 名称/系列键必须先于映射回填;所有批次都放在事务外,避免语句超时。
|
||||
await backfillAdminNormalizedQuantities({ limit: 50000, all: true })
|
||||
const mappingBackfill = await backfillAdminProductMappings({ limit: 20000, all: true })
|
||||
await backfillAdminNormalizedQuantities({
|
||||
limit: 50000,
|
||||
all: true,
|
||||
dateFrom: range.fromDate,
|
||||
dateTo: range.toDate,
|
||||
})
|
||||
const mappingBackfill = await backfillAdminProductMappings({
|
||||
limit: 20000,
|
||||
all: true,
|
||||
dateFrom: range.fromDate,
|
||||
dateTo: range.toDate,
|
||||
})
|
||||
mappedOrderItemCount = mappingBackfill.updatedCount
|
||||
const templateCosts = await syncAdminWorkOrderTemplateCostVersions()
|
||||
|
||||
return {
|
||||
dateFrom: range.fromDate,
|
||||
dateTo: range.toDate,
|
||||
templateCount: data.templates.length,
|
||||
createdProductCount,
|
||||
reusedProductCount,
|
||||
@@ -556,7 +573,7 @@ function compareMappingCandidates(left: TemplateMappingCandidate, right: Templat
|
||||
)
|
||||
}
|
||||
|
||||
async function resetImportedTemplateProductBootstrap() {
|
||||
async function resetImportedTemplateProductBootstrap(range: { fromDate: string; toDate: string }) {
|
||||
let resetOrderItemCount = 0
|
||||
for (let batch = 0; batch < 20; batch += 1) {
|
||||
const result = await query<{ id: number }>(
|
||||
@@ -589,6 +606,8 @@ async function resetImportedTemplateProductBootstrap() {
|
||||
)
|
||||
)
|
||||
WHERE oi.canonical_product_id IS NOT NULL
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
BETWEEN $1::date AND $2::date
|
||||
LIMIT 20000
|
||||
)
|
||||
UPDATE order_items oi
|
||||
@@ -598,21 +617,31 @@ async function resetImportedTemplateProductBootstrap() {
|
||||
WHERE oi.id = c.id
|
||||
RETURNING oi.id
|
||||
`,
|
||||
[range.fromDate, range.toDate],
|
||||
)
|
||||
resetOrderItemCount += result.rowCount || 0
|
||||
if ((result.rowCount || 0) < 20000) break
|
||||
}
|
||||
const deleted = await query(
|
||||
`
|
||||
DELETE FROM channel_product_mappings
|
||||
WHERE source = 'import'
|
||||
AND note LIKE '接单模板 %自动初始化%'
|
||||
DELETE FROM channel_product_mappings m
|
||||
WHERE m.source = 'import'
|
||||
AND m.note LIKE '接单模板 %自动初始化%'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id = m.canonical_product_id
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
BETWEEN $1::date AND $2::date
|
||||
)
|
||||
`,
|
||||
[range.fromDate, range.toDate],
|
||||
)
|
||||
return { deletedMappingCount: deleted.rowCount || 0, resetOrderItemCount }
|
||||
}
|
||||
|
||||
async function loadTemplateProductBootstrapData() {
|
||||
async function loadTemplateProductBootstrapData(range: { fromDate: string; toDate: string }) {
|
||||
const [templatesResult, evidenceResult, productsResult, mappingsResult] = await Promise.all([
|
||||
query<TemplateBootstrapTemplateRow>(
|
||||
`
|
||||
@@ -656,6 +685,8 @@ async function loadTemplateProductBootstrapData() {
|
||||
JOIN work_orders wo ON wo.order_item_id = oi.id
|
||||
JOIN work_product_rules r ON r.id = wo.product_rule_id AND r.enabled = TRUE
|
||||
WHERE wo.product_rule_id IS NOT NULL
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
BETWEEN $1::date AND $2::date
|
||||
GROUP BY
|
||||
o.provider,
|
||||
o.shop_id,
|
||||
@@ -665,6 +696,7 @@ async function loadTemplateProductBootstrapData() {
|
||||
r.rule_key
|
||||
ORDER BY o.provider, o.shop_id, oi.sku_code, oi.sku_name, wo.product_rule_id
|
||||
`,
|
||||
[range.fromDate, range.toDate],
|
||||
),
|
||||
query<{ id: number; canonical_sku: string; canonical_name: string }>(
|
||||
'SELECT id, canonical_sku, canonical_name FROM product_catalog',
|
||||
@@ -681,7 +713,9 @@ async function loadTemplateProductBootstrapData() {
|
||||
),
|
||||
])
|
||||
|
||||
const templates = templatesResult.rows
|
||||
const usedRuleIds = new Set(evidenceResult.rows.map((row) => Number(row.product_rule_id)))
|
||||
// 只初始化时间范围内真正有工单证据的模板,避免把很早以前、已不再使用的模板商品重新带出来。
|
||||
const templates = templatesResult.rows.filter((row) => usedRuleIds.has(Number(row.id)))
|
||||
const productByCanonicalSku = new Map(
|
||||
productsResult.rows.map((row) => [row.canonical_sku, Number(row.id)]),
|
||||
)
|
||||
@@ -737,6 +771,14 @@ export function inferTemplateUnitName(productName: string) {
|
||||
return '件'
|
||||
}
|
||||
|
||||
function resolveBootstrapDateRange(payload: JsonObject) {
|
||||
const range = resolveFinanceDateRange(
|
||||
{ dateFrom: payload.dateFrom, dateTo: payload.dateTo },
|
||||
{ defaultDays: 30, maxDays: 400 },
|
||||
)
|
||||
return { fromDate: range.fromDate, toDate: range.toDate }
|
||||
}
|
||||
|
||||
function normalizeProductNameKey(value: unknown) {
|
||||
return String(value || '')
|
||||
.normalize('NFKC')
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* 接单平台(work_order)商品回填与模板发单金额同步。
|
||||
*/
|
||||
import { query } from '../../db/client.js'
|
||||
import { resolveFinanceDateRange } from '../../utils/finance-date-range.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { rebuildFactsForRange } from './affiliate-product-service.js'
|
||||
import { WORK_ORDER_TEMPLATE_COST_BASELINE } from './cost-center-shared.js'
|
||||
@@ -14,6 +15,14 @@ type WorkOrderSkuBackfillRow = {
|
||||
|
||||
const WORK_ORDER_BACKFILL_BATCH_SIZE = 20000
|
||||
|
||||
function resolveWorkOrderRange(payload: Record<string, unknown>) {
|
||||
const range = resolveFinanceDateRange(
|
||||
{ dateFrom: payload.dateFrom, dateTo: payload.dateTo },
|
||||
{ defaultDays: 30, maxDays: 400 },
|
||||
)
|
||||
return { fromDate: range.fromDate, toDate: range.toDate }
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填接单平台订单项的标准商品。
|
||||
*
|
||||
@@ -21,8 +30,9 @@ const WORK_ORDER_BACKFILL_BATCH_SIZE = 20000
|
||||
* 仍无法归一的长尾项,再按 `provider + shop_id + sku_code + normalized_sku_name`
|
||||
* 补建 `wo:` 商品和 `sku_code_name` 渠道映射,保证后续同类订单能自动命中。
|
||||
*/
|
||||
export async function backfillAdminWorkOrderProductSkus() {
|
||||
export async function backfillAdminWorkOrderProductSkus(payload: Record<string, unknown> = {}) {
|
||||
const now = nowIso()
|
||||
const range = resolveWorkOrderRange(payload)
|
||||
// order_item_id 有唯一索引,这里可以直接 count,避免 DISTINCT ON 被
|
||||
// 三个子查询反复物化后撞 15 秒语句超时。
|
||||
const result = await query<WorkOrderSkuBackfillRow>(
|
||||
@@ -41,7 +51,10 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
JOIN order_items oi ON oi.id = wo.order_item_id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE wo.order_item_id IS NOT NULL
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
BETWEEN $1::date AND $2::date
|
||||
`,
|
||||
[range.fromDate, range.toDate],
|
||||
)
|
||||
|
||||
// 单条 UPDATE 覆盖十几万行会撞 15 秒语句超时;按订单项 id 分批,
|
||||
@@ -57,10 +70,14 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
wo.order_item_id AS id,
|
||||
wo.product_rule_id
|
||||
FROM work_orders wo
|
||||
JOIN order_items oi ON oi.id = wo.order_item_id
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE wo.order_item_id IS NOT NULL
|
||||
AND wo.order_item_id > $1
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
BETWEEN $1::date AND $2::date
|
||||
AND wo.order_item_id > $3
|
||||
ORDER BY wo.order_item_id, wo.id DESC
|
||||
LIMIT $2
|
||||
LIMIT $4
|
||||
)
|
||||
SELECT woi.id, p.id AS canonical_product_id
|
||||
FROM work_order_item woi
|
||||
@@ -68,7 +85,7 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
JOIN product_catalog p ON p.canonical_sku = 'tpl:' || wpr.rule_key
|
||||
ORDER BY woi.id
|
||||
`,
|
||||
[lastRuleItemId, WORK_ORDER_BACKFILL_BATCH_SIZE],
|
||||
[range.fromDate, range.toDate, lastRuleItemId, WORK_ORDER_BACKFILL_BATCH_SIZE],
|
||||
)
|
||||
if (targetRows.rows.length === 0) break
|
||||
const lastSelectedId = Number(targetRows.rows[targetRows.rows.length - 1]?.id || 0)
|
||||
@@ -103,8 +120,11 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
oi.normalized_sku_name AS name,
|
||||
MIN(oi.sku_name) AS sample_name
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND btrim(COALESCE(oi.normalized_sku_name, '')) <> ''
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
BETWEEN $2::date AND $3::date
|
||||
AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
|
||||
GROUP BY 1
|
||||
),
|
||||
@@ -138,7 +158,7 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
SET updated_at = $1
|
||||
RETURNING (xmax = 0) AS inserted
|
||||
`,
|
||||
[now],
|
||||
[now, range.fromDate, range.toDate],
|
||||
)
|
||||
|
||||
const tailMappings = await query<{ id: number }>(
|
||||
@@ -154,6 +174,8 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND btrim(COALESCE(oi.normalized_sku_name, '')) <> ''
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
BETWEEN $2::date AND $3::date
|
||||
AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
|
||||
GROUP BY 1, 2, 3, 5
|
||||
),
|
||||
@@ -183,7 +205,7 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
) DO NOTHING
|
||||
RETURNING id
|
||||
`,
|
||||
[now],
|
||||
[now, range.fromDate, range.toDate],
|
||||
)
|
||||
|
||||
let lastTailItemId = 0
|
||||
@@ -194,12 +216,15 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
WITH tail AS (
|
||||
SELECT oi.id, oi.normalized_sku_name
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND btrim(COALESCE(oi.normalized_sku_name, '')) <> ''
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
BETWEEN $2::date AND $3::date
|
||||
AND oi.id > $1
|
||||
AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
|
||||
ORDER BY oi.id
|
||||
LIMIT $2
|
||||
LIMIT $4
|
||||
)
|
||||
SELECT t.id, p.id AS canonical_product_id
|
||||
FROM tail t
|
||||
@@ -212,7 +237,7 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
) p ON TRUE
|
||||
ORDER BY t.id
|
||||
`,
|
||||
[lastTailItemId, WORK_ORDER_BACKFILL_BATCH_SIZE],
|
||||
[lastTailItemId, range.fromDate, range.toDate, WORK_ORDER_BACKFILL_BATCH_SIZE],
|
||||
)
|
||||
if (tailRows.rows.length === 0) break
|
||||
const lastSelectedId = Number(tailRows.rows[tailRows.rows.length - 1]?.id || 0)
|
||||
@@ -248,9 +273,13 @@ export async function backfillAdminWorkOrderProductSkus() {
|
||||
`
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM order_items oi
|
||||
JOIN orders o ON o.id = oi.order_id
|
||||
WHERE oi.canonical_product_id IS NULL
|
||||
AND (COALESCE(o.paid_at, o.created_at) AT TIME ZONE 'Asia/Shanghai')::date
|
||||
BETWEEN $1::date AND $2::date
|
||||
AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
|
||||
`,
|
||||
[range.fromDate, range.toDate],
|
||||
)
|
||||
const remainingCount = Number(remainingResult.rows[0]?.total || 0)
|
||||
const matchedCount = Math.max(0, workOrderItemCount - remainingCount)
|
||||
|
||||
Reference in New Issue
Block a user