feat: 基本实现拆单收货

This commit is contained in:
2026-07-29 15:53:23 +08:00
parent c443109869
commit 1b45f9dac6
92 changed files with 3223 additions and 22968 deletions
+12 -1
View File
@@ -1,5 +1,6 @@
# MySQL connection string for Netlify Functions runtime and Prisma CLI.
# MySQL connection string for Koa runtime and Prisma CLI.
DATABASE_URL="mysql://supper:Supper%40123456@localhost:10086/supplier_db"
PRISMA_CLIENT_ENGINE_TYPE="binary"
# Optional app settings.
JWT_SECRET="replace-me"
@@ -7,3 +8,13 @@ UPLOAD_TOKEN="replace-me"
# Main points mall bridge API, for example http://localhost/open.
POINTS_MALL_OPEN_API_BASE="http://localhost/open"
# Supplier open API crypto keys used by /supplierOpenApi/pointOrder/queryOrderInfo.
SUPPLIER_OPEN_SM4_KEY_HEX="00112233445566778899aabbccddeeff"
SUPPLIER_OPEN_SM4_IV_HEX="0102030405060708090a0b0c0d0e0f10"
# Redis token cache. Redis DB 9 is used for platform bridge tokens.
REDIS_HOST="127.0.0.1"
REDIS_PORT="6379"
REDIS_PASSWORD=""
REDIS_DB="9"
-30
View File
@@ -1,30 +0,0 @@
{
"version": "5",
"remote": {
"https://edge.netlify.com/": "fd941d61d88673d5f28aab283fb86fcc50f08a3bc80ee5470498fcfa88c65cfb",
"https://edge.netlify.com/bootstrap/config.ts": "6a2ce0e544e15e8f8883a5c18da5948e37fd0f2619f68cb31f3af53c51817025",
"https://edge.netlify.com/bootstrap/context.ts": "e97240232121e2f369f6546ce961490f34d961ea1ea54be3ff09633e3f08373f",
"https://edge.netlify.com/bootstrap/cookie.ts": "8b0baae708989ca183c6f3b4ab3d029e6abcbc2e43f93edeb0ff447b3bbc3a05",
"https://edge.netlify.com/bootstrap/edge_function.ts": "b8253e86aa83c67341f5cfedeba5049d77fbf84dcab7eceff7566b7728ae9b39",
"https://edge.netlify.com/bootstrap/globals/types.ts": "eaa6148ded3121d8dee62dd91c86e7fe76601df0f3ca8d7962243a30f4c8935f"
},
"workspace": {
"packageJson": {
"dependencies": [
"npm:@neondatabase/serverless@^1.0.1",
"npm:@netlify/functions@^4.2.5",
"npm:@prisma/adapter-neon@^6.16.0",
"npm:@prisma/client@^6.16.0",
"npm:@types/crypto-js@^4.2.2",
"npm:@types/node@^24.10.1",
"npm:@types/ws@^8.18.1",
"npm:crypto-js@^4.2.0",
"npm:dotenv@^17.2.3",
"npm:netlify-cli@^23.13.3",
"npm:prisma@^6.19.3",
"npm:typescript@^5.9.3",
"npm:ws@^8.18.3"
]
}
}
}
-14
View File
@@ -1,14 +0,0 @@
[build]
command = "npm run build"
functions = "netlify/functions"
[functions]
node_bundler = "esbuild"
external_node_modules = ["@prisma/client", "@prisma/adapter-neon", "@neondatabase/serverless", "ws"]
[[headers]]
for = "/.netlify/functions/*"
[headers.values]
Access-Control-Allow-Origin = "*"
Access-Control-Allow-Headers = "Content-Type, Authorization"
Access-Control-Allow-Methods = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
@@ -1,106 +0,0 @@
import type { Context } from '@netlify/functions'
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 OrderListRequest = {
supplierId?: string
userId?: string
}
type OrderStatusView = {
text: string
statusClass: string
actionText: string
buttonClass: string
}
const statusMap: Record<number, OrderStatusView> = {
0: { text: '待付款', statusClass: 'pending', actionText: '去支付', buttonClass: 'primary-action' },
1: { text: '待发货', statusClass: 'pending', actionText: '查看订单', buttonClass: 'primary-action' },
2: { text: '待发货', statusClass: 'pending', actionText: '查看订单', buttonClass: 'primary-action' },
3: { text: '待收货', statusClass: 'pending', actionText: '确认收货', buttonClass: 'dark-action' },
4: { text: '已完成', statusClass: 'done', actionText: '再次购买', buttonClass: 'dark-action' },
5: { text: '已完成', statusClass: 'done', 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 subOrders = await prisma.supplierOrderSub.findMany({
where: {
supplierId,
userId,
deleted: 0
},
include: {
mainOrder: true,
products: {
where: { deleted: 0 },
orderBy: { createAt: 'asc' }
}
},
orderBy: [
{ createTime: 'desc' },
{ createAt: 'desc' }
]
})
const orders = subOrders.map(order => {
const status = statusMap[order.orderState] || statusMap[1]
const products = order.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: order.orderId,
orderId: order.orderId,
orderNumberId: order.orderNumberId || '',
mainOrderId: order.mainOrderId,
mainOrderNumberId: order.mainOrder?.orderNumberId || '',
supplierId: order.supplierId,
status: status.text,
statusClass: status.statusClass,
actionText: status.actionText,
buttonClass: status.buttonClass,
orderState: order.orderState,
amount: toYuanText(order.totalPrice),
count: products.reduce((total, item) => total + Number(item.number || 0), 0),
products
}
})
return ok({ orders })
})
@@ -1,87 +0,0 @@
import type { Context } from '@netlify/functions'
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 { createBridgeSignature } from '../utils/crypto.js'
import { withErrorBoundary } from '../utils/handler.js'
type SupplierTokenRequest = {
userId?: string
supplierIdentification?: string
}
type BridgeTokenResponse = {
resultCode?: number
message?: string
accessToken?: string
refreshToken?: string
expire?: number
refreshExpire?: number
time?: string
}
const normalizeBase = (base: string) => base.replace(/\/$/, '')
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 prisma.supplierInfo.findUnique({
where: { supplierIdentification }
})
if (!supplier || supplier.deleted === 1) {
return fail('供应商信息未配置或已停用', 404, 'SUPPLIER_NOT_CONFIGURED')
}
const timestamp = String(Date.now())
const signature = createBridgeSignature(supplier.clientId, supplier.clientSecret, timestamp)
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
if (!apiBase) {
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
}
const response = await fetch(`${apiBase}/connect/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
supplierId: supplier.supplierId,
clientId: supplier.clientId,
clientSecret: supplier.clientSecret,
sendSha256: signature,
code: '',
timestamp
})
})
const tokenPayload = (await response.json().catch(() => ({}))) as BridgeTokenResponse
if (!response.ok || tokenPayload.resultCode !== 0) {
return fail(tokenPayload.message || '获取平台授权令牌失败', 502, 'BRIDGE_TOKEN_FAILED')
}
return ok({
supplierIdentification,
supplierId: supplier.supplierId,
supplierName: supplier.supplierName,
userId,
token: tokenPayload.accessToken,
accessToken: tokenPayload.accessToken,
refreshToken: tokenPayload.refreshToken,
expiresIn: tokenPayload.expire,
refreshExpiresIn: tokenPayload.refreshExpire,
issuedAt: tokenPayload.time
})
})
-14
View File
@@ -1,14 +0,0 @@
export type ApiSuccess<T> = {
success: true
data: T
}
export type ApiFailure = {
success: false
error: {
code: string
message: string
}
}
export type ApiResponse<T> = ApiSuccess<T> | ApiFailure
-22218
View File
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -4,7 +4,8 @@
"private": true,
"type": "module",
"scripts": {
"dev": "netlify dev",
"dev": "tsx src/server.ts",
"start": "tsx src/server.ts",
"build": "prisma generate",
"typecheck": "tsc --noEmit",
"prisma:generate": "prisma generate",
@@ -14,19 +15,25 @@
},
"dependencies": {
"@neondatabase/serverless": "^1.0.1",
"@netlify/functions": "^4.2.5",
"@prisma/adapter-neon": "^6.16.0",
"@prisma/client": "^6.16.0",
"@prisma/adapter-neon": "6.19.3",
"@prisma/client": "6.19.3",
"crypto-js": "^4.2.0",
"dotenv": "^17.2.3",
"ioredis": "^5.4.2",
"@koa/router": "^13.1.1",
"koa": "^2.15.3",
"koa-bodyparser": "^4.4.1",
"ws": "^8.18.3"
},
"devDependencies": {
"@types/koa": "^2.15.0",
"@types/koa-bodyparser": "^4.3.12",
"@types/koa__router": "^12.0.4",
"@types/crypto-js": "^4.2.2",
"@types/node": "^24.10.1",
"@types/ws": "^8.18.1",
"netlify-cli": "^23.13.3",
"prisma": "^6.19.3",
"prisma": "6.19.3",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
}
}
+119 -41
View File
@@ -17,7 +17,6 @@ model User {
updatedAt DateTime @updatedAt
carts CartItem[]
orders Order[]
}
model SupplierProduct {
@@ -44,7 +43,6 @@ model SupplierProduct {
updatedAt DateTime @updatedAt
carts CartItem[]
orderItems OrderItem[]
@@map("supplier_product")
}
@@ -56,6 +54,8 @@ model SupplierInfo {
supplierName String
clientId String
clientSecret String
supplierApiUrl String?
sroNo String?
createTime DateTime @default(now())
updateTime DateTime @updatedAt
creator String?
@@ -81,35 +81,6 @@ model CartItem {
@@unique([userId, productId, skuText])
}
model Order {
id Int @id @default(autoincrement())
orderNo String @unique
userId Int
status OrderStatus @default(PENDING_PAYMENT)
totalAmount Decimal @db.Decimal(10, 2)
payType PayType?
paidAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
items OrderItem[]
}
model OrderItem {
id Int @id @default(autoincrement())
orderId Int
productId Int
name String
imageUrl String?
skuText String?
price Decimal @db.Decimal(10, 2)
quantity Int
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
product SupplierProduct @relation(fields: [productId], references: [id])
}
model SupplierOrderMain {
id Int @id @default(autoincrement())
orderId String @unique
@@ -130,6 +101,8 @@ model SupplierOrderMain {
logisticsOrderId String?
crrgBsnNm String?
orderState Int @default(0)
isSplit Int @default(0)
splitTime DateTime?
totalPrice Decimal @default(0) @db.Decimal(10, 2)
wPayOrAPay Decimal @default(0) @db.Decimal(10, 2)
pointsPrice Decimal @default(0) @db.Decimal(10, 2)
@@ -202,16 +175,121 @@ model SupplierOrderProduct {
@@map("supplier_order_product")
}
enum OrderStatus {
PENDING_PAYMENT
PAID
SHIPPED
COMPLETED
CANCELLED
model SupplierPointsLedger {
id Int @id @default(autoincrement())
ledgerNo String @unique
supplierId String
userId String
mainOrderId String?
subOrderId String?
bizType Int
direction Int
points Decimal @db.Decimal(10, 2)
beforePoints Decimal? @db.Decimal(10, 2)
afterPoints Decimal? @db.Decimal(10, 2)
remark String? @db.Text
createTime DateTime @default(now())
updateTime DateTime @updatedAt
deleted Int @default(0)
@@index([supplierId, userId])
@@index([mainOrderId])
@@index([subOrderId])
@@map("supplier_points_ledger")
}
enum PayType {
POINTS
WECHAT
ALIPAY
model SupplierOrderPushLog {
id Int @id @default(autoincrement())
supplierId String
userId String?
mainOrderId String?
orderId String? @db.VarChar(1000)
orderType Int?
apiPath String
requestBody Json?
responseBody Json?
httpStatus Int?
resultCode Int?
pushStatus Int @default(0)
retryCount Int @default(0)
errorMessage String? @db.Text
lastPushTime DateTime?
nextRetryTime DateTime?
createTime DateTime @default(now())
updateTime DateTime @updatedAt
deleted Int @default(0)
@@index([supplierId, mainOrderId])
@@index([orderType, pushStatus])
@@index([nextRetryTime])
@@map("supplier_order_push_log")
}
model SupplierOrderOperateLog {
id Int @id @default(autoincrement())
supplierId String
userId String?
mainOrderId String?
subOrderId String?
operateType Int
beforeState Int?
afterState Int?
beforeSplit Int?
afterSplit Int?
remark String? @db.Text
operator String?
createTime DateTime @default(now())
deleted Int @default(0)
@@index([supplierId, mainOrderId])
@@index([subOrderId])
@@index([operateType])
@@map("supplier_order_operate_log")
}
model SupplierRefundExchange {
id Int @id @default(autoincrement())
serviceOrderId String @unique
supplierId String
userId String?
mainOrderId String?
subOrderId String?
serviceType Int
serviceState Int @default(0)
amount Decimal @default(0) @db.Decimal(10, 2)
reason String? @db.Text
requestBody Json?
createTime DateTime @default(now())
updateTime DateTime @updatedAt
deleted Int @default(0)
@@index([supplierId, mainOrderId])
@@index([subOrderId])
@@index([serviceType, serviceState])
@@map("supplier_refund_exchange")
}
model SupplierInvoiceInfo {
id Int @id @default(autoincrement())
invoiceBizId String @unique
supplierId String
userId String?
mainOrderId String?
subOrderId String?
settlementId String?
invoiceCode String?
invoiceIds String? @db.VarChar(1000)
orderIds String? @db.VarChar(1000)
invoiceStatus Int @default(0)
fileId String?
filePath String?
requestBody Json?
createTime DateTime @default(now())
updateTime DateTime @updatedAt
deleted Int @default(0)
@@index([supplierId, mainOrderId])
@@index([settlementId])
@@index([invoiceCode])
@@map("supplier_invoice_info")
}
@@ -0,0 +1,69 @@
CREATE TABLE IF NOT EXISTS `supplier_points_ledger` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '积分流水主键ID',
`ledgerNo` VARCHAR(191) NOT NULL COMMENT '流水号,业务唯一',
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
`userId` VARCHAR(191) NOT NULL COMMENT '用户ID',
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
`bizType` INT NOT NULL COMMENT '业务类型:1支付消费,2取消返还,3退货退款,4人工调整',
`direction` INT NOT NULL COMMENT '流水方向:1增加,2扣减',
`points` DECIMAL(10, 2) NOT NULL COMMENT '积分变动金额',
`beforePoints` DECIMAL(10, 2) NULL COMMENT '变动前积分余额',
`afterPoints` DECIMAL(10, 2) NULL COMMENT '变动后积分余额',
`remark` TEXT NULL COMMENT '流水备注',
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `supplier_points_ledger_ledgerNo_key` (`ledgerNo`),
KEY `supplier_points_ledger_supplierId_userId_idx` (`supplierId`, `userId`),
KEY `supplier_points_ledger_mainOrderId_idx` (`mainOrderId`),
KEY `supplier_points_ledger_subOrderId_idx` (`subOrderId`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商用户积分消费流水表';
CREATE TABLE IF NOT EXISTS `supplier_order_push_log` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '订单推送日志主键ID',
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
`userId` VARCHAR(191) NULL COMMENT '用户ID',
`mainOrderId` VARCHAR(191) NULL COMMENT '主单ID',
`orderId` VARCHAR(1000) NULL COMMENT '推送订单ID,多个子单用英文逗号分隔',
`orderType` INT NULL COMMENT '推送事件类型:1取消主单,2拆单子单,3取消子单,7取消已支付未拆单整单',
`apiPath` VARCHAR(191) NOT NULL COMMENT '推送接口路径',
`requestBody` JSON NULL COMMENT '推送请求体',
`responseBody` JSON NULL COMMENT '推送响应体',
`httpStatus` INT NULL COMMENT 'HTTP状态码',
`resultCode` INT NULL COMMENT '远程业务返回码',
`pushStatus` INT NOT NULL DEFAULT 0 COMMENT '推送状态:0待推送,1推送成功,2推送失败',
`retryCount` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
`errorMessage` TEXT NULL COMMENT '失败原因',
`lastPushTime` DATETIME(3) NULL COMMENT '最后推送时间',
`nextRetryTime` DATETIME(3) NULL COMMENT '下次重试时间',
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
PRIMARY KEY (`id`),
KEY `supplier_order_push_log_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
KEY `supplier_order_push_log_orderType_pushStatus_idx` (`orderType`, `pushStatus`),
KEY `supplier_order_push_log_nextRetryTime_idx` (`nextRetryTime`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商订单远程推送日志表';
CREATE TABLE IF NOT EXISTS `supplier_order_operate_log` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '订单操作日志主键ID',
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
`userId` VARCHAR(191) NULL COMMENT '用户ID',
`mainOrderId` VARCHAR(191) NULL COMMENT '主单ID',
`subOrderId` VARCHAR(191) NULL COMMENT '子单ID',
`operateType` INT NOT NULL COMMENT '操作类型:1创建待支付,2支付成功,3拆单,4取消主单,5取消子单,6取消已支付未拆单整单,7已妥投,8已完成,9退款,10换货',
`beforeState` INT NULL COMMENT '操作前订单状态',
`afterState` INT NULL COMMENT '操作后订单状态',
`beforeSplit` INT NULL COMMENT '操作前拆单状态:0未拆单,1已拆单',
`afterSplit` INT NULL COMMENT '操作后拆单状态:0未拆单,1已拆单',
`remark` TEXT NULL COMMENT '操作备注',
`operator` VARCHAR(191) NULL COMMENT '操作人或系统标识',
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
PRIMARY KEY (`id`),
KEY `supplier_order_operate_log_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
KEY `supplier_order_operate_log_subOrderId_idx` (`subOrderId`),
KEY `supplier_order_operate_log_operateType_idx` (`operateType`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商订单操作日志表';
@@ -0,0 +1,56 @@
SET @fk_order_item_order = (
SELECT CONSTRAINT_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'orderitem'
AND REFERENCED_TABLE_NAME = 'order'
LIMIT 1
);
SET @sql = IF(
@fk_order_item_order IS NULL,
'SELECT 1',
CONCAT('ALTER TABLE `orderitem` DROP FOREIGN KEY `', @fk_order_item_order, '`')
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @fk_order_item_product = (
SELECT CONSTRAINT_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'orderitem'
AND REFERENCED_TABLE_NAME = 'supplier_product'
LIMIT 1
);
SET @sql = IF(
@fk_order_item_product IS NULL,
'SELECT 1',
CONCAT('ALTER TABLE `orderitem` DROP FOREIGN KEY `', @fk_order_item_product, '`')
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @fk_order_user = (
SELECT CONSTRAINT_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'order'
AND REFERENCED_TABLE_NAME = 'User'
LIMIT 1
);
SET @sql = IF(
@fk_order_user IS NULL,
'SELECT 1',
CONCAT('ALTER TABLE `order` DROP FOREIGN KEY `', @fk_order_user, '`')
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
DROP TABLE IF EXISTS `orderitem`;
DROP TABLE IF EXISTS `order`;
@@ -0,0 +1,7 @@
ALTER TABLE `supplier_order_main`
MODIFY COLUMN `orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1已支付,2进行中,3已取消,4已妥投,5已完成,6换货,7退货,8废除',
ADD COLUMN `isSplit` TINYINT NOT NULL DEFAULT 0 COMMENT '主单是否拆单:0未拆单,1已拆单' AFTER `orderState`,
ADD COLUMN `splitTime` DATETIME(3) NULL COMMENT '主单拆单时间' AFTER `isSplit`;
ALTER TABLE `supplier_order_sub`
MODIFY COLUMN `orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1已支付,2进行中,3已取消,4已妥投,5已完成,6换货,7退货,8废除';
@@ -0,0 +1,46 @@
CREATE TABLE IF NOT EXISTS `supplier_refund_exchange` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '售后记录主键ID',
`serviceOrderId` VARCHAR(191) NOT NULL COMMENT '退货/换货业务单号',
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
`userId` VARCHAR(191) NULL COMMENT '用户ID',
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
`serviceType` INT NOT NULL COMMENT '售后类型:4退货,5换货',
`serviceState` INT NOT NULL DEFAULT 0 COMMENT '售后状态:0已创建,1处理中,2已完成,3已取消',
`amount` DECIMAL(10, 2) NOT NULL DEFAULT 0 COMMENT '售后金额',
`reason` TEXT NULL COMMENT '售后原因',
`requestBody` JSON NULL COMMENT '原始业务请求体',
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `supplier_refund_exchange_serviceOrderId_key` (`serviceOrderId`),
KEY `supplier_refund_exchange_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
KEY `supplier_refund_exchange_subOrderId_idx` (`subOrderId`),
KEY `supplier_refund_exchange_serviceType_serviceState_idx` (`serviceType`, `serviceState`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商退货换货售后记录表';
CREATE TABLE IF NOT EXISTS `supplier_invoice_info` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '发票信息主键ID',
`invoiceBizId` VARCHAR(191) NOT NULL COMMENT '发票业务唯一ID,优先使用发票号或事件订单号',
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
`userId` VARCHAR(191) NULL COMMENT '用户ID',
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
`settlementId` VARCHAR(191) NULL COMMENT '结算单号',
`invoiceCode` VARCHAR(191) NULL COMMENT '发票代码或纳税识别号',
`invoiceIds` VARCHAR(1000) NULL COMMENT '发票号码列表,多个用英文逗号分隔',
`orderIds` VARCHAR(1000) NULL COMMENT '关联订单号列表,多个用英文逗号分隔',
`invoiceStatus` INT NOT NULL DEFAULT 0 COMMENT '发票状态:0已记录,1已申请,2已开票,3失败',
`fileId` VARCHAR(191) NULL COMMENT '发票文件ID',
`filePath` VARCHAR(191) NULL COMMENT '发票文件路径',
`requestBody` JSON NULL COMMENT '原始业务请求体',
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `supplier_invoice_info_invoiceBizId_key` (`invoiceBizId`),
KEY `supplier_invoice_info_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
KEY `supplier_invoice_info_settlementId_idx` (`settlementId`),
KEY `supplier_invoice_info_invoiceCode_idx` (`invoiceCode`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商发票信息表';
@@ -0,0 +1,3 @@
ALTER TABLE `supplier_info`
ADD COLUMN `supplierApiUrl` VARCHAR(1000) NULL COMMENT '供应商开放接口地址' AFTER `clientSecret`,
ADD COLUMN `sroNo` VARCHAR(100) NULL COMMENT 'BAB7服务商编号' AFTER `supplierApiUrl`;
@@ -1,4 +1,4 @@
import type { Context } from '@netlify/functions'
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'
@@ -9,3 +9,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
return ok({ message: '加入购物车接口待实现' })
})
@@ -1,4 +1,4 @@
import type { Context } from '@netlify/functions'
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'
@@ -9,3 +9,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
return ok({ message: '登录接口待实现' })
})
@@ -1,13 +1,14 @@
import type { Context } from '@netlify/functions'
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
token?: string
userId?: string
orderId?: string
mainOrderId?: string
@@ -18,26 +19,18 @@ type BridgeOrderResponse = {
message?: string
}
const normalizeBase = (base: string) => base.replace(/\/$/, '')
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 token = String(body.token || '').trim()
const userId = String(body.userId || '').trim()
const mainOrderId = String(body.mainOrderId || body.orderId || '').trim()
const orderId = String(body.orderId || mainOrderId).trim()
if (!supplierId || !token || !userId || !mainOrderId) {
return fail('供应商、用户、授权或订单信息不能为空')
}
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
if (!apiBase) {
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
if (!supplierId || !userId || !mainOrderId) {
return fail('供应商、用户或订单信息不能为空')
}
const order = await prisma.supplierOrderMain.findFirst({
@@ -53,47 +46,15 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
return fail('主单不存在或已取消', 404, 'MAIN_ORDER_NOT_FOUND')
}
const bridgeResponse = await fetch(`${apiBase}/pointOrder/pushOrderInfoMsg`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
token
},
body: JSON.stringify({
supplierId,
orderType: 1,
orderId,
mainOrderId,
userId,
time: Date.now(),
token
})
})
const bridgePayload = (await bridgeResponse.json().catch(() => ({}))) as BridgeOrderResponse
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
return fail(bridgePayload.message || '取消主单推送失败', 502, 'BRIDGE_CANCEL_MAIN_FAILED')
if (order.orderState !== ORDER_STATE.PENDING_PAY) {
return fail('已支付主单不能通过未支付取消接口取消', 400, 'MAIN_ORDER_ALREADY_PAID')
}
const subOrders = await prisma.supplierOrderSub.findMany({
where: {
mainOrderId,
supplierId,
userId,
deleted: 0
},
select: {
orderId: true
}
})
const subOrderIds = subOrders.map(item => item.orderId)
await prisma.$transaction(async tx => {
await tx.supplierOrderMain.update({
where: { orderId: mainOrderId },
data: {
deleted: 1,
orderState: ORDER_STATE.CANCELLED,
updateTime: new Date()
}
})
@@ -106,27 +67,34 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
deleted: 0
},
data: {
deleted: 1,
orderState: ORDER_STATE.CANCELLED,
updateTime: new Date()
}
})
await tx.supplierOrderProduct.updateMany({
where: {
subOrderId: { in: subOrderIds },
deleted: 0
},
data: {
deleted: 1
}
})
})
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: 1,
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
})
})
@@ -1,13 +1,14 @@
import type { Context } from '@netlify/functions'
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
token?: string
userId?: string
orderId?: string
mainOrderId?: string
@@ -18,25 +19,17 @@ type BridgeOrderResponse = {
message?: string
}
const normalizeBase = (base: string) => base.replace(/\/$/, '')
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 token = String(body.token || '').trim()
const userId = String(body.userId || '').trim()
const orderId = String(body.orderId || '').trim()
if (!supplierId || !token || !userId || !orderId) {
return fail('供应商、用户、授权或子单信息不能为空')
}
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
if (!apiBase) {
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
if (!supplierId || !userId || !orderId) {
return fail('供应商、用户或子单信息不能为空')
}
const subOrder = await prisma.supplierOrderSub.findFirst({
@@ -54,54 +47,36 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
const mainOrderId = String(body.mainOrderId || subOrder.mainOrderId).trim()
const bridgeResponse = await fetch(`${apiBase}/pointOrder/pushOrderInfoMsg`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
token
},
body: JSON.stringify({
supplierId,
orderType: 3,
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(),
token
})
})
const bridgePayload = (await bridgeResponse.json().catch(() => ({}))) as BridgeOrderResponse
time: Date.now()
}
)
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
return fail(bridgePayload.message || '取消子单推送失败', 502, 'BRIDGE_CANCEL_SUB_FAILED')
}
await prisma.$transaction(async tx => {
await tx.supplierOrderSub.update({
where: { orderId },
data: {
deleted: 1,
updateTime: new Date()
}
})
await tx.supplierOrderProduct.updateMany({
where: {
subOrderId: orderId,
deleted: 0
},
data: {
deleted: 1
}
})
})
return ok({
supplierId,
orderType: 3,
orderType: ORDER_EVENT_TYPE.CANCEL_SUB,
orderId,
mainOrderId,
userId
})
})
@@ -1,13 +1,15 @@
import type { Context } from '@netlify/functions'
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
sku?: string | number | Record<string, unknown>
count?: number
number?: number
price?: string | number
@@ -20,7 +22,6 @@ type CheckoutGoods = {
type OrderCreateRequest = {
supplierId?: string
token?: string
userId?: string
orderNo?: string
orderNumberId?: string
@@ -33,7 +34,16 @@ type BridgeOrderResponse = {
message?: string
}
const normalizeBase = (base: string) => base.replace(/\/$/, '')
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()
@@ -49,13 +59,19 @@ const createOrderNumberId = () => {
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 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(item.sku || item.id || `SKU_${index + 1}`),
sku: String(sku || item.id || `SKU_${index + 1}`),
number: Number(item.count || item.number || 1),
price: Number(item.price || 0),
name: String(item.name || ''),
@@ -66,36 +82,47 @@ const toOrderProducts = (goods: CheckoutGoods[]) => goods.map((item, index) => {
}
})
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 token = String(body.token || '').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 || !token || !userId || !orderId) {
return fail('供应商、用户、授权或订单信息不能为空')
if (!supplierId || !userId || !orderId) {
return fail('供应商、用户或订单信息不能为空')
}
if (amount <= 0) {
return fail('订单金额必须大于0')
}
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
if (!apiBase) {
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
if (!orderProInfos.length) {
return fail('订单商品不能为空')
}
const orderNumberId = String(body.orderNumberId || createOrderNumberId())
const subOrderId = `${orderId}_001`
const subOrderNumberId = `${orderNumberId}_001`
const orderTime = nowText()
const orderProInfos = toOrderProducts(goods)
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,
@@ -113,7 +140,7 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
wPayOrAPayRefund: 0,
logisticsOrderId: null,
crrgBsnNm: null,
orderState: 1,
orderState: ORDER_STATE.PENDING_PAY,
totalPrice: amount,
wPayOrAPay: 0,
pointsPrice: amount,
@@ -122,6 +149,21 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
}
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: {
@@ -129,13 +171,15 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
supplierId,
userId,
aplName: orderInfo.aplName,
updateTime: new Date(),
updateTime: now,
deliveryStatus: orderInfo.deliveryStatus,
freightCost: orderInfo.freightCost,
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
refund: orderInfo.refund,
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
orderState: orderInfo.orderState,
orderState: localOrderState,
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
splitTime: null,
totalPrice: orderInfo.totalPrice,
wPayOrAPay: orderInfo.wPayOrAPay,
pointsPrice: orderInfo.pointsPrice,
@@ -148,14 +192,15 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
supplierId,
userId,
aplName: orderInfo.aplName,
createTime: new Date(),
updateTime: new Date(),
createTime: now,
updateTime: now,
deliveryStatus: orderInfo.deliveryStatus,
freightCost: orderInfo.freightCost,
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
refund: orderInfo.refund,
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
orderState: orderInfo.orderState,
orderState: localOrderState,
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
totalPrice: orderInfo.totalPrice,
wPayOrAPay: orderInfo.wPayOrAPay,
pointsPrice: orderInfo.pointsPrice,
@@ -163,88 +208,53 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
}
})
await tx.supplierOrderSub.upsert({
where: { orderId: subOrderId },
update: {
orderNumberId: subOrderNumberId,
await tx.supplierOrderSub.createMany({
data: subOrders.map(subOrder => ({
orderId: subOrder.orderId,
orderNumberId: subOrder.orderNumberId,
mainOrderId: orderId,
supplierId,
token,
userId,
aplName: orderInfo.aplName,
updateTime: new Date(),
createTime: now,
updateTime: now,
deliveryStatus: orderInfo.deliveryStatus,
freightCost: orderInfo.freightCost,
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
refund: orderInfo.refund,
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
orderState: orderInfo.orderState,
totalPrice: orderInfo.totalPrice,
wPayOrAPay: orderInfo.wPayOrAPay,
pointsPrice: orderInfo.pointsPrice,
invoiceCode: orderInfo.invoiceCode,
deleted: 0
},
create: {
orderId: subOrderId,
orderNumberId: subOrderNumberId,
mainOrderId: orderId,
supplierId,
token,
userId,
aplName: orderInfo.aplName,
createTime: new Date(),
updateTime: new Date(),
deliveryStatus: orderInfo.deliveryStatus,
freightCost: orderInfo.freightCost,
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
refund: orderInfo.refund,
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
orderState: orderInfo.orderState,
totalPrice: orderInfo.totalPrice,
wPayOrAPay: orderInfo.wPayOrAPay,
pointsPrice: orderInfo.pointsPrice,
orderState: localOrderState,
totalPrice: subOrder.totalPrice,
wPayOrAPay: 0,
pointsPrice: subOrder.pointsPrice,
invoiceCode: orderInfo.invoiceCode
}
}))
})
await tx.supplierOrderProduct.deleteMany({
where: { subOrderId }
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
}))
})
if (orderProInfos.length) {
await tx.supplierOrderProduct.createMany({
data: orderProInfos.map(item => ({
subOrderId,
sku: item.sku,
number: item.number,
price: item.price,
name: item.name,
taxRate: item.taxRate,
productImgUrl: item.picInfo[0] || null,
fileId: item.fileId,
filePath: item.filePath,
picInfo: item.picInfo
}))
})
}
})
const bridgeResponse = await fetch(`${apiBase}/pointOrder/pendingOrderInfo`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
token
},
body: JSON.stringify({
supplierId,
const { response: bridgeResponse, payload: bridgePayload } = await bridgeFetchWithAuth<BridgeOrderResponse>(
supplierId,
'/pointOrder/pendingOrderInfo',
{
userId,
orderInfo,
token
})
})
const bridgePayload = (await bridgeResponse.json().catch(() => ({}))) as BridgeOrderResponse
orderInfo
}
)
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
return fail(bridgePayload.message || '提交待支付订单失败', 502, 'BRIDGE_PENDING_ORDER_FAILED')
@@ -253,7 +263,8 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
return ok({
orderId,
orderNumberId,
subOrderId,
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 })
})
@@ -1,13 +1,14 @@
import type { Context } from '@netlify/functions'
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
token?: string
orderId?: string
amount?: number
totalPrice?: number
@@ -20,7 +21,6 @@ type BridgePayResponse = {
message?: string
}
const normalizeBase = (base: string) => base.replace(/\/$/, '')
const centsToYuan = (value?: number) => Number((Number(value || 0) / 100).toFixed(2))
export default withErrorBoundary(async (request: Request, _context: Context) => {
@@ -29,25 +29,19 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
const body = await readJson<OrderPayRequest>(request)
const supplierId = String(body.supplierId || '').trim()
const token = String(body.token || '').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 || !token || !orderId) {
return fail('供应商、授权或订单信息不能为空')
if (!supplierId || !orderId) {
return fail('供应商或订单信息不能为空')
}
if (totalPrice <= 0) {
return fail('支付金额必须大于0')
}
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
if (!apiBase) {
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
}
const order = await prisma.supplierOrderMain.findUnique({
where: { orderId }
})
@@ -56,33 +50,12 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
return fail('待支付订单不存在', 404, 'ORDER_NOT_FOUND')
}
const bridgeResponse = await fetch(`${apiBase}/pointOrder/confirmPurchase`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
token
},
body: JSON.stringify({
supplierId,
orderId,
totalPrice,
wPayOrAPay,
pointsPrice,
token
})
})
const bridgePayload = (await bridgeResponse.json().catch(() => ({}))) as BridgePayResponse
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
return fail(bridgePayload.message || '确认购买失败', 502, 'BRIDGE_CONFIRM_PURCHASE_FAILED')
}
await prisma.$transaction(async tx => {
await tx.supplierOrderMain.update({
where: { orderId },
data: {
orderState: 1,
orderState: ORDER_STATE.FINISH_PAY,
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
totalPrice,
wPayOrAPay,
pointsPrice,
@@ -93,15 +66,44 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
await tx.supplierOrderSub.updateMany({
where: { mainOrderId: orderId, deleted: 0 },
data: {
orderState: 1,
totalPrice,
wPayOrAPay,
pointsPrice,
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,
@@ -114,24 +116,36 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
const subOrderIds = subOrders.map(item => item.orderId).filter(Boolean).join(',')
if (subOrderIds) {
const splitResponse = await fetch(`${apiBase}/pointOrder/pushOrderInfoMsg`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
token
},
body: JSON.stringify({
supplierId,
orderType: 2,
orderId: subOrderIds,
mainOrderId: orderId,
userId: order.userId,
time: Date.now(),
token
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 splitPayload = (await splitResponse.json().catch(() => ({}))) as BridgePayResponse
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')
@@ -146,3 +160,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
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 || '订单事件已保存,推送失败'
})
})
@@ -1,4 +1,4 @@
import type { Context } from '@netlify/functions'
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'
@@ -61,3 +61,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
return ok(toProductDetail(product))
})
@@ -1,4 +1,4 @@
import type { Context } from '@netlify/functions'
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'
@@ -67,3 +67,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
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
}
}
})
})
@@ -1,4 +1,4 @@
import type { Context } from '@netlify/functions'
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'
@@ -35,3 +35,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
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)
})
@@ -1,12 +1,12 @@
import type { Context } from '@netlify/functions'
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
token?: string
userId?: string
}
@@ -16,41 +16,23 @@ type BridgePointsResponse = {
userPoints?: number | string
}
const normalizeBase = (base: string) => base.replace(/\/$/, '')
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 token = String(body.token || '').trim()
const userId = String(body.userId || '').trim()
if (!supplierId || !token || !userId) {
return fail('供应商、用户或授权信息不能为空')
if (!supplierId || !userId) {
return fail('供应商或用户信息不能为空')
}
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
if (!apiBase) {
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
}
const response = await fetch(`${apiBase}/pointOrder/queryPoints`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
token
},
body: JSON.stringify({
supplierId,
token,
userId
})
})
const pointsPayload = (await response.json().catch(() => ({}))) as BridgePointsResponse
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')
@@ -60,3 +42,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
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
})
})
@@ -1,4 +1,4 @@
import type { Context } from '@netlify/functions'
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'
@@ -9,3 +9,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
return ok({ message: '上传接口待实现' })
})
@@ -1,4 +1,4 @@
import type { Context } from '@netlify/functions'
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'
@@ -9,3 +9,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
return ok({ message: '用户信息接口待实现' })
})
+107
View File
@@ -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}`)
})
+203
View File
@@ -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
}
@@ -1,5 +1,6 @@
import type { Context } from '@netlify/functions'
import { fail } from './http.js'
import { fail } from './http.js'
export type Context = Record<string, unknown>
export type FunctionHandler = (request: Request, context: Context) => Promise<Response> | Response
@@ -13,3 +14,4 @@ export const withErrorBoundary = (handler: FunctionHandler): FunctionHandler =>
}
}
}
+58
View File
@@ -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
+21
View File
@@ -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,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')
}
+1 -1
View File
@@ -11,5 +11,5 @@
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["netlify/**/*.ts", "prisma.config.ts"]
"include": ["src/**/*.ts", "prisma.config.ts"]
}