/** * 预测模块 Mock Router * * 拦截所有 /api/forecast/* /api/forecast-change/* /api/forecast-config/* 请求, * 提供完整的预测单、变更单、参数配置 CRUD mock。 */ import { loadMockState, saveMockState } from '../storage' import { createSeedData } from './seed' const KEY = 'forecast' function getState(): any { return loadMockState(KEY, createSeedData) } function save(state: any): void { saveMockState(KEY, state) } const ok = (data: any) => ({ code: 800, msg: '操作成功', data }) const fail = (msg: string) => ({ code: 500, msg, data: null }) function paginate(rows: any[], pageIndex: number, pageRows: number) { const start = (pageIndex - 1) * pageRows return { total: rows.length, pageIndex, pageRows, records: rows.slice(start, start + pageRows) } } const genId = (state: any) => ++state.forecastOrderIdSeq const genChangeId = (state: any) => ++state.changeOrderIdSeq const genConfigId = (state: any) => ++state.configIdSeq const genFoNo = (state: any) => { const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '') return `FO-${dateStr}-${String(genId(state)).padStart(4, '0')}` } const genChangeNo = (state: any) => { const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '') return `CHG-${dateStr}-${String(genChangeId(state)).padStart(4, '0')}` } const now = () => { const d = new Date() const offset = 8 * 60 const local = new Date(d.getTime() + offset * 60000) return local.toISOString().replace('T', ' ').slice(0, 19) } function parseQuery(url: string): URLSearchParams { const queryStr = url.split('?')[1] || '' return new URLSearchParams(queryStr) } // 用户 ID → 姓名映射 const USER_NAMES: Record = { 1001: '张伟', 1002: '李娜', 1003: '王强', 2001: '赵计划', 2002: '孙排产', 2003: '陈经理', } export function forecastRouter(url: string, method: string, data?: any): any | null { const u = url.replace(/^\//, '') const state = getState() // ============================================================ // 预测单 - 分页查询 // ============================================================ if (u.includes('api/forecast/find-pagination') && method === 'post') { const pageIndex = data?.pageIndex || 1 const pageRows = data?.pageRows || 20 const filterParams = data?.params || {} let list = [...state.forecastOrders] if (filterParams.forecastOrderNo) { list = list.filter((fo: any) => fo.forecastOrderNo?.includes(filterParams.forecastOrderNo)) } if (filterParams.foStatus && filterParams.foStatus !== '') { list = list.filter((fo: any) => fo.foStatus === filterParams.foStatus) } if (filterParams.customerCode) { list = list.filter((fo: any) => fo.customerCode?.includes(filterParams.customerCode)) } if (filterParams.customerName) { list = list.filter((fo: any) => fo.customerName?.includes(filterParams.customerName)) } if (filterParams.productCode) { list = list.filter((fo: any) => fo.productCode?.includes(filterParams.productCode)) } if (filterParams.productName) { list = list.filter((fo: any) => fo.productName?.includes(filterParams.productName)) } if (filterParams.productCategory) { list = list.filter((fo: any) => fo.productCategory === filterParams.productCategory) } if (filterParams.submitter) { list = list.filter((fo: any) => String(fo.submitter) === String(filterParams.submitter)) } if (filterParams.tradeType) { list = list.filter((fo: any) => fo.tradeType === filterParams.tradeType) } if (filterParams.submitTimeStart) { list = list.filter((fo: any) => fo.submitTime && fo.submitTime >= filterParams.submitTimeStart) } if (filterParams.submitTimeEnd) { list = list.filter((fo: any) => fo.submitTime && fo.submitTime <= filterParams.submitTimeEnd) } if (filterParams.creationDateStart) { list = list.filter((fo: any) => fo.creationDate >= filterParams.creationDateStart) } if (filterParams.creationDateEnd) { list = list.filter((fo: any) => fo.creationDate <= filterParams.creationDateEnd) } // 默认按创建时间倒序 list.sort((a: any, b: any) => (b.creationDate || '').localeCompare(a.creationDate || '')) // 为每条预测单附加提交人姓名 + 是否有待审批变更单 list = list.map((fo: any) => { const pendingChange = state.changeOrders.some( (c: any) => c.forecastOrderId === fo.forecastOrderId && (c.coStatus === 'DRAFT' || c.coStatus === 'SUBMITTED') ) return { ...fo, submitterName: USER_NAMES[fo.submitter] || `用户${fo.submitter}`, hasPendingChange: pendingChange, } }) const paged = paginate(list, pageIndex, pageRows) return ok(paged) } // ============================================================ // 预测单 - 根据 ID 查询 // ============================================================ if (u.includes('api/forecast/get-id') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === id) if (!fo) return fail('预测单不存在') // 附带变更单列表(附加提交人姓名) const changeOrders = state.changeOrders .filter((c: any) => c.forecastOrderId === id) .map((c: any) => ({ ...c, submitterName: USER_NAMES[c.submitter] || (c.submitter ? `用户${c.submitter}` : ''), })) const detail = { ...fo, submitterName: USER_NAMES[fo.submitter] || `用户${fo.submitter}`, } if (changeOrders.length > 0) { detail.changeOrderList = changeOrders } return ok(detail) } // ============================================================ // 预测单 - 保存草稿 // ============================================================ if (u.includes('api/forecast/save-draft') && method === 'post') { const dto = data?.params || data || {} if (!dto.customerCode) return fail('客户编码不能为空') if (!dto.productCode) return fail('产品编码不能为空') const newId = genId(state) const customer = state.customers.find((c: any) => c.customerCode === dto.customerCode) const product = state.products.find((p: any) => p.productCode === dto.productCode) const newFo: any = { forecastOrderId: newId, forecastOrderNo: null, forecastOrderType: 'MANUAL', foStatus: 'DRAFT', submitter: 1001, tradeType: dto.tradeType || customer?.tradeType || 'DOMESTIC', submitTime: null, forecastReplyTime: null, reasonDesc: dto.reasonDesc || null, customerCode: dto.customerCode, customerName: dto.customerName || customer?.customerName || '', groupCustomerCode: dto.groupCustomerCode || null, groupCustomerName: dto.groupCustomerName || null, productCode: dto.productCode, productName: dto.productName || product?.productName || '', productCategory: dto.productCategory || product?.productCategory || '', materialLevel: dto.materialLevel || product?.materialLevel || 'FINISHED', standardization: dto.standardization || product?.standardization || 'STANDARD', minOrderQty: dto.minOrderQty ?? product?.minOrderQty ?? null, forecastQty: dto.forecastQty || 0, unit: dto.unit || '公斤', shippingPoint: dto.shippingPoint || 'SHANGHAI', expectedDeliveryDate: dto.expectedDeliveryDate || '', customsManualNo: dto.customsManualNo || null, rawMaterialOrigin: dto.rawMaterialOrigin || null, lowCarbonRatio: dto.lowCarbonRatio ?? null, processingFlag: dto.processingFlag ?? 0, designatedFactory: dto.designatedFactory || null, planSuggestedQty: null, planReplyDate: null, planReplyComment: null, rejectReason: null, forceConvertDeadline: null, actualScheduleQty: 0, convertedQty: 0, releasedQty: 0, relatedSoNos: null, creationDate: now(), createdBy: 1001, lastUpdateDate: now(), lastUpdatedBy: 1001, deleteFlag: 0, versionNum: 1, } state.forecastOrders.unshift(newFo) save(state) return ok(newFo) } // ============================================================ // 预测单 - 提交评审 // ============================================================ if (u.includes('api/forecast/submit') && method === 'post') { const dto = data?.params || data || {} if (!dto.customerCode) return fail('客户编码不能为空') if (!dto.productCode) return fail('产品编码不能为空') if (!dto.forecastQty || dto.forecastQty <= 0) return fail('预测数量必须大于0') if (!dto.expectedDeliveryDate) return fail('期望交付日期不能为空') // 如果有 ID,则是草稿提交 if (dto.forecastOrderId) { const idx = state.forecastOrders.findIndex((f: any) => f.forecastOrderId === dto.forecastOrderId) if (idx === -1) return fail('预测单不存在') const fo = state.forecastOrders[idx] if (fo.foStatus !== 'DRAFT') return fail('仅草稿状态可提交') // 更新字段并提交 Object.assign(fo, { tradeType: dto.tradeType || fo.tradeType, reasonDesc: dto.reasonDesc ?? fo.reasonDesc, forecastQty: dto.forecastQty, unit: dto.unit || fo.unit, shippingPoint: dto.shippingPoint || fo.shippingPoint, expectedDeliveryDate: dto.expectedDeliveryDate, customsManualNo: dto.customsManualNo ?? fo.customsManualNo, rawMaterialOrigin: dto.rawMaterialOrigin ?? fo.rawMaterialOrigin, lowCarbonRatio: dto.lowCarbonRatio ?? fo.lowCarbonRatio, processingFlag: dto.processingFlag ?? fo.processingFlag, designatedFactory: dto.designatedFactory ?? fo.designatedFactory, foStatus: 'SUBMITTED', forecastOrderNo: genFoNo(state), submitTime: now(), lastUpdateDate: now(), versionNum: (fo.versionNum || 0) + 1, }) save(state) return ok(fo) } // 新建并提交 const newId = genId(state) const customer = state.customers.find((c: any) => c.customerCode === dto.customerCode) const product = state.products.find((p: any) => p.productCode === dto.productCode) const newFo: any = { forecastOrderId: newId, forecastOrderNo: genFoNo(state), forecastOrderType: 'MANUAL', foStatus: 'SUBMITTED', submitter: 1001, tradeType: dto.tradeType || customer?.tradeType || 'DOMESTIC', submitTime: now(), forecastReplyTime: null, reasonDesc: dto.reasonDesc || null, customerCode: dto.customerCode, customerName: dto.customerName || customer?.customerName || '', groupCustomerCode: dto.groupCustomerCode || null, groupCustomerName: dto.groupCustomerName || null, productCode: dto.productCode, productName: dto.productName || product?.productName || '', productCategory: dto.productCategory || product?.productCategory || '', materialLevel: dto.materialLevel || product?.materialLevel || 'FINISHED', standardization: dto.standardization || product?.standardization || 'STANDARD', minOrderQty: dto.minOrderQty ?? product?.minOrderQty ?? null, forecastQty: dto.forecastQty, unit: dto.unit || '公斤', shippingPoint: dto.shippingPoint || 'SHANGHAI', expectedDeliveryDate: dto.expectedDeliveryDate, customsManualNo: dto.customsManualNo || null, rawMaterialOrigin: dto.rawMaterialOrigin || null, lowCarbonRatio: dto.lowCarbonRatio ?? null, processingFlag: dto.processingFlag ?? 0, designatedFactory: dto.designatedFactory || null, planSuggestedQty: null, planReplyDate: null, planReplyComment: null, rejectReason: null, forceConvertDeadline: null, actualScheduleQty: 0, convertedQty: 0, releasedQty: 0, relatedSoNos: null, creationDate: now(), createdBy: 1001, lastUpdateDate: now(), lastUpdatedBy: 1001, deleteFlag: 0, versionNum: 1, } state.forecastOrders.unshift(newFo) save(state) return ok(newFo) } // ============================================================ // 预测单 - 更新草稿 // ============================================================ if (u.includes('api/forecast/update-draft') && method === 'post') { const dto = data?.params || data || {} if (!dto.forecastOrderId) return fail('预测单ID不能为空') const idx = state.forecastOrders.findIndex((f: any) => f.forecastOrderId === dto.forecastOrderId) if (idx === -1) return fail('预测单不存在') const fo = state.forecastOrders[idx] if (fo.foStatus !== 'DRAFT') return fail('仅草稿状态可编辑') const updatableFields = [ 'customerCode', 'customerName', 'groupCustomerCode', 'groupCustomerName', 'productCode', 'productName', 'productCategory', 'materialLevel', 'standardization', 'minOrderQty', 'tradeType', 'forecastQty', 'unit', 'shippingPoint', 'expectedDeliveryDate', 'customsManualNo', 'rawMaterialOrigin', 'lowCarbonRatio', 'processingFlag', 'designatedFactory', 'reasonDesc', ] for (const field of updatableFields) { if (dto[field] !== undefined) { fo[field] = dto[field] } } fo.lastUpdateDate = now() fo.versionNum = (fo.versionNum || 0) + 1 save(state) return ok(fo) } // ============================================================ // 预测单 - 撤回 // ============================================================ if (u.includes('api/forecast/withdraw') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === id) if (!fo) return fail('预测单不存在') if (fo.foStatus !== 'SUBMITTED' && fo.foStatus !== 'REJECTED') { return fail('仅已提交或已驳回状态可撤回') } fo.foStatus = 'DRAFT' fo.lastUpdateDate = now() fo.versionNum = (fo.versionNum || 0) + 1 save(state) return ok(fo) } // ============================================================ // 预测单 - 评审接收 // ============================================================ if (u.includes('api/forecast/review-accept') && method === 'post') { const dto = data?.params || data || {} if (!dto.forecastOrderId) return fail('预测单ID不能为空') if (!dto.planSuggestedQty || dto.planSuggestedQty <= 0) return fail('建议数量必须大于0') if (!dto.planReplyDate) return fail('计划回复日期不能为空') const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === dto.forecastOrderId) if (!fo) return fail('预测单不存在') if (fo.foStatus !== 'SUBMITTED') return fail('仅已提交状态可评审') fo.foStatus = 'PLAN_RECEIVED' fo.planSuggestedQty = dto.planSuggestedQty fo.planReplyDate = dto.planReplyDate fo.planReplyComment = dto.planReplyComment || null fo.forecastReplyTime = now() fo.actualScheduleQty = dto.planSuggestedQty // 强制转单截止日期 = 计划回复日期 + FORCE_CONVERT_DAYS const forceConvertDays = parseInt( state.activeConfigs.find((c: any) => c.configKey === 'FORCE_CONVERT_DAYS')?.configValue || '30' ) const replyDate = new Date(dto.planReplyDate) replyDate.setDate(replyDate.getDate() + forceConvertDays) fo.forceConvertDeadline = replyDate.toISOString().slice(0, 10) fo.lastUpdateDate = now() fo.versionNum = (fo.versionNum || 0) + 1 save(state) return ok(fo) } // ============================================================ // 预测单 - 评审驳回 // ============================================================ if (u.includes('api/forecast/review-reject') && method === 'post') { const dto = data?.params || data || {} if (!dto.forecastOrderId) return fail('预测单ID不能为空') if (!dto.rejectReason) return fail('驳回原因不能为空') const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === dto.forecastOrderId) if (!fo) return fail('预测单不存在') if (fo.foStatus !== 'SUBMITTED') return fail('仅已提交状态可驳回') fo.foStatus = 'REJECTED' fo.rejectReason = dto.rejectReason fo.forecastReplyTime = now() fo.lastUpdateDate = now() fo.versionNum = (fo.versionNum || 0) + 1 save(state) return ok(fo) } // ============================================================ // 预测单 - 重提 // ============================================================ if (u.includes('api/forecast/resubmit') && method === 'post') { const dto = data?.params || data || {} if (!dto.forecastOrderId) return fail('预测单ID不能为空') const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === dto.forecastOrderId) if (!fo) return fail('预测单不存在') if (fo.foStatus !== 'REJECTED') return fail('仅已驳回状态可重提') fo.foStatus = 'SUBMITTED' fo.rejectReason = null fo.submitTime = now() fo.lastUpdateDate = now() fo.versionNum = (fo.versionNum || 0) + 1 save(state) return ok(fo) } // ============================================================ // 预测单 - 作废 // ============================================================ if (u.includes('api/forecast/discard') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === id) if (!fo) return fail('预测单不存在') if (fo.foStatus === 'DISCARDED') return fail('预测单已作废') fo.foStatus = 'DISCARDED' fo.lastUpdateDate = now() fo.versionNum = (fo.versionNum || 0) + 1 save(state) return ok(null) } // ============================================================ // 预测单 - 删除草稿 // ============================================================ if (u.includes('api/forecast/delete-draft') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const idx = state.forecastOrders.findIndex((f: any) => f.forecastOrderId === id) if (idx === -1) return fail('预测单不存在') if (state.forecastOrders[idx].foStatus !== 'DRAFT') return fail('仅草稿状态可删除') state.forecastOrders.splice(idx, 1) save(state) return ok(null) } // ============================================================ // 预测单 - 检查转单资格 // ============================================================ if (u.includes('api/forecast/check-convert-eligibility') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === id) if (!fo) return fail('预测单不存在') const eligibleStatuses = ['SCHEDULED', 'PARTIAL_CONVERTED'] if (!eligibleStatuses.includes(fo.foStatus)) { return ok({ eligible: false, reason: `当前状态(${fo.foStatus})不允许转单` }) } const remaining = (fo.actualScheduleQty || 0) - (fo.convertedQty || 0) - (fo.releasedQty || 0) if (remaining <= 0) { return ok({ eligible: false, reason: '无可转数量' }) } return ok({ eligible: true }) } // ============================================================ // 预测单 - 检查变更资格 // ============================================================ if (u.includes('api/forecast/check-eligibility') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === id) if (!fo) return fail('预测单不存在') const eligibleStatuses = ['SCHEDULED', 'PARTIAL_CONVERTED'] if (!eligibleStatuses.includes(fo.foStatus)) { return ok({ eligible: false, reason: `当前状态(${fo.foStatus})不允许变更` }) } return ok({ eligible: true }) } // ============================================================ // 变更单 - 分页查询 // ============================================================ if (u.includes('api/forecast-change/find-pagination') && method === 'post') { const pageIndex = data?.pageIndex || 1 const pageRows = data?.pageRows || 20 const filterParams = data?.params || {} let list = [...state.changeOrders] if (filterParams.changeOrderNo) { list = list.filter((c: any) => c.changeOrderNo?.includes(filterParams.changeOrderNo)) } if (filterParams.coStatus && filterParams.coStatus !== '') { list = list.filter((c: any) => c.coStatus === filterParams.coStatus) } if (filterParams.forecastOrderNo) { list = list.filter((c: any) => c.forecastOrderNo?.includes(filterParams.forecastOrderNo)) } list.sort((a: any, b: any) => (b.creationDate || '').localeCompare(a.creationDate || '')) const paged = paginate(list, pageIndex, pageRows) return ok(paged) } // ============================================================ // 变更单 - 根据 ID 查询 // ============================================================ if (u.includes('api/forecast-change/get-id') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const co = state.changeOrders.find((c: any) => c.changeOrderId === id) if (!co) return fail('变更单不存在') return ok(co) } // ============================================================ // 变更单 - 保存草稿 // ============================================================ if (u.includes('api/forecast-change/save-draft') && method === 'post') { const dto = data?.params || data || {} if (!dto.forecastOrderId) return fail('预测单ID不能为空') // 检查预测单是否存在 const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === dto.forecastOrderId) if (!fo) return fail('关联预测单不存在') const newId = genChangeId(state) const newCo: any = { changeOrderId: newId, changeOrderNo: null, forecastOrderId: dto.forecastOrderId, forecastOrderNo: fo.forecastOrderNo, coStatus: 'DRAFT', originalCustomerCode: fo.customerCode, originalCustomerName: fo.customerName, newCustomerCode: dto.newCustomerCode || null, newCustomerName: dto.newCustomerName || null, originalDeliveryDate: fo.expectedDeliveryDate, newDeliveryDate: dto.newDeliveryDate || null, originalCustomsManualNo: fo.customsManualNo, newCustomsManualNo: dto.newCustomsManualNo || null, releaseQty: dto.releaseQty ?? null, rejectReason: null, submitter: null, submitTime: null, approveTime: null, creationDate: now(), createdBy: 1001, lastUpdateDate: now(), deleteFlag: 0, versionNum: 1, } state.changeOrders.unshift(newCo) save(state) return ok(newCo) } // ============================================================ // 变更单 - 提交变更申请 // ============================================================ if (u.includes('api/forecast-change/submit') && method === 'post') { const dto = data?.params || data || {} // 如果传了 changeOrderId,则是草稿提交 if (dto.changeOrderId) { const co = state.changeOrders.find((c: any) => c.changeOrderId === dto.changeOrderId) if (!co) return fail('变更单不存在') if (co.coStatus !== 'DRAFT') return fail('仅草稿状态可提交') if (dto.newCustomerCode !== undefined) co.newCustomerCode = dto.newCustomerCode || null if (dto.newCustomerName !== undefined) co.newCustomerName = dto.newCustomerName || null if (dto.newDeliveryDate !== undefined) co.newDeliveryDate = dto.newDeliveryDate || null if (dto.newCustomsManualNo !== undefined) co.newCustomsManualNo = dto.newCustomsManualNo || null if (dto.releaseQty !== undefined) co.releaseQty = dto.releaseQty ?? null co.coStatus = 'SUBMITTED' co.changeOrderNo = genChangeNo(state) co.submitter = 1001 co.submitTime = now() co.lastUpdateDate = now() co.versionNum = (co.versionNum || 0) + 1 save(state) return ok(co) } // 新建并提交 if (!dto.forecastOrderId) return fail('预测单ID不能为空') const fo = state.forecastOrders.find((f: any) => f.forecastOrderId === dto.forecastOrderId) if (!fo) return fail('关联预测单不存在') const newId = genChangeId(state) const newCo: any = { changeOrderId: newId, changeOrderNo: genChangeNo(state), forecastOrderId: dto.forecastOrderId, forecastOrderNo: fo.forecastOrderNo, coStatus: 'SUBMITTED', originalCustomerCode: fo.customerCode, originalCustomerName: fo.customerName, newCustomerCode: dto.newCustomerCode || null, newCustomerName: dto.newCustomerName || null, originalDeliveryDate: fo.expectedDeliveryDate, newDeliveryDate: dto.newDeliveryDate || null, originalCustomsManualNo: fo.customsManualNo, newCustomsManualNo: dto.newCustomsManualNo || null, releaseQty: dto.releaseQty ?? null, rejectReason: null, submitter: 1001, submitTime: now(), approveTime: null, creationDate: now(), createdBy: 1001, lastUpdateDate: now(), deleteFlag: 0, versionNum: 1, } state.changeOrders.unshift(newCo) save(state) return ok(newCo) } // ============================================================ // 变更单 - 审批通过 // ============================================================ if (u.includes('api/forecast-change/approve') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const co = state.changeOrders.find((c: any) => c.changeOrderId === id) if (!co) return fail('变更单不存在') if (co.coStatus !== 'SUBMITTED') return fail('仅已提交状态可审批') co.coStatus = 'CHANGED' co.approveTime = now() co.lastUpdateDate = now() co.versionNum = (co.versionNum || 0) + 1 save(state) return ok(co) } // ============================================================ // 变更单 - 审批驳回 // ============================================================ if (u.includes('api/forecast-change/reject') && method === 'post') { const dto = data?.params || data || {} if (!dto.changeOrderId) return fail('变更单ID不能为空') const co = state.changeOrders.find((c: any) => c.changeOrderId === dto.changeOrderId) if (!co) return fail('变更单不存在') if (co.coStatus !== 'SUBMITTED') return fail('仅已提交状态可驳回') co.coStatus = 'REJECTED' co.rejectReason = dto.rejectReason || '审批驳回' co.lastUpdateDate = now() co.versionNum = (co.versionNum || 0) + 1 save(state) return ok(co) } // ============================================================ // 变更单 - 撤回 // ============================================================ if (u.includes('api/forecast-change/withdraw') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const co = state.changeOrders.find((c: any) => c.changeOrderId === id) if (!co) return fail('变更单不存在') if (co.coStatus !== 'SUBMITTED' && co.coStatus !== 'REJECTED') { return fail('仅已提交或已驳回状态可撤回') } co.coStatus = 'DRAFT' co.lastUpdateDate = now() co.versionNum = (co.versionNum || 0) + 1 save(state) return ok(co) } // ============================================================ // 变更单 - 作废 // ============================================================ if (u.includes('api/forecast-change/discard') && method === 'get') { const sp = parseQuery(u) const id = Number(sp.get('id')) if (!id) return fail('参数缺失:id') const co = state.changeOrders.find((c: any) => c.changeOrderId === id) if (!co) return fail('变更单不存在') if (co.coStatus === 'DISCARDED') return fail('变更单已作废') co.coStatus = 'DISCARDED' co.lastUpdateDate = now() co.versionNum = (co.versionNum || 0) + 1 save(state) return ok(null) } // ============================================================ // 变更单 - 按预测单 ID 查询变更列表 // ============================================================ if (u.includes('api/forecast-change/list-by-forecast-id') && method === 'get') { const sp = parseQuery(u) const forecastOrderId = Number(sp.get('forecastOrderId')) if (!forecastOrderId) return fail('参数缺失:forecastOrderId') const list = state.changeOrders .filter((c: any) => c.forecastOrderId === forecastOrderId) .sort((a: any, b: any) => (b.creationDate || '').localeCompare(a.creationDate || '')) return ok(list) } // ============================================================ // 参数配置 - 查询所有配置 // ============================================================ if (u.includes('api/forecast-config/find-all') && method === 'get') { // 返回当前生效配置 + 最后修改信息 const lastRecord = state.configRecords[state.configRecords.length - 1] || {} return ok({ configs: state.activeConfigs, lastUpdateDate: lastRecord.lastUpdateDate || '2026-06-01 08:00:00', lastUpdatedBy: lastRecord.lastUpdatedBy || 2001, remark: lastRecord.remark || null, }) } // ============================================================ // 参数配置 - 保存配置 // ============================================================ if (u.includes('api/forecast-config/save') && method === 'post') { const dto = data?.params || data || {} if (!dto.configs || !Array.isArray(dto.configs)) return fail('配置列表不能为空') // 更新生效配置 for (const newConfig of dto.configs) { const existing = state.activeConfigs.find((c: any) => c.configKey === newConfig.configKey) if (existing) { existing.configValue = newConfig.configValue } else { state.activeConfigs.push({ configKey: newConfig.configKey, configValue: newConfig.configValue }) } } // 添加修改记录 for (const newConfig of dto.configs) { const record = { configId: genConfigId(state), configKey: newConfig.configKey, configValue: newConfig.configValue, description: newConfig.configKey === 'ADVANCE_DAYS' ? '提报提前期天数' : '强制转单天数', lastUpdateDate: now(), lastUpdatedBy: 2001, remark: dto.remark || null, } state.configRecords.push(record) } save(state) return ok(null) } return null }