feat: 积分商城-供应商

This commit is contained in:
2026-09-01 18:03:37 +08:00
parent 20d059443c
commit 750c36116d
39 changed files with 3692 additions and 9 deletions
+2
View File
@@ -0,0 +1,2 @@
PORT=8050
SUPPLIED_SERVICE_BASE_URL=http://127.0.0.1:8888
+8
View File
@@ -0,0 +1,8 @@
{
"name": "esf-relay",
"private": true,
"type": "module",
"scripts": {
"start": "node src/server.mjs"
}
}
+71
View File
@@ -0,0 +1,71 @@
import http from 'node:http'
const port = Number(process.env.PORT || 8050)
const suppliedServiceBaseUrl = String(process.env.SUPPLIED_SERVICE_BASE_URL || 'http://127.0.0.1:8888').replace(/\/$/, '')
const addressRoutes = new Map([
['/supplierOpenApi/pointOrder/queryOrderInfo', '/supplierOpenApi/pointOrder/queryOrderInfo'],
['/supplierOpenApi/pointOrder/queryReconciliationInfo', '/supplierOpenApi/pointOrder/queryReconciliationInfo'],
['/supplierOpenApi/pointOrder/unReconciledMsg', '/supplierOpenApi/pointOrder/unReconciledMsg'],
['/supplierOpenApi/pointOrder/reconciliationResult', '/supplierOpenApi/pointOrder/reconciliationResult']
])
const readBody = request => new Promise((resolve, reject) => {
const chunks = []
request.on('data', chunk => chunks.push(chunk))
request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
request.on('error', reject)
})
const getForwardPath = address4 => {
if (typeof address4 !== 'string') {
return null
}
try {
return addressRoutes.get(new URL(address4).pathname) || null
} catch {
return addressRoutes.get(address4) || null
}
}
const writeJson = (response, statusCode, body) => {
response.writeHead(statusCode, { 'Content-Type': 'application/json;charset=UTF-8' })
response.end(JSON.stringify(body))
}
const server = http.createServer(async (request, response) => {
if (request.method !== 'POST' || request.url !== '/esf/S120020967Api') {
writeJson(response, 404, { resultCode: 404, message: '接口不存在' })
return
}
try {
const bodyText = await readBody(request)
const body = JSON.parse(bodyText)
const forwardPath = getForwardPath(body.Address4)
if (!forwardPath) {
writeJson(response, 400, { resultCode: 400, message: '不支持的Address4' })
return
}
const upstreamResponse = await fetch(`${suppliedServiceBaseUrl}${forwardPath}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
body: bodyText
})
const responseBody = await upstreamResponse.text()
response.writeHead(upstreamResponse.status, {
'Content-Type': upstreamResponse.headers.get('content-type') || 'application/json;charset=UTF-8'
})
response.end(responseBody)
} catch (error) {
writeJson(response, 502, {
resultCode: 502,
message: error instanceof Error ? error.message : 'ESF中转请求失败'
})
}
})
server.listen(port, () => {
console.log(`ESF relay running at http://127.0.0.1:${port}/esf/S120020967Api`)
})
+2
View File
@@ -0,0 +1,2 @@
<!doctype html>
<html lang="zh-CN"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>供应商管理后台</title></head><body><div id="app"></div><script type="module" src="/src/main.ts"></script></body></html>
+2697
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "supplied-admin",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"ele-admin-plus": "^1.6.0",
"element-plus": "^2.9.1",
"pinia": "^3.0.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"sass-embedded": "^1.103.1",
"typescript": "^5.7.2",
"vite": "^6.0.5",
"vue-tsc": "^2.2.0"
}
}
+1
View File
@@ -0,0 +1 @@
<template><RouterView /></template>
+16
View File
@@ -0,0 +1,16 @@
const API_BASE = import.meta.env.VITE_SUPPLIED_ADMIN_API_BASE || 'http://127.0.0.1:8888/api/admin'
const normalizeBase = (base: string) => base.replace(/\/$/, '')
export const postAdmin = async <T>(name: string, body: Record<string, unknown>): Promise<T> => {
const response = await fetch(`${normalizeBase(API_BASE)}/${name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
const payload = await response.json().catch(() => ({}))
if (!response.ok || payload.success === false) {
throw new Error(payload.error?.message || payload.msg || '请求失败')
}
return payload.data as T
}
@@ -0,0 +1,23 @@
<template>
<section class="page-card"><div class="page-title"><div><h2>{{ title }}</h2><p>{{ description }}</p></div></div><el-form :inline="true" class="query-form"><el-form-item label="供应商 ID"><el-input v-model="supplierId" placeholder="请输入供应商 ID" clearable /></el-form-item><el-form-item><el-button type="primary" :loading="loading" @click="loadRecords">查询</el-button><el-button @click="reset">重置</el-button></el-form-item></el-form><el-table v-loading="loading" :data="records" border><el-table-column prop="serviceOrderId" label="售后单号" min-width="170" /><el-table-column prop="serviceTypeText" label="售后类型" width="110" /><el-table-column prop="serviceStateText" label="状态" width="130" /><el-table-column prop="reason" label="原因" min-width="160" /><el-table-column prop="applyTime" label="申请时间" min-width="170" /><el-table-column label="操作" min-width="160" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button v-if="scene === 'audit'" link type="success" @click="audit(row, 1)">通过</el-button><el-button v-if="scene === 'audit'" link type="danger" @click="audit(row, 2)">拒绝</el-button><el-button v-if="canReceive(row)" link type="success" @click="receive(row)">确认收货</el-button><el-button v-if="canShip(row)" link type="primary" @click="openShipDialog(row)">换货发货</el-button></template></el-table-column></el-table><el-empty v-if="!loading && queried && !records.length" description="暂无数据" /><el-dialog v-model="detailVisible" title="售后详情" width="620px"><pre class="detail-json">{{ JSON.stringify(detail, null, 2) }}</pre></el-dialog><el-dialog v-model="shipVisible" title="换货发货" width="460px"><el-form label-width="90px"><el-form-item label="快递公司"><el-input v-model="shipForm.logisticsCompany" /></el-form-item><el-form-item label="快递单号"><el-input v-model="shipForm.logisticsNo" /></el-form-item></el-form><template #footer><el-button @click="shipVisible = false">取消</el-button><el-button type="primary" @click="shipExchange">确认发货</el-button></template></el-dialog></section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { postAdmin } from '@/api/admin'
const props = defineProps<{ scene: 'audit' | 'process' | 'history'; title: string; description: string }>()
type AfterSale = { serviceOrderId: string; serviceState?: number; serviceType?: number; isSigned?: boolean; serviceTypeText?: string; serviceStateText?: string; reason?: string; applyTime?: string }
const supplierId = ref(''); const records = ref<AfterSale[]>([]); const loading = ref(false); const queried = ref(false); const detailVisible = ref(false); const detail = ref<unknown>({}); const shipVisible = ref(false); const current = ref<AfterSale>(); const shipForm = ref({ logisticsCompany: '', logisticsNo: '' })
const loadRecords = async () => { if (!supplierId.value.trim()) return ElMessage.warning('请输入供应商 ID'); loading.value = true; try { const data = await postAdmin<{ afterSaleItems?: AfterSale[] }>('merchant-after-sale-list', { supplierId: supplierId.value.trim(), scene: props.scene }); records.value = data.afterSaleItems || []; queried.value = true } catch (error) { ElMessage.error(error instanceof Error ? error.message : '售后查询失败') } finally { loading.value = false } }
const reset = () => { supplierId.value = ''; records.value = []; queried.value = false }
const openDetail = async (row: AfterSale) => { try { detail.value = await postAdmin('merchant-after-sale-detail', { supplierId: supplierId.value.trim(), serviceOrderId: row.serviceOrderId }); detailVisible.value = true } catch (error) { ElMessage.error(error instanceof Error ? error.message : '售后详情查询失败') } }
const audit = async (row: AfterSale, auditStatus: number) => { try { await ElMessageBox.confirm(auditStatus === 1 ? '确认通过该售后申请?' : '确认拒绝该售后申请?', '售后审核'); await postAdmin('merchant-after-sale-audit', { supplierId: supplierId.value.trim(), serviceOrderId: row.serviceOrderId, auditStatus, auditRemark: auditStatus === 1 ? '审核通过' : '商家拒绝' }); ElMessage.success('处理成功'); await loadRecords() } catch (error) { if (error !== 'cancel') ElMessage.error(error instanceof Error ? error.message : '处理失败') } }
const canReceive = (row: AfterSale) => props.scene === 'process' && Number(row.serviceState) === 3
const canShip = (row: AfterSale) => props.scene === 'process' && Number(row.serviceType) === 5 && (Number(row.serviceState) === 4 || (row.isSigned === false && Number(row.serviceState) === 1))
const receive = async (row: AfterSale) => { try { await ElMessageBox.confirm('确认已收到寄回商品?', '售后处理'); await postAdmin('merchant-after-sale-receive', { supplierId: supplierId.value.trim(), serviceOrderId: row.serviceOrderId }); ElMessage.success('已确认收货'); await loadRecords() } catch (error) { if (error !== 'cancel') ElMessage.error(error instanceof Error ? error.message : '操作失败') } }
const openShipDialog = (row: AfterSale) => { current.value = row; shipForm.value = { logisticsCompany: '', logisticsNo: '' }; shipVisible.value = true }
const shipExchange = async () => { if (!current.value || !shipForm.value.logisticsCompany || !shipForm.value.logisticsNo) return ElMessage.warning('请填写完整发货信息'); try { await postAdmin('merchant-after-sale-ship', { supplierId: supplierId.value.trim(), serviceOrderId: current.value.serviceOrderId, ...shipForm.value }); ElMessage.success('换货已发货'); shipVisible.value = false; await loadRecords() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '发货失败') } }
</script>
<style scoped>.detail-json { max-height: 420px; overflow: auto; padding: 12px; border-radius: 4px; background: #f5f7fa; white-space: pre-wrap; word-break: break-all; }</style>
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
@@ -0,0 +1,23 @@
<template>
<el-container class="admin-layout">
<el-aside :width="appStore.sidebarCollapsed ? '64px' : '224px'" class="sidebar">
<div class="brand"><el-icon><ShoppingBag /></el-icon><span v-show="!appStore.sidebarCollapsed">供应商管理</span></div>
<el-menu :collapse="appStore.sidebarCollapsed" :default-active="$route.path" background-color="#001529" text-color="#bfcbd9" active-text-color="#ffffff" router>
<el-menu-item index="/reconciliation"><el-icon><Tickets /></el-icon><template #title>订单对账</template></el-menu-item>
<el-sub-menu index="/orders"><template #title><el-icon><List /></el-icon><span>订单管理</span></template><el-menu-item index="/orders/list">订单列表</el-menu-item><el-menu-item index="/orders/after-sales/audit">售后审核</el-menu-item><el-menu-item index="/orders/after-sales/process">售后处理</el-menu-item><el-menu-item index="/orders/after-sales/history">售后记录</el-menu-item></el-sub-menu>
<el-menu-item index="/products"><el-icon><Goods /></el-icon><template #title>商品管理</template></el-menu-item>
</el-menu>
</el-aside>
<el-container><el-header class="header"><el-button text @click="appStore.toggleSidebar"><el-icon :size="18"><Expand v-if="appStore.sidebarCollapsed" /><Fold v-else /></el-icon></el-button><el-breadcrumb separator="/"><el-breadcrumb-item>供应商管理</el-breadcrumb-item><el-breadcrumb-item>{{ String($route.meta.title || '') }}</el-breadcrumb-item></el-breadcrumb><div class="header-user">管理员</div></el-header><el-main class="main"><RouterView /></el-main></el-container>
</el-container>
</template>
<script setup lang="ts">
import { Expand, Fold, Goods, List, ShoppingBag, Tickets } from '@element-plus/icons-vue'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
</script>
<style scoped lang="scss">
.admin-layout { min-height: 100vh; }.sidebar { overflow: hidden; transition: width .2s; background: #001529; }.brand { display: flex; align-items: center; gap: 10px; height: 56px; padding: 0 20px; color: #fff; font-size: 16px; font-weight: 600; white-space: nowrap; }.sidebar :deep(.el-menu) { border-right: 0; }.header { display: flex; align-items: center; gap: 16px; height: 56px; border-bottom: 1px solid #ebeef5; background: #fff; }.header-user { margin-left: auto; color: #606266; }.main { min-height: calc(100vh - 56px); padding: 20px; background: #f5f7fa; }
</style>
+10
View File
@@ -0,0 +1,10 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import ElePage from 'ele-admin-plus/es/ele-page/index.js'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import { pinia } from './stores'
import './styles/index.scss'
createApp(App).component('ElePage', ElePage).use(pinia).use(router).use(ElementPlus).mount('#app')
+14
View File
@@ -0,0 +1,14 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import AdminLayout from '@/layouts/AdminLayout.vue'
const routes: RouteRecordRaw[] = [{ path: '/', component: AdminLayout, redirect: '/reconciliation', children: [
{ path: 'reconciliation', name: 'Reconciliation', component: () => import('@/views/reconciliation/index.vue'), meta: { title: '订单对账' } },
{ path: 'orders', redirect: '/orders/list' },
{ path: 'orders/list', name: 'Orders', component: () => import('@/views/orders/index.vue'), meta: { title: '订单列表' } },
{ path: 'orders/after-sales/audit', name: 'AfterSaleAudit', component: () => import('@/views/after-sales/audit.vue'), meta: { title: '售后审核' } },
{ path: 'orders/after-sales/process', name: 'AfterSaleProcess', component: () => import('@/views/after-sales/process.vue'), meta: { title: '售后处理' } },
{ path: 'orders/after-sales/history', name: 'AfterSaleHistory', component: () => import('@/views/after-sales/history.vue'), meta: { title: '售后记录' } },
{ path: 'products', name: 'Products', component: () => import('@/views/products/index.vue'), meta: { title: '商品管理' } }
] }]
export default createRouter({ history: createWebHistory(), routes })
+2
View File
@@ -0,0 +1,2 @@
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', { state: () => ({ sidebarCollapsed: false }), actions: { toggleSidebar() { this.sidebarCollapsed = !this.sidebarCollapsed } } })
+2
View File
@@ -0,0 +1,2 @@
import { createPinia } from 'pinia'
export const pinia = createPinia()
+1
View File
@@ -0,0 +1 @@
html, body, #app { min-width: 1180px; min-height: 100%; margin: 0; } body { color: #303133; font-family: "Microsoft YaHei", Arial, sans-serif; }.page-card { min-height: calc(100vh - 96px); padding: 24px; border-radius: 8px; background: #fff; }.page-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 26px; }.page-title h2 { margin: 0 0 8px; font-size: 20px; }.page-title p { margin: 0; color: #909399; font-size: 14px; }.query-form { padding: 18px 18px 2px; margin-bottom: 20px; border-radius: 6px; background: #f8fafc; }
@@ -0,0 +1,2 @@
<template><MerchantAfterSaleTable scene="audit" title="售后审核" description="审核用户提交的退货、换货申请。" /></template>
<script setup lang="ts">import MerchantAfterSaleTable from '@/components/MerchantAfterSaleTable.vue'</script>
@@ -0,0 +1,2 @@
<template><MerchantAfterSaleTable scene="history" title="售后记录" description="查看已完成或已关闭的售后记录。" /></template>
<script setup lang="ts">import MerchantAfterSaleTable from '@/components/MerchantAfterSaleTable.vue'</script>
@@ -0,0 +1,2 @@
<template><MerchantAfterSaleTable scene="process" title="售后处理" description="处理已审核售后,包括确认收货和换货发货。" /></template>
<script setup lang="ts">import MerchantAfterSaleTable from '@/components/MerchantAfterSaleTable.vue'</script>
+30
View File
@@ -0,0 +1,30 @@
<template>
<section class="page-card">
<div class="page-title"><div><h2>订单列表</h2><p>查询订单并对待发货订单确认发货。</p></div></div>
<el-form :inline="true" class="query-form"><el-form-item label="供应商"><el-select v-model="query.supplierId" placeholder="请选择供应商" filterable clearable><el-option v-for="supplier in suppliers" :key="supplier.supplierId" :label="`${supplier.supplierName}(${supplier.supplierId})`" :value="supplier.supplierId" /></el-select></el-form-item><el-form-item label="订单号"><el-input v-model="query.orderId" placeholder="订单编号或子订单号" clearable /></el-form-item><el-form-item label="工号"><el-input v-model="query.userId" placeholder="请输入工号" clearable /></el-form-item><el-form-item label="订单状态"><el-select v-model="query.orderState" placeholder="全部" clearable><el-option v-for="status in orderStates" :key="status.value" :label="status.label" :value="status.value" /></el-select></el-form-item><el-form-item><el-button type="primary" :loading="loading" @click="loadOrders">查询</el-button><el-button @click="reset">重置</el-button></el-form-item></el-form>
<el-table v-loading="loading" :data="orders" border><el-table-column prop="orderNumberId" label="订单编号" min-width="160" /><el-table-column prop="orderId" label="子订单号" min-width="180" /><el-table-column prop="userId" label="工号" min-width="130" /><el-table-column label="订单状态" width="110"><template #default="{ row }">{{ stateText(row.orderState) }}</template></el-table-column><el-table-column prop="createTime" label="下单时间" min-width="170" /><el-table-column label="商品" min-width="180"><template #default="{ row }">{{ productNames(row) }}</template></el-table-column><el-table-column label="操作" width="120" fixed="right"><template #default="{ row }"><el-button v-if="row.orderState === 1" link type="primary" @click="openShipDialog(row)">确认发货</el-button><span v-else>-</span></template></el-table-column></el-table>
<el-empty v-if="!loading && queried && !orders.length" description="暂无订单" />
<el-dialog v-model="shipDialogVisible" title="确认发货" width="460px"><el-form label-width="90px"><el-form-item label="快递公司"><el-input v-model="shipForm.crrgBsnNm" placeholder="请输入快递公司" /></el-form-item><el-form-item label="快递单号"><el-input v-model="shipForm.logisticsOrderId" placeholder="请输入快递单号" /></el-form-item></el-form><template #footer><el-button @click="shipDialogVisible = false">取消</el-button><el-button type="primary" :loading="submitting" @click="submitShip">确认发货</el-button></template></el-dialog>
</section>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { postAdmin } from '@/api/admin'
type OrderProduct = { name?: string }
type OrderItem = { orderId: string; orderNumberId?: string; userId?: string; orderState: number; createTime?: string; products?: OrderProduct[] }
type SupplierItem = { supplierId: string; supplierName: string }
const orderStates = [{ label: '待付款', value: 0 }, { label: '待发货', value: 1 }, { label: '已发货', value: 2 }, { label: '已取消', value: 3 }, { label: '已签收', value: 4 }, { label: '已完成', value: 5 }, { label: '换货', value: 6 }, { label: '已退款', value: 7 }]
const query = reactive<{ supplierId: string; orderId: string; userId: string; orderState?: number }>({ supplierId: '', orderId: '', userId: '' })
const suppliers = ref<SupplierItem[]>([]); const orders = ref<OrderItem[]>([]); const loading = ref(false); const queried = ref(false); const submitting = ref(false); const shipDialogVisible = ref(false); const currentOrder = ref<OrderItem>(); const shipForm = ref({ crrgBsnNm: '', logisticsOrderId: '' })
const stateText = (state: number) => orderStates.find(item => item.value === state)?.label || '未知'
const loadSuppliers = async () => { try { const data = await postAdmin<{ suppliers?: SupplierItem[] }>('merchant-supplier-list', {}); suppliers.value = data.suppliers || [] } catch (error) { ElMessage.error(error instanceof Error ? error.message : '供应商加载失败') } }
const loadOrders = async () => { if (!query.supplierId) return ElMessage.warning('请选择供应商'); loading.value = true; try { const data = await postAdmin<{ orderItems?: OrderItem[] }>('merchant-order-list', query); orders.value = data.orderItems || []; queried.value = true } catch (error) { ElMessage.error(error instanceof Error ? error.message : '订单查询失败') } finally { loading.value = false } }
const reset = () => { query.supplierId = ''; query.orderId = ''; query.userId = ''; query.orderState = undefined; orders.value = []; queried.value = false }
const productNames = (order: OrderItem) => order.products?.map(item => item.name).filter(Boolean).join('、') || '-'
const openShipDialog = (order: OrderItem) => { currentOrder.value = order; shipForm.value = { crrgBsnNm: '', logisticsOrderId: '' }; shipDialogVisible.value = true }
const submitShip = async () => { if (!currentOrder.value || !shipForm.value.crrgBsnNm || !shipForm.value.logisticsOrderId) return ElMessage.warning('请填写完整发货信息'); submitting.value = true; try { await postAdmin('merchant-order-ship', { supplierId: query.supplierId, orderId: currentOrder.value.orderId, userId: currentOrder.value.userId, ...shipForm.value }); ElMessage.success('发货成功'); shipDialogVisible.value = false; await loadOrders() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '发货失败') } finally { submitting.value = false } }
onMounted(loadSuppliers)
</script>
@@ -0,0 +1,32 @@
<template>
<section class="page-card">
<div class="page-title"><div><h2>商品管理</h2><p>管理供应商商品的展示、库存和上下架状态。</p></div><el-button type="primary" @click="openCreate">新增商品</el-button></div>
<el-form inline class="query-form" @submit.prevent><el-form-item label="商品"><el-input v-model="query.keyword" placeholder="商品名称或 SKU" clearable /></el-form-item><el-form-item label="分类"><el-select v-model="query.categoryId" placeholder="全部" clearable><el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item><el-form-item><el-button type="primary" :loading="loading" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item></el-form>
<el-table :data="products" v-loading="loading" border><el-table-column prop="sku" label="SKU" min-width="140" /><el-table-column prop="name" label="商品名称" min-width="180" /><el-table-column prop="category" label="分类" width="120" /><el-table-column prop="price" label="售价" width="100" /><el-table-column prop="stock" label="库存" width="90" /><el-table-column prop="sortOrder" label="排序" width="80" /><el-table-column label="状态" width="90"><template #default="{ row }"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '上架' : '下架' }}</el-tag></template></el-table-column><el-table-column label="操作" width="130" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openEdit(row)">编辑</el-button><el-button link type="danger" @click="remove(row)">删除</el-button></template></el-table-column></el-table>
<el-empty v-if="loaded && !products.length" description="暂无商品" />
<el-dialog v-model="visible" :title="form.id ? '编辑商品' : '新增商品'" width="680px" destroy-on-close><el-form :model="form" label-width="90px"><el-row :gutter="16"><el-col :span="12"><el-form-item label="商品SKU" required><el-input v-model="form.sku" /></el-form-item></el-col><el-col :span="12"><el-form-item label="商品名称" required><el-input v-model="form.name" /></el-form-item></el-col><el-col :span="12"><el-form-item label="售价" required><el-input-number v-model="form.price" :min="0" :precision="2" /></el-form-item></el-col><el-col :span="12"><el-form-item label="原价"><el-input-number v-model="form.originPrice" :min="0" :precision="2" /></el-form-item></el-col><el-col :span="12"><el-form-item label="库存"><el-input-number v-model="form.stock" :min="0" :precision="0" /></el-form-item></el-col><el-col :span="12"><el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" :precision="0" /></el-form-item></el-col><el-col :span="12"><el-form-item label="分类"><el-select v-model="form.categoryId" @change="setCategory"><el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="12"><el-form-item label="状态"><el-switch v-model="form.enabled" active-text="上架" inactive-text="下架" /></el-form-item></el-col></el-row><el-form-item label="图片地址"><el-input v-model="form.imageUrl" /></el-form-item><el-form-item label="商品描述"><el-input v-model="form.description" type="textarea" :rows="3" /></el-form-item></el-form><template #footer><el-button @click="visible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template></el-dialog>
</section>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { postAdmin } from '@/api/admin'
type Category = { id: string; name: string }
type Product = { id: number; sku: string; name: string; categoryId: string; category: string; price: string; originPrice: string; stock: number; sortOrder: number; enabled: boolean; imageUrl: string; description: string }
type ProductForm = { id?: number; sku: string; name: string; price: number; originPrice?: number; stock: number; sortOrder: number; categoryId: string; category: string; imageUrl: string; description: string; enabled: boolean }
const query = reactive({ keyword: '', categoryId: '' })
const categories = ref<Category[]>([]); const products = ref<Product[]>([]); const loading = ref(false); const loaded = ref(false); const visible = ref(false); const saving = ref(false)
const emptyForm = (): ProductForm => ({ sku: '', name: '', price: 0, stock: 0, sortOrder: 0, categoryId: '', category: '', imageUrl: '', description: '', enabled: true })
const form = reactive<ProductForm>(emptyForm())
const setForm = (value: ProductForm) => Object.assign(form, value)
const load = async () => { loading.value = true; try { const data = await postAdmin<{ categories: Category[]; products: Product[] }>('merchant-product-list', query); categories.value = data.categories || []; products.value = data.products || []; loaded.value = true } catch (error) { ElMessage.error(error instanceof Error ? error.message : '商品查询失败') } finally { loading.value = false } }
const reset = () => { query.keyword = ''; query.categoryId = ''; load() }
const openCreate = () => { setForm(emptyForm()); visible.value = true }
const openEdit = (product: Product) => { setForm({ id: product.id, sku: product.sku, name: product.name, price: Number(product.price), originPrice: product.originPrice ? Number(product.originPrice) : undefined, stock: product.stock, sortOrder: product.sortOrder, categoryId: product.categoryId, category: product.category, imageUrl: product.imageUrl, description: product.description, enabled: product.enabled }); visible.value = true }
const setCategory = (categoryId: string) => { form.category = categories.value.find(item => item.id === categoryId)?.name || '' }
const save = async () => { saving.value = true; try { await postAdmin('merchant-product-save', form); ElMessage.success('保存成功'); visible.value = false; await load() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '保存失败') } finally { saving.value = false } }
const remove = async (product: Product) => { try { await ElMessageBox.confirm(`确认删除商品“${product.name}”?`, '删除商品', { type: 'warning' }); await postAdmin('merchant-product-delete', { id: product.id }); ElMessage.success('删除成功'); await load() } catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '删除失败') } }
onMounted(load)
</script>
@@ -0,0 +1,67 @@
<template>
<ele-page>
<section class="page-card">
<div class="page-title"><div><h2>订单对账</h2><p>查看 ZHGH 回传的子订单对账结果,并处理异常订单。</p></div></div>
<el-form inline class="query-form" @submit.prevent>
<el-form-item label="供应商"><el-input v-model="query.supplierId" placeholder="供应商 ID" clearable /></el-form-item>
<el-form-item label="子订单"><el-input v-model="query.orderId" placeholder="子订单 ID" clearable /></el-form-item>
<el-form-item label="对账状态"><el-select v-model="query.reconciliationStatus" clearable placeholder="全部"><el-option label="未对账" :value="0" /><el-option label="对账成功" :value="1" /><el-option label="对账异常" :value="2" /></el-select></el-form-item>
<el-form-item><el-button type="primary" :loading="loading" @click="load">查询</el-button><el-button @click="reset">重置</el-button></el-form-item>
</el-form>
<el-table :data="items" v-loading="loading" border>
<el-table-column prop="orderId" label="子订单ID" min-width="180" />
<el-table-column prop="mainOrderId" label="主订单ID" min-width="180" />
<el-table-column prop="reconciliationStatus" label="对账状态" width="110"><template #default="{ row }"><el-tag :type="statusType(row.reconciliationStatus)">{{ statusText(row.reconciliationStatus) }}</el-tag></template></el-table-column>
<el-table-column prop="diffReason" label="差异说明" min-width="220" show-overflow-tooltip />
<el-table-column label="平台金额" min-width="120"><template #default="{ row }">{{ row.platformTotalPrice || '-' }}</template></el-table-column>
<el-table-column label="供应商金额" min-width="120"><template #default="{ row }">{{ row.supplierTotalPrice || '-' }}</template></el-table-column>
<el-table-column prop="reconciliationBatchNo" label="对账批次" min-width="150" show-overflow-tooltip />
<el-table-column prop="reconciliationTime" label="对账时间" min-width="170" />
<el-table-column label="操作" width="90" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openEdit(row)">处理</el-button></template></el-table-column>
</el-table>
<el-empty v-if="loaded && !items.length" description="暂无对账结果" />
</section>
<el-dialog v-model="dialogVisible" title="处理对账结果" width="460px">
<el-form label-width="90px"><el-form-item label="子订单"><span>{{ editing?.orderId }}</span></el-form-item><el-form-item label="对账状态"><el-select v-model="form.reconciliationStatus"><el-option label="未对账" :value="0" /><el-option label="对账成功" :value="1" /><el-option label="对账异常" :value="2" /></el-select></el-form-item><el-form-item label="差异类型"><el-input-number v-model="form.diffType" :min="0" :precision="0" /></el-form-item><el-form-item label="差异说明"><el-input v-model="form.diffReason" type="textarea" :rows="3" /></el-form-item></el-form>
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template>
</el-dialog>
</ele-page>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { postAdmin } from '@/api/admin'
type ReconciliationItem = { supplierId: string; orderId: string; mainOrderId: string; reconciliationStatus: number; diffType?: number; diffReason: string; platformTotalPrice: string; supplierTotalPrice: string; reconciliationBatchNo: string; reconciliationTime: string }
const query = reactive<{ supplierId: string; orderId: string; reconciliationStatus?: number }>({ supplierId: '', orderId: '' })
const items = ref<ReconciliationItem[]>([])
const loading = ref(false)
const loaded = ref(false)
const dialogVisible = ref(false)
const saving = ref(false)
const editing = ref<ReconciliationItem>()
const form = reactive<{ reconciliationStatus: number; diffType?: number; diffReason: string }>({ reconciliationStatus: 0, diffReason: '' })
const statusText = (status: number) => ['未对账', '对账成功', '对账异常'][status] || '未知'
const statusType = (status: number) => status === 1 ? 'success' : status === 2 ? 'danger' : 'info'
const load = async () => {
if (!query.supplierId.trim()) return ElMessage.warning('请输入供应商 ID')
loading.value = true
try {
const data = await postAdmin<{ reconciliationItems: ReconciliationItem[] }>('merchant-reconciliation-list', query)
items.value = data.reconciliationItems || []
loaded.value = true
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '查询失败') } finally { loading.value = false }
}
const reset = () => { query.supplierId = ''; query.orderId = ''; query.reconciliationStatus = undefined; items.value = []; loaded.value = false }
const openEdit = (row: ReconciliationItem) => { editing.value = row; form.reconciliationStatus = row.reconciliationStatus; form.diffType = row.diffType; form.diffReason = row.diffReason || ''; dialogVisible.value = true }
const save = async () => {
if (!editing.value) return
saving.value = true
try {
await postAdmin('merchant-reconciliation-update', { supplierId: editing.value.supplierId, orderId: editing.value.orderId, reconciliationStatus: form.reconciliationStatus, diffType: form.diffType, diffReason: form.diffReason })
ElMessage.success('保存成功'); dialogVisible.value = false; await load()
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '保存失败') } finally { saving.value = false }
}
</script>
+4
View File
@@ -0,0 +1,4 @@
{
"compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ES2020", "DOM"], "baseUrl": ".", "paths": { "@/*": ["src/*"] } },
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
}
+5
View File
@@ -0,0 +1,5 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
+31
View File
@@ -159,6 +159,37 @@ model SupplierOrderSub {
@@map("supplier_order_sub")
}
model SupplierReconciliationOrder {
id Int @id @default(autoincrement())
supplierId String
orderId String
mainOrderId String?
userId String?
reconciliationStatus Int @default(0)
diffType Int?
diffReason String? @db.Text
platformTotalPrice Decimal? @db.Decimal(12, 2)
platformCashAmount Decimal? @db.Decimal(12, 2)
platformPointsPrice Decimal? @db.Decimal(12, 2)
platformRefundPrice Decimal? @db.Decimal(12, 2)
supplierTotalPrice Decimal? @db.Decimal(12, 2)
supplierCashAmount Decimal? @db.Decimal(12, 2)
supplierPointsPrice Decimal? @db.Decimal(12, 2)
supplierRefundPrice Decimal? @db.Decimal(12, 2)
reconciliationBatchNo String?
reconciliationTime DateTime?
resultPayload Json?
createAt DateTime @default(now())
updateAt DateTime @updatedAt
deleted Int @default(0)
@@unique([supplierId, orderId])
@@index([supplierId, reconciliationStatus])
@@index([reconciliationBatchNo])
@@index([mainOrderId])
@@map("supplier_reconciliation_order")
}
model SupplierOrderProduct {
id Int @id @default(autoincrement())
subOrderId String
@@ -113,3 +113,33 @@ CREATE TABLE IF NOT EXISTS `supplier_order_product` (
FOREIGN KEY (`subOrderId`) REFERENCES `supplier_order_sub` (`orderId`)
ON DELETE CASCADE ON UPDATE CASCADE
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商子订单商品表';
CREATE TABLE IF NOT EXISTS `supplier_reconciliation_order` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
`orderId` VARCHAR(191) NOT NULL COMMENT '子订单ID',
`mainOrderId` VARCHAR(191) NULL COMMENT '主订单ID',
`userId` VARCHAR(191) NULL COMMENT '平台用户ID',
`reconciliationStatus` TINYINT NOT NULL DEFAULT 0 COMMENT '对账状态:0未对账,1对账成功,2对账异常',
`diffType` INT NULL COMMENT '差异类型',
`diffReason` TEXT NULL COMMENT '差异说明',
`platformTotalPrice` DECIMAL(12, 2) NULL COMMENT '平台订单总金额',
`platformCashAmount` DECIMAL(12, 2) NULL COMMENT '平台现金支付金额',
`platformPointsPrice` DECIMAL(12, 2) NULL COMMENT '平台积分支付金额',
`platformRefundPrice` DECIMAL(12, 2) NULL COMMENT '平台退款金额',
`supplierTotalPrice` DECIMAL(12, 2) NULL COMMENT '供应商订单总金额',
`supplierCashAmount` DECIMAL(12, 2) NULL COMMENT '供应商现金支付金额',
`supplierPointsPrice` DECIMAL(12, 2) NULL COMMENT '供应商积分支付金额',
`supplierRefundPrice` DECIMAL(12, 2) NULL COMMENT '供应商退款金额',
`reconciliationBatchNo` VARCHAR(191) NULL COMMENT '对账批次号',
`reconciliationTime` DATETIME(3) NULL COMMENT '对账完成时间',
`resultPayload` JSON NULL COMMENT '对账回调原始结果',
`createAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
`updateAt` DATETIME(3) NOT NULL COMMENT '记录更新时间',
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `supplier_reconciliation_order_supplierId_orderId_key` (`supplierId`, `orderId`),
KEY `supplier_reconciliation_order_supplierId_reconciliationStatus_idx` (`supplierId`, `reconciliationStatus`),
KEY `supplier_reconciliation_order_reconciliationBatchNo_idx` (`reconciliationBatchNo`),
KEY `supplier_reconciliation_order_mainOrderId_idx` (`mainOrderId`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商子订单对账结果表';
@@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS `supplier_reconciliation_order`
(
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
`orderId` VARCHAR(191) NOT NULL COMMENT '子订单ID',
`mainOrderId` VARCHAR(191) DEFAULT NULL COMMENT '主订单ID',
`userId` VARCHAR(191) DEFAULT NULL COMMENT '用户ID',
`reconciliationStatus` TINYINT NOT NULL DEFAULT 0 COMMENT '对账状态:0未对账,1对账成功,2对账异常',
`diffType` VARCHAR(64) DEFAULT NULL COMMENT '差异类型',
`diffReason` VARCHAR(1000) DEFAULT NULL COMMENT '差异说明',
`platformTotalPrice` DECIMAL(12, 2) DEFAULT NULL COMMENT '平台订单总金额',
`platformCashAmount` DECIMAL(12, 2) DEFAULT NULL COMMENT '平台现金支付金额',
`platformPointsPrice` DECIMAL(12, 2) DEFAULT NULL COMMENT '平台积分支付金额',
`platformRefundPrice` DECIMAL(12, 2) DEFAULT NULL COMMENT '平台退款金额',
`supplierTotalPrice` DECIMAL(12, 2) DEFAULT NULL COMMENT '供应商订单总金额',
`supplierCashAmount` DECIMAL(12, 2) DEFAULT NULL COMMENT '供应商现金支付金额',
`supplierPointsPrice` DECIMAL(12, 2) DEFAULT NULL COMMENT '供应商积分支付金额',
`supplierRefundPrice` DECIMAL(12, 2) DEFAULT NULL COMMENT '供应商退款金额',
`reconciliationBatchNo` VARCHAR(64) DEFAULT NULL COMMENT '对账批次号',
`reconciliationTime` DATETIME(3) DEFAULT NULL COMMENT '对账完成时间',
`resultPayload` JSON DEFAULT NULL COMMENT '对账回调原始结果',
`createAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updateAt` DATETIME(3) NOT NULL,
`deleted` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_supplier_order` (`supplierId`, `orderId`),
KEY `idx_supplier_status` (`supplierId`, `reconciliationStatus`),
KEY `idx_batch_no` (`reconciliationBatchNo`),
KEY `idx_main_order` (`mainOrderId`)
) ENGINE = InnoDB
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci
COMMENT ='供应商子订单对账结果表';
@@ -23,6 +23,13 @@ export const merchantOrderListController = withErrorBoundary(async (request: Req
return toHttpResponse(await merchantService.listOrders(await readJson<MerchantOrderListInput>(request)))
})
export const merchantSupplierListController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
return toHttpResponse(await merchantService.listSuppliers())
})
export const merchantOrderShipController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
@@ -14,7 +14,11 @@ 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 {
supplierOpenOrderService,
type SupplierOrderQueryRequest,
type SupplierReconciliationQueryRequest
} from '../services/supplier-open-order.service.js'
import { toHttpResponse } from './controller-response.js'
type OrderListRequest = {
@@ -239,3 +243,21 @@ export const supplierOrderInfoController = withErrorBoundary(async (request: Req
return Response.json(await supplierOpenOrderService.queryOrderInfo(body))
})
export const supplierReconciliationInfoController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
let body: SupplierReconciliationQueryRequest
try {
body = supplierOpenOrderService.parseBody(await readJson<SupplierReconciliationQueryRequest>(request))
} catch (error) {
return Response.json({
resultCode: 400,
message: error instanceof Error ? error.message : '请求参数解密失败',
orderInfos: []
})
}
return Response.json(await supplierOpenOrderService.queryReconciliationInfo(body))
})
@@ -1,5 +1,6 @@
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 { productService } from '../services/product.service.js'
import { toHttpResponse } from './controller-response.js'
@@ -25,3 +26,28 @@ export const productDetailController = withErrorBoundary(async (request: Request
return toHttpResponse(await productService.getProductDetail(id, sku))
})
export const merchantProductListController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
const body = await readJson<{ keyword?: string; categoryId?: string }>(request)
return toHttpResponse(await productService.listManageProducts(String(body.keyword || '').trim(), String(body.categoryId || '').trim()))
})
export const merchantProductSaveController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
return toHttpResponse(await productService.saveManageProduct(
await readJson<Parameters<typeof productService.saveManageProduct>[0]>(request)
))
})
export const merchantProductDeleteController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
const body = await readJson<{ id?: number }>(request)
return toHttpResponse(await productService.deleteManageProduct(Number(body.id || 0)))
})
@@ -0,0 +1,75 @@
import type { Context } from '../utils/handler.js'
import { withErrorBoundary } from '../utils/handler.js'
import { guardMethod } from '../utils/method.js'
import { readJson } from '../utils/request.js'
import { toHttpResponse } from './controller-response.js'
import { supplierOpenOrderService } from '../services/supplier-open-order.service.js'
import {
reconciliationResultService,
type ReconciliationResultCallbackInput,
type ReconciliationResultListInput,
type ReconciliationResultUpdateInput
} from '../services/reconciliation-result.service.js'
type UnReconciledMsgInput = {
supplierId?: string
token?: string
timestamp?: number
orderIds?: string
unReconciledType?: number
unReconciledMsg?: string
uniqueSeqNo?: string
AplParmObjct?: string
}
export const supplierReconciliationResultController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
let body: ReconciliationResultCallbackInput
try {
body = supplierOpenOrderService.parseBody(await readJson<ReconciliationResultCallbackInput>(request)) as ReconciliationResultCallbackInput
} catch (error) {
return Response.json({ resultCode: 400, message: error instanceof Error ? error.message : '请求参数解密失败' })
}
return Response.json(await reconciliationResultService.receiveCallback(body))
})
export const supplierUnReconciledMsgController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
let body: UnReconciledMsgInput
try {
body = supplierOpenOrderService.parseBody(await readJson<UnReconciledMsgInput>(request)) as UnReconciledMsgInput
} catch (error) {
return Response.json({ resultCode: 400, message: error instanceof Error ? error.message : '请求参数解密失败' })
}
const reconciliationResults = String(body.orderIds || '').split(',').map(orderId => orderId.trim()).filter(Boolean).map(orderId => ({
orderId,
reconciliationStatus: 2,
diffType: body.unReconciledType === undefined ? undefined : Number(body.unReconciledType),
diffReason: body.unReconciledMsg
}))
return Response.json(await reconciliationResultService.receiveCallback({
supplierId: body.supplierId,
token: body.token,
timestamp: body.timestamp,
reconciliationBatchNo: body.uniqueSeqNo,
reconciliationResults
}))
})
export const merchantReconciliationListController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
return toHttpResponse(await reconciliationResultService.list(await readJson<ReconciliationResultListInput>(request)))
})
export const merchantReconciliationUpdateController = withErrorBoundary(async (request: Request, _context: Context) => {
const methodResponse = guardMethod(request, ['POST'])
if (methodResponse) return methodResponse
return toHttpResponse(await reconciliationResultService.update(await readJson<ReconciliationResultUpdateInput>(request)))
})
+28 -2
View File
@@ -137,12 +137,38 @@ export const orderDao = {
})
},
findMerchantSubOrders(supplierId: string, orderState?: number) {
findOpenApiReconciliationSubOrders(supplierId: string, orderIds: string[], startDate?: Date, endDate?: Date, orderState?: number) {
return prisma.supplierOrderSub.findMany({
where: {
supplierId,
deleted: 0,
...(Number.isInteger(orderState) ? { orderState } : {})
...(Number.isInteger(orderState) ? { orderState } : {}),
...(orderIds.length ? { orderId: { in: orderIds } } : {}),
...(startDate || endDate ? {
completeTime: {
...(startDate ? { gte: startDate } : {}),
...(endDate ? { lte: endDate } : {})
}
} : {})
},
include: {
products: {
where: { deleted: 0 },
orderBy: { createAt: 'asc' }
}
},
orderBy: { createAt: 'asc' }
})
},
findMerchantSubOrders(supplierId: string, orderState?: number, orderId?: string, userId?: string) {
return prisma.supplierOrderSub.findMany({
where: {
supplierId,
deleted: 0,
...(Number.isInteger(orderState) ? { orderState } : {}),
...(orderId ? { OR: [{ orderId: { contains: orderId } }, { orderNumberId: { contains: orderId } }] } : {}),
...(userId ? { userId: { contains: userId } } : {})
},
include: {
products: {
+26
View File
@@ -33,5 +33,31 @@ export const productDao = {
]
}
})
},
findManageProducts(keyword: string, categoryId: string) {
return prisma.supplierProduct.findMany({
where: {
deleted: 0,
...(keyword ? { OR: [{ name: { contains: keyword } }, { sku: { contains: keyword } }] } : {}),
...(categoryId ? { categoryId } : {})
},
orderBy: [
{ sortOrder: 'asc' },
{ createdAt: 'desc' }
]
})
},
createProduct(data: Parameters<typeof prisma.supplierProduct.create>[0]['data']) {
return prisma.supplierProduct.create({ data })
},
updateProduct(id: number, data: Parameters<typeof prisma.supplierProduct.update>[0]['data']) {
return prisma.supplierProduct.update({ where: { id }, data })
},
findManageProduct(id: number) {
return prisma.supplierProduct.findFirst({ where: { id, deleted: 0 } })
}
}
+8
View File
@@ -11,5 +11,13 @@ export const supplierDao = {
return prisma.supplierInfo.findFirst({
where: { supplierId, deleted: 0 }
})
},
findActiveSuppliers() {
return prisma.supplierInfo.findMany({
where: { deleted: 0 },
select: { supplierId: true, supplierName: true },
orderBy: { supplierName: 'asc' }
})
}
}
+30 -3
View File
@@ -33,8 +33,15 @@ import {
orderPayController,
orderPushEventController,
orderShipController,
supplierOrderInfoController
supplierOrderInfoController,
supplierReconciliationInfoController
} from './controllers/order.controller.js'
import {
merchantReconciliationListController,
merchantReconciliationUpdateController,
supplierReconciliationResultController,
supplierUnReconciledMsgController
} from './controllers/reconciliation.controller.js'
import {
merchantAfterSaleAuditController,
merchantAfterSaleDetailController,
@@ -42,9 +49,16 @@ import {
merchantAfterSaleReceiveController,
merchantAfterSaleShipController,
merchantOrderListController,
merchantOrderShipController
merchantOrderShipController,
merchantSupplierListController
} from './controllers/merchant.controller.js'
import { productDetailController, productListController } from './controllers/product.controller.js'
import {
merchantProductDeleteController,
merchantProductListController,
merchantProductSaveController,
productDetailController,
productListController
} from './controllers/product.controller.js'
import {
supplierDebugTokensController,
supplierInfoController,
@@ -75,6 +89,7 @@ const handlers: Record<string, FunctionHandler> = {
'merchant-after-sale-ship': merchantAfterSaleShipController,
'merchant-order-list': merchantOrderListController,
'merchant-order-ship': merchantOrderShipController,
'merchant-supplier-list': merchantSupplierListController,
'order-cancel-main': orderCancelMainController,
'order-cancel-paid-main': orderCancelPaidMainController,
'order-cancel-sub': orderCancelSubController,
@@ -87,9 +102,17 @@ const handlers: Record<string, FunctionHandler> = {
'order-ship': orderShipController,
'product-detail': productDetailController,
'product-list': productListController,
'merchant-product-list': merchantProductListController,
'merchant-product-save': merchantProductSaveController,
'merchant-product-delete': merchantProductDeleteController,
'supplier-debug-tokens': supplierDebugTokensController,
'supplier-info': supplierInfoController,
'supplier-order-info': supplierOrderInfoController,
'supplier-reconciliation-info': supplierReconciliationInfoController,
'supplier-reconciliation-result': supplierReconciliationResultController,
'supplier-unreconciled-msg': supplierUnReconciledMsgController,
'merchant-reconciliation-list': merchantReconciliationListController,
'merchant-reconciliation-update': merchantReconciliationUpdateController,
'supplier-points': supplierPointsController,
'supplier-token': supplierTokenController,
upload: uploadController,
@@ -174,7 +197,11 @@ router.delete('/api/supplier-logs', ctx => {
}
})
router.all('/api/:name', async ctx => dispatch(ctx.params.name)(ctx))
router.all('/api/admin/:name', async ctx => dispatch(ctx.params.name)(ctx))
router.post('/supplierOpenApi/pointOrder/queryOrderInfo', dispatch('supplier-order-info'))
router.post('/supplierOpenApi/pointOrder/queryReconciliationInfo', dispatch('supplier-reconciliation-info'))
router.post('/supplierOpenApi/pointOrder/reconciliationResult', dispatch('supplier-reconciliation-result'))
router.post('/supplierOpenApi/pointOrder/unReconciledMsg', dispatch('supplier-unreconciled-msg'))
router.post('/supplierOpenApi/debug/tokens', dispatch('supplier-debug-tokens'))
app.use(router.routes())
@@ -5,6 +5,7 @@ import {
ORDER_STATE
} from '../constants/order.js'
import { orderDao } from '../dao/order.dao.js'
import { supplierDao } from '../dao/supplier.dao.js'
import { afterSaleService, type AfterSaleAuditInput, type AfterSaleLogisticsInput } from './after-sale.service.js'
import { orderActionService } from './order-action.service.js'
import { logger } from '../utils/logger.js'
@@ -13,6 +14,8 @@ import { serviceFail, serviceOk } from '../types/service-result.js'
export type MerchantOrderListInput = {
supplierId?: string
orderState?: number
orderId?: string
userId?: string
}
export type MerchantShipInput = {
@@ -105,14 +108,16 @@ const isOrderSigned = (orderState?: number | null) => signedOrderStates.has(Numb
export const merchantService = {
async listOrders(input: MerchantOrderListInput) {
const supplierId = String(input.supplierId || '').trim()
const orderState = Number(input.orderState ?? ORDER_STATE.FINISH_PAY)
const orderState = input.orderState === undefined ? undefined : Number(input.orderState)
const orderId = String(input.orderId || '').trim()
const userId = String(input.userId || '').trim()
logger.info('merchant order list query', { supplierId, orderState })
if (!supplierId) {
return serviceFail('供应商不能为空')
}
const orders = await orderDao.findMerchantSubOrders(supplierId, orderState)
const orders = await orderDao.findMerchantSubOrders(supplierId, orderState, orderId, userId)
const afterSales = await orderDao.findMerchantAfterSales(supplierId, pendingShipExcludeAfterSaleStates)
const afterSaleSubOrderIds = new Set(afterSales.map(item => item.subOrderId).filter(Boolean))
const orderItems = orders.filter(order => !afterSaleSubOrderIds.has(order.orderId)).map(order => ({
@@ -139,6 +144,11 @@ export const merchantService = {
return serviceOk({ orderItems })
},
async listSuppliers() {
const suppliers = await supplierDao.findActiveSuppliers()
return serviceOk({ suppliers })
},
shipOrder(input: MerchantShipInput) {
return orderActionService.shipOrder({
supplierId: String(input.supplierId || '').trim(),
@@ -35,7 +35,9 @@ const toProductItem = (product: ProductRecord) => {
category: product.category || '',
tag: product.tag || '',
sales: product.sales || '',
stock: product.stock
stock: product.stock,
sortOrder: product.sortOrder,
enabled: product.enabled
}
}
@@ -64,5 +66,67 @@ export const productService = {
}
return serviceOk(toProductItem(product))
},
async listManageProducts(keyword: string, categoryId: string) {
const products = await productDao.findManageProducts(keyword, categoryId)
return serviceOk({ categories, products: products.map(toProductItem) })
},
async saveManageProduct(input: {
id?: number
sku?: string
name?: string
price?: number
number?: number
stock?: number
taxRate?: number
categoryId?: string
category?: string
imageUrl?: string
description?: string
detail?: string
originPrice?: number
tag?: string
sales?: string
sortOrder?: number
enabled?: boolean
}) {
const sku = String(input.sku || '').trim()
const name = String(input.name || '').trim()
const price = Number(input.price)
if (!sku || !name || Number.isNaN(price) || price < 0) {
return serviceFail('商品SKU、名称和价格不能为空')
}
const data = {
sku,
name,
price,
number: Math.max(Number(input.number || 0), 0),
stock: Math.max(Number(input.stock || 0), 0),
taxRate: Number(input.taxRate ?? 0.13),
categoryId: String(input.categoryId || '').trim() || null,
category: String(input.category || '').trim() || null,
imageUrl: String(input.imageUrl || '').trim() || null,
description: String(input.description || '').trim() || null,
detail: String(input.detail || '').trim() || null,
originPrice: input.originPrice === undefined || input.originPrice === null ? null : Number(input.originPrice),
tag: String(input.tag || '').trim() || null,
sales: String(input.sales || '').trim() || null,
sortOrder: Math.max(Number(input.sortOrder || 0), 0),
enabled: input.enabled !== false
}
const id = Number(input.id || 0)
const product = id > 0 ? await productDao.updateProduct(id, data) : await productDao.createProduct(data)
return serviceOk(toProductItem(product))
},
async deleteManageProduct(id: number) {
if (!Number.isInteger(id) || id <= 0) return serviceFail('商品ID不正确')
const product = await productDao.findManageProduct(id)
if (!product) return serviceFail('商品不存在')
await productDao.updateProduct(id, { deleted: 1 })
return serviceOk({})
}
}
@@ -0,0 +1,176 @@
import type { Prisma } from '@prisma/client'
import { prisma } from '../utils/prisma.js'
import { supplierDao } from '../dao/supplier.dao.js'
import { verifySupplierOpenSignature } from '../utils/supplier-open-crypto.js'
import { serviceFail, serviceOk } from '../types/service-result.js'
export type ReconciliationResultItem = {
orderId?: string
mainOrderId?: string
userId?: string
reconciliationStatus?: number
diffType?: number
diffReason?: string
platformTotalPrice?: number
platformCashAmount?: number
platformPointsPrice?: number
platformRefundPrice?: number
supplierTotalPrice?: number
supplierCashAmount?: number
supplierPointsPrice?: number
supplierRefundPrice?: number
}
export type ReconciliationResultCallbackInput = {
supplierId?: string
token?: string
timestamp?: number
reconciliationBatchNo?: string
reconciliationTime?: string
reconciliationResults?: ReconciliationResultItem[]
AplParmObjct?: string
}
export type ReconciliationResultListInput = {
supplierId?: string
reconciliationStatus?: number
orderId?: string
}
export type ReconciliationResultUpdateInput = {
supplierId?: string
orderId?: string
reconciliationStatus?: number
diffType?: number
diffReason?: string
}
const toNumber = (value?: number) => value === undefined || value === null ? null : Number(value)
const toTime = (value?: string) => {
if (!value) return new Date()
const time = new Date(value)
return Number.isNaN(time.getTime()) ? new Date() : time
}
const resultItem = (record: Awaited<ReturnType<typeof prisma.supplierReconciliationOrder.findMany>>[number]) => ({
supplierId: record.supplierId,
orderId: record.orderId,
mainOrderId: record.mainOrderId || '',
userId: record.userId || '',
reconciliationStatus: record.reconciliationStatus,
diffType: record.diffType,
diffReason: record.diffReason || '',
platformTotalPrice: record.platformTotalPrice?.toFixed(2) || '',
platformCashAmount: record.platformCashAmount?.toFixed(2) || '',
platformPointsPrice: record.platformPointsPrice?.toFixed(2) || '',
platformRefundPrice: record.platformRefundPrice?.toFixed(2) || '',
supplierTotalPrice: record.supplierTotalPrice?.toFixed(2) || '',
supplierCashAmount: record.supplierCashAmount?.toFixed(2) || '',
supplierPointsPrice: record.supplierPointsPrice?.toFixed(2) || '',
supplierRefundPrice: record.supplierRefundPrice?.toFixed(2) || '',
reconciliationBatchNo: record.reconciliationBatchNo || '',
reconciliationTime: record.reconciliationTime,
updateAt: record.updateAt
})
export const reconciliationResultService = {
async receiveCallback(input: ReconciliationResultCallbackInput) {
const supplierId = String(input.supplierId || '').trim()
const token = String(input.token || '').trim()
const timestamp = Number(input.timestamp || 0)
const records = Array.isArray(input.reconciliationResults) ? input.reconciliationResults : []
if (!supplierId || !token || !timestamp || !records.length) {
return { resultCode: 400, message: '供应商、签名或对账结果不能为空' }
}
const supplier = await supplierDao.findActiveBySupplierId(supplierId)
if (!supplier) return { resultCode: 404, message: '供应商信息不存在' }
if (!verifySupplierOpenSignature(supplier.supplierId, supplier.clientId, timestamp, token)) {
return { resultCode: 401, message: '签名无效或已过期' }
}
const reconciliationTime = toTime(input.reconciliationTime)
for (const record of records) {
const orderId = String(record.orderId || '').trim()
const status = Number(record.reconciliationStatus)
if (!orderId || ![0, 1, 2].includes(status)) {
return { resultCode: 400, message: '子订单ID或对账状态不正确' }
}
}
await prisma.$transaction(records.map(record => {
const orderId = String(record.orderId || '').trim()
const data = {
mainOrderId: String(record.mainOrderId || '').trim() || null,
userId: String(record.userId || '').trim() || null,
reconciliationStatus: Number(record.reconciliationStatus),
diffType: record.diffType === undefined || record.diffType === null ? null : Number(record.diffType),
diffReason: String(record.diffReason || '').trim() || null,
platformTotalPrice: toNumber(record.platformTotalPrice),
platformCashAmount: toNumber(record.platformCashAmount),
platformPointsPrice: toNumber(record.platformPointsPrice),
platformRefundPrice: toNumber(record.platformRefundPrice),
supplierTotalPrice: toNumber(record.supplierTotalPrice),
supplierCashAmount: toNumber(record.supplierCashAmount),
supplierPointsPrice: toNumber(record.supplierPointsPrice),
supplierRefundPrice: toNumber(record.supplierRefundPrice),
reconciliationBatchNo: String(input.reconciliationBatchNo || '').trim() || null,
reconciliationTime,
resultPayload: record as Prisma.InputJsonValue,
deleted: 0
}
return prisma.supplierReconciliationOrder.upsert({
where: { supplierId_orderId: { supplierId, orderId } },
update: data,
create: { supplierId, orderId, ...data }
})
}))
return { resultCode: 0, message: '成功' }
},
async list(input: ReconciliationResultListInput) {
const supplierId = String(input.supplierId || '').trim()
if (!supplierId) return serviceFail('供应商不能为空')
const reconciliationStatus = input.reconciliationStatus === undefined ? undefined : Number(input.reconciliationStatus)
if (reconciliationStatus !== undefined && ![0, 1, 2].includes(reconciliationStatus)) {
return serviceFail('对账状态不正确')
}
const records = await prisma.supplierReconciliationOrder.findMany({
where: {
supplierId,
deleted: 0,
...(reconciliationStatus === undefined ? {} : { reconciliationStatus }),
...(String(input.orderId || '').trim() ? { orderId: { contains: String(input.orderId).trim() } } : {})
},
orderBy: { updateAt: 'desc' }
})
return serviceOk({ reconciliationItems: records.map(resultItem) })
},
async update(input: ReconciliationResultUpdateInput) {
const supplierId = String(input.supplierId || '').trim()
const orderId = String(input.orderId || '').trim()
const reconciliationStatus = Number(input.reconciliationStatus)
if (!supplierId || !orderId || ![0, 1, 2].includes(reconciliationStatus)) {
return serviceFail('供应商、子订单或对账状态不正确')
}
const record = await prisma.supplierReconciliationOrder.findUnique({
where: { supplierId_orderId: { supplierId, orderId } }
})
if (!record || record.deleted !== 0) return serviceFail('对账结果不存在')
await prisma.supplierReconciliationOrder.update({
where: { supplierId_orderId: { supplierId, orderId } },
data: {
reconciliationStatus,
diffType: input.diffType === undefined || input.diffType === null ? null : Number(input.diffType),
diffReason: String(input.diffReason || '').trim() || null
}
})
return serviceOk({})
}
}
@@ -13,6 +13,19 @@ export type SupplierOrderQueryRequest = {
AplParmObjct?: string
}
export type SupplierReconciliationQueryRequest = {
supplierId?: string
token?: string
reconciliationType?: number
orderId?: string
startDate?: string
endDate?: string
page?: number
pageSize?: number
timestamp?: number
AplParmObjct?: string
}
const parseBody = (body: SupplierOrderQueryRequest) => {
if (!body.AplParmObjct) return body
@@ -149,5 +162,66 @@ export const supplierOpenOrderService = {
}))
return { resultCode: 0, message: '成功', orderInfo }
},
async queryReconciliationInfo(body: SupplierReconciliationQueryRequest) {
const supplierId = String(body.supplierId || '').trim()
const token = String(body.token || '').trim()
const reconciliationType = Number(body.reconciliationType || 0)
const orderIds = String(body.orderId || '')
.split(',')
.map(item => item.trim())
.filter(Boolean)
const timestamp = Number(body.timestamp || 0)
if (!supplierId || !token || !timestamp) {
return { resultCode: 400, message: '供应商或签名信息不能为空', orderInfos: [] }
}
if (reconciliationType === 1 && !body.startDate) {
return { resultCode: 400, message: '请选择开始日期', orderInfos: [] }
}
if (reconciliationType === 2 && !orderIds.length) {
return { resultCode: 400, message: '订单号不能为空', orderInfos: [] }
}
if (![1, 2].includes(reconciliationType)) {
return { resultCode: 400, message: '对账类型不正确', orderInfos: [] }
}
const supplier = await supplierDao.findActiveBySupplierId(supplierId)
if (!supplier) {
return { resultCode: 404, message: '供应商信息不存在', orderInfos: [] }
}
if (!verifySupplierOpenSignature(supplier.supplierId, supplier.clientId, timestamp, token)) {
return { resultCode: 401, message: '签名无效或已过期', orderInfos: [] }
}
const startDate = body.startDate ? new Date(`${body.startDate}T00:00:00`) : undefined
const endDate = body.endDate ? new Date(`${body.endDate}T23:59:59`) : undefined
if ((startDate && Number.isNaN(startDate.getTime())) || (endDate && Number.isNaN(endDate.getTime()))) {
return { resultCode: 400, message: '对账日期格式不正确', orderInfos: [] }
}
const orders = await orderDao.findOpenApiReconciliationSubOrders(
supplierId,
orderIds,
startDate,
endDate,
reconciliationType === 1 ? ORDER_STATE.COMPLETED : undefined
)
const page = Math.max(Number(body.page || 1), 1)
const pageSize = Math.max(Number(body.pageSize || 100), 1)
const totalPage = Math.max(Math.ceil(orders.length / pageSize), 1)
const pagedOrders = orders.slice((page - 1) * pageSize, page * pageSize)
const orderInfos = pagedOrders.map(order => ({
orderId: order.orderId,
supplierId: order.supplierId,
totalPrice: toMoney(order.totalPrice),
pointsPrice: toMoney(order.pointsPrice),
wPayOrAPay: toMoney(order.wPayOrAPay),
invoiceNakedPrice: toMoney(order.totalPrice),
orderProInfos: toOrderProductInfo(order)
}))
return { resultCode: 0, message: '成功', totalPage, currentPage: page, orderInfos }
}
}