feat: 积分商城-供应商-对账
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user