feat: 积分商城-供应商-对账

This commit is contained in:
2026-09-03 18:06:07 +08:00
parent 750c36116d
commit 5c8fbddb2e
9 changed files with 1195 additions and 16 deletions
@@ -16,6 +16,8 @@ import { orderListService } from '../services/order-list.service.js'
import { orderPushEventService, type OrderPushEventInput } from '../services/order-push-event.service.js'
import {
supplierOpenOrderService,
type SupplierInvoiceApplyRequest,
type SupplierInvoiceQueryRequest,
type SupplierOrderQueryRequest,
type SupplierReconciliationQueryRequest
} from '../services/supplier-open-order.service.js'
@@ -261,3 +263,37 @@ export const supplierReconciliationInfoController = withErrorBoundary(async (req
return Response.json(await supplierOpenOrderService.queryReconciliationInfo(body))
})
export const supplierInvoiceApplyController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
let body: SupplierInvoiceApplyRequest
try {
body = supplierOpenOrderService.parseBody(await readJson<SupplierInvoiceApplyRequest>(request)) as SupplierInvoiceApplyRequest
} catch (error) {
return Response.json({
resultCode: 400,
message: error instanceof Error ? error.message : '请求参数解密失败'
})
}
return Response.json(await supplierOpenOrderService.applyForInvoices(body))
})
export const supplierInvoiceInfoController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
let body: SupplierInvoiceQueryRequest
try {
body = supplierOpenOrderService.parseBody(await readJson<SupplierInvoiceQueryRequest>(request)) as SupplierInvoiceQueryRequest
} catch (error) {
return Response.json({
resultCode: 400,
message: error instanceof Error ? error.message : '请求参数解密失败'
})
}
return Response.json(await supplierOpenOrderService.queryInvoicesInfo(body))
})
@@ -10,7 +10,7 @@ type SupplierInfoRequest = {
}
type SupplierTokenRequest = {
userId?: string
AplParmObjct?: string
supplierIdentification?: string
}
@@ -39,10 +39,10 @@ export const supplierTokenController = withErrorBoundary(async (request: Request
if (methodResponse) return methodResponse
const body = await readJson<SupplierTokenRequest>(request)
const userId = String(body.userId || '').trim()
const encryptedParam = String(body.AplParmObjct || '').trim()
const supplierIdentification = String(body.supplierIdentification || '').trim().toLowerCase()
return toHttpResponse(await supplierService.warmSupplierToken(userId, supplierIdentification))
return toHttpResponse(await supplierService.warmSupplierToken(encryptedParam, supplierIdentification))
})
export const supplierPointsController = withErrorBoundary(async (request: Request, _context: Context) => {
+6
View File
@@ -33,6 +33,8 @@ import {
orderPayController,
orderPushEventController,
orderShipController,
supplierInvoiceApplyController,
supplierInvoiceInfoController,
supplierOrderInfoController,
supplierReconciliationInfoController
} from './controllers/order.controller.js'
@@ -107,6 +109,8 @@ const handlers: Record<string, FunctionHandler> = {
'merchant-product-delete': merchantProductDeleteController,
'supplier-debug-tokens': supplierDebugTokensController,
'supplier-info': supplierInfoController,
'supplier-invoice-apply': supplierInvoiceApplyController,
'supplier-invoice-info': supplierInvoiceInfoController,
'supplier-order-info': supplierOrderInfoController,
'supplier-reconciliation-info': supplierReconciliationInfoController,
'supplier-reconciliation-result': supplierReconciliationResultController,
@@ -200,6 +204,8 @@ router.all('/api/:name', async ctx => dispatch(ctx.params.name)(ctx))
router.all('/api/admin/:name', async ctx => dispatch(ctx.params.name)(ctx))
router.post('/supplierOpenApi/pointOrder/queryOrderInfo', dispatch('supplier-order-info'))
router.post('/supplierOpenApi/pointOrder/queryReconciliationInfo', dispatch('supplier-reconciliation-info'))
router.post('/supplierOpenApi/pointOrder/applyForInvoices', dispatch('supplier-invoice-apply'))
router.post('/supplierOpenApi/pointOrder/queryInvoicesInfo', dispatch('supplier-invoice-info'))
router.post('/supplierOpenApi/pointOrder/reconciliationResult', dispatch('supplier-reconciliation-result'))
router.post('/supplierOpenApi/pointOrder/unReconciledMsg', dispatch('supplier-unreconciled-msg'))
router.post('/supplierOpenApi/debug/tokens', dispatch('supplier-debug-tokens'))
@@ -2,6 +2,7 @@ import { supplierDao } from '../dao/supplier.dao.js'
import { orderDao } from '../dao/order.dao.js'
import { ORDER_EVENT_TYPE, ORDER_STATE } from '../constants/order.js'
import { logger } from '../utils/logger.js'
import { prisma } from '../utils/prisma.js'
import { sm4CbcDecrypt, verifySupplierOpenSignature } from '../utils/supplier-open-crypto.js'
export type SupplierOrderQueryRequest = {
@@ -11,6 +12,7 @@ export type SupplierOrderQueryRequest = {
orderId?: string
timestamp?: number
AplParmObjct?: string
RsrvFld1?: string
}
export type SupplierReconciliationQueryRequest = {
@@ -24,10 +26,51 @@ export type SupplierReconciliationQueryRequest = {
pageSize?: number
timestamp?: number
AplParmObjct?: string
RsrvFld1?: string
}
const parseBody = (body: SupplierOrderQueryRequest) => {
if (!body.AplParmObjct) return body
export type SupplierInvoiceApplyRequest = {
supplierId?: string
token?: string
settlementId?: string
orderIds?: string
orderNum?: number
orderTotalPrice?: number | string
invoiceDate?: string
invoiceTitle?: string
invoiceCode?: string
invoiceAddress?: string
invoiceContact?: string
invoiceNakedPrice?: number | string
invoiceTaxPrice?: number | string
invoiceType?: string
invoiceContent?: string
remark?: string
bankAccount?: string
registeredAddress?: string
bankName?: string
timestamp?: number
AplParmObjct?: string
RsrvFld1?: string
}
export type SupplierInvoiceQueryRequest = {
supplierId?: string
token?: string
settlementId?: string
timestamp?: number
AplParmObjct?: string
}
type SupplierOpenEncryptedRequest = {
AplParmObjct?: string
ApiParamObject?: string
RsrvFld1?: string
}
const parseBody = <T extends SupplierOpenEncryptedRequest>(body: T) => {
const encryptedData = body.AplParmObjct || body.ApiParamObject
if (!encryptedData) return body
const keyHex = process.env.SUPPLIER_OPEN_SM4_KEY_HEX || process.env.SM4CBC_KEY_HEX || ''
const ivHex = process.env.SUPPLIER_OPEN_SM4_IV_HEX || process.env.SM4CBC_IV_HEX || ''
@@ -35,7 +78,10 @@ const parseBody = (body: SupplierOrderQueryRequest) => {
throw new Error('供应商开放接口SM4配置缺失')
}
return JSON.parse(sm4CbcDecrypt(body.AplParmObjct, keyHex, ivHex)) as SupplierOrderQueryRequest
return {
...JSON.parse(sm4CbcDecrypt(encryptedData, keyHex, ivHex)),
RsrvFld1: body.RsrvFld1
} as T
}
const toText = (date?: Date | null) => {
@@ -223,5 +269,160 @@ export const supplierOpenOrderService = {
}))
return { resultCode: 0, message: '成功', totalPage, currentPage: page, orderInfos }
},
async applyForInvoices(body: SupplierInvoiceApplyRequest) {
const supplierId = String(body.supplierId || '').trim()
const token = String(body.token || '').trim()
const settlementId = String(body.settlementId || '').trim()
const orderIds = [...new Set(String(body.orderIds || '')
.split(',')
.map(item => item.trim())
.filter(Boolean))]
const orderNum = Number(body.orderNum || 0)
const orderTotalPrice = Number(body.orderTotalPrice)
const timestamp = Number(body.timestamp || 0)
logger.info('supplier open invoice apply', { supplierId, settlementId, orderCount: orderIds.length, timestamp })
if (!supplierId || !token || !timestamp || !settlementId || !orderIds.length) {
return { resultCode: 400, message: '供应商、结算单、订单或签名信息不能为空' }
}
if (!body.invoiceDate || !body.invoiceTitle || !body.invoiceCode || !body.invoiceAddress || !body.invoiceContact) {
return { resultCode: 400, message: '开票日期、抬头、纳税人识别号、地址和联系电话不能为空' }
}
if (!Number.isInteger(orderNum) || orderNum <= 0 || !Number.isFinite(orderTotalPrice) || orderTotalPrice < 0) {
return { resultCode: 400, message: '订单数量或订单总金额不正确' }
}
const supplier = await supplierDao.findActiveBySupplierId(supplierId)
if (!supplier) {
return { resultCode: 404, message: '供应商信息不存在' }
}
if (!verifySupplierOpenSignature(supplier.supplierId, String(body.RsrvFld1 || supplier.clientId), timestamp, token)) {
return { resultCode: 401, message: '签名无效或已过期' }
}
const orders = await orderDao.findOpenApiSubOrders(supplierId, orderIds)
if (orders.length !== orderIds.length || orderNum !== orderIds.length) {
return { resultCode: 400, message: '订单数量校验不通过' }
}
const actualTotalPrice = orders.reduce((total, order) => total + Number(order.totalPrice || 0), 0)
if (Math.abs(actualTotalPrice - orderTotalPrice) >= 0.01) {
return { resultCode: 400, message: '订单总金额校验不通过' }
}
const orderIdText = orderIds.join(',')
const firstOrder = orders[0]
await orderDao.transaction(async tx => {
await tx.supplierInvoiceInfo.upsert({
where: { invoiceBizId: settlementId },
update: {
supplierId,
userId: firstOrder?.userId || null,
mainOrderId: firstOrder?.mainOrderId || null,
subOrderId: firstOrder?.orderId || null,
settlementId,
invoiceCode: body.invoiceCode,
orderIds: orderIdText,
invoiceStatus: 1,
requestBody: JSON.parse(JSON.stringify(body)),
deleted: 0
},
create: {
invoiceBizId: settlementId,
supplierId,
userId: firstOrder?.userId || null,
mainOrderId: firstOrder?.mainOrderId || null,
subOrderId: firstOrder?.orderId || null,
settlementId,
invoiceCode: body.invoiceCode,
orderIds: orderIdText,
invoiceStatus: 1,
requestBody: JSON.parse(JSON.stringify(body))
}
})
})
return { resultCode: 0, message: '开票申请已受理' }
},
async queryInvoicesInfo(body: SupplierInvoiceQueryRequest) {
const supplierId = String(body.supplierId || '').trim()
const token = String(body.token || '').trim()
const settlementId = String(body.settlementId || '').trim()
const timestamp = Number(body.timestamp || 0)
if (!supplierId || !token || !timestamp || !settlementId) {
return { resultCode: 400, message: '供应商、结算单或签名信息不能为空' }
}
const supplier = await supplierDao.findActiveBySupplierId(supplierId)
if (!supplier) {
return { resultCode: 404, message: '供应商信息不存在' }
}
if (!verifySupplierOpenSignature(supplier.supplierId, supplier.clientId, timestamp, token)) {
return { resultCode: 401, message: '签名无效或已过期' }
}
const records = await prisma.supplierInvoiceInfo.findMany({
where: { supplierId, settlementId, deleted: 0 },
orderBy: { createTime: 'asc' }
})
const completedRecords = records.filter(item => item.invoiceStatus === 2)
const failedRecords = records.filter(item => item.invoiceStatus === 3)
const application = records.find(item => item.invoiceStatus === 1)
const applicationOrderIds = String(application?.orderIds || '')
const invoiceInfos = completedRecords.flatMap(record => {
const payload = record.requestBody && typeof record.requestBody === 'object'
? record.requestBody as Record<string, unknown>
: {}
const invoiceIds = String(record.invoiceIds || record.invoiceBizId)
.split(',')
.map(item => item.trim())
.filter(Boolean)
return invoiceIds.map(invoiceId => ({
invoiceId,
invoiceCode: record.invoiceCode || '',
invoiceDate: payload.invoiceDate || null,
invoiceNakeAmount: Number(payload.invoiceNakeAmount || payload.invoiceNakedPrice || 0),
invoiceTaxRate: Number(payload.invoiceTaxRate || 0),
invoiceTaxAmount: Number(payload.invoiceTaxAmount || payload.invoiceTaxPrice || 0),
invoiceAmount: Number(payload.invoiceAmount || 0),
invoiceType: Number(payload.invoiceType || 0),
url: payload.url || record.filePath || '',
imageEncode: payload.imageEncode || '',
fileType: payload.fileType || '',
checkCode: payload.checkCode || '',
invoiceAddress: payload.invoiceAddress || ''
}))
})
const completedOrderIds = completedRecords
.flatMap(item => String(item.orderIds || '').split(','))
.map(item => item.trim())
.filter(Boolean)
const sucOrderIds = completedRecords.length && !completedOrderIds.length
? applicationOrderIds.split(',').map(item => item.trim()).filter(Boolean)
: completedOrderIds
const failOrderIds = failedRecords.length ? applicationOrderIds : ''
const failMsg = failedRecords.length
? String((failedRecords[0].requestBody as Record<string, unknown> | null)?.failMsg || '开票失败')
: completedRecords.length ? '' : '发票尚未开具'
return {
resultCode: 0,
message: '成功',
getInvoiceInfo: {
bSuccess: completedRecords.length ? 0 : 1,
settlementId,
sucOrderIds: [...new Set(sucOrderIds)].join(','),
failOrderIds,
failMsg,
invoiceInfos
}
}
}
}
@@ -2,7 +2,7 @@ import { supplierDao } from '../dao/supplier.dao.js'
import { bridgeFetchWithAuth, findSupplierByIdentification, getBridgeApiBase, getBridgeToken, warmBridgeToken } from '../utils/bridge-auth.js'
import { sha256 } from '../utils/crypto.js'
import { logger } from '../utils/logger.js'
import { createSupplierOpenSignature } from '../utils/supplier-open-crypto.js'
import { createSupplierOpenSignature, sm4CbcDecrypt } from '../utils/supplier-open-crypto.js'
import { serviceFail, serviceOk } from '../types/service-result.js'
type BridgePointsResponse = {
@@ -33,11 +33,29 @@ export const supplierService = {
})
},
async warmSupplierToken(userId: string, supplierIdentification: string) {
logger.info('supplier token warmup', { userId, supplierIdentification })
async warmSupplierToken(encryptedParam: string, supplierIdentification: string) {
logger.info('supplier token warmup', { supplierIdentification })
if (!userId || !supplierIdentification) {
return serviceFail('用户信息和供应商唯一标识不能为空')
if (!encryptedParam || !supplierIdentification) {
return serviceFail('加密用户参数和供应商唯一标识不能为空')
}
const keyHex = process.env.SUPPLIER_OPEN_SM4_KEY_HEX || process.env.SM4CBC_KEY_HEX || ''
const ivHex = process.env.SUPPLIER_OPEN_SM4_IV_HEX || process.env.SM4CBC_IV_HEX || ''
if (!keyHex || !ivHex) {
return serviceFail('供应商开放接口SM4配置缺失', 500, 'SM4_CONFIG_MISSING')
}
let userId = ''
try {
const decrypted = sm4CbcDecrypt(encryptedParam.replace(/ /g, '+'), keyHex, ivHex)
const params = JSON.parse(decrypted) as { userId?: string }
userId = String(params.userId || '').trim()
} catch (error) {
return serviceFail('用户参数解密失败', 400, 'INVALID_USER_PARAM')
}
if (!userId) {
return serviceFail('用户信息不能为空', 400, 'USER_ID_MISSING')
}
const supplier = await findSupplierByIdentification(supplierIdentification).catch(error => error)