feat: 基本实现拆单收货--

This commit is contained in:
2026-07-29 18:22:21 +08:00
parent 1b45f9dac6
commit 63ff3edb11
46 changed files with 4402 additions and 1892 deletions
@@ -2,7 +2,15 @@
<section class="cart-panel" :class="{ 'cart-panel--page': pageMode }"> <section class="cart-panel" :class="{ 'cart-panel--page': pageMode }">
<van-notice-bar v-if="showNotice" text="满 199 元包邮,快去凑单吧~" left-icon="info-o" /> <van-notice-bar v-if="showNotice" text="满 199 元包邮,快去凑单吧~" left-icon="info-o" />
<section class="cart-list"> <section class="cart-list">
<van-empty v-if="!items.length" description="购物车还是空的" /> <div v-if="!items.length" class="cart-empty">
<van-empty description="购物车还是空的" />
<van-button
class="cart-empty__button"
round
type="danger"
@click="$router.push('/joyful-selection/products')"
>去购物</van-button>
</div>
<template v-else> <template v-else>
<article <article
v-for="item in items" v-for="item in items"
@@ -37,10 +45,11 @@
</template> </template>
</section> </section>
<van-submit-bar <van-submit-bar
v-if="items.length"
:fixed="fixedSubmit" :fixed="fixedSubmit"
:price="selectedTotalPrice" :price="selectedTotalPrice"
:button-text="submitText" :button-text="submitText"
@submit="$emit('checkout', selectedItems)" @submit="handleCheckout"
> >
<van-checkbox <van-checkbox
:value="isAllChecked" :value="isAllChecked"
@@ -139,6 +148,14 @@ export default {
} }
this.persistCart() this.persistCart()
},
handleCheckout() {
if (!this.items.length || !this.selectedItems.length) {
this.$toast('请选择要结算的商品')
return
}
this.$emit('checkout', this.selectedItems)
} }
} }
} }
@@ -173,6 +190,19 @@ export default {
background: #fff; background: #fff;
} }
.cart-empty {
padding-top: 48px;
text-align: center;
}
.cart-empty__button {
width: 120px;
height: 38px;
background: #ff5b20;
border-color: #ff5b20;
font-weight: 700;
}
.cart-item { .cart-item {
display: flex; display: flex;
min-height: 128px; min-height: 128px;
@@ -28,6 +28,11 @@ export default {
}, },
methods: { methods: {
goCheckout(items) { goCheckout(items) {
if (!items || !items.length) {
this.$toast('请选择要结算的商品')
return
}
sessionStorage.setItem('joyful-selection-checkout-items', JSON.stringify(items)) sessionStorage.setItem('joyful-selection-checkout-items', JSON.stringify(items))
this.$router.push({ path: '/joyful-selection/checkout', query: { source: 'cart' } }) this.$router.push({ path: '/joyful-selection/checkout', query: { source: 'cart' } })
} }
@@ -143,6 +143,11 @@ export default {
return return
} }
if (!this.goods.length) {
this.$toast('暂无可提交的商品')
return
}
this.submitting = true this.submitting = true
try { try {
await this.fetchBridgeToken() await this.fetchBridgeToken()
@@ -1,5 +1,6 @@
<template> <template>
<van-sku <van-sku
:key="skuKey"
v-model="show" v-model="show"
:class="`sku-popup--${mode}`" :class="`sku-popup--${mode}`"
:sku="sku" :sku="sku"
@@ -46,6 +47,7 @@ export default {
data: () => ({ data: () => ({
show: false, show: false,
mode: 'cart', mode: 'cart',
skuKey: 0,
currentGoods: emptyGoods currentGoods: emptyGoods
}), }),
computed: { computed: {
@@ -103,6 +105,7 @@ export default {
open(goods, mode = 'cart') { open(goods, mode = 'cart') {
this.currentGoods = goods this.currentGoods = goods
this.mode = mode this.mode = mode
this.skuKey += 1
this.show = true this.show = true
}, },
getCartPayload(data) { getCartPayload(data) {
@@ -14,71 +14,54 @@
<van-tab v-for="tab in tabs" :key="tab.label" :title="tab.label" /> <van-tab v-for="tab in tabs" :key="tab.label" :title="tab.label" />
</van-tabs> </van-tabs>
<section class="order-list"> <van-pull-refresh v-model="refreshing" class="order-list" @refresh="onRefresh">
<van-empty v-if="visibleOrders.length === 0" description="暂无订单" /> <van-empty v-if="visibleOrders.length === 0" description="暂无订单" />
<template v-else> <template v-else>
<article v-for="order in visibleOrders" :key="order.viewId" class="order-card"> <article v-for="order in visibleOrders" :key="order.viewId" class="order-card">
<header class="main-order-header"> <header class="order-card__header">
<span>主单号:{{ order.mainOrderId || '-' }}</span> <span>订单号:{{ order.orderNumberId || order.orderId || '-' }}</span>
<van-button <b :class="order.statusClass">{{ order.status }}</b>
v-if="order.canCancel"
class="cancel-main-btn"
size="mini"
round
plain
:loading="isOperating('cancel-main', order.mainOrderId)"
@click.stop="cancelMainOrder(order)"
>
取消订单
</van-button>
</header> </header>
<div class="sub-order-list"> <div class="order-card__body" @click="goOrderDetail(order)">
<section <div class="order-goods">
v-for="subOrder in order.subOrders" <van-image
:key="subOrder.id" v-if="firstProduct(order)"
class="sub-order-row" class="order-thumb"
@click="goOrderDetail(order, subOrder)" :src="firstProduct(order).image"
> fit="cover"
<div class="sub-order-title"> />
<span>子单号:{{ subOrder.orderId || '-' }}</span> <div class="order-goods__info">
<b :class="subOrder.statusClass">{{ subOrder.status }}</b> <h3>{{ firstProduct(order).name || '-' }}</h3>
<p>{{ skuText(firstProduct(order)) }}</p>
<p>创建时间:{{ order.createTime || '-' }}</p>
</div> </div>
<div class="order-body"> </div>
<div class="thumb-row"> <div class="order-summary">
<van-image <p>共{{ order.count }}件商品</p>
v-for="item in previewProducts(subOrder.products)" <p>合计:<b>¥{{ order.amount }}</b></p>
:key="item.id" </div>
class="order-thumb"
:src="item.image"
fit="cover"
/>
</div>
<div class="order-summary">
<p>共{{ subOrder.count }}件商品</p>
<p>合计:<b>¥{{ subOrder.amount }}</b></p>
</div>
</div>
<footer v-if="subActions(subOrder).length">
<van-button
v-for="action in subActions(subOrder)"
:key="action.orderType"
size="small"
round
plain
:class="action.buttonClass"
:loading="isOperating(action.orderType, subOrder.orderId)"
@click.stop="submitSubOrderEvent(order, subOrder, action)"
>
{{ action.text }}
</van-button>
</footer>
</section>
</div> </div>
<footer v-if="orderActions(order).length">
<div class="order-actions">
<van-button
v-for="action in orderActions(order)"
:key="action.key"
size="small"
round
:class="action.buttonClass"
:loading="isOperating(action.key, action.orderId)"
@click.stop="submitOrderAction(order, action)"
>
{{ action.text }}
</van-button>
</div>
</footer>
</article> </article>
</template> </template>
</section> </van-pull-refresh>
</main> </main>
</div> </div>
</template> </template>
@@ -122,6 +105,17 @@ const ORDER_STATE_VIEW = {
[ORDER_STATE.REFUNDED]: { text: '退货', statusClass: 'muted' } [ORDER_STATE.REFUNDED]: { text: '退货', statusClass: 'muted' }
} }
const SKU_COLOR_MAP = {
cream: '奶油白',
blue: '雾霾蓝',
orange: '暖橙色'
}
const SKU_SIZE_MAP = {
standard: '标准款',
plus: '升级款'
}
export default { export default {
name: 'JoyfulSelectionOrders', name: 'JoyfulSelectionOrders',
components: { components: {
@@ -139,7 +133,9 @@ export default {
{ label: '售后', type: 'after-sale' } { label: '售后', type: 'after-sale' }
], ],
orders: [], orders: [],
orderItems: [],
loading: false, loading: false,
refreshing: false,
operationKey: '' operationKey: ''
}), }),
computed: { computed: {
@@ -150,31 +146,27 @@ export default {
visibleOrders() { visibleOrders() {
const tab = this.activeTab const tab = this.activeTab
const orderItems = this.orderItems.length ? this.orderItems : this.buildOrderItems()
if (tab.type === 'all') { if (tab.type === 'all') {
return this.orders.map(order => this.normalizeOrder(order, order.subOrders)) return orderItems
} }
if (tab.type === 'pending-main') { if (tab.type === 'pending-main') {
return this.orders return orderItems.filter(order => order.displayType === 'main' && Number(order.orderState) === ORDER_STATE.PENDING_PAY)
.filter(order => Number(order.orderState) === ORDER_STATE.PENDING_PAY)
.map(order => this.normalizeOrder(order, order.subOrders))
} }
if (tab.type === 'sub-state') { if (tab.type === 'sub-state') {
return this.splitSubOrdersByState(tab.state) return orderItems.filter(order => order.displayType === 'sub' && Number(order.orderState) === tab.state)
} }
if (tab.type === 'cancelled') { if (tab.type === 'cancelled') {
return this.cancelledOrders() return orderItems.filter(order => Number(order.orderState) === ORDER_STATE.CANCELLED)
} }
if (tab.type === 'after-sale') { if (tab.type === 'after-sale') {
return this.orders.flatMap(order => { return orderItems.filter(order => {
const subOrders = this.normalizedSubOrders(order).filter(subOrder => { return order.displayType === 'sub' && [ORDER_STATE.EXCHANGE, ORDER_STATE.REFUNDED].includes(Number(order.orderState))
return [ORDER_STATE.EXCHANGE, ORDER_STATE.REFUNDED].includes(Number(subOrder.orderState))
})
return subOrders.map(subOrder => this.normalizeOrder(order, [subOrder]))
}) })
} }
@@ -210,6 +202,38 @@ export default {
normalizedSubOrders(order) { normalizedSubOrders(order) {
return (order.subOrders || []).map(subOrder => this.normalizeSubOrder(order, subOrder)) return (order.subOrders || []).map(subOrder => this.normalizeSubOrder(order, subOrder))
}, },
buildOrderItems() {
return this.orders.flatMap(order => {
const subOrders = this.normalizedSubOrders(order)
const products = subOrders.flatMap(subOrder => subOrder.products || [])
const isMainOrderItem = Number(order.orderState) === ORDER_STATE.PENDING_PAY ||
(Number(order.orderState) === ORDER_STATE.CANCELLED && Number(order.isSplit) !== ORDER_SPLIT_STATE.SPLIT)
if (isMainOrderItem) {
return [{
...order,
viewId: `main-${order.mainOrderId || order.id}`,
displayType: 'main',
orderId: order.mainOrderId || order.orderId,
orderNumberId: order.mainOrderNumberId || order.orderNumberId,
status: ORDER_STATE_VIEW[Number(order.orderState)]?.text || order.status,
statusClass: ORDER_STATE_VIEW[Number(order.orderState)]?.statusClass || order.statusClass,
amount: order.amount || subOrders.reduce((total, subOrder) => total + Number(subOrder.amount || 0), 0).toFixed(2),
count: subOrders.reduce((total, subOrder) => total + Number(subOrder.count || 0), 0),
products,
subOrders
}]
}
return subOrders.map(subOrder => ({
...subOrder,
viewId: `sub-${subOrder.orderId}`,
displayType: 'sub',
mainOrderId: order.mainOrderId,
mainOrderNumberId: order.mainOrderNumberId
}))
})
},
splitSubOrdersByState(state) { splitSubOrdersByState(state) {
return this.orders.flatMap(order => { return this.orders.flatMap(order => {
if (Number(order.isSplit) !== ORDER_SPLIT_STATE.SPLIT) { if (Number(order.isSplit) !== ORDER_SPLIT_STATE.SPLIT) {
@@ -237,6 +261,17 @@ export default {
previewProducts(products = []) { previewProducts(products = []) {
return products.slice(0, 3) return products.slice(0, 3)
}, },
firstProduct(order) {
return order.products?.[0] || {}
},
skuText(product = {}) {
const sku = String(product.sku || product.skuText || '').trim()
const parts = sku.split('-').slice(-2).filter(Boolean)
const color = SKU_COLOR_MAP[parts[0]] || parts[0]
const size = SKU_SIZE_MAP[parts[1]] || parts[1]
return color || size ? [color, size].filter(Boolean).join(' | ') : '-'
},
actionKey(type, orderId) { actionKey(type, orderId) {
return `${type}-${orderId}` return `${type}-${orderId}`
}, },
@@ -256,13 +291,15 @@ export default {
actions.push({ orderType: ORDER_EVENT_TYPE.EXCHANGE_ORDER, text: '换货', buttonClass: 'dark-action' }) actions.push({ orderType: ORDER_EVENT_TYPE.EXCHANGE_ORDER, text: '换货', buttonClass: 'dark-action' })
} }
if (![ORDER_STATE.PENDING_PAY, ORDER_STATE.CANCELLED].includes(state)) {
actions.push({ orderType: ORDER_EVENT_TYPE.INVOICE_INFO, text: '发票信息', buttonClass: 'dark-action' })
}
return actions return actions
}, },
goOrderDetail(order, subOrder) { goOrderDetail(order) {
const subOrder = order.displayType === 'main' ? order.subOrders?.[0] : order
if (!subOrder?.orderId) {
return
}
this.$router.push({ this.$router.push({
path: '/joyful-selection/order-detail', path: '/joyful-selection/order-detail',
query: { query: {
@@ -271,8 +308,10 @@ export default {
} }
}) })
}, },
async loadOrders() { async loadOrders(showLoading = true) {
this.loading = true if (showLoading) {
this.loading = true
}
try { try {
await this.fetchBridgeToken() await this.fetchBridgeToken()
@@ -280,6 +319,7 @@ export default {
if (!this.supplierId || !this.userId) { if (!this.supplierId || !this.userId) {
Toast('用户信息不完整,请重新进入商城') Toast('用户信息不完整,请重新进入商城')
this.orders = [] this.orders = []
this.orderItems = []
return return
} }
@@ -289,13 +329,49 @@ export default {
}) })
this.orders = data?.orders || [] this.orders = data?.orders || []
this.orderItems = data?.orderItems || []
} catch (error) { } catch (error) {
Toast(error.message || '查询订单失败,请稍后再试') Toast(error.message || '查询订单失败,请稍后再试')
this.orders = [] this.orders = []
this.orderItems = []
} finally { } finally {
this.loading = false if (showLoading) {
this.loading = false
}
} }
}, },
async onRefresh() {
try {
await this.loadOrders(false)
} finally {
this.refreshing = false
}
},
orderActions(order) {
if (order.displayType === 'main') {
return Number(order.orderState) === ORDER_STATE.PENDING_PAY
? [{ key: 'cancel-main', orderId: order.mainOrderId, text: '取消订单', buttonClass: 'dark-action' }]
: []
}
return this.subActions(order).map(action => ({
...action,
key: action.orderType,
orderId: order.orderId
}))
},
submitOrderAction(order, action) {
if (action.key === 'cancel-main') {
this.cancelMainOrder(order)
return
}
this.submitSubOrderEvent(
{ mainOrderId: order.mainOrderId },
order,
action
)
},
async cancelMainOrder(order) { async cancelMainOrder(order) {
try { try {
await Dialog.confirm({ await Dialog.confirm({
@@ -409,152 +485,159 @@ export default {
.orders-page ::v-deep .van-tab { .orders-page ::v-deep .van-tab {
color: #333; color: #333;
font-size: 12px; font-size: 13px;
} }
.order-list { .order-list {
padding: 8px 8px 14px; min-height: calc(100vh - 90px);
box-sizing: border-box;
padding: 12px 10px 18px;
} }
.order-card { .order-card {
overflow: hidden; overflow: hidden;
margin-bottom: 10px; margin-bottom: 14px;
border-radius: 8px; border-radius: 8px;
background: #fff; background: #fff;
} }
.main-order-header { .order-card__header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px 12px 10px; padding: 14px 14px 6px;
border-bottom: 1px solid #f4f4f4;
} }
.main-order-header span { .order-card__header span {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
color: #555; color: #555;
font-size: 12px; font-size: 13px;
font-weight: 600; font-weight: 600;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.cancel-main-btn { .order-card__header b {
flex: none; flex: none;
margin-left: 10px; margin-left: 10px;
color: #ff5b20; font-size: 14px;
border-color: #ff5b20;
}
.sub-order-row {
padding: 10px 12px 12px;
}
.sub-order-row + .sub-order-row {
border-top: 1px solid #f4f4f4;
}
.sub-order-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.sub-order-title span {
min-width: 0;
overflow: hidden;
color: #777;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.sub-order-title b {
flex: none;
margin-left: 10px;
font-size: 12px;
font-weight: 700; font-weight: 700;
} }
.sub-order-title .pending { .order-card__header .pending {
color: #ff5b20; color: #ff5b20;
} }
.sub-order-title .done { .order-card__header .done {
color: #21b36b; color: #21b36b;
} }
.sub-order-title .muted { .order-card__header .muted {
color: #999; color: #999;
} }
.order-body { .order-card__body {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: space-between; justify-content: space-between;
padding: 12px 14px 10px;
} }
.thumb-row { .order-goods {
display: flex; display: flex;
min-width: 0; min-width: 0;
flex: 1; flex: 1;
gap: 8px; align-items: center;
} }
.order-thumb { .order-thumb {
width: 62px; width: 70px;
height: 62px; height: 70px;
flex: none; flex: none;
overflow: hidden; overflow: hidden;
border-radius: 4px; border-radius: 4px;
background: #f5f5f5; background: #f5f5f5;
} }
.order-goods__info {
min-width: 0;
flex: 1;
padding-left: 10px;
}
.order-goods__info h3 {
overflow: hidden;
margin: 0 0 6px;
color: #222;
font-size: 14px;
font-weight: 700;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-goods__info p {
overflow: hidden;
margin: 4px 0 0;
color: #777;
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-summary { .order-summary {
flex: none; flex: none;
margin-left: 10px; margin-left: 12px;
color: #777; color: #777;
font-size: 11px; font-size: 13px;
text-align: right; text-align: right;
white-space: nowrap; white-space: nowrap;
} }
.order-summary p { .order-summary p {
margin: 0; margin: 0;
line-height: 22px; line-height: 25px;
} }
.order-summary b { .order-summary b {
color: #222; color: #222;
font-size: 12px; font-size: 14px;
} }
.sub-order-row footer { .order-card footer {
display: flex; display: flex;
flex-wrap: wrap; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 8px; padding: 0 14px 14px;
padding-top: 10px;
} }
.sub-order-row footer .van-button { .order-actions {
min-width: 72px; display: flex;
height: 28px; min-width: 0;
padding: 0 12px; flex: 1;
border-radius: 15px; justify-content: flex-end;
font-size: 11px; gap: 10px;
}
.order-actions .van-button {
min-width: 88px;
height: 34px;
padding: 0 16px;
border: 0;
border-radius: 6px;
font-size: 14px;
font-weight: 700;
} }
.primary-action { .primary-action {
color: #ff5b20; color: #ff5b20;
border-color: #ff5b20; background: #fff1e8;
} }
.dark-action { .dark-action {
color: #333; color: #333;
border-color: #ddd; background: #f6f7f9;
} }
</style> </style>
+2293
View File
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,4 @@
// Supplier order states and operation codes shared by order services and controllers.
export const ORDER_STATE = { export const ORDER_STATE = {
PENDING_PAY: 0, PENDING_PAY: 0,
FINISH_PAY: 1, FINISH_PAY: 1,
@@ -0,0 +1,33 @@
import type { Context } from '../utils/handler.js'
import { guardMethod } from '../utils/method.js'
import { withErrorBoundary } from '../utils/handler.js'
import { basicService } from '../services/basic.service.js'
import { toHttpResponse } from './controller-response.js'
export const loginController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
return toHttpResponse(basicService.login())
})
export const cartAddController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
return toHttpResponse(basicService.addCart())
})
export const uploadController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
return toHttpResponse(basicService.upload())
})
export const userInfoController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['GET'])
if (methodResponse) return methodResponse
return toHttpResponse(basicService.userInfo())
})
@@ -0,0 +1,10 @@
import { fail, ok } from '../utils/http.js'
import type { ServiceResult } from '../types/service-result.js'
export const toHttpResponse = <T>(result: ServiceResult<T>) => {
if (result.success) {
return ok(result.data)
}
return fail(result.error.message, result.error.status, result.error.code)
}
@@ -0,0 +1,149 @@
import type { Context } from '../utils/handler.js'
import { guardMethod } from '../utils/method.js'
import { readJson } from '../utils/request.js'
import { withErrorBoundary } from '../utils/handler.js'
import { orderActionService } from '../services/order-action.service.js'
import { orderCreateService, type CheckoutGoods } from '../services/order-create.service.js'
import { orderListService } from '../services/order-list.service.js'
import { orderPushEventService, type OrderPushEventInput } from '../services/order-push-event.service.js'
import { supplierOpenOrderService, type SupplierOrderQueryRequest } from '../services/supplier-open-order.service.js'
import { toHttpResponse } from './controller-response.js'
type OrderListRequest = {
supplierId?: string
userId?: string
}
type OrderActionRequest = {
supplierId?: string
userId?: string
orderId?: string
mainOrderId?: string
amount?: number
totalPrice?: number
wPayOrAPay?: number
pointsPrice?: number
}
type OrderCreateRequest = {
supplierId?: string
userId?: string
orderNo?: string
orderNumberId?: string
amount?: number
goods?: CheckoutGoods[]
}
const centsToYuan = (value?: number) => Number((Number(value || 0) / 100).toFixed(2))
export const orderListController = 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()
return toHttpResponse(await orderListService.listOrders(supplierId, userId))
})
export const orderCreateController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
const body = await readJson<OrderCreateRequest>(request)
return toHttpResponse(await orderCreateService.createOrder({
supplierId: String(body.supplierId || '').trim(),
userId: String(body.userId || '').trim(),
orderNo: String(body.orderNo || '').trim(),
orderNumberId: body.orderNumberId,
amount: body.amount,
goods: Array.isArray(body.goods) ? body.goods : []
}))
})
export const orderCancelMainController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
const body = await readJson<OrderActionRequest>(request)
return toHttpResponse(await orderActionService.cancelPendingMain({
supplierId: String(body.supplierId || '').trim(),
userId: String(body.userId || '').trim(),
orderId: String(body.orderId || '').trim(),
mainOrderId: String(body.mainOrderId || body.orderId || '').trim()
}))
})
export const orderCancelPaidMainController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
const body = await readJson<OrderActionRequest>(request)
return toHttpResponse(await orderActionService.cancelPaidMain({
supplierId: String(body.supplierId || '').trim(),
userId: String(body.userId || '').trim(),
orderId: String(body.orderId || '').trim(),
mainOrderId: String(body.mainOrderId || body.orderId || '').trim()
}))
})
export const orderCancelSubController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
const body = await readJson<OrderActionRequest>(request)
return toHttpResponse(await orderActionService.cancelSub({
supplierId: String(body.supplierId || '').trim(),
userId: String(body.userId || '').trim(),
orderId: String(body.orderId || '').trim(),
mainOrderId: String(body.mainOrderId || '').trim()
}))
})
export const orderPayController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
const body = await readJson<OrderActionRequest>(request)
const totalPrice = Number(body.totalPrice ?? centsToYuan(body.amount))
return toHttpResponse(await orderActionService.payOrder({
supplierId: String(body.supplierId || '').trim(),
orderId: String(body.orderId || '').trim(),
totalPrice,
wPayOrAPay: Number(body.wPayOrAPay ?? 0),
pointsPrice: Number(body.pointsPrice ?? totalPrice)
}))
})
export const orderPushEventController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
const body = await readJson<OrderPushEventInput>(request)
return toHttpResponse(await orderPushEventService.pushEvent(body))
})
export const supplierOrderInfoController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
let body: SupplierOrderQueryRequest
try {
body = supplierOpenOrderService.parseBody(await readJson<SupplierOrderQueryRequest>(request))
} catch (error) {
return Response.json({
resultCode: 400,
message: error instanceof Error ? error.message : '请求参数解密失败',
orderInfo: []
})
}
return Response.json(await supplierOpenOrderService.queryOrderInfo(body))
})
@@ -0,0 +1,27 @@
import type { Context } from '../utils/handler.js'
import { guardMethod } from '../utils/method.js'
import { withErrorBoundary } from '../utils/handler.js'
import { productService } from '../services/product.service.js'
import { toHttpResponse } from './controller-response.js'
export const productListController = 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()
return toHttpResponse(await productService.listProducts(keyword, categoryId))
})
export const productDetailController = 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()
return toHttpResponse(await productService.getProductDetail(id, sku))
})
@@ -0,0 +1,67 @@
import type { Context } from '../utils/handler.js'
import { guardMethod } from '../utils/method.js'
import { readJson } from '../utils/request.js'
import { withErrorBoundary } from '../utils/handler.js'
import { supplierService } from '../services/supplier.service.js'
import { toHttpResponse } from './controller-response.js'
type SupplierInfoRequest = {
supplierIdentification?: string
}
type SupplierTokenRequest = {
userId?: string
supplierIdentification?: string
}
type SupplierPointsRequest = {
supplierId?: string
userId?: string
}
type SupplierDebugTokensRequest = {
supplierIdentification?: string
forceRefreshZhghToken?: boolean
}
export const supplierInfoController = 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()
return toHttpResponse(await supplierService.getSupplierInfo(supplierIdentification))
})
export const supplierTokenController = 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()
return toHttpResponse(await supplierService.warmSupplierToken(userId, supplierIdentification))
})
export const supplierPointsController = 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()
return toHttpResponse(await supplierService.getUserPoints(supplierId, userId))
})
export const supplierDebugTokensController = 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()
return toHttpResponse(await supplierService.getDebugTokens(supplierIdentification, Boolean(body.forceRefreshZhghToken)))
})
+103
View File
@@ -0,0 +1,103 @@
import type { Prisma } from '@prisma/client'
import { prisma } from '../utils/prisma.js'
export type PrismaTransaction = Prisma.TransactionClient
export const orderDao = {
transaction<T>(handler: (tx: PrismaTransaction) => Promise<T>) {
return prisma.$transaction(handler)
},
findMainOrdersWithSubs(supplierId: string, userId: string) {
return 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' }
]
})
},
findMainOrder(supplierId: string, userId: string, mainOrderId: string) {
return prisma.supplierOrderMain.findFirst({
where: {
orderId: mainOrderId,
supplierId,
userId,
deleted: 0
}
})
},
findSubOrder(supplierId: string, userId: string, orderId: string) {
return prisma.supplierOrderSub.findFirst({
where: {
orderId,
supplierId,
userId,
deleted: 0
}
})
},
findPayingMainOrder(orderId: string) {
return prisma.supplierOrderMain.findUnique({
where: { orderId }
})
},
findSubOrderIdsByMain(mainOrderId: string) {
return prisma.supplierOrderSub.findMany({
where: {
mainOrderId,
deleted: 0
},
select: {
orderId: true
}
})
},
findOpenApiSubOrders(supplierId: string, orderIds: string[]) {
return prisma.supplierOrderSub.findMany({
where: {
supplierId,
orderId: { in: orderIds },
deleted: 0
},
include: {
products: {
where: { deleted: 0 },
orderBy: { createAt: 'asc' }
}
},
orderBy: { createAt: 'asc' }
})
},
updatePushLog(id: number, data: Parameters<typeof prisma.supplierOrderPushLog.update>[0]['data']) {
return prisma.supplierOrderPushLog.update({
where: { id },
data
})
}
}
+37
View File
@@ -0,0 +1,37 @@
import { prisma } from '../utils/prisma.js'
export type ProductRecord = Awaited<ReturnType<typeof prisma.supplierProduct.findMany>>[number]
export const productDao = {
// 商品列表查询只负责组装数据库条件,业务格式化放在 service。
findEnabledProducts(keyword: string, categoryId: string) {
return prisma.supplierProduct.findMany({
where: {
enabled: true,
deleted: 0,
...(keyword ? { name: { contains: keyword } } : {}),
...(categoryId && categoryId !== 'recommend' ? { categoryId } : {})
},
orderBy: [
{ sortOrder: 'asc' },
{ createdAt: 'desc' }
]
})
},
findEnabledProduct(id: string, sku: string) {
const numericId = Number(id)
const querySku = sku || (id && !Number.isInteger(numericId) ? id : '')
return prisma.supplierProduct.findFirst({
where: {
enabled: true,
deleted: 0,
OR: [
...(Number.isInteger(numericId) && numericId > 0 ? [{ id: numericId }] : []),
...(querySku ? [{ sku: querySku }] : [])
]
}
})
}
}
+15
View File
@@ -0,0 +1,15 @@
import { prisma } from '../utils/prisma.js'
export const supplierDao = {
findActiveByIdentification(supplierIdentification: string) {
return prisma.supplierInfo.findFirst({
where: { supplierIdentification, deleted: 0 }
})
},
findActiveBySupplierId(supplierId: string) {
return prisma.supplierInfo.findFirst({
where: { supplierId, deleted: 0 }
})
}
}
+1 -12
View File
@@ -1,12 +1 @@
import type { Context } from '../utils/handler.js' export { cartAddController as default } from '../controllers/basic.controller.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: '加入购物车接口待实现' })
})
+1 -12
View File
@@ -1,12 +1 @@
import type { Context } from '../utils/handler.js' export { loginController as default } from '../controllers/basic.controller.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: '登录接口待实现' })
})
@@ -1,100 +1 @@
import type { Context } from '../utils/handler.js' export { orderCancelMainController as default } from '../controllers/order.controller.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
})
})
@@ -1,104 +1 @@
import type { Context } from '../utils/handler.js' export { orderCancelPaidMainController as default } from '../controllers/order.controller.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,82 +1 @@
import type { Context } from '../utils/handler.js' export { orderCancelSubController as default } from '../controllers/order.controller.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
})
})
+1 -270
View File
@@ -1,270 +1 @@
import type { Context } from '../utils/handler.js' export { orderCreateController as default } from '../controllers/order.controller.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
})
})
+1 -133
View File
@@ -1,133 +1 @@
import type { Context } from '../utils/handler.js' export { orderListController as default } from '../controllers/order.controller.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 -163
View File
@@ -1,163 +1 @@
import type { Context } from '../utils/handler.js' export { orderPayController as default } from '../controllers/order.controller.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
})
})
@@ -1,310 +1 @@
import type { Context } from '../utils/handler.js' export { orderPushEventController as default } from '../controllers/order.controller.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,64 +1 @@
import type { Context } from '../utils/handler.js' export { productDetailController as default } from '../controllers/product.controller.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))
})
+1 -70
View File
@@ -1,70 +1 @@
import type { Context } from '../utils/handler.js' export { productListController as default } from '../controllers/product.controller.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)
})
})
@@ -1,83 +1 @@
import type { Context } from '../utils/handler.js' export { supplierDebugTokensController as default } from '../controllers/supplier.controller.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,38 +1 @@
import type { Context } from '../utils/handler.js' export { supplierInfoController as default } from '../controllers/supplier.controller.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
})
})
@@ -1,143 +1 @@
import type { Context } from '../utils/handler.js' export { supplierOrderInfoController as default } from '../controllers/order.controller.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,45 +1 @@
import type { Context } from '../utils/handler.js' export { supplierPointsController as default } from '../controllers/supplier.controller.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)
})
})
@@ -1,44 +1 @@
import type { Context } from '../utils/handler.js' export { supplierTokenController as default } from '../controllers/supplier.controller.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 -12
View File
@@ -1,12 +1 @@
import type { Context } from '../utils/handler.js' export { uploadController as default } from '../controllers/basic.controller.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: '上传接口待实现' })
})
+1 -12
View File
@@ -1,12 +1 @@
import type { Context } from '../utils/handler.js' export { userInfoController as default } from '../controllers/basic.controller.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: '用户信息接口待实现' })
})
+44 -37
View File
@@ -3,48 +3,54 @@ import Koa from 'koa'
import Router from '@koa/router' import Router from '@koa/router'
import bodyParser from 'koa-bodyparser' import bodyParser from 'koa-bodyparser'
import type { FunctionHandler } from './utils/handler.js' import type { FunctionHandler } from './utils/handler.js'
import cartAdd from './functions/cart-add.js' import { logger } from './utils/logger.js'
import login from './functions/login.js' import {
import orderCancelMain from './functions/order-cancel-main.js' cartAddController,
import orderCancelPaidMain from './functions/order-cancel-paid-main.js' loginController,
import orderCancelSub from './functions/order-cancel-sub.js' uploadController,
import orderCreate from './functions/order-create.js' userInfoController
import orderList from './functions/order-list.js' } from './controllers/basic.controller.js'
import orderPay from './functions/order-pay.js' import {
import orderPushEvent from './functions/order-push-event.js' orderCancelMainController,
import productDetail from './functions/product-detail.js' orderCancelPaidMainController,
import productList from './functions/product-list.js' orderCancelSubController,
import supplierDebugTokens from './functions/supplier-debug-tokens.js' orderCreateController,
import supplierInfo from './functions/supplier-info.js' orderListController,
import supplierOrderInfo from './functions/supplier-order-info.js' orderPayController,
import supplierPoints from './functions/supplier-points.js' orderPushEventController,
import supplierToken from './functions/supplier-token.js' supplierOrderInfoController
import upload from './functions/upload.js' } from './controllers/order.controller.js'
import userInfo from './functions/user-info.js' import { productDetailController, productListController } from './controllers/product.controller.js'
import {
supplierDebugTokensController,
supplierInfoController,
supplierPointsController,
supplierTokenController
} from './controllers/supplier.controller.js'
const app = new Koa() const app = new Koa()
const router = new Router() const router = new Router()
const port = Number(process.env.PORT || 8888) const port = Number(process.env.PORT || 8888)
const handlers: Record<string, FunctionHandler> = { const handlers: Record<string, FunctionHandler> = {
'cart-add': cartAdd, 'cart-add': cartAddController,
login, login: loginController,
'order-cancel-main': orderCancelMain, 'order-cancel-main': orderCancelMainController,
'order-cancel-paid-main': orderCancelPaidMain, 'order-cancel-paid-main': orderCancelPaidMainController,
'order-cancel-sub': orderCancelSub, 'order-cancel-sub': orderCancelSubController,
'order-create': orderCreate, 'order-create': orderCreateController,
'order-list': orderList, 'order-list': orderListController,
'order-pay': orderPay, 'order-pay': orderPayController,
'order-push-event': orderPushEvent, 'order-push-event': orderPushEventController,
'product-detail': productDetail, 'product-detail': productDetailController,
'product-list': productList, 'product-list': productListController,
'supplier-debug-tokens': supplierDebugTokens, 'supplier-debug-tokens': supplierDebugTokensController,
'supplier-info': supplierInfo, 'supplier-info': supplierInfoController,
'supplier-order-info': supplierOrderInfo, 'supplier-order-info': supplierOrderInfoController,
'supplier-points': supplierPoints, 'supplier-points': supplierPointsController,
'supplier-token': supplierToken, 'supplier-token': supplierTokenController,
upload, upload: uploadController,
'user-info': userInfo 'user-info': userInfoController
} }
const corsHeaders = { const corsHeaders = {
@@ -75,6 +81,7 @@ const writeResponse = async (ctx: Koa.Context, response: Response) => {
} }
const dispatch = (name: string) => async (ctx: Koa.Context) => { const dispatch = (name: string) => async (ctx: Koa.Context) => {
logger.info('dispatch api request', { name, method: ctx.method, path: ctx.path })
const handler = handlers[name] const handler = handlers[name]
if (!handler) { if (!handler) {
ctx.status = 404 ctx.status = 404
@@ -103,5 +110,5 @@ app.use(router.routes())
app.use(router.allowedMethods()) app.use(router.allowedMethods())
app.listen(port, () => { app.listen(port, () => {
console.log(`supplied-service Koa server running at http://localhost:${port}`) logger.info(`Koa server running at http://localhost:${port}`)
}) })
@@ -0,0 +1,24 @@
import { logger } from '../utils/logger.js'
import { serviceOk } from '../types/service-result.js'
export const basicService = {
login() {
logger.info('login placeholder called')
return serviceOk({ message: '登录接口待实现' })
},
addCart() {
logger.info('cart add placeholder called')
return serviceOk({ message: '加入购物车接口待实现' })
},
upload() {
logger.info('upload placeholder called')
return serviceOk({ message: '上传接口待实现' })
},
userInfo() {
logger.info('user info placeholder called')
return serviceOk({ message: '用户信息接口待实现' })
}
}
@@ -0,0 +1,255 @@
import { ORDER_EVENT_TYPE, ORDER_SPLIT_STATE, ORDER_STATE } from '../constants/order.js'
import { orderDao } from '../dao/order.dao.js'
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
import { logger } from '../utils/logger.js'
import { serviceFail, serviceOk } from '../types/service-result.js'
type BridgeOrderResponse = {
resultCode?: number
message?: string
}
type CancelMainInput = {
supplierId: string
userId: string
orderId: string
mainOrderId: string
}
type CancelSubInput = {
supplierId: string
userId: string
orderId: string
mainOrderId: string
}
type PayOrderInput = {
supplierId: string
orderId: string
totalPrice: number
wPayOrAPay: number
pointsPrice: number
}
export const orderActionService = {
async cancelPendingMain(input: CancelMainInput) {
const { supplierId, userId, mainOrderId } = input
const orderId = input.orderId || mainOrderId
logger.info('cancel pending main order', { supplierId, userId, mainOrderId })
if (!supplierId || !userId || !mainOrderId) {
return serviceFail('供应商、用户或订单信息不能为空')
}
const order = await orderDao.findMainOrder(supplierId, userId, mainOrderId)
if (!order) {
return serviceFail('主单不存在或已取消', 404, 'MAIN_ORDER_NOT_FOUND')
}
if (order.orderState !== ORDER_STATE.PENDING_PAY) {
return serviceFail('已支付主单不能通过未支付取消接口取消', 400, 'MAIN_ORDER_ALREADY_PAID')
}
await orderDao.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, payload } = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/pushOrderInfoMsg', {
orderType: ORDER_EVENT_TYPE.CANCEL_MAIN,
orderId,
mainOrderId,
userId,
time: Date.now()
})
if (!response.ok || payload.resultCode !== 0) {
return serviceFail(payload.message || '取消主单推送失败', 502, 'BRIDGE_CANCEL_MAIN_FAILED')
}
return serviceOk({ supplierId, orderType: ORDER_EVENT_TYPE.CANCEL_MAIN, orderId, mainOrderId, userId })
},
async cancelPaidMain(input: CancelMainInput) {
const { supplierId, userId, mainOrderId } = input
const orderId = input.orderId || mainOrderId
logger.info('cancel paid unsplit main order', { supplierId, userId, mainOrderId })
if (!supplierId || !userId || !mainOrderId) {
return serviceFail('供应商、用户或订单信息不能为空')
}
const order = await orderDao.findMainOrder(supplierId, userId, mainOrderId)
if (!order) {
return serviceFail('主单不存在或已取消', 404, 'MAIN_ORDER_NOT_FOUND')
}
if (order.orderState === ORDER_STATE.PENDING_PAY) {
return serviceFail('未支付主单请使用未支付取消接口', 400, 'MAIN_ORDER_NOT_PAID')
}
if (order.orderState !== ORDER_STATE.FINISH_PAY || order.isSplit === ORDER_SPLIT_STATE.SPLIT) {
return serviceFail('该主单已拆单,不能按已支付未拆单整单取消', 400, 'MAIN_ORDER_ALREADY_SPLIT')
}
await orderDao.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, payload } = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/pushOrderInfoMsg', {
orderType: ORDER_EVENT_TYPE.CANCEL_PAID_NOT_SPLIT_MAIN,
orderId,
mainOrderId,
userId,
time: Date.now()
})
if (!response.ok || payload.resultCode !== 0) {
return serviceFail(payload.message || '取消已支付未拆单整单推送失败', 502, 'BRIDGE_CANCEL_PAID_MAIN_FAILED')
}
return serviceOk({ supplierId, orderType: ORDER_EVENT_TYPE.CANCEL_PAID_NOT_SPLIT_MAIN, orderId, mainOrderId, userId })
},
async cancelSub(input: CancelSubInput) {
const { supplierId, userId, orderId } = input
logger.info('cancel sub order', { supplierId, userId, orderId })
if (!supplierId || !userId || !orderId) {
return serviceFail('供应商、用户或子单信息不能为空')
}
const subOrder = await orderDao.findSubOrder(supplierId, userId, orderId)
if (!subOrder) {
return serviceFail('子单不存在或已取消', 404, 'SUB_ORDER_NOT_FOUND')
}
const mainOrderId = input.mainOrderId || subOrder.mainOrderId
await orderDao.transaction(async tx => {
await tx.supplierOrderSub.update({
where: { orderId },
data: { orderState: ORDER_STATE.CANCELLED, updateTime: new Date() }
})
})
const { response, payload } = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/pushOrderInfoMsg', {
orderType: ORDER_EVENT_TYPE.CANCEL_SUB,
orderId,
mainOrderId,
userId,
time: Date.now()
})
if (!response.ok || payload.resultCode !== 0) {
return serviceFail(payload.message || '取消子单推送失败', 502, 'BRIDGE_CANCEL_SUB_FAILED')
}
return serviceOk({ supplierId, orderType: ORDER_EVENT_TYPE.CANCEL_SUB, orderId, mainOrderId, userId })
},
async payOrder(input: PayOrderInput) {
const { supplierId, orderId, totalPrice, wPayOrAPay, pointsPrice } = input
logger.info('pay order', { supplierId, orderId, totalPrice, wPayOrAPay, pointsPrice })
if (!supplierId || !orderId) {
return serviceFail('供应商或订单信息不能为空')
}
if (totalPrice <= 0) {
return serviceFail('支付金额必须大于0')
}
const order = await orderDao.findPayingMainOrder(orderId)
if (!order || order.deleted === 1) {
return serviceFail('待支付订单不存在', 404, 'ORDER_NOT_FOUND')
}
if (order.orderState !== ORDER_STATE.PENDING_PAY) {
logger.warn('order is not payable', { supplierId, orderId, orderState: order.orderState })
return serviceFail('当前订单状态不可支付,请刷新订单状态', 400, 'ORDER_NOT_PAYABLE')
}
await orderDao.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 confirm = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/confirmPurchase', {
orderId,
totalPrice,
wPayOrAPay,
pointsPrice
})
if (!confirm.response.ok || confirm.payload.resultCode !== 0) {
return serviceFail(confirm.payload.message || '确认购买推送失败', 502, 'BRIDGE_CONFIRM_PURCHASE_FAILED')
}
const purchaseResult = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/purchaseResult', {
orderId,
totalPrice,
wPayOrAPay,
pointsPrice,
purchaseResult: true,
message: '支付成功'
})
if (!purchaseResult.response.ok || purchaseResult.payload.resultCode !== 0) {
return serviceFail(purchaseResult.payload.message || '支付结果通知推送失败', 502, 'BRIDGE_PURCHASE_RESULT_FAILED')
}
const subOrders = await orderDao.findSubOrderIdsByMain(orderId)
const subOrderIds = subOrders.map(item => item.orderId).filter(Boolean).join(',')
if (subOrderIds) {
await orderDao.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 split = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/pushOrderInfoMsg', {
orderType: ORDER_EVENT_TYPE.SPLIT_SUB_ORDER,
orderId: subOrderIds,
mainOrderId: orderId,
userId: order.userId,
time: Date.now()
})
if (!split.response.ok || split.payload.resultCode !== 0) {
return serviceFail(split.payload.message || '推送拆单信息失败', 502, 'BRIDGE_SPLIT_ORDER_FAILED')
}
}
return serviceOk({ orderId, subOrderIds, totalPrice, wPayOrAPay, pointsPrice })
}
}
@@ -0,0 +1,256 @@
import { ORDER_SPLIT_STATE, ORDER_STATE } from '../constants/order.js'
import { orderDao } from '../dao/order.dao.js'
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
import { logger } from '../utils/logger.js'
import { serviceFail, serviceOk } from '../types/service-result.js'
export 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 OrderCreateInput = {
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 const orderCreateService = {
async createOrder(input: OrderCreateInput) {
const supplierId = input.supplierId
const userId = input.userId
const orderId = input.orderNo
const amount = toYuan(input.amount)
const goods = Array.isArray(input.goods) ? input.goods : []
const orderProInfos = toOrderProducts(goods)
logger.info('create pending order', { supplierId, userId, orderId, goodsCount: goods.length, amount })
if (!supplierId || !userId || !orderId) {
return serviceFail('供应商、用户或订单信息不能为空')
}
if (amount <= 0) {
return serviceFail('订单金额必须大于0')
}
if (!orderProInfos.length) {
return serviceFail('订单商品不能为空')
}
const orderNumberId = String(input.orderNumberId || createOrderNumberId())
const orderTime = nowText()
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 orderDao.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: ORDER_STATE.PENDING_PAY,
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: ORDER_STATE.PENDING_PAY,
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: ORDER_STATE.PENDING_PAY,
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, payload } = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/pendingOrderInfo', {
userId,
orderInfo
})
if (!response.ok || payload.resultCode !== 0) {
return serviceFail(payload.message || '提交待支付订单失败', 502, 'BRIDGE_PENDING_ORDER_FAILED')
}
return serviceOk({
orderId,
orderNumberId,
subOrderIds: subOrders.map(item => item.orderId),
orderInfo
})
}
}
@@ -0,0 +1,145 @@
import { ORDER_SPLIT_STATE, ORDER_STATE } from '../constants/order.js'
import { orderDao } from '../dao/order.dao.js'
import { logger } from '../utils/logger.js'
import { serviceFail, serviceOk } from '../types/service-result.js'
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 ''
}
const toDateTimeText = (date?: Date | null) => {
if (!date) return ''
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 sumProductsCount = (subOrders: Array<{ products: Array<{ number: number | null }> }>) => {
return subOrders.reduce((total, subOrder) => {
return total + subOrder.products.reduce((subTotal, product) => subTotal + Number(product.number || 0), 0)
}, 0)
}
export const orderListService = {
async listOrders(supplierId: string, userId: string) {
logger.info('order list query', { supplierId, userId })
if (!supplierId || !userId) {
return serviceFail('供应商或用户信息不能为空')
}
const mainOrders = await orderDao.findMainOrdersWithSubs(supplierId, userId)
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,
createTime: toDateTimeText(subOrder.createTime || subOrder.createAt),
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
}
})
const products = subOrders.flatMap(subOrder => subOrder.products)
const isMainOrderItem = mainOrder.orderState === ORDER_STATE.PENDING_PAY ||
(mainOrder.orderState === ORDER_STATE.CANCELLED && mainOrder.isSplit !== ORDER_SPLIT_STATE.SPLIT)
const orderItems = isMainOrderItem
? [{
id: mainOrder.orderId,
viewId: `main-${mainOrder.orderId}`,
displayType: 'main',
orderId: mainOrder.orderId,
mainOrderId: mainOrder.orderId,
orderNumberId: mainOrder.orderNumberId || '',
mainOrderNumberId: mainOrder.orderNumberId || '',
supplierId: mainOrder.supplierId,
createTime: toDateTimeText(mainOrder.createTime || mainOrder.createAt),
status: mainStatus.text,
statusClass: mainStatus.statusClass,
actionText: mainStatus.actionText,
buttonClass: mainStatus.buttonClass,
orderState: mainOrder.orderState,
amount: toYuanText(mainOrder.totalPrice),
count: sumProductsCount(subOrders),
products,
subOrders
}]
: subOrders.map(subOrder => ({
...subOrder,
viewId: `sub-${subOrder.orderId}`,
displayType: 'sub',
mainOrderNumberId: mainOrder.orderNumberId || ''
}))
return {
id: mainOrder.orderId,
mainOrderId: mainOrder.orderId,
mainOrderNumberId: mainOrder.orderNumberId || '',
supplierId: mainOrder.supplierId,
createTime: toDateTimeText(mainOrder.createTime || mainOrder.createAt),
status: mainStatus.text,
statusClass: mainStatus.statusClass,
orderState: mainOrder.orderState,
isSplit: mainOrder.isSplit,
splitTime: mainOrder.splitTime,
canCancel: mainOrder.orderState === ORDER_STATE.PENDING_PAY,
products,
amount: toYuanText(mainOrder.totalPrice),
count: sumProductsCount(subOrders),
orderItems,
subOrders
}
})
logger.info('order list result', { supplierId, userId, count: orders.length })
return serviceOk({ orders, orderItems: orders.flatMap(order => order.orderItems) })
}
}
@@ -0,0 +1,290 @@
import {
ORDER_EVENT_TYPE,
ORDER_OPERATE_TYPE,
ORDER_PUSH_STATUS,
ORDER_STATE
} from '../constants/order.js'
import { orderDao } from '../dao/order.dao.js'
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
import { logger } from '../utils/logger.js'
import { serviceFail, serviceOk } from '../types/service-result.js'
export type OrderPushEventInput = {
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: OrderPushEventInput) => ({
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 const orderPushEventService = {
async pushEvent(body: OrderPushEventInput) {
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()
logger.info('push order event', { supplierId, userId, orderType, orderId, mainOrderId })
if (!supplierId || !userId || !orderType || !orderId) {
return serviceFail('供应商、用户或订单事件信息不能为空')
}
if (!supportedOrderTypes.has(orderType)) {
return serviceFail('暂不支持该订单事件类型', 400, 'ORDER_EVENT_NOT_SUPPORTED')
}
const bridgeBody = buildBridgeBody(body)
const now = new Date()
let pushLogId = 0
let affectedSubOrderIds: string[] = []
await orderDao.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 orderDao.updatePushLog(pushLogId, {
pushStatus: ORDER_PUSH_STATUS.FAILED,
errorMessage: message,
lastPushTime: new Date()
})
logger.warn('push order event bridge failed', { supplierId, orderId, message })
return serviceOk({ localSaved: true, pushSuccess: false, message, affectedSubOrderIds })
}
const pushSuccess = bridgeResponse.ok && bridgePayload.resultCode === 0
await orderDao.updatePushLog(pushLogId, {
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 serviceOk({
localSaved: true,
pushSuccess,
orderType,
orderId,
mainOrderId,
affectedSubOrderIds,
message: pushSuccess ? '订单事件已保存并推送成功' : bridgePayload.message || '订单事件已保存,推送失败'
})
}
}
@@ -0,0 +1,68 @@
import { productDao, type ProductRecord } from '../dao/product.dao.js'
import { logger } from '../utils/logger.js'
import { serviceFail, serviceOk } from '../types/service-result.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: ProductRecord) => {
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 const productService = {
async listProducts(keyword: string, categoryId: string) {
logger.info('product list query', { keyword, categoryId })
const products = await productDao.findEnabledProducts(keyword, categoryId)
return serviceOk({
categories,
products: products.map(toProductItem)
})
},
async getProductDetail(id: string, sku: string) {
logger.info('product detail query', { id, sku })
if (!id && !sku) {
return serviceFail('商品参数不能为空')
}
const product = await productDao.findEnabledProduct(id, sku)
if (!product) {
return serviceFail('商品不存在或已下架', 404, 'PRODUCT_NOT_FOUND')
}
return serviceOk(toProductItem(product))
}
}
@@ -0,0 +1,108 @@
import { supplierDao } from '../dao/supplier.dao.js'
import { orderDao } from '../dao/order.dao.js'
import { logger } from '../utils/logger.js'
import { sm4CbcDecrypt, verifySupplierOpenSignature } from '../utils/supplier-open-crypto.js'
export type SupplierOrderQueryRequest = {
supplierId?: string
token?: string
orderType?: number
orderId?: string
timestamp?: number
AplParmObjct?: string
}
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 const supplierOpenOrderService = {
parseBody,
async queryOrderInfo(body: SupplierOrderQueryRequest) {
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)
logger.info('supplier open order query', { supplierId, orderCount: orderIds.length, timestamp })
if (!supplierId || !token || !timestamp || !orderIds.length) {
return { resultCode: 400, message: '供应商、订单或签名信息不能为空', orderInfo: [] }
}
const supplier = await supplierDao.findActiveBySupplierId(supplierId)
if (!supplier) {
return { resultCode: 404, message: '供应商信息不存在', orderInfo: [] }
}
if (!verifySupplierOpenSignature(supplier.supplierId, supplier.clientId, timestamp, token)) {
return { resultCode: 401, message: '签名无效或已过期', orderInfo: [] }
}
const orders = await orderDao.findOpenApiSubOrders(supplierId, orderIds)
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 { resultCode: 0, message: '成功', orderInfo }
}
}
@@ -0,0 +1,145 @@
import { supplierDao } from '../dao/supplier.dao.js'
import { bridgeFetchWithAuth, findSupplierByIdentification, getBridgeApiBase, getBridgeToken, warmBridgeToken } from '../utils/bridge-auth.js'
import { sha256 } from '../utils/crypto.js'
import { logger } from '../utils/logger.js'
import { createSupplierOpenSignature } from '../utils/supplier-open-crypto.js'
import { serviceFail, serviceOk } from '../types/service-result.js'
type BridgePointsResponse = {
resultCode?: number
message?: string
userPoints?: number | string
}
export const supplierService = {
async getSupplierInfo(supplierIdentification: string) {
logger.info('supplier info query', { supplierIdentification })
if (!supplierIdentification) {
return serviceFail('供应商唯一标识不能为空')
}
const supplier = await supplierDao.findActiveByIdentification(supplierIdentification)
if (!supplier) {
return serviceFail('供应商信息未配置或已停用', 404, 'SUPPLIER_NOT_CONFIGURED')
}
return serviceOk({
id: supplier.id,
supplierIdentification: supplier.supplierIdentification,
supplierId: supplier.supplierId,
supplierName: supplier.supplierName
})
},
async warmSupplierToken(userId: string, supplierIdentification: string) {
logger.info('supplier token warmup', { userId, supplierIdentification })
if (!userId || !supplierIdentification) {
return serviceFail('用户信息和供应商唯一标识不能为空')
}
const supplier = await findSupplierByIdentification(supplierIdentification).catch(error => error)
if (supplier instanceof Error) {
return serviceFail(supplier.message || '供应商信息未配置或已停用', 404, 'SUPPLIER_NOT_CONFIGURED')
}
try {
await warmBridgeToken(supplier)
} catch (error) {
return serviceFail(error instanceof Error ? error.message : '获取平台授权令牌失败', 502, 'BRIDGE_TOKEN_FAILED')
}
return serviceOk({
supplierIdentification,
supplierId: supplier.supplierId,
supplierName: supplier.supplierName,
userId
})
},
async getUserPoints(supplierId: string, userId: string) {
logger.info('supplier points query', { supplierId, userId })
if (!supplierId || !userId) {
return serviceFail('供应商或用户信息不能为空')
}
const { response, payload: pointsPayload } = await bridgeFetchWithAuth<BridgePointsResponse>(
supplierId,
'/pointOrder/queryPoints',
{ userId }
)
if (!response.ok || pointsPayload.resultCode !== 0) {
return serviceFail(pointsPayload.message || '查询用户积分失败', 502, 'BRIDGE_POINTS_FAILED')
}
return serviceOk({
userPoints: Number(pointsPayload.userPoints || 0)
})
},
async getDebugTokens(supplierIdentification: string, forceRefreshZhghToken: boolean) {
logger.info('supplier debug tokens query', { supplierIdentification, forceRefreshZhghToken })
if (!supplierIdentification) {
return serviceFail('供应商唯一标识不能为空')
}
const supplier = await findSupplierByIdentification(supplierIdentification).catch(error => error)
if (supplier instanceof Error) {
return serviceFail(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, forceRefreshZhghToken)
return serviceOk({
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,16 @@
export type ServiceError = {
message: string
status?: number
code?: string
}
export type ServiceResult<T = unknown> =
| { success: true; data: T }
| { success: false; error: ServiceError }
export const serviceOk = <T>(data: T): ServiceResult<T> => ({ success: true, data })
export const serviceFail = (message: string, status?: number, code?: string): ServiceResult<never> => ({
success: false,
error: { message, status, code }
})
+4 -13
View File
@@ -1,6 +1,6 @@
import type { SupplierInfo } from '@prisma/client' import type { SupplierInfo } from '@prisma/client'
import { supplierDao } from '../dao/supplier.dao.js'
import { createBridgeSignature } from './crypto.js' import { createBridgeSignature } from './crypto.js'
import { prisma } from './prisma.js'
import { getRedis } from './redis.js' import { getRedis } from './redis.js'
type BridgeTokenResponse = { type BridgeTokenResponse = {
@@ -33,12 +33,7 @@ const TOKEN_EXPIRE_BUFFER_MS = 60_000
export const getBridgeApiBase = () => normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '') export const getBridgeApiBase = () => normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
export const findSupplierForBridge = async (supplierId: string) => { export const findSupplierForBridge = async (supplierId: string) => {
const supplier = await prisma.supplierInfo.findFirst({ const supplier = await supplierDao.findActiveBySupplierId(supplierId)
where: {
supplierId,
deleted: 0
}
})
if (!supplier) { if (!supplier) {
throw new Error('供应商信息未配置或已停用') throw new Error('供应商信息未配置或已停用')
@@ -47,13 +42,9 @@ export const findSupplierForBridge = async (supplierId: string) => {
} }
export const findSupplierByIdentification = async (supplierIdentification: string) => { export const findSupplierByIdentification = async (supplierIdentification: string) => {
const supplier = await prisma.supplierInfo.findUnique({ const supplier = await supplierDao.findActiveByIdentification(supplierIdentification.toLowerCase())
where: {
supplierIdentification: supplierIdentification.toLowerCase()
}
})
if (!supplier || supplier.deleted === 1) { if (!supplier) {
throw new Error('供应商信息未配置或已停用') throw new Error('供应商信息未配置或已停用')
} }
return supplier return supplier
+3 -3
View File
@@ -1,4 +1,5 @@
import { fail } from './http.js' import { fail } from './http.js'
import { logger } from './logger.js'
export type Context = Record<string, unknown> export type Context = Record<string, unknown>
@@ -9,9 +10,8 @@ export const withErrorBoundary = (handler: FunctionHandler): FunctionHandler =>
try { try {
return await handler(request, context) return await handler(request, context)
} catch (error) { } catch (error) {
console.error(error) logger.error('unhandled request error', error)
return fail('服务异常,请稍后再试', 500, 'INTERNAL_SERVER_ERROR') return fail('服务异常,请稍后再试', 500, 'INTERNAL_SERVER_ERROR')
} }
} }
} }
+23
View File
@@ -0,0 +1,23 @@
type LogLevel = 'info' | 'warn' | 'error'
const write = (level: LogLevel, message: string, meta?: unknown) => {
const line = `[supplied-service] ${new Date().toISOString()} ${level.toUpperCase()} ${message}`
if (level === 'error') {
console.error(line, meta || '')
return
}
if (level === 'warn') {
console.warn(line, meta || '')
return
}
console.log(line, meta || '')
}
export const logger = {
info: (message: string, meta?: unknown) => write('info', message, meta),
warn: (message: string, meta?: unknown) => write('warn', message, meta),
error: (message: string, meta?: unknown) => write('error', message, meta)
}