拆分打手平台售后与订单模块
This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
import { listKuaishouIndustryVoucherWorkOrderSyncSources } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
import {
|
||||
deleteWorkProductRuleMapping,
|
||||
listWorkProductMatchLogs,
|
||||
listWorkProductRuleMappings,
|
||||
listWorkProductRuleMappingsPage,
|
||||
upsertWorkProductRuleMapping,
|
||||
} from '../../repositories/worker-platform/product-match-repo.js'
|
||||
import { listWorkProductRules } from '../../repositories/worker-platform/work-product-rule-repo.js'
|
||||
import type {
|
||||
WorkProductMatchLogRow,
|
||||
WorkProductRuleMappingRow,
|
||||
} from '../../repositories/worker-platform/types.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import {
|
||||
mapWorkProductRule,
|
||||
normalizeBoolean,
|
||||
normalizeOptionalId,
|
||||
normalizePositiveInteger,
|
||||
resolveKuaishouWorkProductMatchContext,
|
||||
resolveMatchingProductRuleDecision,
|
||||
} from './mappers.js'
|
||||
import { reprocessKuaishouSendCodeWorkOrders } from './sync-work-orders-from-send-code.js'
|
||||
import { getWorkerProductMatchConfig } from './worker-product-match-config-service.js'
|
||||
|
||||
export async function reprocessAdminKuaishouSendCodeWorkOrders(payload: JsonObject = {}) {
|
||||
const limit = Math.min(500, normalizePositiveInteger(payload.limit, 100))
|
||||
return reprocessKuaishouSendCodeWorkOrders(limit)
|
||||
}
|
||||
|
||||
export async function listAdminWorkProductRuleMappings(query: JsonObject = {}) {
|
||||
const ruleId = normalizeOptionalId(query.ruleId ?? query.rule_id) || 0
|
||||
const hasPagination = query.page !== undefined || query.pageSize !== undefined || query.keyword
|
||||
if (hasPagination) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const keyword = String(query.keyword || '').trim()
|
||||
const result = await listWorkProductRuleMappingsPage({
|
||||
page,
|
||||
pageSize,
|
||||
keyword,
|
||||
ruleId,
|
||||
})
|
||||
return {
|
||||
items: result.items.map(mapWorkProductRuleMapping),
|
||||
pagination: { page, pageSize, total: result.total },
|
||||
}
|
||||
}
|
||||
const items = await listWorkProductRuleMappings({ ruleId })
|
||||
return { items: items.map(mapWorkProductRuleMapping) }
|
||||
}
|
||||
|
||||
export async function saveAdminWorkProductRuleMapping(payload: JsonObject = {}) {
|
||||
const ruleId = normalizeOptionalId(payload.ruleId ?? payload.rule_id)
|
||||
const sellerIds = normalizeKuaishouMatchIds(
|
||||
payload.sellerIds ?? payload.seller_ids ?? payload.sellerId ?? payload.seller_id,
|
||||
)
|
||||
const relItemIds = normalizeKuaishouMatchIdList(payload.relItemId ?? payload.rel_item_id)
|
||||
const relItemId = relItemIds.join(',')
|
||||
const itemTitle = String(payload.itemTitle ?? payload.item_title ?? '').trim()
|
||||
const rawMappingType = String(payload.mappingType ?? payload.mapping_type ?? '').trim()
|
||||
const mappingType =
|
||||
rawMappingType === 'sku_series'
|
||||
? 'sku_series'
|
||||
: rawMappingType === 'sku_exact' || rawMappingType === 'sku_override'
|
||||
? 'sku_exact'
|
||||
: 'product_default'
|
||||
const relSkuId =
|
||||
mappingType === 'sku_exact'
|
||||
? normalizeKuaishouMatchId(payload.relSkuId ?? payload.rel_sku_id)
|
||||
: ''
|
||||
const skuNick =
|
||||
mappingType === 'product_default'
|
||||
? ''
|
||||
: String(payload.skuNick ?? payload.sku_nick ?? '').trim()
|
||||
const skuMatchMode =
|
||||
mappingType === 'sku_series' &&
|
||||
String(payload.skuMatchMode ?? payload.sku_match_mode ?? '').trim() === 'exact'
|
||||
? 'exact'
|
||||
: 'fuzzy'
|
||||
const requiresProductScope = mappingType === 'product_default'
|
||||
if (
|
||||
!ruleId ||
|
||||
sellerIds.length === 0 ||
|
||||
(requiresProductScope && relItemIds.length === 0 && !itemTitle)
|
||||
) {
|
||||
throw createHttpError(
|
||||
'请选择接单模板,并填写至少一个店铺;商品默认规则还需填写大标题或关联商品 ID',
|
||||
{
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_mapping_identity_required',
|
||||
},
|
||||
)
|
||||
}
|
||||
if (
|
||||
mappingType === 'product_default' &&
|
||||
sellerIds.length > 1 &&
|
||||
(!itemTitle || relItemIds.length > 0)
|
||||
) {
|
||||
throw createHttpError('多店共享规则请填写快手大标题,并留空关联商品 ID', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_mapping_multi_shop_scope_invalid',
|
||||
})
|
||||
}
|
||||
if (mappingType === 'sku_exact' && !relSkuId && !skuNick) {
|
||||
throw createHttpError('SKU 精确规则需要关联 SKU ID 或具体 SKU 名称', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_mapping_sku_required',
|
||||
})
|
||||
}
|
||||
if (mappingType === 'sku_series' && !skuNick) {
|
||||
throw createHttpError('SKU 数量系列需要填写系列名称', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_mapping_series_required',
|
||||
})
|
||||
}
|
||||
const rule = (await listWorkProductRules()).find((item) => Number(item.id) === ruleId)
|
||||
if (!rule) {
|
||||
throw createHttpError('接单模板不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_product_mapping_rule_not_found',
|
||||
})
|
||||
}
|
||||
const mappingId = normalizeOptionalId(payload.mappingId ?? payload.mapping_id)
|
||||
if (normalizeBoolean(payload.enabled, true)) {
|
||||
await assertNoConflictingWorkProductRuleMapping({
|
||||
mappingId,
|
||||
sellerIds,
|
||||
relItemId,
|
||||
itemTitle,
|
||||
relSkuId,
|
||||
skuNick,
|
||||
mappingType,
|
||||
})
|
||||
}
|
||||
const mapping = await upsertWorkProductRuleMapping({
|
||||
mappingId,
|
||||
ruleId,
|
||||
sellerIds,
|
||||
relItemId,
|
||||
itemTitle,
|
||||
relSkuId,
|
||||
skuNick,
|
||||
skuMatchMode,
|
||||
mappingType,
|
||||
enabled: normalizeBoolean(payload.enabled, true),
|
||||
now: nowIso(),
|
||||
})
|
||||
if (!mapping) {
|
||||
throw createHttpError('商品映射保存失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'work_product_mapping_save_failed',
|
||||
})
|
||||
}
|
||||
return {
|
||||
mapping: mapWorkProductRuleMapping({
|
||||
...mapping,
|
||||
rule_key: rule.rule_key,
|
||||
product_name: rule.product_name,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAdminWorkProductRuleMapping(mappingId: number | string) {
|
||||
const normalizedMappingId = normalizeOptionalId(mappingId)
|
||||
if (!normalizedMappingId) {
|
||||
throw createHttpError('商品映射 ID 不正确', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_mapping_id_invalid',
|
||||
})
|
||||
}
|
||||
return deleteWorkProductRuleMapping(normalizedMappingId)
|
||||
}
|
||||
|
||||
export async function listAdminKuaishouMatchSources(payload: JsonObject = {}) {
|
||||
const limit = Math.min(500, normalizePositiveInteger(payload.limit, 100))
|
||||
const sources = await listKuaishouIndustryVoucherWorkOrderSyncSources(limit)
|
||||
const grouped = new Map<string, JsonObject>()
|
||||
for (const source of sources) {
|
||||
const rawPayload = safeParseJson(source.raw_payload_json)
|
||||
const body = resolveKuaishouMatchPayload(rawPayload)
|
||||
const ext = safeParseJson(body.ext)
|
||||
const sellerId = normalizeKuaishouMatchId(body.sellerId)
|
||||
const relItemId = normalizeKuaishouMatchId(ext.relItemId)
|
||||
const itemTitle = String(body.itemTitle || '').trim()
|
||||
const relSkuId = normalizeKuaishouMatchId(ext.relSkuId)
|
||||
const skuNick = String(ext.skuNick || '').trim()
|
||||
if (!sellerId || (!relItemId && !itemTitle)) continue
|
||||
const key = [
|
||||
sellerId,
|
||||
relItemId,
|
||||
normalizeMatchTextForAdmin(itemTitle),
|
||||
relSkuId,
|
||||
normalizeMatchTextForAdmin(skuNick),
|
||||
].join('|')
|
||||
const current = grouped.get(key)
|
||||
grouped.set(key, {
|
||||
sellerId,
|
||||
relItemId,
|
||||
itemTitle,
|
||||
relSkuId,
|
||||
skuNick,
|
||||
sampleOid: String(body.oid || source.oid || '').trim(),
|
||||
lastSeenAt: source.updated_at,
|
||||
seenCount: Number(current?.seenCount || 0) + 1,
|
||||
rawPayload,
|
||||
})
|
||||
}
|
||||
return {
|
||||
items: Array.from(grouped.values()).sort((left, right) =>
|
||||
String(right.lastSeenAt).localeCompare(String(left.lastSeenAt)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export async function testAdminKuaishouProductMatch(payload: JsonObject = {}) {
|
||||
const body = resolveKuaishouMatchPayload(payload.rawPayload ?? payload.raw_payload ?? payload)
|
||||
const ext = safeParseJson(body.ext)
|
||||
const order = {
|
||||
provider: '91kaquan',
|
||||
platform: 'kuaishou',
|
||||
shop_id: String(body.sellerId || '').trim(),
|
||||
} as OrderRow
|
||||
const item = {
|
||||
id: 0,
|
||||
order_id: 0,
|
||||
sku_code: String(body.itemId || '').trim(),
|
||||
sku_name: String(ext.skuNick || '').trim(),
|
||||
quantity: Math.max(1, Number(body.num) || 1),
|
||||
spec_json: {},
|
||||
item_snapshot_json: {
|
||||
kuaishouSendCode: {
|
||||
sellerId: String(body.sellerId || '').trim(),
|
||||
itemId: String(body.itemId || '').trim(),
|
||||
itemTitle: String(body.itemTitle || '').trim(),
|
||||
skuId: String(body.skuId || '').trim(),
|
||||
relItemId: normalizeKuaishouMatchId(ext.relItemId),
|
||||
relSkuId: normalizeKuaishouMatchId(ext.relSkuId),
|
||||
skuNick: String(ext.skuNick || '').trim(),
|
||||
},
|
||||
},
|
||||
} as OrderItemRow
|
||||
const [rules, mappings] = await Promise.all([
|
||||
listWorkProductRules({ enabled: true }),
|
||||
listWorkProductRuleMappings({ enabled: true }),
|
||||
])
|
||||
const decision = resolveMatchingProductRuleDecision(order, item, rules, mappings, {
|
||||
legacyFallbackEnabled: getWorkerProductMatchConfig().legacyFallbackEnabled,
|
||||
})
|
||||
const context = resolveKuaishouWorkProductMatchContext(item)
|
||||
return {
|
||||
context,
|
||||
status: decision.reason,
|
||||
mappingId: decision.mappingId,
|
||||
rule: decision.rule ? mapWorkProductRule(decision.rule) : null,
|
||||
candidates: decision.candidates,
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAdminWorkProductMatchLogs(payload: JsonObject = {}) {
|
||||
const limit = Math.min(500, normalizePositiveInteger(payload.limit, 100))
|
||||
return { items: (await listWorkProductMatchLogs(limit)).map(mapWorkProductMatchLog) }
|
||||
}
|
||||
|
||||
function mapWorkProductRuleMapping(mapping: WorkProductRuleMappingRow) {
|
||||
const sellerIds = parseKuaishouMappingSellerIds(mapping.seller_ids_json)
|
||||
return {
|
||||
mappingId: Number(mapping.id),
|
||||
ruleId: Number(mapping.rule_id),
|
||||
ruleKey: String(mapping.rule_key || ''),
|
||||
productName: String(mapping.product_name || ''),
|
||||
sellerIds,
|
||||
sellerId: sellerIds[0] || '',
|
||||
relItemId: String(mapping.rel_item_id || ''),
|
||||
itemTitle: String(mapping.item_title || ''),
|
||||
relSkuId: String(mapping.rel_sku_id || ''),
|
||||
skuNick: String(mapping.sku_nick || ''),
|
||||
skuMatchMode: mapping.sku_match_mode === 'exact' ? 'exact' : 'fuzzy',
|
||||
mappingType:
|
||||
mapping.mapping_type === 'sku_series'
|
||||
? 'sku_series'
|
||||
: mapping.mapping_type === 'sku_exact' || mapping.mapping_type === 'sku_override'
|
||||
? 'sku_exact'
|
||||
: 'product_default',
|
||||
enabled: mapping.enabled === true,
|
||||
createdAt: mapping.created_at,
|
||||
updatedAt: mapping.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
function mapWorkProductMatchLog(log: WorkProductMatchLogRow) {
|
||||
return {
|
||||
logId: Number(log.id),
|
||||
orderId: log.order_id ? Number(log.order_id) : null,
|
||||
orderItemId: log.order_item_id ? Number(log.order_item_id) : null,
|
||||
source: String(log.source || ''),
|
||||
sellerId: String(log.seller_id || ''),
|
||||
relItemId: String(log.rel_item_id || ''),
|
||||
itemTitle: String(log.item_title || ''),
|
||||
relSkuId: String(log.rel_sku_id || ''),
|
||||
skuNick: String(log.sku_nick || ''),
|
||||
status: String(log.match_status || ''),
|
||||
ruleId: log.rule_id ? Number(log.rule_id) : null,
|
||||
mappingId: log.mapping_id ? Number(log.mapping_id) : null,
|
||||
ruleKey: String(log.rule_key || ''),
|
||||
productName: String(log.product_name || ''),
|
||||
candidates: parseJsonArray(log.candidates_json),
|
||||
rawPayload: safeParseJson(log.raw_payload_json),
|
||||
createdAt: log.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKuaishouMatchPayload(value: unknown): JsonObject {
|
||||
const raw = safeParseJson(value)
|
||||
const body = safeParseJson(raw.body)
|
||||
return Object.keys(body).length > 0 ? body : raw
|
||||
}
|
||||
|
||||
function normalizeKuaishouMatchId(value: unknown) {
|
||||
const normalized = String(value || '').trim()
|
||||
return normalized === '0' ? '' : normalized
|
||||
}
|
||||
|
||||
function normalizeKuaishouMatchIds(value: unknown) {
|
||||
const values = Array.isArray(value)
|
||||
? value
|
||||
: String(value ?? '')
|
||||
.split(/[,,\n]/)
|
||||
.map((item) => item.trim())
|
||||
return Array.from(new Set(values.map(normalizeKuaishouMatchId).filter(Boolean)))
|
||||
}
|
||||
|
||||
function normalizeKuaishouMatchIdList(value: unknown) {
|
||||
const values = Array.isArray(value) ? value : [value]
|
||||
return normalizeKuaishouMatchIds(
|
||||
values.flatMap((item) => String(item ?? '').split(/[,,;;\s\n\r]+/)),
|
||||
)
|
||||
}
|
||||
|
||||
function parseKuaishouMappingSellerIds(value: unknown) {
|
||||
if (Array.isArray(value)) return normalizeKuaishouMatchIds(value)
|
||||
try {
|
||||
return normalizeKuaishouMatchIds(JSON.parse(String(value || '[]')))
|
||||
} catch {
|
||||
return normalizeKuaishouMatchIds(value)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMatchTextForAdmin(value: unknown) {
|
||||
return String(value || '')
|
||||
.normalize('NFKC')
|
||||
.toLowerCase()
|
||||
.replace(/[\s\u3000]+/g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function assertNoConflictingWorkProductRuleMapping(input: {
|
||||
mappingId: number | null
|
||||
sellerIds: string[]
|
||||
relItemId: string
|
||||
itemTitle: string
|
||||
relSkuId: string
|
||||
skuNick: string
|
||||
mappingType: 'product_default' | 'sku_exact' | 'sku_series'
|
||||
}) {
|
||||
return listWorkProductRuleMappings({ enabled: true }).then((mappings) => {
|
||||
const conflict = mappings.find((mapping) => {
|
||||
if (input.mappingId && Number(mapping.id) === input.mappingId) return false
|
||||
const existingType =
|
||||
mapping.mapping_type === 'sku_series'
|
||||
? 'sku_series'
|
||||
: mapping.mapping_type === 'sku_exact' || mapping.mapping_type === 'sku_override'
|
||||
? 'sku_exact'
|
||||
: 'product_default'
|
||||
if (existingType !== input.mappingType) return false
|
||||
const existingSellerIds = parseKuaishouMappingSellerIds(mapping.seller_ids_json)
|
||||
if (!existingSellerIds.some((sellerId) => input.sellerIds.includes(sellerId))) return false
|
||||
|
||||
const inputItemIds = normalizeKuaishouMatchIdList(input.relItemId)
|
||||
const existingItemIds = normalizeKuaishouMatchIdList(mapping.rel_item_id)
|
||||
if (
|
||||
!mappingProductScopesOverlap(
|
||||
inputItemIds,
|
||||
input.itemTitle,
|
||||
existingItemIds,
|
||||
mapping.item_title,
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (input.mappingType === 'product_default') return true
|
||||
if (input.mappingType === 'sku_series') {
|
||||
// 精确系列是模糊系列的子集,两者并存仍可能产生同分冲突,因此统一拦截。
|
||||
return (
|
||||
normalizeMatchTextForAdmin(input.skuNick) === normalizeMatchTextForAdmin(mapping.sku_nick)
|
||||
)
|
||||
}
|
||||
|
||||
const inputSkuId = normalizeKuaishouMatchId(input.relSkuId)
|
||||
const existingSkuId = normalizeKuaishouMatchId(mapping.rel_sku_id)
|
||||
const skuIdConflict = Boolean(inputSkuId && existingSkuId && inputSkuId === existingSkuId)
|
||||
const skuNameConflict =
|
||||
Boolean(input.skuNick && mapping.sku_nick) &&
|
||||
normalizeMatchTextForAdmin(input.skuNick) === normalizeMatchTextForAdmin(mapping.sku_nick)
|
||||
return skuIdConflict || skuNameConflict
|
||||
})
|
||||
|
||||
if (conflict) {
|
||||
throw createHttpError('商品映射与已有启用映射冲突,请停用或调整其中一条', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_product_mapping_conflict',
|
||||
context: {
|
||||
mappingId: Number(conflict.id),
|
||||
ruleKey: String(conflict.rule_key || ''),
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function mappingProductScopesOverlap(
|
||||
leftItemIds: string[],
|
||||
leftTitle: string,
|
||||
rightItemIds: string[],
|
||||
rightTitle: string,
|
||||
) {
|
||||
if (leftItemIds.length > 0 && rightItemIds.length > 0) {
|
||||
return leftItemIds.some((itemId) => rightItemIds.includes(itemId))
|
||||
}
|
||||
// 只有一侧指定商品 ID 时,另一侧可能是全商品范围,按可能重叠处理并拦截。
|
||||
if (leftItemIds.length > 0 || rightItemIds.length > 0) return true
|
||||
const normalizedLeftTitle = normalizeMatchTextForAdmin(leftTitle)
|
||||
const normalizedRightTitle = normalizeMatchTextForAdmin(rightTitle)
|
||||
if (normalizedLeftTitle || normalizedRightTitle) {
|
||||
return Boolean(
|
||||
normalizedLeftTitle && normalizedRightTitle && normalizedLeftTitle === normalizedRightTitle,
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function parseJsonArray(value: unknown): unknown[] {
|
||||
if (Array.isArray(value)) return value
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '[]'))
|
||||
return Array.isArray(parsed) ? parsed : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,474 @@
|
||||
import {
|
||||
countWorkCategoryUsages,
|
||||
deleteWorkCategory,
|
||||
getWorkCategoryById,
|
||||
listAllWorkCategories,
|
||||
upsertWorkCategory,
|
||||
} from '../../repositories/worker-platform/work-category-repo.js'
|
||||
import {
|
||||
countWorkerLevelUsages,
|
||||
deleteWorkerLevel,
|
||||
getWorkerLevelById,
|
||||
getWorkerLevelByKey,
|
||||
listWorkerLevels,
|
||||
upsertWorkerLevel,
|
||||
} from '../../repositories/worker-platform/worker-repo.js'
|
||||
import {
|
||||
countWorkProductRulesByCategory,
|
||||
deleteWorkProductRule,
|
||||
getWorkProductRuleById,
|
||||
listOpenWorkOrdersByProductRuleId,
|
||||
listWorkProductRulesPage,
|
||||
syncOpenWorkOrderPriceFromRule,
|
||||
upsertWorkProductRule,
|
||||
} from '../../repositories/worker-platform/work-product-rule-repo.js'
|
||||
import type { WorkOrderRow, WorkProductRuleRow } from '../../repositories/worker-platform/types.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { publishWorkOrderRealtimeChange } from '../realtime/realtime-event-service.js'
|
||||
import {
|
||||
DEFAULT_CATEGORY_KEY,
|
||||
DEFAULT_LEVEL_KEY,
|
||||
DEFAULT_LEVEL_NAME,
|
||||
mapWorkCategory,
|
||||
mapWorkProductRule,
|
||||
mapWorkerLevel,
|
||||
normalizeAmountFen,
|
||||
normalizeBoolean,
|
||||
normalizeEnabledStatus,
|
||||
normalizeInteger,
|
||||
normalizeMatchType,
|
||||
normalizeOptionalId,
|
||||
normalizePositiveInteger,
|
||||
normalizeRequirementFieldsFromPayload,
|
||||
normalizeSlugKey,
|
||||
normalizeWorkProductRuleMatch,
|
||||
resolveSkuNameQuantity,
|
||||
resolveWorkOrderRulePricing,
|
||||
} from './mappers.js'
|
||||
import { ensureWorkerPlatformDefaults, normalizeWorkOrderTimeoutPolicy } from './worker-service.js'
|
||||
|
||||
export async function listAdminWorkerLevels() {
|
||||
await ensureWorkerPlatformDefaults()
|
||||
return { items: (await listWorkerLevels()).map(mapWorkerLevel) }
|
||||
}
|
||||
|
||||
export async function listAdminWorkCategories() {
|
||||
await ensureWorkerPlatformDefaults()
|
||||
return { items: (await listAllWorkCategories()).map(mapWorkCategory) }
|
||||
}
|
||||
|
||||
export async function saveAdminWorkCategory(payload: JsonObject = {}) {
|
||||
const now = nowIso()
|
||||
const categoryKey = normalizeSlugKey(
|
||||
payload.categoryKey || payload.category_key || payload.name,
|
||||
DEFAULT_CATEGORY_KEY,
|
||||
)
|
||||
const name = String(payload.name || '').trim() || '默认分类'
|
||||
const category = await upsertWorkCategory({
|
||||
categoryKey,
|
||||
name,
|
||||
sortOrder: normalizeInteger(payload.sortOrder, 100),
|
||||
status: normalizeEnabledStatus(payload.status),
|
||||
now,
|
||||
})
|
||||
return { category: mapWorkCategory(category) }
|
||||
}
|
||||
|
||||
export async function deleteAdminWorkCategory(categoryId: number | string) {
|
||||
const category = await getWorkCategoryById(Number(categoryId))
|
||||
if (!category) {
|
||||
throw createHttpError('分类不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_category_not_found',
|
||||
})
|
||||
}
|
||||
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',
|
||||
},
|
||||
)
|
||||
}
|
||||
const { deleted } = await deleteWorkCategory(Number(categoryId))
|
||||
return { deleted }
|
||||
}
|
||||
|
||||
export async function saveAdminWorkerLevel(payload: JsonObject = {}) {
|
||||
const now = nowIso()
|
||||
const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY
|
||||
const existingLevel = await getWorkerLevelByKey(levelKey)
|
||||
const existingPermissions = safeParseJson(existingLevel?.permission_json)
|
||||
const name = String(payload.name || '').trim() || DEFAULT_LEVEL_NAME
|
||||
const permissions = {
|
||||
depositFreeAmount: normalizeAmountFen(
|
||||
payload.depositFreeAmount ?? payload.depositFreeAmountYuan,
|
||||
0,
|
||||
),
|
||||
maxActiveOrders: normalizePositiveInteger(payload.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(payload.upgradeThreshold, 0),
|
||||
autoAcceptWithoutEvidence: normalizeBoolean(
|
||||
payload.autoAcceptWithoutEvidence,
|
||||
normalizeBoolean(existingPermissions.autoAcceptWithoutEvidence, levelKey === 'vip5'),
|
||||
),
|
||||
visibleDelaySeconds: normalizeInteger(payload.visibleDelaySeconds, -1),
|
||||
}
|
||||
const level = await upsertWorkerLevel({
|
||||
levelKey,
|
||||
name,
|
||||
sortOrder: normalizeInteger(payload.sortOrder, Number(existingLevel?.sort_order || 100)),
|
||||
status: String(payload.status || 'active').trim() === 'disabled' ? 'disabled' : 'active',
|
||||
permissionJson: JSON.stringify(permissions),
|
||||
now,
|
||||
})
|
||||
return { level: mapWorkerLevel(level) }
|
||||
}
|
||||
|
||||
export async function deleteAdminWorkerLevel(levelId: number | string) {
|
||||
const level = await getWorkerLevelById(Number(levelId))
|
||||
if (!level) {
|
||||
throw createHttpError('等级不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_level_not_found',
|
||||
})
|
||||
}
|
||||
if (level.level_key === DEFAULT_LEVEL_KEY) {
|
||||
throw createHttpError('默认等级(VIP1)不可删除', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_level_default_protected',
|
||||
})
|
||||
}
|
||||
const usageCount = await countWorkerLevelUsages(Number(levelId))
|
||||
if (usageCount > 0) {
|
||||
throw createHttpError(`该等级正在被 ${usageCount} 名打手使用,请先调整打手等级后再删除`, {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_level_in_use',
|
||||
})
|
||||
}
|
||||
const { deleted } = await deleteWorkerLevel(Number(levelId))
|
||||
return { deleted }
|
||||
}
|
||||
|
||||
export async function listAdminWorkProductRules(query: JsonObject = {}) {
|
||||
await ensureWorkerPlatformDefaults()
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const enabledValue = String(query.enabled ?? '').trim()
|
||||
const enabled = enabledValue
|
||||
? ['true', '1', 'enabled', 'active'].includes(enabledValue.toLowerCase())
|
||||
: null
|
||||
const keyword = String(query.keyword || '').trim()
|
||||
const categoryId = normalizeOptionalId(query.categoryId ?? query.category_id) || 0
|
||||
const [{ items, total }, categoryRows] = await Promise.all([
|
||||
listWorkProductRulesPage({
|
||||
page,
|
||||
pageSize,
|
||||
enabled,
|
||||
keyword,
|
||||
categoryId,
|
||||
}),
|
||||
countWorkProductRulesByCategory({ enabled, keyword }),
|
||||
])
|
||||
return {
|
||||
items: items.map(mapWorkProductRule),
|
||||
pagination: { page, pageSize, total },
|
||||
categoryCounts: categoryRows.map((row) => ({
|
||||
categoryId: row.categoryId,
|
||||
total: row.total,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveSharingQuantity(
|
||||
totalAmount: number,
|
||||
fallbackQuantity: number,
|
||||
unitReward: number,
|
||||
): number {
|
||||
if (totalAmount > 0 && unitReward > 0) {
|
||||
return Math.max(1, Math.round(totalAmount / unitReward))
|
||||
}
|
||||
return Math.max(1, fallbackQuantity)
|
||||
}
|
||||
|
||||
function hasConfiguredSharingValue(value: unknown) {
|
||||
return value !== undefined && value !== null && String(value).trim() !== ''
|
||||
}
|
||||
|
||||
function hasOwnPayload(payload: JsonObject, key: string) {
|
||||
return Object.prototype.hasOwnProperty.call(payload, key)
|
||||
}
|
||||
|
||||
export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
|
||||
const defaults = await ensureWorkerPlatformDefaults()
|
||||
const ruleId = normalizeOptionalId(payload.ruleId ?? payload.rule_id)
|
||||
const existingRule = ruleId ? await getWorkProductRuleById(ruleId) : null
|
||||
if (ruleId && !existingRule) {
|
||||
throw createHttpError('接单模板不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_product_rule_not_found',
|
||||
})
|
||||
}
|
||||
const hasLegacyMatchPayload = [
|
||||
'match',
|
||||
'sellerId',
|
||||
'seller_id',
|
||||
'itemId',
|
||||
'item_id',
|
||||
'relItemId',
|
||||
'rel_item_id',
|
||||
'skuId',
|
||||
'sku_id',
|
||||
'relSkuId',
|
||||
'rel_sku_id',
|
||||
'itemTitle',
|
||||
'item_title',
|
||||
'skuNick',
|
||||
'sku_nick',
|
||||
'itemTitleMatchType',
|
||||
'item_title_match_type',
|
||||
'skuNickMatchType',
|
||||
'sku_nick_match_type',
|
||||
].some((key) => Object.prototype.hasOwnProperty.call(payload, key))
|
||||
const match = hasLegacyMatchPayload
|
||||
? normalizeWorkProductRuleMatch(payload)
|
||||
: safeParseJson(existingRule?.match_json)
|
||||
const productName = String(
|
||||
payload.productName || payload.product_name || payload.skuName || '',
|
||||
).trim()
|
||||
const skuCode = String(payload.skuCode || payload.sku_code || '').trim()
|
||||
const matchedProductName = String(match.itemTitle || match.skuNick || '').trim()
|
||||
if (!productName && !skuCode && !matchedProductName) {
|
||||
throw createHttpError('请填写商品名或 SKU', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_rule_target_required',
|
||||
})
|
||||
}
|
||||
|
||||
const rewardAmount = normalizeAmountFen(payload.rewardAmount ?? payload.rewardAmountYuan, 0)
|
||||
const unitPriceFen =
|
||||
payload.unitPrice === undefined && payload.unitPriceYuan === undefined
|
||||
? 0
|
||||
: normalizeAmountFen(payload.unitPrice ?? payload.unitPriceYuan, 0)
|
||||
const sharingEnabled = normalizeBoolean(payload.sharingEnabled ?? payload.sharing_enabled, false)
|
||||
const hasManualSharingConfig =
|
||||
hasConfiguredSharingValue(payload.sharingTotalQuantity ?? payload.sharing_total_quantity) ||
|
||||
hasConfiguredSharingValue(payload.sharingUnitReward ?? payload.sharingUnitRewardYuan) ||
|
||||
hasConfiguredSharingValue(payload.sharingTotalAmount ?? payload.sharingTotalAmountYuan)
|
||||
const sharingAutoFromOrder =
|
||||
sharingEnabled &&
|
||||
unitPriceFen > 0 &&
|
||||
(normalizeBoolean(payload.sharingAutoFromOrder ?? payload.sharing_auto_from_order, false) ||
|
||||
!hasManualSharingConfig)
|
||||
const hasManualSharingQuantity = hasConfiguredSharingValue(
|
||||
payload.sharingTotalQuantity ?? payload.sharing_total_quantity,
|
||||
)
|
||||
const hasManualSharingUnitReward = hasConfiguredSharingValue(
|
||||
payload.sharingUnitReward ?? payload.sharingUnitRewardYuan,
|
||||
)
|
||||
if (
|
||||
sharingEnabled &&
|
||||
unitPriceFen > 0 &&
|
||||
!sharingAutoFromOrder &&
|
||||
hasManualSharingQuantity !== hasManualSharingUnitReward
|
||||
) {
|
||||
throw createHttpError('按件计价的拼单覆盖值请同时填写拼单数量和拼单单价', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_rule_sharing_override_incomplete',
|
||||
})
|
||||
}
|
||||
const sharingUnitReward = normalizeAmountFen(
|
||||
payload.sharingUnitReward ?? payload.sharingUnitRewardYuan,
|
||||
0,
|
||||
)
|
||||
const sharingTotalAmount = normalizeAmountFen(
|
||||
payload.sharingTotalAmount ?? payload.sharingTotalAmountYuan,
|
||||
0,
|
||||
)
|
||||
const sharingTotalQuantity = resolveSharingQuantity(
|
||||
sharingTotalAmount,
|
||||
normalizePositiveInteger(payload.sharingTotalQuantity ?? payload.sharing_total_quantity, 1),
|
||||
sharingUnitReward,
|
||||
)
|
||||
const resolvedSharingTotalAmount =
|
||||
sharingTotalAmount > 0 ? sharingTotalAmount : sharingUnitReward * sharingTotalQuantity
|
||||
if (sharingEnabled && !sharingAutoFromOrder) {
|
||||
if (resolvedSharingTotalAmount <= 0) {
|
||||
throw createHttpError('启用拼单时请填写拼单总价或单价', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_rule_sharing_amount_invalid',
|
||||
})
|
||||
}
|
||||
}
|
||||
const finalRewardAmount = sharingEnabled
|
||||
? sharingAutoFromOrder
|
||||
? 0
|
||||
: resolvedSharingTotalAmount
|
||||
: rewardAmount
|
||||
const fields = normalizeRequirementFieldsFromPayload(payload)
|
||||
const ruleKey = existingRule?.rule_key || randomId('rule-')
|
||||
const rule = await upsertWorkProductRule({
|
||||
ruleKey,
|
||||
provider: hasOwnPayload(payload, 'provider')
|
||||
? String(payload.provider || '').trim()
|
||||
: String(existingRule?.provider || '').trim(),
|
||||
platform: hasOwnPayload(payload, 'platform')
|
||||
? String(payload.platform || '').trim()
|
||||
: String(existingRule?.platform || '').trim(),
|
||||
shopId:
|
||||
hasOwnPayload(payload, 'shopId') || hasOwnPayload(payload, 'shop_id')
|
||||
? String(payload.shopId || payload.shop_id || '').trim()
|
||||
: String(existingRule?.shop_id || '').trim(),
|
||||
skuCode:
|
||||
hasOwnPayload(payload, 'skuCode') || hasOwnPayload(payload, 'sku_code')
|
||||
? skuCode
|
||||
: String(existingRule?.sku_code || '').trim(),
|
||||
productName: productName || matchedProductName,
|
||||
matchType:
|
||||
hasOwnPayload(payload, 'matchType') || hasOwnPayload(payload, 'match_type')
|
||||
? normalizeMatchType(payload.matchType || payload.match_type)
|
||||
: normalizeMatchType(existingRule?.match_type),
|
||||
categoryId:
|
||||
normalizeOptionalId(payload.categoryId || payload.category_id) ||
|
||||
defaults.category?.id ||
|
||||
null,
|
||||
enabled: normalizeBoolean(payload.enabled, true),
|
||||
autoCreate: normalizeBoolean(payload.autoCreate ?? payload.auto_create, true),
|
||||
rewardAmount: finalRewardAmount,
|
||||
unitPriceFen,
|
||||
// 商品规则不配置押金;工单创建后由运营按需手工填写。
|
||||
requiredDepositAmount: 0,
|
||||
depositThresholdAmount: 0,
|
||||
sharingEnabled,
|
||||
sharingTotalQuantity,
|
||||
sharingUnitReward,
|
||||
sharingAutoFromOrder,
|
||||
timeoutMinutes: Math.max(0, normalizeInteger(payload.timeoutMinutes, 0)),
|
||||
timeoutPolicy: normalizeWorkOrderTimeoutPolicy(payload.timeoutPolicy),
|
||||
requirementJson: JSON.stringify({ fields }),
|
||||
matchJson: JSON.stringify(match),
|
||||
sortOrder: normalizeInteger(payload.sortOrder, 100),
|
||||
now: nowIso(),
|
||||
})
|
||||
return { rule: mapWorkProductRule(rule) }
|
||||
}
|
||||
|
||||
/** 根据工单创建时的规格,计算当前模板可同步的价格。 */
|
||||
export function resolveWorkOrderTemplateSyncPrice(input: {
|
||||
rule: WorkProductRuleRow
|
||||
workOrder: WorkOrderRow
|
||||
}): { rewardAmount: number; sharingUnitReward?: number } | null {
|
||||
const material = safeParseJson(input.workOrder.material_json)
|
||||
const source = safeParseJson(material.source)
|
||||
const sourceSkuQuantity = Number(source.skuQuantity || 0)
|
||||
const skuQuantity =
|
||||
Number.isSafeInteger(sourceSkuQuantity) && sourceSkuQuantity > 0
|
||||
? sourceSkuQuantity
|
||||
: resolveSkuNameQuantity(input.workOrder.product_name)
|
||||
const pricing = resolveWorkOrderRulePricing({
|
||||
skuQuantity,
|
||||
unitPriceFen: Number(input.rule.unit_price_fen || 0),
|
||||
fixedRewardAmount: Number(input.rule.reward_amount || 0),
|
||||
sharingEnabled: input.rule.sharing_enabled === true,
|
||||
sharingAutoFromOrder: input.rule.sharing_auto_from_order === true,
|
||||
sharingTotalQuantity: Number(input.rule.sharing_total_quantity || 1),
|
||||
sharingUnitReward: Number(input.rule.sharing_unit_reward || 0),
|
||||
})
|
||||
|
||||
if (input.workOrder.sharing_enabled === true) {
|
||||
if (input.rule.sharing_enabled !== true || pricing.sharingUnitReward <= 0) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
rewardAmount: pricing.rewardAmount,
|
||||
sharingUnitReward: pricing.sharingUnitReward,
|
||||
}
|
||||
}
|
||||
return pricing.rewardAmount > 0 ? { rewardAmount: pricing.rewardAmount } : null
|
||||
}
|
||||
|
||||
/** 将指定模板的当前价格同步到仍在接单大厅的来源工单。 */
|
||||
export async function syncAdminWorkProductRulePrice(ruleId: number | string, actorName = '') {
|
||||
const normalizedRuleId = normalizeOptionalId(ruleId)
|
||||
const rule = normalizedRuleId ? await getWorkProductRuleById(normalizedRuleId) : null
|
||||
if (!rule) {
|
||||
throw createHttpError('接单模板不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_product_rule_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
const candidates = await listOpenWorkOrdersByProductRuleId(rule.id)
|
||||
const result = {
|
||||
candidateCount: candidates.length,
|
||||
updatedCount: 0,
|
||||
updatedNormalCount: 0,
|
||||
updatedPartialSharingCount: 0,
|
||||
skippedInvalidPricingCount: 0,
|
||||
skippedUnchangedCount: 0,
|
||||
skippedUnavailableCount: 0,
|
||||
}
|
||||
const now = nowIso()
|
||||
|
||||
for (const workOrder of candidates) {
|
||||
const price = resolveWorkOrderTemplateSyncPrice({ rule, workOrder })
|
||||
if (!price) {
|
||||
result.skippedInvalidPricingCount += 1
|
||||
continue
|
||||
}
|
||||
const synced = await syncOpenWorkOrderPriceFromRule({
|
||||
workOrderId: Number(workOrder.id),
|
||||
productRuleId: Number(rule.id),
|
||||
rewardAmount: price.rewardAmount,
|
||||
...(price.sharingUnitReward === undefined
|
||||
? {}
|
||||
: { sharingUnitReward: price.sharingUnitReward }),
|
||||
actorName,
|
||||
now,
|
||||
})
|
||||
if (!synced.updated) {
|
||||
if (synced.reason === 'price_unchanged') {
|
||||
result.skippedUnchangedCount += 1
|
||||
} else {
|
||||
result.skippedUnavailableCount += 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
result.updatedCount += 1
|
||||
if (synced.sharingPartial) {
|
||||
result.updatedPartialSharingCount += 1
|
||||
} else {
|
||||
result.updatedNormalCount += 1
|
||||
}
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: synced.workOrderId,
|
||||
workerIds: synced.workerIds,
|
||||
hallChanged: true,
|
||||
})
|
||||
}
|
||||
|
||||
return { rule: mapWorkProductRule(rule), ...result }
|
||||
}
|
||||
|
||||
export async function deleteAdminWorkProductRule(ruleId: number | string) {
|
||||
const normalizedRuleId = normalizeOptionalId(ruleId)
|
||||
if (!normalizedRuleId) {
|
||||
throw createHttpError('物品规则 ID 不正确', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_product_rule_id_invalid',
|
||||
})
|
||||
}
|
||||
const { deleted } = await deleteWorkProductRule(normalizedRuleId)
|
||||
if (!deleted) {
|
||||
throw createHttpError('物品规则不存在或已删除', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_product_rule_not_found',
|
||||
})
|
||||
}
|
||||
return { deleted }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import {
|
||||
getWorkerAnnouncementConfig,
|
||||
saveWorkerAnnouncementConfig,
|
||||
} from './worker-announcement-config-service.js'
|
||||
import {
|
||||
getWorkerPlatformNotificationConfig,
|
||||
saveWorkerPlatformNotificationConfig,
|
||||
} from './worker-platform-notification-config-service.js'
|
||||
import {
|
||||
getWorkerProductMatchConfig,
|
||||
saveWorkerProductMatchConfig,
|
||||
} from './worker-product-match-config-service.js'
|
||||
|
||||
export async function getAdminWorkerPlatformNotificationConfig() {
|
||||
return getWorkerPlatformNotificationConfig()
|
||||
}
|
||||
|
||||
export async function saveAdminWorkerPlatformNotificationConfig(payload: JsonObject = {}) {
|
||||
return saveWorkerPlatformNotificationConfig(payload)
|
||||
}
|
||||
|
||||
export async function getAdminWorkerProductMatchConfig() {
|
||||
return getWorkerProductMatchConfig()
|
||||
}
|
||||
|
||||
export async function getAdminWorkerAnnouncementConfig() {
|
||||
return getWorkerAnnouncementConfig()
|
||||
}
|
||||
|
||||
export async function saveAdminWorkerAnnouncementConfig(payload: JsonObject = {}) {
|
||||
return saveWorkerAnnouncementConfig(payload)
|
||||
}
|
||||
|
||||
export async function saveAdminWorkerProductMatchConfig(payload: JsonObject = {}) {
|
||||
return saveWorkerProductMatchConfig(payload)
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
import {
|
||||
addWorkerWalletCredit,
|
||||
getWorkerFinanceRequestById,
|
||||
listWorkerFinanceRequests,
|
||||
listWorkerWithdrawalAccounts,
|
||||
reviewWorkerFinanceRequest,
|
||||
upsertWorkerWithdrawalAccount,
|
||||
} from '../../repositories/worker-platform/worker-repo.js'
|
||||
import type { WorkerWithdrawalAccountRow } from '../../repositories/worker-platform/types.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { resolveAdminNotificationEntity } from '../admin/admin-notification-service.js'
|
||||
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||
import {
|
||||
publishWorkerFinanceRealtimeChange,
|
||||
publishWorkerWalletRealtimeChange,
|
||||
} from '../realtime/realtime-event-service.js'
|
||||
import {
|
||||
mapFinanceRequest,
|
||||
mapWallet,
|
||||
mapWorkerUser,
|
||||
normalizeAdminFinanceReviewStatus,
|
||||
normalizeAmountFen,
|
||||
normalizeFinanceRequestStatus,
|
||||
normalizeFinanceRequestType,
|
||||
normalizeOptionalId,
|
||||
normalizeProofFiles,
|
||||
normalizeWithdrawChannel,
|
||||
} from './mappers.js'
|
||||
import { getRequiredWorker } from './worker-service.js'
|
||||
import { getWorkerFinanceConfig, saveWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||
|
||||
function mapAdminWorkerWithdrawalAccount(account: WorkerWithdrawalAccountRow) {
|
||||
const alipayQrCodeImage = refreshUploadedFileUrls(safeParseJson(account.alipay_qr_code_json))
|
||||
const wechatQrCodeImage = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json))
|
||||
return {
|
||||
accountChannel: account.account_channel,
|
||||
accountName: account.account_name,
|
||||
accountNo: account.account_channel === 'alipay' ? account.account_no : '',
|
||||
alipayQrCodeImage: String(alipayQrCodeImage.url || '').trim() ? alipayQrCodeImage : null,
|
||||
wechatQrCodeImage: String(wechatQrCodeImage.url || '').trim() ? wechatQrCodeImage : null,
|
||||
createdAt: account.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
export async function creditAdminWorkerWallet(workerId: number | string, payload: JsonObject = {}) {
|
||||
await getRequiredWorker(workerId)
|
||||
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
|
||||
if (amount <= 0) {
|
||||
throw createHttpError('充值金额必须大于 0', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_credit_amount_invalid',
|
||||
})
|
||||
}
|
||||
const wallet = await addWorkerWalletCredit({
|
||||
workerId: Number(workerId),
|
||||
amount,
|
||||
note: String(payload.note || '后台人工充值').trim(),
|
||||
payloadJson: JSON.stringify({ source: 'admin_manual_credit' }),
|
||||
now: nowIso(),
|
||||
})
|
||||
publishWorkerWalletRealtimeChange(Number(workerId))
|
||||
return { wallet: mapWallet(wallet) }
|
||||
}
|
||||
|
||||
export async function listAdminWorkerWithdrawalAccounts(workerId: number | string) {
|
||||
const worker = await getRequiredWorker(workerId)
|
||||
const items = await listWorkerWithdrawalAccounts(worker.id)
|
||||
return {
|
||||
worker: mapWorkerUser(worker),
|
||||
items: items.map(mapAdminWorkerWithdrawalAccount),
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveAdminWorkerWithdrawalAccount(
|
||||
workerId: number | string,
|
||||
accountChannelValue: unknown,
|
||||
payload: JsonObject = {},
|
||||
) {
|
||||
const worker = await getRequiredWorker(workerId)
|
||||
const accountChannel = normalizeWithdrawChannel(accountChannelValue)
|
||||
const currentAccounts = await listWorkerWithdrawalAccounts(worker.id)
|
||||
const current = currentAccounts.find((item) => item.account_channel === accountChannel)
|
||||
const accountName = String(payload.accountName || payload.realName || '').trim()
|
||||
const accountNo =
|
||||
accountChannel === 'alipay'
|
||||
? String(payload.accountNo || payload.account || current?.account_no || '').trim()
|
||||
: ''
|
||||
const uploadedAlipayQrCode = normalizeProofFiles(
|
||||
payload.alipayQrCodeImages || payload.alipayQrCodes,
|
||||
)[0]
|
||||
const alipayQrCodeImage =
|
||||
accountChannel === 'alipay'
|
||||
? uploadedAlipayQrCode || safeParseJson(current?.alipay_qr_code_json)
|
||||
: {}
|
||||
const uploadedWechatQrCode = normalizeProofFiles(
|
||||
payload.wechatQrCodeImages || payload.wechatQrCodes,
|
||||
)[0]
|
||||
const wechatQrCodeImage =
|
||||
accountChannel === 'wechat'
|
||||
? uploadedWechatQrCode || safeParseJson(current?.wechat_qr_code_json)
|
||||
: {}
|
||||
|
||||
if (!accountName) {
|
||||
throw createHttpError('请填写收款人姓名', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_worker_withdraw_account_name_required',
|
||||
})
|
||||
}
|
||||
if (accountChannel === 'alipay' && !current && !accountNo) {
|
||||
throw createHttpError('请填写支付宝账号', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_worker_withdraw_alipay_account_required',
|
||||
})
|
||||
}
|
||||
if (accountChannel === 'alipay' && !String(alipayQrCodeImage.url || '').trim()) {
|
||||
throw createHttpError('请上传支付宝收款二维码', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_worker_withdraw_alipay_qr_required',
|
||||
})
|
||||
}
|
||||
if (accountChannel === 'wechat' && !String(wechatQrCodeImage.url || '').trim()) {
|
||||
throw createHttpError('请上传微信收款二维码', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_worker_withdraw_wechat_qr_required',
|
||||
})
|
||||
}
|
||||
|
||||
const saved = await upsertWorkerWithdrawalAccount({
|
||||
workerId: worker.id,
|
||||
accountChannel,
|
||||
accountName,
|
||||
accountNo,
|
||||
alipayQrCodeJson: JSON.stringify(alipayQrCodeImage),
|
||||
wechatQrCodeJson: JSON.stringify(wechatQrCodeImage),
|
||||
now: nowIso(),
|
||||
})
|
||||
if (!saved) {
|
||||
throw createHttpError('保存打手提现信息失败', {
|
||||
statusCode: 500,
|
||||
errorCode: 'admin_worker_withdraw_account_save_failed',
|
||||
})
|
||||
}
|
||||
return { account: mapAdminWorkerWithdrawalAccount(saved) }
|
||||
}
|
||||
|
||||
export async function getAdminWorkerFinanceConfig() {
|
||||
return getWorkerFinanceConfig()
|
||||
}
|
||||
|
||||
export async function saveAdminWorkerFinanceConfig(payload: JsonObject = {}) {
|
||||
const config = await saveWorkerFinanceConfig(payload)
|
||||
return config
|
||||
}
|
||||
|
||||
export async function listAdminWorkerFinanceRequests(query: JsonObject = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const requestId = normalizeOptionalId(query.requestId ?? query.request_id) || 0
|
||||
const status = normalizeFinanceRequestStatus(query.status)
|
||||
const requestType = normalizeFinanceRequestType(query.requestType)
|
||||
const keyword = String(query.keyword || '').trim()
|
||||
const { items, total } = await listWorkerFinanceRequests({
|
||||
page,
|
||||
pageSize,
|
||||
requestId,
|
||||
status,
|
||||
requestType,
|
||||
keyword,
|
||||
})
|
||||
return {
|
||||
items: items.map(mapFinanceRequest),
|
||||
pagination: { page, pageSize, total },
|
||||
}
|
||||
}
|
||||
|
||||
export async function reviewAdminWorkerFinanceRequest(
|
||||
requestId: number | string,
|
||||
payload: JsonObject = {},
|
||||
) {
|
||||
const current = await getWorkerFinanceRequestById(requestId)
|
||||
if (!current) {
|
||||
throw createHttpError('资金申请不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_finance_request_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
const status = normalizeAdminFinanceReviewStatus(payload.status)
|
||||
const reviewedNote = String(payload.reviewedNote || payload.note || '').trim()
|
||||
const reviewed = await reviewWorkerFinanceRequest({
|
||||
requestId: Number(current.id),
|
||||
status,
|
||||
reviewedNote,
|
||||
now: nowIso(),
|
||||
})
|
||||
|
||||
if (reviewed.failureReason === 'request_not_pending') {
|
||||
throw createHttpError('该申请已处理,请刷新后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_finance_request_reviewed',
|
||||
})
|
||||
}
|
||||
|
||||
if (reviewed.failureReason === 'withdraw_insufficient') {
|
||||
throw createHttpError('打手余额不足,暂时无法通过该提现申请', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_finance_request_withdraw_insufficient',
|
||||
})
|
||||
}
|
||||
|
||||
if (!reviewed.request) {
|
||||
throw createHttpError('资金申请不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_finance_request_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
await resolveAdminNotificationEntity(
|
||||
'worker_finance_request',
|
||||
Number(reviewed.request.id),
|
||||
'finance_request_reviewed',
|
||||
)
|
||||
publishWorkerFinanceRealtimeChange({
|
||||
requestId: Number(reviewed.request.id),
|
||||
workerId: Number(reviewed.request.worker_id),
|
||||
walletChanged: reviewed.request.request_type === 'withdraw' && status === 'approved',
|
||||
})
|
||||
|
||||
return {
|
||||
request: mapFinanceRequest(reviewed.request),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import {
|
||||
createAfterSalesCase,
|
||||
getAfterSalesCaseById,
|
||||
listAfterSalesCaseEvents,
|
||||
listAfterSalesCases,
|
||||
listWorkerAfterSalesCases,
|
||||
resolveAfterSalesCase,
|
||||
submitWorkerAfterSalesResponse,
|
||||
} from '../../repositories/worker-platform/after-sales-repo.js'
|
||||
import { createWorkOrderEvent } from '../../repositories/worker-platform/work-order-event-repo.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import {
|
||||
publishWorkerWalletRealtimeChange,
|
||||
publishWorkOrderRealtimeChange,
|
||||
} from '../realtime/realtime-event-service.js'
|
||||
import {
|
||||
mapWorkerAfterSalesCase,
|
||||
normalizeAmountFen,
|
||||
normalizeOptionalId,
|
||||
normalizeProofFiles,
|
||||
normalizeUploadedFiles,
|
||||
} from './mappers.js'
|
||||
import { requireActiveWorkerSession, type WorkerSession } from './worker-service.js'
|
||||
|
||||
export async function listAdminAfterSalesCases(query: JsonObject = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const workOrderId = normalizeOptionalId(query.workOrderId ?? query.work_order_id)
|
||||
const result = await listAfterSalesCases({
|
||||
page,
|
||||
pageSize,
|
||||
status: String(query.status || '').trim(),
|
||||
keyword: String(query.keyword || '').trim(),
|
||||
...(workOrderId ? { workOrderId } : {}),
|
||||
})
|
||||
return {
|
||||
items: result.items.map(mapWorkerAfterSalesCase),
|
||||
pagination: { page, pageSize, total: result.total },
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminAfterSalesCaseEvents(caseId: number | string) {
|
||||
const afterSalesCase = await getAfterSalesCaseById(caseId)
|
||||
if (!afterSalesCase) {
|
||||
throw createHttpError('售后问题单不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'after_sales_case_not_found',
|
||||
})
|
||||
}
|
||||
return {
|
||||
items: (await listAfterSalesCaseEvents(afterSalesCase.id)).map((event) => ({
|
||||
eventId: Number(event.id),
|
||||
actorType: event.actor_type,
|
||||
actorId: event.actor_id,
|
||||
eventType: event.event_type,
|
||||
payload: safeParseJson(event.payload_json),
|
||||
createdAt: event.created_at,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAdminAfterSalesCase(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const complaintNote = String(payload.complaintNote || payload.note || '').trim()
|
||||
if (!complaintNote) {
|
||||
throw createHttpError('请填写用户投诉说明', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_complaint_required',
|
||||
})
|
||||
}
|
||||
if (complaintNote.length > 2_000) {
|
||||
throw createHttpError('用户投诉说明不能超过 2000 个字符', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_complaint_too_long',
|
||||
})
|
||||
}
|
||||
const problemType = String(payload.problemType || 'fake_evidence').trim() || 'fake_evidence'
|
||||
if (problemType.length > 64) {
|
||||
throw createHttpError('问题类型不正确', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_problem_type_invalid',
|
||||
})
|
||||
}
|
||||
const shareId = normalizeOptionalId(payload.shareId ?? payload.workOrderShareId)
|
||||
const now = nowIso()
|
||||
const result = await createAfterSalesCase({
|
||||
caseNo: randomId('AS'),
|
||||
workOrderId: Number(workOrderId),
|
||||
workOrderShareId: shareId || null,
|
||||
problemType,
|
||||
complaintNote,
|
||||
complaintFilesJson: JSON.stringify(normalizeProofFiles(payload.files || payload.proofFiles)),
|
||||
createdBy: actorName,
|
||||
now,
|
||||
})
|
||||
if (result.failureReason === 'work_order_not_accepted') {
|
||||
throw createHttpError('只有已验收的工单可以发起售后问题单', {
|
||||
statusCode: 409,
|
||||
errorCode: 'after_sales_case_work_order_status_invalid',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'work_order_share_not_accepted') {
|
||||
throw createHttpError('该拼单份额尚未验收,不能发起售后问题单', {
|
||||
statusCode: 409,
|
||||
errorCode: 'after_sales_case_share_status_invalid',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'worker_missing') {
|
||||
throw createHttpError('请指定已验收的责任打手或拼单份额', {
|
||||
statusCode: 409,
|
||||
errorCode: 'after_sales_case_worker_required',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'active_case_exists' || !result.case) {
|
||||
throw createHttpError('该责任打手已有处理中售后问题单', {
|
||||
statusCode: 409,
|
||||
errorCode: 'after_sales_case_active_exists',
|
||||
})
|
||||
}
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: Number(result.case.work_order_id),
|
||||
actorType: 'admin',
|
||||
actorId: actorName,
|
||||
eventType: 'after_sales_case_created',
|
||||
fromStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
toStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
payloadJson: JSON.stringify({
|
||||
caseId: Number(result.case.id),
|
||||
caseNo: result.case.case_no,
|
||||
workerId: Number(result.case.worker_id),
|
||||
shareId: result.case.work_order_share_id ? Number(result.case.work_order_share_id) : null,
|
||||
problemType,
|
||||
}),
|
||||
now,
|
||||
})
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: Number(result.case.work_order_id),
|
||||
workerIds: [Number(result.case.worker_id)],
|
||||
})
|
||||
return { case: mapWorkerAfterSalesCase(result.case) }
|
||||
}
|
||||
|
||||
export async function resolveAdminAfterSalesCase(
|
||||
caseId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const action = String(payload.action || '').trim()
|
||||
if (!['not_upheld', 'remedied', 'upheld'].includes(action)) {
|
||||
throw createHttpError('售后处置方式不正确', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_resolution_action_invalid',
|
||||
})
|
||||
}
|
||||
const resolutionNote = String(payload.resolutionNote || payload.note || '').trim()
|
||||
if (!resolutionNote) {
|
||||
throw createHttpError('请填写售后处置备注', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_resolution_note_required',
|
||||
})
|
||||
}
|
||||
if (resolutionNote.length > 2_000) {
|
||||
throw createHttpError('售后处置备注不能超过 2000 个字符', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_resolution_note_too_long',
|
||||
})
|
||||
}
|
||||
const recoveryAmount = normalizeAmountFen(payload.recoveryAmount ?? payload.amount, 0)
|
||||
if (action === 'upheld' && recoveryAmount <= 0) {
|
||||
throw createHttpError('问题成立时请填写大于 0 的追缴金额', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_recovery_amount_required',
|
||||
})
|
||||
}
|
||||
const result = await resolveAfterSalesCase({
|
||||
caseId: Number(caseId),
|
||||
action: action as 'not_upheld' | 'remedied' | 'upheld',
|
||||
resolutionNote,
|
||||
recoveryAmount: action === 'upheld' ? recoveryAmount : 0,
|
||||
resolvedBy: actorName,
|
||||
now: nowIso(),
|
||||
})
|
||||
if (result.failureReason === 'not_found') {
|
||||
throw createHttpError('售后问题单不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'after_sales_case_not_found',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'not_resolvable' || !result.case) {
|
||||
throw createHttpError('该售后问题单已处置,请刷新后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'after_sales_case_not_resolvable',
|
||||
})
|
||||
}
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: Number(result.case.work_order_id),
|
||||
actorType: 'admin',
|
||||
actorId: actorName,
|
||||
eventType: 'after_sales_case_resolved',
|
||||
fromStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
toStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
payloadJson: JSON.stringify({
|
||||
caseId: Number(result.case.id),
|
||||
action,
|
||||
recoveryAmount: Number(result.case.recovery_amount || 0),
|
||||
debtAmount: Number(result.case.debt_amount || 0),
|
||||
}),
|
||||
now: nowIso(),
|
||||
})
|
||||
const workerId = Number(result.case.worker_id)
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: Number(result.case.work_order_id),
|
||||
workerIds: workerId > 0 ? [workerId] : [],
|
||||
})
|
||||
if (workerId > 0) publishWorkerWalletRealtimeChange(workerId)
|
||||
return { case: mapWorkerAfterSalesCase(result.case) }
|
||||
}
|
||||
|
||||
export async function getWorkerAfterSalesCaseSummary(session: WorkerSession) {
|
||||
requireActiveWorkerSession(session)
|
||||
return { items: (await listWorkerAfterSalesCases(session.workerId)).map(mapWorkerAfterSalesCase) }
|
||||
}
|
||||
|
||||
export async function submitWorkerAfterSalesCaseResponse(
|
||||
caseId: number | string,
|
||||
payload: JsonObject = {},
|
||||
session: WorkerSession,
|
||||
) {
|
||||
requireActiveWorkerSession(session)
|
||||
const response = String(payload.response || payload.note || '').trim()
|
||||
const files = normalizeUploadedFiles(payload.files || payload.proofFiles)
|
||||
if (!response && files.length === 0) {
|
||||
throw createHttpError('请填写说明或上传补救材料', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_response_required',
|
||||
})
|
||||
}
|
||||
if (response.length > 2_000) {
|
||||
throw createHttpError('售后说明不能超过 2000 个字符', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_response_too_long',
|
||||
})
|
||||
}
|
||||
const now = nowIso()
|
||||
const result = await submitWorkerAfterSalesResponse({
|
||||
caseId: Number(caseId),
|
||||
workerId: Number(session.workerId),
|
||||
response,
|
||||
filesJson: JSON.stringify(files),
|
||||
now,
|
||||
})
|
||||
if (result.failureReason === 'not_found') {
|
||||
throw createHttpError('售后问题单不存在或不属于当前账号', {
|
||||
statusCode: 404,
|
||||
errorCode: 'after_sales_case_not_found',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'not_pending' || !result.case) {
|
||||
throw createHttpError('该售后问题单已提交说明或已处置', {
|
||||
statusCode: 409,
|
||||
errorCode: 'after_sales_case_response_status_invalid',
|
||||
})
|
||||
}
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: Number(result.case.work_order_id),
|
||||
actorType: 'worker',
|
||||
actorId: String(session.workerId),
|
||||
eventType: 'after_sales_case_responded',
|
||||
fromStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
toStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
payloadJson: JSON.stringify({ caseId: Number(result.case.id), fileCount: files.length }),
|
||||
now,
|
||||
})
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: Number(result.case.work_order_id),
|
||||
workerIds: [Number(session.workerId)],
|
||||
})
|
||||
return { case: mapWorkerAfterSalesCase(result.case) }
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
export * from './mappers.js'
|
||||
export * from './worker-service.js'
|
||||
export * from './after-sales-service.js'
|
||||
export * from './admin-service.js'
|
||||
export * from './admin-worker-finance-service.js'
|
||||
export * from './admin-worker-catalog-service.js'
|
||||
export * from './admin-kuaishou-match-service.js'
|
||||
export * from './admin-worker-config-service.js'
|
||||
export * from './worker-product-match-config-service.js'
|
||||
export * from './worker-announcement-config-service.js'
|
||||
export * from './worker-hall-config-service.js'
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
updateWorkOrder,
|
||||
} from '../../repositories/worker-platform/work-order-repo.js'
|
||||
import { syncWorkerOrdersForSourceOrder } from './admin-service.js'
|
||||
import { createWorkOrderEvent } from '../../repositories/worker-platform/work-order-repo.js'
|
||||
import { createWorkOrderEvent } from '../../repositories/worker-platform/work-order-event-repo.js'
|
||||
import { OPEN_91_PLATFORM, OPEN_91_PROVIDER } from '../open-91/config.js'
|
||||
import {
|
||||
findKuaishouIndustryShopConfig,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
countWorkerAcceptedOrders,
|
||||
createWorkerCancelRequest,
|
||||
createWorkerOrderFeedback,
|
||||
listWorkerAfterSalesCases,
|
||||
countWorkerActiveOrders,
|
||||
countWorkerTimeoutEvents,
|
||||
countWorkerWithdrawRequestsOnDay,
|
||||
@@ -65,7 +64,6 @@ import {
|
||||
releaseDepositUnfreeze,
|
||||
settleOverdueWorkOrder,
|
||||
submitWorkOrderShareAcceptance,
|
||||
submitWorkerAfterSalesResponse,
|
||||
updateWorkOrder,
|
||||
updateWorkOrderAcceptanceRecord,
|
||||
updateWorkOrderDraftAcceptance,
|
||||
@@ -124,7 +122,6 @@ import {
|
||||
mapWorkOrderEvents,
|
||||
mapWorkOrderShare,
|
||||
mapWorkerCancelRequest,
|
||||
mapWorkerAfterSalesCase,
|
||||
mapWorkerOrderFeedback,
|
||||
mapWorkerUser,
|
||||
normalizeAmountFen,
|
||||
@@ -1858,68 +1855,6 @@ export async function getWorkerOrderFeedbackSummary(session: WorkerSession) {
|
||||
return { items: (await listWorkerOrderFeedbacks(session.workerId)).map(mapWorkerOrderFeedback) }
|
||||
}
|
||||
|
||||
export async function getWorkerAfterSalesCaseSummary(session: WorkerSession) {
|
||||
requireActiveWorkerSession(session)
|
||||
return { items: (await listWorkerAfterSalesCases(session.workerId)).map(mapWorkerAfterSalesCase) }
|
||||
}
|
||||
|
||||
export async function submitWorkerAfterSalesCaseResponse(
|
||||
caseId: number | string,
|
||||
payload: JsonObject = {},
|
||||
session: WorkerSession,
|
||||
) {
|
||||
requireActiveWorkerSession(session)
|
||||
const response = String(payload.response || payload.note || '').trim()
|
||||
const files = normalizeUploadedFiles(payload.files || payload.proofFiles)
|
||||
if (!response && files.length === 0) {
|
||||
throw createHttpError('请填写说明或上传补救材料', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_response_required',
|
||||
})
|
||||
}
|
||||
if (response.length > 2_000) {
|
||||
throw createHttpError('售后说明不能超过 2000 个字符', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_response_too_long',
|
||||
})
|
||||
}
|
||||
const now = nowIso()
|
||||
const result = await submitWorkerAfterSalesResponse({
|
||||
caseId: Number(caseId),
|
||||
workerId: Number(session.workerId),
|
||||
response,
|
||||
filesJson: JSON.stringify(files),
|
||||
now,
|
||||
})
|
||||
if (result.failureReason === 'not_found') {
|
||||
throw createHttpError('售后问题单不存在或不属于当前账号', {
|
||||
statusCode: 404,
|
||||
errorCode: 'after_sales_case_not_found',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'not_pending' || !result.case) {
|
||||
throw createHttpError('该售后问题单已提交说明或已处置', {
|
||||
statusCode: 409,
|
||||
errorCode: 'after_sales_case_response_status_invalid',
|
||||
})
|
||||
}
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: Number(result.case.work_order_id),
|
||||
actorType: 'worker',
|
||||
actorId: String(session.workerId),
|
||||
eventType: 'after_sales_case_responded',
|
||||
fromStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
toStatus: WORK_ORDER_STATUS.ACCEPTED,
|
||||
payloadJson: JSON.stringify({ caseId: Number(result.case.id), fileCount: files.length }),
|
||||
now,
|
||||
})
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: Number(result.case.work_order_id),
|
||||
workerIds: [Number(session.workerId)],
|
||||
})
|
||||
return { case: mapWorkerAfterSalesCase(result.case) }
|
||||
}
|
||||
|
||||
export async function submitWorkerOrderFeedback(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
|
||||
Reference in New Issue
Block a user