fix(supply-chain): 修复限价管理合并入 mainline 后的 18 处类型错误(type-check 0 错误)

提交自身的错误:
- breach-orders/route.ts:filtersSchema 去掉重复的 platform / shopName 键(保留带长度上限的一份)
- supply-chain-breach-order.service.ts:BreachOrderListFilters 去掉重复声明的 platform / shopName
- BreachOrderTab.tsx:MultiFilterChip 的 field 联合类型补齐店铺款式编码/店铺商品编码/申诉原因/平台/店铺(与 filter-options 接口支持的字段一致)
- breach-orders/[id]/route.ts 与 breach-orders/route.ts:多结果分支的处理器统一声明为 handleRequest<unknown>(…): Promise<NextResponse<unknown>>,
  与既有 reference-image-estimate 路由同一写法,解决 ctx.user 可选态与返回形状不匹配

补齐缺失的配套改动:
- errors.ts:新增 ErrorCode.RESOURCE_NOT_FOUND(含 ERROR_MESSAGES 与 HTTP 404 映射)
- ProductArchiveFilters:补 materialLevel / inspectionReport 两个筛选项类型(查询逻辑此前已随功能提交)
- bom-costing-multimodal-gateway.ts:BomCostingMultimodalOperation 增加 IMAGE_EXTRACTION(破价申诉凭证识别用),
  breach-evidence.service 对可空的 providerConfig 加空值保护
- supply-chain-product-archive-select.ts:去掉重复的 sabcRating 选择项
- supply-chain-product-archive.repository.ts:storageType 候选值已在专用分支处理,去掉恒为 false 的重复判断

验证:git 干净检出后 npm run type-check(next typegen + tsc --noEmit)0 错误;
breach/supply-chain/permission 相关 230 个测试套件 1650 用例通过,失败 7 个套件均为 mainline 原有问题。
This commit is contained in:
居居 2026-10-16 18:09:57 +08:00 committed by WuYongJie
parent 62452b6343
commit 7fc43502fd
10 changed files with 32 additions and 14 deletions

View File

@ -1,4 +1,4 @@
import { NextRequest } from 'next/server'
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { ErrorCode } from '@/server/core/errors'
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
@ -46,7 +46,8 @@ export async function GET(request: NextRequest, context: { params: Promise<{ id:
}
export async function POST(request: NextRequest, context: { params: Promise<{ id: string }> }) {
return handleRequest(request, async (ctx) => {
// 申诉与审核返回不同结果形状,统一按 unknown 返回(与既有路由一致)。
return handleRequest<unknown>(request, async (ctx): Promise<NextResponse<unknown>> => {
const { id } = await context.params
const parsed = actionSchema.safeParse(await request.json().catch(() => null))
if (!parsed.success) {

View File

@ -1,4 +1,4 @@
import { NextRequest } from 'next/server'
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { ErrorCode } from '@/server/core/errors'
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
@ -13,8 +13,8 @@ const listQuerySchema = z.object({
const filtersSchema = z.object({
status: z.array(z.string()).optional(),
platform: z.array(z.string()).optional(),
shopName: z.array(z.string()).optional(),
platform: z.array(z.string().max(40)).max(50).optional(),
shopName: z.array(z.string().max(120)).max(50).optional(),
ownerName: z.array(z.string()).optional(),
keyword: z.string().optional(),
itemCode: z.array(z.string().max(120)).max(50).optional(),
@ -23,8 +23,6 @@ const filtersSchema = z.object({
orderNo: z.array(z.string().max(80)).max(50).optional(),
ownerKeyword: z.array(z.string().max(60)).max(50).optional(),
appealType: z.array(z.string().max(60)).max(50).optional(),
platform: z.array(z.string().max(40)).max(50).optional(),
shopName: z.array(z.string().max(120)).max(50).optional(),
listedFrom: z.string().max(10).optional(),
listedTo: z.string().max(10).optional(),
flowFrom: z.string().max(10).optional(),
@ -90,7 +88,9 @@ export async function GET(request: NextRequest) {
}
export async function POST(request: NextRequest) {
return handleRequest(request, async (ctx) => {
// 一个接口分支返回多种结果形状(生成 / 导入 / 标记离职…),统一按 unknown 返回,
// 与 reference-image-estimate 等既有路由保持一致。
return handleRequest<unknown>(request, async (ctx): Promise<NextResponse<unknown>> => {
const parsed = actionSchema.safeParse(await request.json().catch(() => null))
if (!parsed.success) return jsonError(ErrorCode.VALIDATION_INVALID_FORMAT, '请求内容不正确', 400, ctx.requestId)
const viewer = await supplyChainBreachOrderService.resolveViewer(ctx.user)

View File

@ -86,7 +86,8 @@ function MultiFilterChip({
}: {
label: string
placeholder: string
field: 'owner' | 'itemCode' | 'orderNo'
/** 与 `/api/supply-chain/limit-management/breach-orders/filter-options` 支持的字段保持一致 */
field: 'owner' | 'itemCode' | 'orderNo' | 'shopStyleCode' | 'shopItemCode' | 'appealType' | 'platform' | 'shopName'
values: string[]
onChange: (values: string[]) => void
width?: number

View File

@ -210,6 +210,10 @@ export type ProductArchiveFilters = {
productStatus?: ProductArchiveStatusFilter[]
productionType?: string[]
material?: string[]
/** 材质等级(从「材质」文本识别 E0 / E1 / ENF) */
materialLevel?: string[]
/** 是否有检测报告(外部检测报告接口按 SPU 匹配,「有 / 无」) */
inspectionReport?: string[]
color?: string[]
brand?: string[]
supplier?: string[]

View File

@ -44,6 +44,9 @@ export enum ErrorCode {
SUPPLY_CHAIN_CATEGORY_GROUP_TIER_NOT_FOUND = 'SUPPLY_CHAIN_CATEGORY_GROUP_TIER_001',
SUPPLY_CHAIN_CATEGORY_GROUP_TIER_EXISTS = 'SUPPLY_CHAIN_CATEGORY_GROUP_TIER_002',
// 通用资源错误 RESOURCE_xxx(跨模块的「资源不存在」,如破价订单查询)
RESOURCE_NOT_FOUND = 'RESOURCE_001',
// 停产需求管理错误 PRODUCTION_STOP_xxx
PRODUCTION_STOP_DEMAND_NOT_FOUND = 'PRODUCTION_STOP_001',
PRODUCTION_STOP_ITEM_NOT_FOUND = 'PRODUCTION_STOP_002',
@ -148,6 +151,7 @@ export const ERROR_MESSAGES: Record<ErrorCode, string> = {
[ErrorCode.PRODUCTION_STOP_REASON_CATEGORY_IN_USE]: '该停产原因分类已被需求使用,不能删除',
[ErrorCode.SUPPLY_CHAIN_CATEGORY_GROUP_TIER_NOT_FOUND]: '分类档位不存在',
[ErrorCode.SUPPLY_CHAIN_CATEGORY_GROUP_TIER_EXISTS]: '该分类下已存在同名规模标签',
[ErrorCode.RESOURCE_NOT_FOUND]: '资源不存在',
// 认证错误
[ErrorCode.AUTH_INVALID_CREDENTIALS]: '用户名或密码错误',
[ErrorCode.AUTH_INVALID_TOKEN]: '无效的认证令牌',
@ -262,6 +266,7 @@ export const ERROR_HTTP_STATUS: Record<ErrorCode, number> = {
[ErrorCode.SUPPLY_CHAIN_OWNER_CONFIG_NOT_FOUND]: HTTP_STATUS.NOT_FOUND,
[ErrorCode.SUPPLY_CHAIN_CATEGORY_GROUP_TIER_NOT_FOUND]: HTTP_STATUS.NOT_FOUND,
[ErrorCode.SUPPLY_CHAIN_CATEGORY_GROUP_TIER_EXISTS]: HTTP_STATUS.CONFLICT,
[ErrorCode.RESOURCE_NOT_FOUND]: HTTP_STATUS.NOT_FOUND,
[ErrorCode.PRODUCTION_STOP_DEMAND_NOT_FOUND]: HTTP_STATUS.NOT_FOUND,
[ErrorCode.PRODUCTION_STOP_ITEM_NOT_FOUND]: HTTP_STATUS.NOT_FOUND,
[ErrorCode.PRODUCTION_STOP_REASON_CATEGORY_NOT_FOUND]: HTTP_STATUS.NOT_FOUND,

View File

@ -28,7 +28,6 @@ export const productArchiveSelect = {
opsHead: true,
productStatus: true,
productionType: true,
sabcRating: true,
material: true,
color: true,
weight: true,

View File

@ -137,7 +137,9 @@ class SupplyChainProductArchiveRepositoryClass implements SupplyChainProductArch
const query = await preparePeriodArchiveQuery(params.filters, params.field)
// 无其他筛选时走 SQL 侧 DISTINCT + 搜索下推,不再把全表取值拉进内存去重。
if (query.ids === null && Object.keys(query.where).length === 0) {
return listColumnOptionsFromSql(field, params, params.field === 'storageType')
// 「入库/代发 / 材质等级 / 是否有检测报告」都在上面的专用分支里算过候选值,
// 走到这里的一定是原始档案列,按原值出候选值即可。
return listColumnOptionsFromSql(field, params, false)
}
const records = await readSelectedArchives(query, { [field]: true } as Prisma.SupplyChainProductArchiveSelect)
const values = records.map((record) => {

View File

@ -40,7 +40,13 @@ interface BomCostingChatCompletionResponse {
completedFromJsonPayload: boolean
}
export type BomCostingMultimodalOperation = 'IMAGE_FACTS' | 'SYNTHESIS'
/**
* 多模态调用用途:
* - IMAGE_FACTS:图片事实抽取(BOM 识图 / 后台截图识别)
* - IMAGE_EXTRACTION:纯图片信息提取(破价申诉凭证识别:优惠明细、链接时间)
* - SYNTHESIS:结合图片与文本的复核/汇总
*/
export type BomCostingMultimodalOperation = 'IMAGE_FACTS' | 'IMAGE_EXTRACTION' | 'SYNTHESIS'
const BOM_COSTING_REASONING_EFFORTS = new Set([
'none',

View File

@ -38,6 +38,7 @@ export async function recognizeBreachEvidence(image: string): Promise<BreachEvid
try {
if (!(await isBomCostingMultimodalConfigured())) return null
const config = await getBomCostingMultimodalProviderConfig()
if (!config) return null
const result = await callBomCostingMultimodalModel({
config,
operation: 'IMAGE_EXTRACTION',
@ -116,6 +117,7 @@ export async function recognizeLegacyLinkEvidence(image: string): Promise<Legacy
try {
if (!(await isBomCostingMultimodalConfigured())) return null
const config = await getBomCostingMultimodalProviderConfig()
if (!config) return null
const result = await callBomCostingMultimodalModel({
config,
operation: 'IMAGE_FACTS',

View File

@ -37,8 +37,6 @@ export type BreachOrderListFilters = {
orderNo?: string[]
ownerKeyword?: string[]
appealType?: string[]
platform?: string[]
shopName?: string[]
dateFrom?: string
dateTo?: string
listedFrom?: string