feat: 基本实现拆单收货
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return ok({ message: '加入购物车接口待实现' })
|
||||
})
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return ok({ message: '登录接口待实现' })
|
||||
})
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderCancelMainRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
}
|
||||
|
||||
type BridgeOrderResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderCancelMainRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const mainOrderId = String(body.mainOrderId || body.orderId || '').trim()
|
||||
const orderId = String(body.orderId || mainOrderId).trim()
|
||||
|
||||
if (!supplierId || !userId || !mainOrderId) {
|
||||
return fail('供应商、用户或订单信息不能为空')
|
||||
}
|
||||
|
||||
const order = await prisma.supplierOrderMain.findFirst({
|
||||
where: {
|
||||
orderId: mainOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return fail('主单不存在或已取消', 404, 'MAIN_ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
if (order.orderState !== ORDER_STATE.PENDING_PAY) {
|
||||
return fail('已支付主单不能通过未支付取消接口取消', 400, 'MAIN_ORDER_ALREADY_PAID')
|
||||
}
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId: mainOrderId },
|
||||
data: {
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: {
|
||||
mainOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
},
|
||||
data: {
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { response: bridgeResponse, payload: bridgePayload } = await bridgeFetchWithAuth<BridgeOrderResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pushOrderInfoMsg',
|
||||
{
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_MAIN,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId,
|
||||
time: Date.now()
|
||||
}
|
||||
)
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '取消主单推送失败', 502, 'BRIDGE_CANCEL_MAIN_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
supplierId,
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_MAIN,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_SPLIT_STATE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderCancelPaidMainRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
}
|
||||
|
||||
type BridgeOrderResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderCancelPaidMainRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const mainOrderId = String(body.mainOrderId || body.orderId || '').trim()
|
||||
const orderId = String(body.orderId || mainOrderId).trim()
|
||||
|
||||
if (!supplierId || !userId || !mainOrderId) {
|
||||
return fail('供应商、用户或订单信息不能为空')
|
||||
}
|
||||
|
||||
const order = await prisma.supplierOrderMain.findFirst({
|
||||
where: {
|
||||
orderId: mainOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return fail('主单不存在或已取消', 404, 'MAIN_ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
if (order.orderState === ORDER_STATE.PENDING_PAY) {
|
||||
return fail('未支付主单请使用未支付取消接口', 400, 'MAIN_ORDER_NOT_PAID')
|
||||
}
|
||||
|
||||
if (order.orderState !== ORDER_STATE.FINISH_PAY || order.isSplit === ORDER_SPLIT_STATE.SPLIT) {
|
||||
return fail('该主单已拆单,不能按已支付未拆单整单取消', 400, 'MAIN_ORDER_ALREADY_SPLIT')
|
||||
}
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId: mainOrderId },
|
||||
data: {
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: {
|
||||
mainOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
},
|
||||
data: {
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { response: bridgeResponse, payload: bridgePayload } = await bridgeFetchWithAuth<BridgeOrderResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pushOrderInfoMsg',
|
||||
{
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_PAID_NOT_SPLIT_MAIN,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId,
|
||||
time: Date.now()
|
||||
}
|
||||
)
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '取消已支付未拆单整单推送失败', 502, 'BRIDGE_CANCEL_PAID_MAIN_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
supplierId,
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_PAID_NOT_SPLIT_MAIN,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderCancelSubRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
}
|
||||
|
||||
type BridgeOrderResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderCancelSubRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const orderId = String(body.orderId || '').trim()
|
||||
|
||||
if (!supplierId || !userId || !orderId) {
|
||||
return fail('供应商、用户或子单信息不能为空')
|
||||
}
|
||||
|
||||
const subOrder = await prisma.supplierOrderSub.findFirst({
|
||||
where: {
|
||||
orderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
}
|
||||
})
|
||||
|
||||
if (!subOrder) {
|
||||
return fail('子单不存在或已取消', 404, 'SUB_ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
const mainOrderId = String(body.mainOrderId || subOrder.mainOrderId).trim()
|
||||
|
||||
await prisma.supplierOrderSub.update({
|
||||
where: { orderId },
|
||||
data: {
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
const { response: bridgeResponse, payload: bridgePayload } = await bridgeFetchWithAuth<BridgeOrderResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pushOrderInfoMsg',
|
||||
{
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_SUB,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId,
|
||||
time: Date.now()
|
||||
}
|
||||
)
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '取消子单推送失败', 502, 'BRIDGE_CANCEL_SUB_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
supplierId,
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_SUB,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_SPLIT_STATE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type CheckoutGoods = {
|
||||
id?: string | number
|
||||
sku?: string | number | Record<string, unknown>
|
||||
count?: number
|
||||
number?: number
|
||||
price?: string | number
|
||||
name?: string
|
||||
image?: string
|
||||
imageUrl?: string
|
||||
picInfo?: string[]
|
||||
taxRate?: number
|
||||
}
|
||||
|
||||
type OrderCreateRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
orderNo?: string
|
||||
orderNumberId?: string
|
||||
amount?: number
|
||||
goods?: CheckoutGoods[]
|
||||
}
|
||||
|
||||
type BridgeOrderResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
type OrderProductSnapshot = {
|
||||
sku: string
|
||||
number: number
|
||||
price: number
|
||||
name: string
|
||||
taxRate: number
|
||||
picInfo: string[]
|
||||
fileId: string | null
|
||||
filePath: string | null
|
||||
}
|
||||
|
||||
const nowText = () => {
|
||||
const now = new Date()
|
||||
const pad = (value: number) => String(value).padStart(2, '0')
|
||||
|
||||
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
||||
}
|
||||
|
||||
const createOrderNumberId = () => {
|
||||
const now = new Date()
|
||||
const pad = (value: number, size = 2) => String(value).padStart(size, '0')
|
||||
|
||||
return `TR${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}${pad(now.getMilliseconds(), 3)}`
|
||||
}
|
||||
|
||||
const toYuan = (centAmount?: number) => Number((Number(centAmount || 0) / 100).toFixed(2))
|
||||
const toMoney = (value: number) => Number(value.toFixed(2))
|
||||
const createSubOrderId = (orderId: string, index: number) => `${orderId}_${String(index + 1).padStart(3, '0')}`
|
||||
const createSubOrderNumberId = (orderNumberId: string, index: number) => `${orderNumberId}_${String(index + 1).padStart(3, '0')}`
|
||||
|
||||
const toOrderProducts = (goods: CheckoutGoods[]) => goods.map((item, index) => {
|
||||
const image = item.image || item.imageUrl || item.picInfo?.[0] || ''
|
||||
const sku = typeof item.sku === 'object' && item.sku
|
||||
? [item.id, item.sku.color, item.sku.size].filter(Boolean).join('-')
|
||||
: item.sku
|
||||
|
||||
return {
|
||||
sku: String(sku || item.id || `SKU_${index + 1}`),
|
||||
number: Number(item.count || item.number || 1),
|
||||
price: Number(item.price || 0),
|
||||
name: String(item.name || ''),
|
||||
taxRate: Number(item.taxRate || 0.13),
|
||||
picInfo: item.picInfo?.length ? item.picInfo : image ? [image] : [],
|
||||
fileId: image || null,
|
||||
filePath: image || null
|
||||
}
|
||||
})
|
||||
|
||||
const productSubtotal = (item: OrderProductSnapshot) => toMoney(Number(item.price || 0) * Number(item.number || 0))
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderCreateRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const orderId = String(body.orderNo || '').trim()
|
||||
const amount = toYuan(body.amount)
|
||||
const goods = Array.isArray(body.goods) ? body.goods : []
|
||||
const orderProInfos = toOrderProducts(goods)
|
||||
|
||||
if (!supplierId || !userId || !orderId) {
|
||||
return fail('供应商、用户或订单信息不能为空')
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
return fail('订单金额必须大于0')
|
||||
}
|
||||
|
||||
if (!orderProInfos.length) {
|
||||
return fail('订单商品不能为空')
|
||||
}
|
||||
|
||||
const orderNumberId = String(body.orderNumberId || createOrderNumberId())
|
||||
const orderTime = nowText()
|
||||
const localOrderState = ORDER_STATE.PENDING_PAY
|
||||
const now = new Date()
|
||||
const subOrders = orderProInfos.map((item, index) => {
|
||||
const subTotal = productSubtotal(item)
|
||||
|
||||
return {
|
||||
orderId: createSubOrderId(orderId, index),
|
||||
orderNumberId: createSubOrderNumberId(orderNumberId, index),
|
||||
product: item,
|
||||
totalPrice: subTotal,
|
||||
pointsPrice: subTotal
|
||||
}
|
||||
})
|
||||
const orderInfo = {
|
||||
orderId,
|
||||
orderNumberId,
|
||||
supplierId,
|
||||
aplName: '积分商城',
|
||||
createTime: orderTime,
|
||||
updateTime: orderTime,
|
||||
deliveryTime: null,
|
||||
orderFinishTime: null,
|
||||
completeTime: null,
|
||||
deliveryStatus: '0',
|
||||
freightCost: 0,
|
||||
wPayOrAPayFreightCost: 0,
|
||||
refund: 0,
|
||||
wPayOrAPayRefund: 0,
|
||||
logisticsOrderId: null,
|
||||
crrgBsnNm: null,
|
||||
orderState: ORDER_STATE.PENDING_PAY,
|
||||
totalPrice: amount,
|
||||
wPayOrAPay: 0,
|
||||
pointsPrice: amount,
|
||||
invoiceCode: null,
|
||||
orderProInfos
|
||||
}
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
const existingSubOrders = await tx.supplierOrderSub.findMany({
|
||||
where: { mainOrderId: orderId },
|
||||
select: { orderId: true }
|
||||
})
|
||||
const existingSubOrderIds = existingSubOrders.map(item => item.orderId)
|
||||
|
||||
if (existingSubOrderIds.length) {
|
||||
await tx.supplierOrderProduct.deleteMany({
|
||||
where: { subOrderId: { in: existingSubOrderIds } }
|
||||
})
|
||||
await tx.supplierOrderSub.deleteMany({
|
||||
where: { mainOrderId: orderId }
|
||||
})
|
||||
}
|
||||
|
||||
await tx.supplierOrderMain.upsert({
|
||||
where: { orderId },
|
||||
update: {
|
||||
orderNumberId,
|
||||
supplierId,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
updateTime: now,
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
freightCost: orderInfo.freightCost,
|
||||
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
|
||||
refund: orderInfo.refund,
|
||||
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
|
||||
orderState: localOrderState,
|
||||
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
|
||||
splitTime: null,
|
||||
totalPrice: orderInfo.totalPrice,
|
||||
wPayOrAPay: orderInfo.wPayOrAPay,
|
||||
pointsPrice: orderInfo.pointsPrice,
|
||||
invoiceCode: orderInfo.invoiceCode,
|
||||
deleted: 0
|
||||
},
|
||||
create: {
|
||||
orderId,
|
||||
orderNumberId,
|
||||
supplierId,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
createTime: now,
|
||||
updateTime: now,
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
freightCost: orderInfo.freightCost,
|
||||
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
|
||||
refund: orderInfo.refund,
|
||||
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
|
||||
orderState: localOrderState,
|
||||
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
|
||||
totalPrice: orderInfo.totalPrice,
|
||||
wPayOrAPay: orderInfo.wPayOrAPay,
|
||||
pointsPrice: orderInfo.pointsPrice,
|
||||
invoiceCode: orderInfo.invoiceCode
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderSub.createMany({
|
||||
data: subOrders.map(subOrder => ({
|
||||
orderId: subOrder.orderId,
|
||||
orderNumberId: subOrder.orderNumberId,
|
||||
mainOrderId: orderId,
|
||||
supplierId,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
createTime: now,
|
||||
updateTime: now,
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
freightCost: orderInfo.freightCost,
|
||||
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
|
||||
refund: orderInfo.refund,
|
||||
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
|
||||
orderState: localOrderState,
|
||||
totalPrice: subOrder.totalPrice,
|
||||
wPayOrAPay: 0,
|
||||
pointsPrice: subOrder.pointsPrice,
|
||||
invoiceCode: orderInfo.invoiceCode
|
||||
}))
|
||||
})
|
||||
|
||||
await tx.supplierOrderProduct.createMany({
|
||||
data: subOrders.map(subOrder => ({
|
||||
subOrderId: subOrder.orderId,
|
||||
sku: subOrder.product.sku,
|
||||
number: subOrder.product.number,
|
||||
price: subOrder.product.price,
|
||||
name: subOrder.product.name,
|
||||
taxRate: subOrder.product.taxRate,
|
||||
productImgUrl: subOrder.product.picInfo[0] || null,
|
||||
fileId: subOrder.product.fileId,
|
||||
filePath: subOrder.product.filePath,
|
||||
picInfo: subOrder.product.picInfo
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
const { response: bridgeResponse, payload: bridgePayload } = await bridgeFetchWithAuth<BridgeOrderResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pendingOrderInfo',
|
||||
{
|
||||
userId,
|
||||
orderInfo
|
||||
}
|
||||
)
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '提交待支付订单失败', 502, 'BRIDGE_PENDING_ORDER_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
orderId,
|
||||
orderNumberId,
|
||||
subOrderIds: subOrders.map(item => item.orderId),
|
||||
orderInfo
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_STATE } from '../utils/order-state.js'
|
||||
|
||||
type OrderListRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
type OrderStatusView = {
|
||||
text: string
|
||||
statusClass: string
|
||||
actionText: string
|
||||
buttonClass: string
|
||||
}
|
||||
|
||||
const statusMap: Record<number, OrderStatusView> = {
|
||||
[ORDER_STATE.PENDING_PAY]: { text: '待付款', statusClass: 'pending', actionText: '去支付', buttonClass: 'primary-action' },
|
||||
[ORDER_STATE.FINISH_PAY]: { text: '待发货', statusClass: 'pending', actionText: '查看订单', buttonClass: 'primary-action' },
|
||||
[ORDER_STATE.IN_PROGRESS]: { text: '待收货', statusClass: 'pending', actionText: '查看订单', buttonClass: 'primary-action' },
|
||||
[ORDER_STATE.CANCELLED]: { text: '已取消', statusClass: 'muted', actionText: '', buttonClass: 'dark-action' },
|
||||
[ORDER_STATE.COMPLETED]: { text: '已完成', statusClass: 'done', actionText: '再次购买', buttonClass: 'dark-action' },
|
||||
[ORDER_STATE.EXCHANGE]: { text: '换货', statusClass: 'muted', actionText: '查看订单', buttonClass: 'dark-action' },
|
||||
[ORDER_STATE.REFUNDED]: { text: '退货', statusClass: 'muted', actionText: '查看订单', buttonClass: 'dark-action' }
|
||||
}
|
||||
|
||||
const toYuanText = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => {
|
||||
if (value && typeof value === 'object' && 'toFixed' in value) {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
const firstImage = (picInfo: unknown) => {
|
||||
if (Array.isArray(picInfo)) {
|
||||
return String(picInfo[0] || '')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderListRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
|
||||
if (!supplierId || !userId) {
|
||||
return fail('供应商或用户信息不能为空')
|
||||
}
|
||||
|
||||
const mainOrders = await prisma.supplierOrderMain.findMany({
|
||||
where: {
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
},
|
||||
include: {
|
||||
subOrders: {
|
||||
where: { deleted: 0 },
|
||||
include: {
|
||||
products: {
|
||||
where: { deleted: 0 },
|
||||
orderBy: { createAt: 'asc' }
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ createTime: 'desc' },
|
||||
{ createAt: 'desc' }
|
||||
]
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ createTime: 'desc' },
|
||||
{ createAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
const orders = mainOrders.map(mainOrder => {
|
||||
const mainStatus = statusMap[mainOrder.orderState] || statusMap[ORDER_STATE.FINISH_PAY]
|
||||
const subOrders = mainOrder.subOrders.map(subOrder => {
|
||||
const status = statusMap[subOrder.orderState] || mainStatus
|
||||
const products = subOrder.products.map(product => ({
|
||||
id: product.id,
|
||||
sku: product.sku,
|
||||
name: product.name,
|
||||
number: product.number,
|
||||
price: toYuanText(product.price),
|
||||
image: product.productImgUrl || firstImage(product.picInfo)
|
||||
}))
|
||||
|
||||
return {
|
||||
id: subOrder.orderId,
|
||||
orderId: subOrder.orderId,
|
||||
orderNumberId: subOrder.orderNumberId || '',
|
||||
mainOrderId: subOrder.mainOrderId,
|
||||
supplierId: subOrder.supplierId,
|
||||
status: status.text,
|
||||
statusClass: status.statusClass,
|
||||
actionText: status.actionText,
|
||||
buttonClass: status.buttonClass,
|
||||
orderState: subOrder.orderState,
|
||||
amount: toYuanText(subOrder.totalPrice),
|
||||
count: products.reduce((total, item) => total + Number(item.number || 0), 0),
|
||||
products
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: mainOrder.orderId,
|
||||
mainOrderId: mainOrder.orderId,
|
||||
mainOrderNumberId: mainOrder.orderNumberId || '',
|
||||
supplierId: mainOrder.supplierId,
|
||||
status: mainStatus.text,
|
||||
statusClass: mainStatus.statusClass,
|
||||
orderState: mainOrder.orderState,
|
||||
isSplit: mainOrder.isSplit,
|
||||
splitTime: mainOrder.splitTime,
|
||||
canCancel: mainOrder.orderState === ORDER_STATE.PENDING_PAY,
|
||||
subOrders
|
||||
}
|
||||
})
|
||||
|
||||
return ok({ orders })
|
||||
})
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_SPLIT_STATE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderPayRequest = {
|
||||
supplierId?: string
|
||||
orderId?: string
|
||||
amount?: number
|
||||
totalPrice?: number
|
||||
wPayOrAPay?: number
|
||||
pointsPrice?: number
|
||||
}
|
||||
|
||||
type BridgePayResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
const centsToYuan = (value?: number) => Number((Number(value || 0) / 100).toFixed(2))
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderPayRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const orderId = String(body.orderId || '').trim()
|
||||
const totalPrice = Number(body.totalPrice ?? centsToYuan(body.amount))
|
||||
const wPayOrAPay = Number(body.wPayOrAPay ?? 0)
|
||||
const pointsPrice = Number(body.pointsPrice ?? totalPrice)
|
||||
|
||||
if (!supplierId || !orderId) {
|
||||
return fail('供应商或订单信息不能为空')
|
||||
}
|
||||
|
||||
if (totalPrice <= 0) {
|
||||
return fail('支付金额必须大于0')
|
||||
}
|
||||
|
||||
const order = await prisma.supplierOrderMain.findUnique({
|
||||
where: { orderId }
|
||||
})
|
||||
|
||||
if (!order || order.deleted === 1) {
|
||||
return fail('待支付订单不存在', 404, 'ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId },
|
||||
data: {
|
||||
orderState: ORDER_STATE.FINISH_PAY,
|
||||
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
|
||||
totalPrice,
|
||||
wPayOrAPay,
|
||||
pointsPrice,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: { mainOrderId: orderId, deleted: 0 },
|
||||
data: {
|
||||
orderState: ORDER_STATE.FINISH_PAY,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { response: confirmResponse, payload: confirmPayload } = await bridgeFetchWithAuth<BridgePayResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/confirmPurchase',
|
||||
{
|
||||
orderId,
|
||||
totalPrice,
|
||||
wPayOrAPay,
|
||||
pointsPrice
|
||||
}
|
||||
)
|
||||
|
||||
if (!confirmResponse.ok || confirmPayload.resultCode !== 0) {
|
||||
return fail(confirmPayload.message || '确认购买推送失败', 502, 'BRIDGE_CONFIRM_PURCHASE_FAILED')
|
||||
}
|
||||
|
||||
const { response: purchaseResultResponse, payload: purchaseResultPayload } = await bridgeFetchWithAuth<BridgePayResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/purchaseResult',
|
||||
{
|
||||
orderId,
|
||||
totalPrice,
|
||||
wPayOrAPay,
|
||||
pointsPrice,
|
||||
purchaseResult: true,
|
||||
message: '支付成功'
|
||||
}
|
||||
)
|
||||
|
||||
if (!purchaseResultResponse.ok || purchaseResultPayload.resultCode !== 0) {
|
||||
return fail(purchaseResultPayload.message || '支付结果通知推送失败', 502, 'BRIDGE_PURCHASE_RESULT_FAILED')
|
||||
}
|
||||
|
||||
const subOrders = await prisma.supplierOrderSub.findMany({
|
||||
where: {
|
||||
mainOrderId: orderId,
|
||||
deleted: 0
|
||||
},
|
||||
select: {
|
||||
orderId: true
|
||||
}
|
||||
})
|
||||
const subOrderIds = subOrders.map(item => item.orderId).filter(Boolean).join(',')
|
||||
|
||||
if (subOrderIds) {
|
||||
await prisma.$transaction(async tx => {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId },
|
||||
data: {
|
||||
isSplit: ORDER_SPLIT_STATE.SPLIT,
|
||||
splitTime: new Date(),
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: { mainOrderId: orderId, deleted: 0 },
|
||||
data: {
|
||||
orderState: ORDER_STATE.IN_PROGRESS,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { response: splitResponse, payload: splitPayload } = await bridgeFetchWithAuth<BridgePayResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pushOrderInfoMsg',
|
||||
{
|
||||
orderType: ORDER_EVENT_TYPE.SPLIT_SUB_ORDER,
|
||||
orderId: subOrderIds,
|
||||
mainOrderId: orderId,
|
||||
userId: order.userId,
|
||||
time: Date.now()
|
||||
}
|
||||
)
|
||||
|
||||
if (!splitResponse.ok || splitPayload.resultCode !== 0) {
|
||||
return fail(splitPayload.message || '推送拆单信息失败', 502, 'BRIDGE_SPLIT_ORDER_FAILED')
|
||||
}
|
||||
}
|
||||
|
||||
return ok({
|
||||
orderId,
|
||||
subOrderIds,
|
||||
totalPrice,
|
||||
wPayOrAPay,
|
||||
pointsPrice
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import {
|
||||
ORDER_EVENT_TYPE,
|
||||
ORDER_OPERATE_TYPE,
|
||||
ORDER_PUSH_STATUS,
|
||||
ORDER_STATE
|
||||
} from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderPushEventRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
orderType?: number
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
subOrderId?: string
|
||||
time?: number
|
||||
amount?: number
|
||||
reason?: string
|
||||
settlementId?: string
|
||||
invoiceBizId?: string
|
||||
invoiceCode?: string
|
||||
invoiceIds?: string
|
||||
orderIds?: string
|
||||
fileId?: string
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
type BridgeOrderResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
const PUSH_API_PATH = '/pointOrder/pushOrderInfoMsg'
|
||||
const supportedOrderTypes = new Set<number>([
|
||||
ORDER_EVENT_TYPE.RETURN_ORDER,
|
||||
ORDER_EVENT_TYPE.EXCHANGE_ORDER,
|
||||
ORDER_EVENT_TYPE.CONFIRM_RECEIVE,
|
||||
ORDER_EVENT_TYPE.INVOICE_INFO
|
||||
])
|
||||
|
||||
const splitIds = (value?: string) => String(value || '')
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const eventState = (orderType: number) => {
|
||||
if (orderType === ORDER_EVENT_TYPE.RETURN_ORDER) return ORDER_STATE.REFUNDED
|
||||
if (orderType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) return ORDER_STATE.EXCHANGE
|
||||
if (orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE) return ORDER_STATE.COMPLETED
|
||||
return null
|
||||
}
|
||||
|
||||
const operateType = (orderType: number) => {
|
||||
if (orderType === ORDER_EVENT_TYPE.RETURN_ORDER) return ORDER_OPERATE_TYPE.REFUND
|
||||
if (orderType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) return ORDER_OPERATE_TYPE.EXCHANGE
|
||||
if (orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE) return ORDER_OPERATE_TYPE.COMPLETE
|
||||
if (orderType === ORDER_EVENT_TYPE.INVOICE_INFO) return ORDER_OPERATE_TYPE.INVOICE_INFO
|
||||
return orderType
|
||||
}
|
||||
|
||||
const buildBridgeBody = (body: OrderPushEventRequest) => ({
|
||||
supplierId: String(body.supplierId || '').trim(),
|
||||
orderType: Number(body.orderType),
|
||||
orderId: String(body.orderId || '').trim(),
|
||||
mainOrderId: String(body.mainOrderId || '').trim(),
|
||||
userId: String(body.userId || '').trim(),
|
||||
time: Number(body.time || Date.now())
|
||||
})
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderPushEventRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const orderType = Number(body.orderType)
|
||||
const orderId = String(body.orderId || '').trim()
|
||||
const mainOrderId = String(body.mainOrderId || '').trim()
|
||||
|
||||
if (!supplierId || !userId || !orderType || !orderId) {
|
||||
return fail('供应商、用户或订单事件信息不能为空')
|
||||
}
|
||||
|
||||
if (!supportedOrderTypes.has(orderType)) {
|
||||
return fail('暂不支持该订单事件类型', 400, 'ORDER_EVENT_NOT_SUPPORTED')
|
||||
}
|
||||
|
||||
const bridgeBody = buildBridgeBody(body)
|
||||
const now = new Date()
|
||||
let pushLogId = 0
|
||||
let affectedSubOrderIds: string[] = []
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
const mainOrder = mainOrderId
|
||||
? await tx.supplierOrderMain.findFirst({
|
||||
where: { orderId: mainOrderId, supplierId, deleted: 0 }
|
||||
})
|
||||
: null
|
||||
|
||||
const explicitSubIds = splitIds(body.subOrderId || '')
|
||||
const orderIds = splitIds(orderId)
|
||||
const subOrdersByOrderId = await tx.supplierOrderSub.findMany({
|
||||
where: {
|
||||
supplierId,
|
||||
deleted: 0,
|
||||
OR: [
|
||||
{ orderId: { in: orderIds } },
|
||||
...(explicitSubIds.length ? [{ orderId: { in: explicitSubIds } }] : []),
|
||||
...(mainOrderId ? [{ mainOrderId }] : [])
|
||||
]
|
||||
},
|
||||
orderBy: { createAt: 'asc' }
|
||||
})
|
||||
|
||||
const directlyMatched = subOrdersByOrderId.filter(item => orderIds.includes(item.orderId) || explicitSubIds.includes(item.orderId))
|
||||
const mainOrderSubs = mainOrderId ? subOrdersByOrderId.filter(item => item.mainOrderId === mainOrderId) : []
|
||||
const targetSubOrders = directlyMatched.length ? directlyMatched : mainOrderSubs.length === 1 ? mainOrderSubs : []
|
||||
affectedSubOrderIds = targetSubOrders.map(item => item.orderId)
|
||||
|
||||
const nextState = eventState(orderType)
|
||||
if (nextState !== null && targetSubOrders.length) {
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: {
|
||||
orderId: { in: affectedSubOrderIds },
|
||||
supplierId,
|
||||
deleted: 0
|
||||
},
|
||||
data: {
|
||||
orderState: nextState,
|
||||
orderFinishTime: orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE ? now : undefined,
|
||||
completeTime: orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE ? now : undefined,
|
||||
updateTime: now
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderOperateLog.createMany({
|
||||
data: targetSubOrders.map(subOrder => ({
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: subOrder.mainOrderId,
|
||||
subOrderId: subOrder.orderId,
|
||||
operateType: operateType(orderType),
|
||||
beforeState: subOrder.orderState,
|
||||
afterState: nextState,
|
||||
remark: body.reason || null,
|
||||
operator: 'system'
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
if (orderType === ORDER_EVENT_TYPE.RETURN_ORDER || orderType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) {
|
||||
await tx.supplierRefundExchange.upsert({
|
||||
where: { serviceOrderId: orderId },
|
||||
update: {
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || targetSubOrders[0]?.mainOrderId || null,
|
||||
subOrderId: targetSubOrders[0]?.orderId || null,
|
||||
serviceType: orderType,
|
||||
serviceState: 1,
|
||||
amount: Number(body.amount || targetSubOrders[0]?.totalPrice || 0),
|
||||
reason: body.reason || null,
|
||||
requestBody: body,
|
||||
deleted: 0
|
||||
},
|
||||
create: {
|
||||
serviceOrderId: orderId,
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || targetSubOrders[0]?.mainOrderId || null,
|
||||
subOrderId: targetSubOrders[0]?.orderId || null,
|
||||
serviceType: orderType,
|
||||
serviceState: 1,
|
||||
amount: Number(body.amount || targetSubOrders[0]?.totalPrice || 0),
|
||||
reason: body.reason || null,
|
||||
requestBody: body
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (orderType === ORDER_EVENT_TYPE.INVOICE_INFO) {
|
||||
const invoiceBizId = String(body.invoiceBizId || body.invoiceCode || orderId).trim()
|
||||
await tx.supplierInvoiceInfo.upsert({
|
||||
where: { invoiceBizId },
|
||||
update: {
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || null,
|
||||
subOrderId: targetSubOrders[0]?.orderId || null,
|
||||
settlementId: body.settlementId || null,
|
||||
invoiceCode: body.invoiceCode || null,
|
||||
invoiceIds: body.invoiceIds || orderId,
|
||||
orderIds: body.orderIds || mainOrderId || targetSubOrders[0]?.orderId || null,
|
||||
invoiceStatus: 2,
|
||||
fileId: body.fileId || null,
|
||||
filePath: body.filePath || null,
|
||||
requestBody: body,
|
||||
deleted: 0
|
||||
},
|
||||
create: {
|
||||
invoiceBizId,
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || null,
|
||||
subOrderId: targetSubOrders[0]?.orderId || null,
|
||||
settlementId: body.settlementId || null,
|
||||
invoiceCode: body.invoiceCode || null,
|
||||
invoiceIds: body.invoiceIds || orderId,
|
||||
orderIds: body.orderIds || mainOrderId || targetSubOrders[0]?.orderId || null,
|
||||
invoiceStatus: 2,
|
||||
fileId: body.fileId || null,
|
||||
filePath: body.filePath || null,
|
||||
requestBody: body
|
||||
}
|
||||
})
|
||||
|
||||
if (body.invoiceCode && mainOrder) {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId: mainOrder.orderId },
|
||||
data: { invoiceCode: body.invoiceCode, updateTime: now }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE && mainOrderId) {
|
||||
const unfinishedCount = await tx.supplierOrderSub.count({
|
||||
where: {
|
||||
mainOrderId,
|
||||
deleted: 0,
|
||||
orderState: { notIn: [ORDER_STATE.COMPLETED, ORDER_STATE.CANCELLED, ORDER_STATE.REFUNDED] }
|
||||
}
|
||||
})
|
||||
if (unfinishedCount === 0) {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId: mainOrderId },
|
||||
data: {
|
||||
orderState: ORDER_STATE.COMPLETED,
|
||||
completeTime: now,
|
||||
updateTime: now
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const pushLog = await tx.supplierOrderPushLog.create({
|
||||
data: {
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || targetSubOrders[0]?.mainOrderId || null,
|
||||
orderId,
|
||||
orderType,
|
||||
apiPath: PUSH_API_PATH,
|
||||
requestBody: bridgeBody,
|
||||
pushStatus: ORDER_PUSH_STATUS.PENDING
|
||||
}
|
||||
})
|
||||
pushLogId = pushLog.id
|
||||
})
|
||||
|
||||
let bridgeResponse: Response | null = null
|
||||
let bridgePayload: BridgeOrderResponse = {}
|
||||
try {
|
||||
const result = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, PUSH_API_PATH, bridgeBody)
|
||||
bridgeResponse = result.response
|
||||
bridgePayload = result.payload
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '订单事件推送失败'
|
||||
await prisma.supplierOrderPushLog.update({
|
||||
where: { id: pushLogId },
|
||||
data: {
|
||||
pushStatus: ORDER_PUSH_STATUS.FAILED,
|
||||
errorMessage: message,
|
||||
lastPushTime: new Date()
|
||||
}
|
||||
})
|
||||
return ok({ localSaved: true, pushSuccess: false, message, affectedSubOrderIds })
|
||||
}
|
||||
|
||||
const pushSuccess = bridgeResponse.ok && bridgePayload.resultCode === 0
|
||||
|
||||
await prisma.supplierOrderPushLog.update({
|
||||
where: { id: pushLogId },
|
||||
data: {
|
||||
responseBody: bridgePayload,
|
||||
httpStatus: bridgeResponse.status,
|
||||
resultCode: bridgePayload.resultCode,
|
||||
pushStatus: pushSuccess ? ORDER_PUSH_STATUS.SUCCESS : ORDER_PUSH_STATUS.FAILED,
|
||||
errorMessage: pushSuccess ? null : bridgePayload.message || '订单事件推送失败',
|
||||
lastPushTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
return ok({
|
||||
localSaved: true,
|
||||
pushSuccess,
|
||||
orderType,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
affectedSubOrderIds,
|
||||
message: pushSuccess ? '订单事件已保存并推送成功' : bridgePayload.message || '订单事件已保存,推送失败'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
|
||||
const toProductDetail = (product: NonNullable<Awaited<ReturnType<typeof prisma.supplierProduct.findFirst>>>) => {
|
||||
const picInfo = Array.isArray(product.picInfo) ? product.picInfo : []
|
||||
const image = product.imageUrl || String(picInfo[0] || '')
|
||||
const price = product.price.toFixed(2)
|
||||
|
||||
return {
|
||||
id: product.id,
|
||||
sku: product.sku,
|
||||
number: product.number,
|
||||
price,
|
||||
name: product.name,
|
||||
taxRate: Number(product.taxRate),
|
||||
picInfo,
|
||||
desc: product.description || '',
|
||||
detail: product.detail || product.description || '',
|
||||
originPrice: product.originPrice ? product.originPrice.toFixed(2) : price,
|
||||
image,
|
||||
imageUrl: image,
|
||||
categoryId: product.categoryId || '',
|
||||
category: product.category || '',
|
||||
tag: product.tag || '',
|
||||
sales: product.sales || '',
|
||||
stock: product.stock
|
||||
}
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['GET'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const url = new URL(request.url)
|
||||
const id = (url.searchParams.get('id') || '').trim()
|
||||
const sku = (url.searchParams.get('sku') || '').trim()
|
||||
const numericId = Number(id)
|
||||
const querySku = sku || (id && !Number.isInteger(numericId) ? id : '')
|
||||
|
||||
if (!id && !sku) {
|
||||
return fail('商品参数不能为空')
|
||||
}
|
||||
|
||||
const product = await prisma.supplierProduct.findFirst({
|
||||
where: {
|
||||
enabled: true,
|
||||
deleted: 0,
|
||||
OR: [
|
||||
...(Number.isInteger(numericId) && numericId > 0 ? [{ id: numericId }] : []),
|
||||
...(querySku ? [{ sku: querySku }] : [])
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
return fail('商品不存在或已下架', 404, 'PRODUCT_NOT_FOUND')
|
||||
}
|
||||
|
||||
return ok(toProductDetail(product))
|
||||
})
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
|
||||
const categories = [
|
||||
{ id: 'recommend', name: '推荐' },
|
||||
{ id: 'homeLiving', name: '居家生活' },
|
||||
{ id: 'digitalAppliance', name: '数码家电' },
|
||||
{ id: 'beautyCare', name: '美妆个护' },
|
||||
{ id: 'foodDrink', name: '食品饮料' },
|
||||
{ id: 'motherBaby', name: '母婴玩具' },
|
||||
{ id: 'fashionBag', name: '服饰鞋包' },
|
||||
{ id: 'sportsOutdoor', name: '运动户外' }
|
||||
]
|
||||
|
||||
const toProductItem = (product: Awaited<ReturnType<typeof prisma.supplierProduct.findMany>>[number]) => {
|
||||
const picInfo = Array.isArray(product.picInfo) ? product.picInfo : []
|
||||
const image = product.imageUrl || String(picInfo[0] || '')
|
||||
const price = product.price.toFixed(2)
|
||||
|
||||
return {
|
||||
id: product.id,
|
||||
sku: product.sku,
|
||||
number: product.number,
|
||||
price,
|
||||
name: product.name,
|
||||
taxRate: Number(product.taxRate),
|
||||
picInfo,
|
||||
desc: product.description || '',
|
||||
detail: product.detail || product.description || '',
|
||||
originPrice: product.originPrice ? product.originPrice.toFixed(2) : price,
|
||||
image,
|
||||
imageUrl: image,
|
||||
categoryId: product.categoryId || '',
|
||||
category: product.category || '',
|
||||
tag: product.tag || '',
|
||||
sales: product.sales || '',
|
||||
stock: product.stock
|
||||
}
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['GET'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const url = new URL(request.url)
|
||||
const keyword = (url.searchParams.get('keyword') || '').trim()
|
||||
const categoryId = (url.searchParams.get('categoryId') || '').trim()
|
||||
|
||||
const products = await prisma.supplierProduct.findMany({
|
||||
where: {
|
||||
enabled: true,
|
||||
deleted: 0,
|
||||
...(keyword ? { name: { contains: keyword } } : {}),
|
||||
...(categoryId && categoryId !== 'recommend' ? { categoryId } : {})
|
||||
},
|
||||
orderBy: [
|
||||
{ sortOrder: 'asc' },
|
||||
{ createdAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
return ok({
|
||||
categories,
|
||||
products: products.map(toProductItem)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import {
|
||||
findSupplierByIdentification,
|
||||
getBridgeApiBase,
|
||||
getBridgeToken
|
||||
} from '../utils/bridge-auth.js'
|
||||
import { sha256 } from '../utils/crypto.js'
|
||||
import { createSupplierOpenSignature } from '../utils/supplier-open-crypto.js'
|
||||
|
||||
type SupplierDebugTokensRequest = {
|
||||
supplierIdentification?: string
|
||||
forceRefreshZhghToken?: boolean
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<SupplierDebugTokensRequest>(request)
|
||||
const supplierIdentification = String(body.supplierIdentification || '').trim().toLowerCase()
|
||||
|
||||
if (!supplierIdentification) {
|
||||
return fail('供应商唯一标识不能为空')
|
||||
}
|
||||
|
||||
const supplier = await findSupplierByIdentification(supplierIdentification).catch(error => error)
|
||||
|
||||
if (supplier instanceof Error) {
|
||||
return fail(supplier.message || '供应商信息未配置或已停用', 404, 'SUPPLIER_NOT_CONFIGURED')
|
||||
}
|
||||
|
||||
const timestamp = Date.now()
|
||||
const orderType = 2
|
||||
const orderId = 'SUB_ORDER_ID_1,SUB_ORDER_ID_2'
|
||||
const mainOrderId = 'MAIN_ORDER_ID'
|
||||
const userId = 'USER_ID'
|
||||
const supplierOpenToken = createSupplierOpenSignature(supplier.supplierId, supplier.clientId, timestamp)
|
||||
const zhghBridgeToken = await getBridgeToken(supplier.supplierId, Boolean(body.forceRefreshZhghToken))
|
||||
|
||||
return ok({
|
||||
supplierOpen: {
|
||||
name: 'supplierOpenToken',
|
||||
usage: '调用供应商开放接口 /supplierOpenApi/pointOrder/queryOrderInfo',
|
||||
supplierId: supplier.supplierId,
|
||||
clientId: supplier.clientId,
|
||||
timestamp,
|
||||
token: supplierOpenToken,
|
||||
orderType,
|
||||
orderId,
|
||||
queryOrderInfoBody: {
|
||||
supplierId: supplier.supplierId,
|
||||
token: supplierOpenToken,
|
||||
orderType,
|
||||
orderId,
|
||||
timestamp
|
||||
}
|
||||
},
|
||||
zhghBridge: {
|
||||
name: 'zhghBridgeToken',
|
||||
usage: '调用 zhgh /open/pointOrder/* 桥接接口',
|
||||
apiBase: getBridgeApiBase(),
|
||||
supplierId: supplier.supplierId,
|
||||
clientId: supplier.clientId,
|
||||
timestamp,
|
||||
sendSha256: sha256(`${supplier.clientId}${supplier.clientSecret}${timestamp}`),
|
||||
token: zhghBridgeToken,
|
||||
splitOrderBody: {
|
||||
supplierId: supplier.supplierId,
|
||||
token: zhghBridgeToken,
|
||||
orderType,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId,
|
||||
time: timestamp
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
|
||||
type SupplierInfoRequest = {
|
||||
supplierIdentification?: string
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<SupplierInfoRequest>(request)
|
||||
const supplierIdentification = String(body.supplierIdentification || '').trim().toLowerCase()
|
||||
|
||||
if (!supplierIdentification) {
|
||||
return fail('供应商唯一标识不能为空')
|
||||
}
|
||||
|
||||
const supplier = await prisma.supplierInfo.findUnique({
|
||||
where: { supplierIdentification }
|
||||
})
|
||||
|
||||
if (!supplier || supplier.deleted === 1) {
|
||||
return fail('供应商信息未配置或已停用', 404, 'SUPPLIER_NOT_CONFIGURED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
id: supplier.id,
|
||||
supplierIdentification: supplier.supplierIdentification,
|
||||
supplierId: supplier.supplierId,
|
||||
supplierName: supplier.supplierName
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { sm4CbcDecrypt, verifySupplierOpenSignature } from '../utils/supplier-open-crypto.js'
|
||||
|
||||
type SupplierOrderQueryRequest = {
|
||||
supplierId?: string
|
||||
token?: string
|
||||
orderType?: number
|
||||
orderId?: string
|
||||
timestamp?: number
|
||||
AplParmObjct?: string
|
||||
}
|
||||
|
||||
const success = (orderInfo: unknown[]) => Response.json({
|
||||
resultCode: 0,
|
||||
message: '成功',
|
||||
orderInfo
|
||||
})
|
||||
|
||||
const fail = (resultCode: number, message: string) => Response.json({
|
||||
resultCode,
|
||||
message,
|
||||
orderInfo: []
|
||||
})
|
||||
|
||||
const parseBody = (body: SupplierOrderQueryRequest) => {
|
||||
if (!body.AplParmObjct) 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 || ''
|
||||
if (!keyHex || !ivHex) {
|
||||
throw new Error('供应商开放接口SM4配置缺失')
|
||||
}
|
||||
|
||||
return JSON.parse(sm4CbcDecrypt(body.AplParmObjct, keyHex, ivHex)) as SupplierOrderQueryRequest
|
||||
}
|
||||
|
||||
const toText = (date?: Date | null) => {
|
||||
if (!date) return null
|
||||
const pad = (value: number) => String(value).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const toMoney = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => Number(value || 0).toFixed(2)
|
||||
|
||||
const productSku = (product: { sku: string; subOrderId: string; name: string }) => {
|
||||
if (product.sku && product.sku !== '[object Object]') {
|
||||
return product.sku
|
||||
}
|
||||
|
||||
return `${product.subOrderId}-${product.name || 'SKU'}`
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
let body: SupplierOrderQueryRequest
|
||||
try {
|
||||
body = parseBody(await readJson<SupplierOrderQueryRequest>(request))
|
||||
} catch (error) {
|
||||
return fail(400, error instanceof Error ? error.message : '请求参数解密失败')
|
||||
}
|
||||
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const token = String(body.token || '').trim()
|
||||
const orderIds = String(body.orderId || '')
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
const timestamp = Number(body.timestamp || 0)
|
||||
|
||||
if (!supplierId || !token || !timestamp || !orderIds.length) {
|
||||
return fail(400, '供应商、订单或签名信息不能为空')
|
||||
}
|
||||
|
||||
const supplier = await prisma.supplierInfo.findFirst({
|
||||
where: { supplierId, deleted: 0 }
|
||||
})
|
||||
if (!supplier) {
|
||||
return fail(404, '供应商信息不存在')
|
||||
}
|
||||
|
||||
if (!verifySupplierOpenSignature(supplier.supplierId, supplier.clientId, timestamp, token)) {
|
||||
return fail(401, '签名无效或已过期')
|
||||
}
|
||||
|
||||
const orders = await prisma.supplierOrderSub.findMany({
|
||||
where: {
|
||||
supplierId,
|
||||
orderId: { in: orderIds },
|
||||
deleted: 0
|
||||
},
|
||||
include: {
|
||||
products: {
|
||||
where: { deleted: 0 },
|
||||
orderBy: { createAt: 'asc' }
|
||||
}
|
||||
},
|
||||
orderBy: { createAt: 'asc' }
|
||||
})
|
||||
|
||||
const orderInfo = orders.map(order => ({
|
||||
orderId: order.orderId,
|
||||
orderNumberId: order.orderNumberId,
|
||||
supplierId: order.supplierId,
|
||||
token: order.token,
|
||||
aplName: order.aplName,
|
||||
createTime: toText(order.createTime),
|
||||
updateTime: toText(order.updateTime),
|
||||
deliveryTime: toText(order.deliveryTime),
|
||||
orderFinishTime: toText(order.orderFinishTime),
|
||||
completeTime: toText(order.completeTime),
|
||||
deliveryStatus: order.deliveryStatus,
|
||||
freightCost: toMoney(order.freightCost),
|
||||
wPayOrAPayFreightCost: toMoney(order.wPayOrAPayFreightCost),
|
||||
refund: toMoney(order.refund),
|
||||
wPayOrAPayRefund: toMoney(order.wPayOrAPayRefund),
|
||||
logisticsOrderId: order.logisticsOrderId,
|
||||
crrgBsnNm: order.crrgBsnNm,
|
||||
orderState: order.orderState,
|
||||
totalPrice: toMoney(order.totalPrice),
|
||||
wPayOrAPay: toMoney(order.wPayOrAPay),
|
||||
pointsPrice: toMoney(order.pointsPrice),
|
||||
invoiceCode: order.invoiceCode,
|
||||
orderProInfos: order.products.map(product => ({
|
||||
sku: productSku(product),
|
||||
number: product.number,
|
||||
price: toMoney(product.price),
|
||||
name: product.name,
|
||||
taxRate: Number(product.taxRate || 0),
|
||||
picInfo: Array.isArray(product.picInfo) ? product.picInfo : product.productImgUrl ? [product.productImgUrl] : [],
|
||||
fileId: product.fileId,
|
||||
filePath: product.filePath
|
||||
}))
|
||||
}))
|
||||
|
||||
return success(orderInfo)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type SupplierPointsRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
type BridgePointsResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
userPoints?: number | string
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<SupplierPointsRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
|
||||
if (!supplierId || !userId) {
|
||||
return fail('供应商或用户信息不能为空')
|
||||
}
|
||||
|
||||
const { response, payload: pointsPayload } = await bridgeFetchWithAuth<BridgePointsResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/queryPoints',
|
||||
{ userId }
|
||||
)
|
||||
|
||||
if (!response.ok || pointsPayload.resultCode !== 0) {
|
||||
return fail(pointsPayload.message || '查询用户积分失败', 502, 'BRIDGE_POINTS_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
userPoints: Number(pointsPayload.userPoints || 0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { findSupplierByIdentification, warmBridgeToken } from '../utils/bridge-auth.js'
|
||||
|
||||
type SupplierTokenRequest = {
|
||||
userId?: string
|
||||
supplierIdentification?: string
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<SupplierTokenRequest>(request)
|
||||
const userId = String(body.userId || '').trim()
|
||||
const supplierIdentification = String(body.supplierIdentification || '').trim().toLowerCase()
|
||||
|
||||
if (!userId || !supplierIdentification) {
|
||||
return fail('用户信息和供应商唯一标识不能为空')
|
||||
}
|
||||
|
||||
const supplier = await findSupplierByIdentification(supplierIdentification).catch(error => error)
|
||||
|
||||
if (supplier instanceof Error) {
|
||||
return fail(supplier.message || '供应商信息未配置或已停用', 404, 'SUPPLIER_NOT_CONFIGURED')
|
||||
}
|
||||
|
||||
try {
|
||||
await warmBridgeToken(supplier)
|
||||
} catch (error) {
|
||||
return fail(error instanceof Error ? error.message : '获取平台授权令牌失败', 502, 'BRIDGE_TOKEN_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
supplierIdentification,
|
||||
supplierId: supplier.supplierId,
|
||||
supplierName: supplier.supplierName,
|
||||
userId
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return ok({ message: '上传接口待实现' })
|
||||
})
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['GET'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
return ok({ message: '用户信息接口待实现' })
|
||||
})
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'dotenv/config'
|
||||
import Koa from 'koa'
|
||||
import Router from '@koa/router'
|
||||
import bodyParser from 'koa-bodyparser'
|
||||
import type { FunctionHandler } from './utils/handler.js'
|
||||
import cartAdd from './functions/cart-add.js'
|
||||
import login from './functions/login.js'
|
||||
import orderCancelMain from './functions/order-cancel-main.js'
|
||||
import orderCancelPaidMain from './functions/order-cancel-paid-main.js'
|
||||
import orderCancelSub from './functions/order-cancel-sub.js'
|
||||
import orderCreate from './functions/order-create.js'
|
||||
import orderList from './functions/order-list.js'
|
||||
import orderPay from './functions/order-pay.js'
|
||||
import orderPushEvent from './functions/order-push-event.js'
|
||||
import productDetail from './functions/product-detail.js'
|
||||
import productList from './functions/product-list.js'
|
||||
import supplierDebugTokens from './functions/supplier-debug-tokens.js'
|
||||
import supplierInfo from './functions/supplier-info.js'
|
||||
import supplierOrderInfo from './functions/supplier-order-info.js'
|
||||
import supplierPoints from './functions/supplier-points.js'
|
||||
import supplierToken from './functions/supplier-token.js'
|
||||
import upload from './functions/upload.js'
|
||||
import userInfo from './functions/user-info.js'
|
||||
|
||||
const app = new Koa()
|
||||
const router = new Router()
|
||||
const port = Number(process.env.PORT || 8888)
|
||||
|
||||
const handlers: Record<string, FunctionHandler> = {
|
||||
'cart-add': cartAdd,
|
||||
login,
|
||||
'order-cancel-main': orderCancelMain,
|
||||
'order-cancel-paid-main': orderCancelPaidMain,
|
||||
'order-cancel-sub': orderCancelSub,
|
||||
'order-create': orderCreate,
|
||||
'order-list': orderList,
|
||||
'order-pay': orderPay,
|
||||
'order-push-event': orderPushEvent,
|
||||
'product-detail': productDetail,
|
||||
'product-list': productList,
|
||||
'supplier-debug-tokens': supplierDebugTokens,
|
||||
'supplier-info': supplierInfo,
|
||||
'supplier-order-info': supplierOrderInfo,
|
||||
'supplier-points': supplierPoints,
|
||||
'supplier-token': supplierToken,
|
||||
upload,
|
||||
'user-info': userInfo
|
||||
}
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
|
||||
}
|
||||
|
||||
const requestBody = (ctx: Koa.Context) => {
|
||||
if (ctx.method === 'GET' || ctx.method === 'HEAD') {
|
||||
return undefined
|
||||
}
|
||||
return JSON.stringify(ctx.request.body || {})
|
||||
}
|
||||
|
||||
const toRequest = (ctx: Koa.Context) =>
|
||||
new Request(`${ctx.protocol}://${ctx.host}${ctx.originalUrl}`, {
|
||||
method: ctx.method,
|
||||
headers: ctx.headers as HeadersInit,
|
||||
body: requestBody(ctx)
|
||||
})
|
||||
|
||||
const writeResponse = async (ctx: Koa.Context, response: Response) => {
|
||||
ctx.status = response.status
|
||||
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
||||
response.headers.forEach((value, key) => ctx.set(key, value))
|
||||
ctx.body = response.body ? await response.text() : undefined
|
||||
}
|
||||
|
||||
const dispatch = (name: string) => async (ctx: Koa.Context) => {
|
||||
const handler = handlers[name]
|
||||
if (!handler) {
|
||||
ctx.status = 404
|
||||
ctx.body = { success: false, error: { code: 'NOT_FOUND', message: '接口不存在' } }
|
||||
return
|
||||
}
|
||||
const response = await handler(toRequest(ctx), {})
|
||||
await writeResponse(ctx, response)
|
||||
}
|
||||
|
||||
app.use(bodyParser())
|
||||
app.use(async (ctx, next) => {
|
||||
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
||||
if (ctx.method === 'OPTIONS') {
|
||||
ctx.status = 204
|
||||
return
|
||||
}
|
||||
await next()
|
||||
})
|
||||
|
||||
router.all('/api/:name', async ctx => dispatch(ctx.params.name)(ctx))
|
||||
router.post('/supplierOpenApi/pointOrder/queryOrderInfo', dispatch('supplier-order-info'))
|
||||
router.post('/supplierOpenApi/debug/tokens', dispatch('supplier-debug-tokens'))
|
||||
|
||||
app.use(router.routes())
|
||||
app.use(router.allowedMethods())
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`supplied-service Koa server running at http://localhost:${port}`)
|
||||
})
|
||||
@@ -0,0 +1,203 @@
|
||||
import type { SupplierInfo } from '@prisma/client'
|
||||
import { createBridgeSignature } from './crypto.js'
|
||||
import { prisma } from './prisma.js'
|
||||
import { getRedis } from './redis.js'
|
||||
|
||||
type BridgeTokenResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
accessToken?: string
|
||||
refreshToken?: string
|
||||
expire?: number
|
||||
refreshExpire?: number
|
||||
time?: string
|
||||
}
|
||||
|
||||
type CachedBridgeToken = {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiresAt: number
|
||||
refreshExpiresAt: number
|
||||
}
|
||||
|
||||
export type BridgeResponse<T> = {
|
||||
response: Response
|
||||
payload: T
|
||||
token: string
|
||||
}
|
||||
|
||||
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
||||
const tokenKey = (supplierId: string) => `points-mall:bridge-token:${supplierId}`
|
||||
const TOKEN_EXPIRE_BUFFER_MS = 60_000
|
||||
|
||||
export const getBridgeApiBase = () => normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
||||
|
||||
export const findSupplierForBridge = async (supplierId: string) => {
|
||||
const supplier = await prisma.supplierInfo.findFirst({
|
||||
where: {
|
||||
supplierId,
|
||||
deleted: 0
|
||||
}
|
||||
})
|
||||
|
||||
if (!supplier) {
|
||||
throw new Error('供应商信息未配置或已停用')
|
||||
}
|
||||
return supplier
|
||||
}
|
||||
|
||||
export const findSupplierByIdentification = async (supplierIdentification: string) => {
|
||||
const supplier = await prisma.supplierInfo.findUnique({
|
||||
where: {
|
||||
supplierIdentification: supplierIdentification.toLowerCase()
|
||||
}
|
||||
})
|
||||
|
||||
if (!supplier || supplier.deleted === 1) {
|
||||
throw new Error('供应商信息未配置或已停用')
|
||||
}
|
||||
return supplier
|
||||
}
|
||||
|
||||
const readCachedToken = async (supplierId: string) => {
|
||||
const raw = await getRedis().get(tokenKey(supplierId))
|
||||
if (!raw) return null
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as CachedBridgeToken
|
||||
} catch (_error) {
|
||||
await getRedis().del(tokenKey(supplierId))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const writeCachedToken = async (supplierId: string, token: CachedBridgeToken) => {
|
||||
const ttlSeconds = Math.max(60, Math.floor((token.refreshExpiresAt - Date.now()) / 1000))
|
||||
await getRedis().set(tokenKey(supplierId), JSON.stringify(token), 'EX', ttlSeconds)
|
||||
}
|
||||
|
||||
const isTokenUsable = (token: CachedBridgeToken | null) =>
|
||||
Boolean(token?.accessToken && token.expiresAt - TOKEN_EXPIRE_BUFFER_MS > Date.now())
|
||||
|
||||
const isRefreshUsable = (token: CachedBridgeToken | null) =>
|
||||
Boolean(token?.refreshToken && token.refreshExpiresAt - TOKEN_EXPIRE_BUFFER_MS > Date.now())
|
||||
|
||||
const toCachedToken = (payload: BridgeTokenResponse): CachedBridgeToken => {
|
||||
const now = Date.now()
|
||||
const expire = Number(payload.expire || 0)
|
||||
const refreshExpire = Number(payload.refreshExpire || 0)
|
||||
|
||||
if (!payload.accessToken || !payload.refreshToken || expire <= 0 || refreshExpire <= 0) {
|
||||
throw new Error(payload.message || '平台授权令牌返回异常')
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: payload.accessToken,
|
||||
refreshToken: payload.refreshToken,
|
||||
expiresAt: now + expire * 1000,
|
||||
refreshExpiresAt: now + refreshExpire * 1000
|
||||
}
|
||||
}
|
||||
|
||||
const requestToken = async (supplier: SupplierInfo, refreshToken?: string) => {
|
||||
const apiBase = getBridgeApiBase()
|
||||
if (!apiBase) {
|
||||
throw new Error('平台积分商城接口地址未配置')
|
||||
}
|
||||
|
||||
const timestamp = String(Date.now())
|
||||
const signature = createBridgeSignature(supplier.clientId, supplier.clientSecret, timestamp)
|
||||
const path = refreshToken ? '/connect/refreshToken' : '/connect/token'
|
||||
const response = await fetch(`${apiBase}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
supplierId: supplier.supplierId,
|
||||
clientId: supplier.clientId,
|
||||
clientSecret: supplier.clientSecret,
|
||||
sendSha256: signature,
|
||||
refreshToken,
|
||||
code: '',
|
||||
timestamp
|
||||
})
|
||||
})
|
||||
const payload = (await response.json().catch(() => ({}))) as BridgeTokenResponse
|
||||
|
||||
if (!response.ok || payload.resultCode !== 0) {
|
||||
throw new Error(payload.message || '获取平台授权令牌失败')
|
||||
}
|
||||
|
||||
const cached = toCachedToken(payload)
|
||||
await writeCachedToken(supplier.supplierId, cached)
|
||||
return cached
|
||||
}
|
||||
|
||||
export const getBridgeToken = async (supplierId: string, forceRefresh = false) => {
|
||||
const supplier = await findSupplierForBridge(supplierId)
|
||||
const cached = await readCachedToken(supplier.supplierId)
|
||||
|
||||
if (!forceRefresh && isTokenUsable(cached)) {
|
||||
return cached!.accessToken
|
||||
}
|
||||
|
||||
if (isRefreshUsable(cached)) {
|
||||
try {
|
||||
const refreshed = await requestToken(supplier, cached!.refreshToken)
|
||||
return refreshed.accessToken
|
||||
} catch (_error) {
|
||||
await getRedis().del(tokenKey(supplier.supplierId))
|
||||
}
|
||||
}
|
||||
|
||||
const created = await requestToken(supplier)
|
||||
return created.accessToken
|
||||
}
|
||||
|
||||
export const warmBridgeToken = async (supplier: SupplierInfo) => {
|
||||
await getBridgeToken(supplier.supplierId)
|
||||
}
|
||||
|
||||
const isInvalidAccessTokenPayload = (payload: unknown) => {
|
||||
if (!payload || typeof payload !== 'object') return false
|
||||
const message = String((payload as { message?: unknown }).message || '')
|
||||
return message.includes('accessToken') || message.includes('令牌') || message.includes('token')
|
||||
}
|
||||
|
||||
export const bridgeFetchWithAuth = async <T>(
|
||||
supplierId: string,
|
||||
path: string,
|
||||
body: Record<string, unknown>
|
||||
): Promise<BridgeResponse<T>> => {
|
||||
const apiBase = getBridgeApiBase()
|
||||
if (!apiBase) {
|
||||
throw new Error('平台积分商城接口地址未配置')
|
||||
}
|
||||
|
||||
const call = async (forceRefresh = false) => {
|
||||
const token = await getBridgeToken(supplierId, forceRefresh)
|
||||
const response = await fetch(`${apiBase}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
supplierId,
|
||||
token
|
||||
})
|
||||
})
|
||||
const payload = (await response.json().catch(() => ({}))) as T
|
||||
return { response, payload, token }
|
||||
}
|
||||
|
||||
const first = await call(false)
|
||||
const resultCode = Number((first.payload as { resultCode?: unknown }).resultCode)
|
||||
if (first.response.status === 401 || resultCode === 401 || isInvalidAccessTokenPayload(first.payload)) {
|
||||
return call(true)
|
||||
}
|
||||
|
||||
return first
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
export const sha256 = (value: string) => {
|
||||
return CryptoJS.SHA256(value).toString(CryptoJS.enc.Hex)
|
||||
}
|
||||
|
||||
export const createBridgeSignature = (clientId: string, clientSecret: string, timestamp: string) => {
|
||||
return sha256(`${clientId}${clientSecret}${timestamp}`)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { fail } from './http.js'
|
||||
|
||||
export type Context = Record<string, unknown>
|
||||
|
||||
export type FunctionHandler = (request: Request, context: Context) => Promise<Response> | Response
|
||||
|
||||
export const withErrorBoundary = (handler: FunctionHandler): FunctionHandler => {
|
||||
return async (request, context) => {
|
||||
try {
|
||||
return await handler(request, context)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return fail('服务异常,请稍后再试', 500, 'INTERNAL_SERVER_ERROR')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
export type JsonBody = Record<string, unknown> | unknown[]
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
|
||||
}
|
||||
|
||||
export const json = (body: JsonBody, init: ResponseInit = {}) =>
|
||||
Response.json(body, {
|
||||
...init,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...corsHeaders,
|
||||
...(init.headers || {})
|
||||
}
|
||||
})
|
||||
|
||||
export const ok = (data: unknown = null) => json({ success: true, data })
|
||||
|
||||
export const fail = (message: string, status = 400, code = 'BAD_REQUEST') =>
|
||||
json({ success: false, error: { code, message } }, { status })
|
||||
|
||||
export const methodNotAllowed = (allowedMethods: string[]) =>
|
||||
json(
|
||||
{ success: false, error: { code: 'METHOD_NOT_ALLOWED', message: '请求方式不支持' } },
|
||||
{
|
||||
status: 405,
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
Allow: allowedMethods.join(', ')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const handleOptions = () =>
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
headers: corsHeaders
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { handleOptions, methodNotAllowed } from './http.js'
|
||||
|
||||
export const guardMethod = (request: Request, allowedMethods: string[]) => {
|
||||
if (request.method === 'OPTIONS') {
|
||||
return handleOptions()
|
||||
}
|
||||
|
||||
if (!allowedMethods.includes(request.method)) {
|
||||
return methodNotAllowed(allowedMethods)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
export const ORDER_STATE = {
|
||||
PENDING_PAY: 0,
|
||||
FINISH_PAY: 1,
|
||||
IN_PROGRESS: 2,
|
||||
CANCELLED: 3,
|
||||
RECEIVED: 4,
|
||||
COMPLETED: 5,
|
||||
EXCHANGE: 6,
|
||||
REFUNDED: 7
|
||||
} as const
|
||||
|
||||
export const ORDER_SPLIT_STATE = {
|
||||
NOT_SPLIT: 0,
|
||||
SPLIT: 1
|
||||
} as const
|
||||
|
||||
export const ORDER_EVENT_TYPE = {
|
||||
CANCEL_MAIN: 1,
|
||||
SPLIT_SUB_ORDER: 2,
|
||||
CANCEL_SUB: 3,
|
||||
RETURN_ORDER: 4,
|
||||
EXCHANGE_ORDER: 5,
|
||||
CONFIRM_RECEIVE: 6,
|
||||
CANCEL_PAID_NOT_SPLIT_MAIN: 7,
|
||||
INVOICE_INFO: 11
|
||||
} as const
|
||||
|
||||
export const POINTS_LEDGER_BIZ_TYPE = {
|
||||
PAY_CONSUME: 1,
|
||||
CANCEL_REFUND: 2,
|
||||
REFUND: 3,
|
||||
MANUAL_ADJUST: 4
|
||||
} as const
|
||||
|
||||
export const POINTS_LEDGER_DIRECTION = {
|
||||
INCREASE: 1,
|
||||
DECREASE: 2
|
||||
} as const
|
||||
|
||||
export const ORDER_PUSH_STATUS = {
|
||||
PENDING: 0,
|
||||
SUCCESS: 1,
|
||||
FAILED: 2
|
||||
} as const
|
||||
|
||||
export const ORDER_OPERATE_TYPE = {
|
||||
CREATE_PENDING: 1,
|
||||
PAY_SUCCESS: 2,
|
||||
SPLIT_SUB_ORDER: 3,
|
||||
CANCEL_MAIN: 4,
|
||||
CANCEL_SUB: 5,
|
||||
CANCEL_PAID_NOT_SPLIT_MAIN: 6,
|
||||
RECEIVE: 7,
|
||||
COMPLETE: 8,
|
||||
REFUND: 9,
|
||||
EXCHANGE: 10,
|
||||
INVOICE_INFO: 12
|
||||
} as const
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const connectionString = process.env.DATABASE_URL
|
||||
|
||||
if (!connectionString) {
|
||||
throw new Error('缺少数据库连接配置 DATABASE_URL')
|
||||
}
|
||||
|
||||
const globalForPrisma = globalThis as typeof globalThis & {
|
||||
prisma?: PrismaClient
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma || new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Redis from 'ioredis'
|
||||
|
||||
let redisClient: Redis | null = null
|
||||
|
||||
export const getRedis = () => {
|
||||
if (redisClient) return redisClient
|
||||
|
||||
const redisUrl = process.env.REDIS_URL || ''
|
||||
const db = Number(process.env.REDIS_DB || 9)
|
||||
|
||||
redisClient = redisUrl
|
||||
? new Redis(redisUrl, { db })
|
||||
: new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
db
|
||||
})
|
||||
|
||||
return redisClient
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export const readJson = async <T = unknown>(request: Request): Promise<T> => {
|
||||
const contentType = request.headers.get('content-type') || ''
|
||||
|
||||
if (!contentType.includes('application/json')) {
|
||||
return {} as T
|
||||
}
|
||||
|
||||
return request.json() as Promise<T>
|
||||
}
|
||||
|
||||
export const getBearerToken = (request: Request) => {
|
||||
const authorization = request.headers.get('authorization') || ''
|
||||
const match = authorization.match(/^Bearer\s+(.+)$/i)
|
||||
|
||||
return match?.[1] || ''
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createCipheriv, createDecipheriv, createHash, timingSafeEqual } from 'node:crypto'
|
||||
|
||||
const hexToBuffer = (value: string) => Buffer.from(value, 'hex')
|
||||
|
||||
export const sm3 = (value: string) => createHash('sm3').update(value, 'utf8').digest('hex')
|
||||
|
||||
export const createSupplierOpenSignature = (supplierId: string, clientId: string, timestamp: number | string) => {
|
||||
return sm3(`${supplierId}${clientId}${timestamp}`)
|
||||
}
|
||||
|
||||
export const verifySupplierOpenSignature = (
|
||||
supplierId: string,
|
||||
clientId: string,
|
||||
timestamp: number | string,
|
||||
token: string,
|
||||
timeWindowMs = 300_000
|
||||
) => {
|
||||
const timestampValue = Number(timestamp || 0)
|
||||
if (!timestampValue || Math.abs(Date.now() - timestampValue) > timeWindowMs) {
|
||||
return false
|
||||
}
|
||||
|
||||
const expected = createSupplierOpenSignature(supplierId, clientId, timestampValue)
|
||||
const expectedBuffer = Buffer.from(expected)
|
||||
const tokenBuffer = Buffer.from(String(token || ''))
|
||||
return expectedBuffer.length === tokenBuffer.length && timingSafeEqual(expectedBuffer, tokenBuffer)
|
||||
}
|
||||
|
||||
export const sm4CbcDecrypt = (encryptedData: string, keyHex: string, ivHex: string) => {
|
||||
const decipher = createDecipheriv('sm4-cbc', hexToBuffer(keyHex), hexToBuffer(ivHex))
|
||||
return Buffer.concat([
|
||||
decipher.update(Buffer.from(encryptedData, 'base64')),
|
||||
decipher.final()
|
||||
]).toString('utf8')
|
||||
}
|
||||
|
||||
export const sm4CbcEncrypt = (data: string, keyHex: string, ivHex: string) => {
|
||||
const cipher = createCipheriv('sm4-cbc', hexToBuffer(keyHex), hexToBuffer(ivHex))
|
||||
return Buffer.concat([
|
||||
cipher.update(Buffer.from(data, 'utf8')),
|
||||
cipher.final()
|
||||
]).toString('base64')
|
||||
}
|
||||
Reference in New Issue
Block a user