Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21646b8797 | ||
|
|
ff77ba16cc | ||
|
|
f42ea5b270 | ||
|
|
3abe4ce9c9 | ||
|
|
fc5df9c048 | ||
|
|
5c8fbddb2e | ||
|
|
750c36116d |
@@ -1,10 +1,65 @@
|
|||||||
# points-mall
|
# points-mall
|
||||||
|
|
||||||
积分商城-供应商模块-订单操作模块
|
积分商城供应商项目,对接 zhgh 工会平台,包含商城 H5、供应商管理后台及业务服务。
|
||||||
|
|
||||||
## 页面与接口
|
## 项目组成
|
||||||
|
|
||||||
|
| 目录 | 用途 | 技术栈 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `supplied-h5` | 商品浏览、购物车、下单支付、订单与售后、商家处理台 | Vue 3、Vant 3、Pinia、Vue Router 4、Vue CLI |
|
||||||
|
| `supplied-admin` | 商品管理、订单管理、售后处理、订单对账 | Vue 3、Element Plus、Pinia、Vue Router 4、Vite |
|
||||||
|
| `supplied-service` | 商城接口、管理端接口、工会平台对接、文件处理 | Koa、TypeScript、Prisma、MySQL、Redis、MinIO |
|
||||||
|
|
||||||
|
H5 和管理端的 Vue 版本声明均为 `^3.5.13`,H5 的 Vant 版本声明为 `^3.6.16`。
|
||||||
|
|
||||||
|
## 主要功能
|
||||||
|
|
||||||
|
- 商城 H5:商品与规格选择、购物车、地址管理、下单支付、订单查询及售后申请。
|
||||||
|
- 管理后台:账号登录、商品维护、订单筛选与分页、确认发货、重新推送拆单、售后审核与处理、订单对账。
|
||||||
|
- 平台对接:供应商授权、订单事件推送、对账及发票相关接口。
|
||||||
|
- 调试日志:查看供应商后端与 zhgh 之间的请求记录。
|
||||||
|
|
||||||
|
## 本地访问入口
|
||||||
|
|
||||||
|
以下为默认开发地址,实际地址以端口和环境配置为准。
|
||||||
|
|
||||||
|
| 入口 | 地址 |
|
||||||
|
| --- | --- |
|
||||||
|
| 管理端登录 | `http://localhost:5173/login` |
|
||||||
|
| 管理端订单列表 | `http://localhost:5173/orders/list` |
|
||||||
|
| 商城 H5 | `http://localhost:3001/` |
|
||||||
|
| 商城 API | `http://localhost:8888/api` |
|
||||||
|
| 管理端 API | `http://localhost:8888/api/admin` |
|
||||||
|
|
||||||
|
### 商家处理与日志
|
||||||
|
|
||||||
- 商家处理台:`http://localhost:3001/#/joyful-selection/merchant`
|
- 商家处理台:`http://localhost:3001/#/joyful-selection/merchant`
|
||||||
- 供应商日志页面:`http://localhost:3001/#/joyful-selection/supplier-logs`
|
- 供应商日志页面:`http://localhost:3001/#/joyful-selection/supplier-logs`
|
||||||
- 供应商日志接口:`GET http://localhost:8888/api/supplier-logs`
|
- 供应商日志接口:`GET http://localhost:8888/api/supplier-logs`
|
||||||
- 日志内容包含:当前后端接收请求、向zhgh发送请求、接收zhgh请求。
|
- 日志内容包含:当前后端接收请求、向zhgh发送请求、接收zhgh请求。
|
||||||
|
|
||||||
|
|
||||||
|
## 管理端登录
|
||||||
|
|
||||||
|
当前本地开发数据库已初始化以下账号;密码由初始化脚本生成并单独交付,不在文档中保存。
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 账号 | `admin` |
|
||||||
|
| 登录后进入订单列表 | `http://localhost:5173/login?redirect=/orders/list` |
|
||||||
|
|
||||||
|
- 登录账号存储在 `supplied_user`,密码使用随机盐和 scrypt 哈希保存,不保存明文密码。
|
||||||
|
- 管理端使用 Bearer Token,登录有效期为 8 小时;同一账号重新登录会替换旧 Token。
|
||||||
|
- `/api/admin/*` 除登录接口外均校验登录状态;退出登录会使当前 Token 失效。
|
||||||
|
- 同一启用账号连续输错密码 5 次后锁定 15 分钟。
|
||||||
|
|
||||||
|
## 数据库与配置
|
||||||
|
|
||||||
|
- 管理账号表:`supplied_user`。
|
||||||
|
- 购物车表:`supplied_cart_item`;保留业务用户 `userId`,不关联后台管理员账号。
|
||||||
|
- 原 `user` 空表已移除,购物车表名统一使用 `supplied_cart_item`。
|
||||||
|
- 后端配置位于 `supplied-service/.env`,字段参考 `.env.example`。
|
||||||
|
- H5 使用 `VUE_APP_SUPPLIED_API_BASE` 指定后端地址,使用 `VUE_APP_SUPPLIER_IDENTIFICATION` 指定供应商标识。
|
||||||
|
- 管理端使用 `VITE_SUPPLIED_ADMIN_API_BASE` 指定管理接口地址。
|
||||||
|
|
||||||
|
后端目录、接口及初始化脚本说明见 [supplied-service/README.md](supplied-service/README.md)。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
PORT=8050
|
||||||
|
SUPPLIED_SERVICE_BASE_URL=http://127.0.0.1:8888
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "esf-relay",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/server.mjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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/applyForInvoices', '/supplierOpenApi/pointOrder/applyForInvoices'],
|
||||||
|
['/supplierOpenApi/pointOrder/queryInvoicesInfo', '/supplierOpenApi/pointOrder/queryInvoicesInfo'],
|
||||||
|
['/supplierOpenApi/pointOrder/queryFileBase64Info', '/supplierOpenApi/pointOrder/queryFileBase64Info'],
|
||||||
|
['/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`)
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
Generated
+2697
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<template><RouterView /></template>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { clearAdminSession, readAdminSession } from '@/utils/auth'
|
||||||
|
|
||||||
|
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 resolveImageUrl = (value: string) => /^\/api\/(?:file|upload-preview)\?/.test(value)
|
||||||
|
? `${normalizeBase(API_BASE).replace(/\/api\/admin$/, '')}${value.replace('/api/upload-preview?', '/api/file?')}`
|
||||||
|
: value
|
||||||
|
|
||||||
|
export const postAdmin = async <T>(name: string, body: Record<string, unknown>): Promise<T> => {
|
||||||
|
const token = readAdminSession()?.token
|
||||||
|
const response = await fetch(`${normalizeBase(API_BASE)}/${name}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
const payload = await response.json().catch(() => ({}))
|
||||||
|
if (response.status === 401 && name !== 'login') {
|
||||||
|
clearAdminSession()
|
||||||
|
if (name !== 'admin-session' && window.location.pathname !== '/login') {
|
||||||
|
window.location.replace(`/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!response.ok || payload.success === false) {
|
||||||
|
throw new Error(payload.error?.message || payload.msg || '请求失败')
|
||||||
|
}
|
||||||
|
return payload.data as T
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<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" @submit.prevent="loadRecords"><el-form-item label="售后单号"><el-input v-model="serviceOrderId" placeholder="请输入售后单号" 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><div v-if="scene !== 'process'" v-loading="loading" class="audit-list">
|
||||||
|
<article v-for="row in records" :key="row.serviceOrderId" class="audit-card">
|
||||||
|
<header class="audit-card-header"><strong>{{ row.serviceOrderId }}</strong><el-tag type="warning">{{ row.serviceTypeText || '-' }}</el-tag></header>
|
||||||
|
<div v-for="product in row.products || []" :key="product.id || product.sku" class="audit-product">
|
||||||
|
<el-image :src="resolveImageUrl(product.image || '')" :preview-src-list="product.image ? [resolveImageUrl(product.image)] : []" preview-teleported fit="cover"><template #error><span class="image-empty">暂无图片</span></template></el-image>
|
||||||
|
<div class="audit-product-info"><strong>{{ product.name }}</strong><span>{{ specText(product.sku) }}</span><b>¥{{ money(product.price) }}</b></div><span class="audit-quantity">×{{ product.number }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="audit-summary"><dl><dt>原因</dt><dd>{{ row.reason || '-' }}</dd><dt>描述</dt><dd>{{ row.problemDescription || '-' }}</dd></dl><el-tag :type="row.isSigned === true ? 'success' : 'info'">{{ row.isSigned === true ? '已签收' : row.isSigned === false ? '未签收' : '签收状态未知' }}</el-tag></div>
|
||||||
|
<div v-if="scene === 'audit'" class="after-sale-status"><el-tag :type="row.serviceState === 0 ? 'warning' : 'primary'">{{ currentStatusText(row) }}</el-tag></div>
|
||||||
|
<div v-if="scene === 'history'" class="history-result"><span class="result-stamp" :class="{ approved: row.serviceState === 5, rejected: row.serviceState === 2 }">{{ historyResultText(row) }}</span></div>
|
||||||
|
<footer class="audit-card-footer"><span>{{ row.applyTime || '-' }}</span><div><el-button @click="openDetail(row)">查看详情</el-button><el-button v-if="scene === 'audit' && row.serviceState === 0" type="danger" plain :disabled="auditing" @click="audit(row, 2)">拒绝</el-button><el-button v-if="scene === 'audit' && row.serviceState === 0" type="primary" :disabled="auditing" @click="audit(row, 1)">通过</el-button><el-button v-if="canReceive(row)" type="primary" :disabled="auditing" @click="receive(row)">确认收货</el-button><el-button v-if="canShip(row)" type="primary" :disabled="auditing" @click="openShipDialog(row)">换货发货</el-button></div></footer>
|
||||||
|
</article>
|
||||||
|
<el-empty v-if="!loading && !records.length" :description="scene === 'audit' ? '暂无待处理售后' : '暂无售后记录'" />
|
||||||
|
</div><el-table v-else 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><template #empty><el-empty :image-size="80" description="暂无售后记录" /></template></el-table><el-dialog v-model="detailVisible" title="售后详情" :width="scene !== 'process' ? '1080px' : '760px'">
|
||||||
|
<div v-if="scene !== 'process' && detail.afterSale" class="audit-detail">
|
||||||
|
<section class="audit-detail-section audit-progress"><span v-if="scene === 'history'" class="result-stamp detail-result-stamp" :class="{ approved: detail.afterSale.serviceState === 5, rejected: detail.afterSale.serviceState === 2 }" aria-label="处理结果">{{ historyResultText(detail.afterSale) }}</span><h3>售后进度</h3><el-steps align-center>
|
||||||
|
<el-step title="提交申请" status="success"><template #description><p>用户提交售后申请</p><p>{{ detail.afterSale.applyTime || '-' }}</p></template></el-step>
|
||||||
|
<el-step title="商家审核" :status="detail.afterSale.auditTime ? (detail.afterSale.auditStatus === 2 ? 'error' : 'success') : detail.afterSale.serviceState === 6 ? 'wait' : 'process'"><template #description><p>{{ detail.afterSale.auditRemark || (detail.afterSale.serviceState === 6 ? '申请已撤销' : '待商家审核') }}</p><p>{{ detail.afterSale.auditTime || '-' }}</p></template></el-step>
|
||||||
|
<el-step v-if="detail.afterSale.isSigned && detail.afterSale.auditStatus === 1" title="商品寄回" :status="detail.afterSale.userShipTime ? 'success' : 'process'"><template #description><p>{{ detail.afterSale.userShipTime ? '用户已寄回商品' : '等待用户填写寄回物流' }}</p><p>{{ detail.afterSale.userShipTime || '-' }}</p></template></el-step>
|
||||||
|
<el-step v-if="detail.afterSale.isSigned && detail.afterSale.auditStatus === 1" title="商家收货" :status="detail.afterSale.merchantReceiveTime ? 'success' : detail.afterSale.userShipTime ? 'process' : 'wait'"><template #description><p>{{ detail.afterSale.merchantReceiveTime ? '商家已确认收货' : '等待商家收到商品' }}</p><p>{{ detail.afterSale.merchantReceiveTime || '-' }}</p></template></el-step>
|
||||||
|
<el-step :title="detail.afterSale.serviceState === 6 ? '已撤销' : detail.afterSale.serviceState === 2 ? '审核未通过' : '售后完成'" :status="detail.afterSale.serviceState === 5 ? 'success' : 'wait'"><template #description><p>{{ detail.afterSale.serviceState === 5 ? '售后已完成' : detail.afterSale.serviceState === 2 ? '审核未通过,售后已结束' : detail.afterSale.serviceState === 6 ? '用户已撤销售后申请' : '等待售后处理完成' }}</p><p>{{ detail.afterSale.finishTime || '-' }}</p></template></el-step>
|
||||||
|
</el-steps></section>
|
||||||
|
<section class="audit-detail-section"><h3>售后商品</h3><div v-for="product in detail.afterSale.products || []" :key="product.id || product.sku" class="audit-product"><el-image :src="resolveImageUrl(product.image || '')" :preview-src-list="product.image ? [resolveImageUrl(product.image)] : []" preview-teleported fit="cover"><template #error><span class="image-empty">暂无图片</span></template></el-image><div class="audit-product-info"><strong>{{ product.name }}</strong><span>{{ specText(product.sku) }}</span><b>¥{{ money(product.price) }}</b></div><span class="audit-quantity">×{{ product.number }}</span></div></section>
|
||||||
|
<section class="audit-detail-section"><h3>售后信息</h3><el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="售后单号">{{ detail.afterSale.serviceOrderId }}</el-descriptions-item><el-descriptions-item label="子订单号">{{ detail.afterSale.orderNumberId || detail.afterSale.subOrderId || '-' }}</el-descriptions-item><el-descriptions-item label="售后类型"><el-tag type="warning">{{ detail.afterSale.serviceTypeText || '-' }}</el-tag></el-descriptions-item><el-descriptions-item label="签收状态"><el-tag :type="detail.afterSale.isSigned === true ? 'success' : 'info'">{{ detail.afterSale.isSigned === true ? '已签收' : detail.afterSale.isSigned === false ? '未签收' : '-' }}</el-tag></el-descriptions-item><el-descriptions-item label="当前状态"><el-tag>{{ currentStatusText(detail.afterSale) }}</el-tag></el-descriptions-item><el-descriptions-item v-if="detail.afterSale.userLogisticsNo" label="寄回物流">{{ detail.afterSale.userLogisticsCompany || '-' }} / {{ detail.afterSale.userLogisticsNo }}</el-descriptions-item><el-descriptions-item v-if="detail.afterSale.merchantReceiveTime" label="商家收货时间">{{ detail.afterSale.merchantReceiveTime }}</el-descriptions-item><el-descriptions-item v-if="detail.afterSale.merchantLogisticsNo" label="换货物流">{{ detail.afterSale.merchantLogisticsCompany || '-' }} / {{ detail.afterSale.merchantLogisticsNo }}</el-descriptions-item><el-descriptions-item label="售后原因">{{ detail.afterSale.reason || '-' }}</el-descriptions-item><el-descriptions-item label="问题描述">{{ detail.afterSale.problemDescription || '-' }}</el-descriptions-item><el-descriptions-item label="申请时间">{{ detail.afterSale.applyTime || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions><div v-if="detailImages.length" class="audit-evidence"><el-image v-for="url in detailImages" :key="url" :src="url" :preview-src-list="detailImages" preview-teleported fit="cover" /></div></section>
|
||||||
|
</div><pre v-else class="detail-json">{{ JSON.stringify(detail, null, 2) }}</pre>
|
||||||
|
<template #footer><el-button @click="detailVisible = false">关闭</el-button><template v-if="scene === 'audit' && detail.afterSale?.serviceState === 0"><el-button type="danger" plain :disabled="auditing" @click="audit(detail.afterSale, 2)">拒绝</el-button><el-button type="primary" :disabled="auditing" @click="audit(detail.afterSale, 1)">通过</el-button></template><el-button v-if="detail.afterSale && canReceive(detail.afterSale)" type="primary" :disabled="auditing" @click="receive(detail.afterSale)">确认收货</el-button><el-button v-if="detail.afterSale && canShip(detail.afterSale)" type="primary" :disabled="auditing" @click="openShipDialog(detail.afterSale)">换货发货</el-button></template>
|
||||||
|
</el-dialog><el-dialog v-model="shipVisible" title="换货发货" width="460px" append-to-body><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" :loading="auditing" @click="shipExchange">确认发货</el-button></template></el-dialog></section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { postAdmin, resolveImageUrl } from '@/api/admin'
|
||||||
|
const props = defineProps<{ scene: 'audit' | 'process' | 'history'; title: string; description: string }>()
|
||||||
|
type AfterSaleProduct = { id?: number; sku: string; name: string; number: number; price: string; image?: string }
|
||||||
|
type AfterSale = { serviceOrderId: string; supplierId: string; serviceState?: number; logisticsState?: number; serviceType?: number; isSigned?: boolean; serviceTypeText?: string; serviceStateText?: string; reason?: string; applyTime?: string; problemDescription?: string; products?: AfterSaleProduct[]; subOrderId?: string; orderNumberId?: string; auditStatus?: number; auditTime?: string; auditRemark?: string; finishTime?: string; userLogisticsNo?: string; userLogisticsCompany?: string; merchantLogisticsNo?: string; merchantLogisticsCompany?: string; userShipTime?: string; merchantReceiveTime?: string; merchantShipTime?: string; images?: Array<string | { url?: string; previewUrl?: string; fileUrl?: string }> }
|
||||||
|
const serviceOrderId = ref('')
|
||||||
|
const auditing = ref(false)
|
||||||
|
const currentStatusText = (row: AfterSale) => {
|
||||||
|
if (row.serviceState === 1 && row.logisticsState === 1) return '审核通过,待用户寄回'
|
||||||
|
if (row.serviceState === 3) return '用户已寄回,待商家收货'
|
||||||
|
if (row.serviceState === 4 && row.serviceType === 5) return '商家已收货,待换货发货'
|
||||||
|
if (row.serviceState === 1 && row.serviceType === 5 && row.isSigned === false) return '审核通过,待换货发货'
|
||||||
|
return row.serviceStateText || '-'
|
||||||
|
}
|
||||||
|
const historyResultText = (row: AfterSale) => row.serviceState === 5 ? '审核通过' : row.serviceStateText || '-'
|
||||||
|
const money = (value: string) => Number(value || 0).toFixed(2)
|
||||||
|
const specText = (sku: string) => {
|
||||||
|
const labels: Record<string, string> = { cream: '奶油白', blue: '雾霾蓝', orange: '暖橙色', standard: '标准款', plus: '升级款' }
|
||||||
|
const parts = String(sku || '').split('-').slice(-2)
|
||||||
|
return parts.some(part => labels[part]) ? parts.map(part => labels[part] || part).join(' | ') : '-'
|
||||||
|
}
|
||||||
|
const detailImages = computed(() => (detail.value.afterSale?.images || []).map(item => resolveImageUrl(typeof item === 'string' ? item : item.fileUrl || item.url || item.previewUrl || '')).filter(Boolean))
|
||||||
|
const records = ref<AfterSale[]>([]); const loading = ref(false); const queried = ref(false); const detailVisible = ref(false); const detail = ref<{ afterSale?: AfterSale }>({}); const shipVisible = ref(false); const current = ref<AfterSale>(); const shipForm = ref({ logisticsCompany: '', logisticsNo: '' })
|
||||||
|
const loadRecords = async () => { loading.value = true; try { const data = await postAdmin<{ afterSaleItems?: AfterSale[] }>('merchant-after-sale-list', { scene: props.scene, serviceOrderId: serviceOrderId.value.trim() }); records.value = data.afterSaleItems || []; queried.value = true } catch (error) { ElMessage.error(error instanceof Error ? error.message : '售后查询失败') } finally { loading.value = false } }
|
||||||
|
const reset = () => { serviceOrderId.value = ''; return loadRecords() }
|
||||||
|
const openDetail = async (row: AfterSale) => { try { detail.value = await postAdmin<{ afterSale: AfterSale }>('merchant-after-sale-detail', { supplierId: row.supplierId, serviceOrderId: row.serviceOrderId }); detailVisible.value = true } catch (error) { ElMessage.error(error instanceof Error ? error.message : '售后详情查询失败') } }
|
||||||
|
const audit = async (row: AfterSale, auditStatus: number) => { if (auditing.value) return; auditing.value = true; try { await ElMessageBox.confirm(auditStatus === 1 ? '确认通过该售后申请?' : '确认拒绝该售后申请?', '售后审核'); await postAdmin('merchant-after-sale-audit', { supplierId: row.supplierId, serviceOrderId: row.serviceOrderId, auditStatus, auditRemark: auditStatus === 1 ? '审核通过' : '商家拒绝' }); ElMessage.success('处理成功'); detailVisible.value = false; await loadRecords() } catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '处理失败') } finally { auditing.value = false } }
|
||||||
|
const canReceive = (row: AfterSale) => props.scene !== 'history' && Number(row.serviceState) === 3
|
||||||
|
const canShip = (row: AfterSale) => props.scene !== 'history' && Number(row.serviceType) === 5 && (Number(row.serviceState) === 4 || (row.isSigned === false && Number(row.serviceState) === 1))
|
||||||
|
const receive = async (row: AfterSale) => { if (auditing.value) return; auditing.value = true; try { await ElMessageBox.confirm('确认已收到寄回商品?', '售后处理'); await postAdmin('merchant-after-sale-receive', { supplierId: row.supplierId, serviceOrderId: row.serviceOrderId }); ElMessage.success('已确认收货'); await loadRecords(); if (detailVisible.value) await openDetail(row) } catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '操作失败') } finally { auditing.value = false } }
|
||||||
|
const openShipDialog = (row: AfterSale) => { current.value = row; shipForm.value = { logisticsCompany: '', logisticsNo: '' }; shipVisible.value = true }
|
||||||
|
const shipExchange = async () => { if (auditing.value) return; if (!current.value || !shipForm.value.logisticsCompany || !shipForm.value.logisticsNo) return ElMessage.warning('请填写完整发货信息'); auditing.value = true; try { await postAdmin('merchant-after-sale-ship', { supplierId: current.value.supplierId, serviceOrderId: current.value.serviceOrderId, ...shipForm.value }); ElMessage.success('换货已发货'); shipVisible.value = false; await loadRecords(); if (detailVisible.value) await openDetail(current.value) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '发货失败') } finally { auditing.value = false } }
|
||||||
|
onMounted(loadRecords)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.after-sale-status { margin-top: 16px; }
|
||||||
|
.audit-card-footer > div { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.audit-card-footer > div .el-button { margin-left: 0; }
|
||||||
|
.history-result { display: flex; justify-content: flex-end; margin-top: 16px; padding: 8px 6px; }
|
||||||
|
.result-stamp { position: relative; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; width: 112px; height: 56px; margin: 6px 0; border: 2px solid currentColor; border-radius: 50%; color: #909399; font-size: 16px; font-weight: 700; letter-spacing: 2px; line-height: 1; transform: rotate(-14deg); }
|
||||||
|
.result-stamp::after { position: absolute; inset: 4px; border: 1px dashed currentColor; border-radius: 50%; content: ""; }
|
||||||
|
.result-stamp.approved { color: #389e63; }
|
||||||
|
.result-stamp.rejected { color: #d9534f; }
|
||||||
|
.audit-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 390px), 1fr)); gap: 20px; }
|
||||||
|
.audit-list > .el-empty { grid-column: 1 / -1; }
|
||||||
|
.audit-card { padding: 20px; border: 1px solid #e9eef5; border-radius: 12px; background: #fff; }
|
||||||
|
.audit-card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 18px; }
|
||||||
|
.audit-card-header strong { min-width: 0; overflow-wrap: anywhere; line-height: 22px; }
|
||||||
|
.audit-card-header .el-tag { flex-shrink: 0; }
|
||||||
|
.audit-product { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; }
|
||||||
|
.audit-product > .el-image { width: 72px; height: 72px; flex-shrink: 0; border-radius: 8px; }
|
||||||
|
.audit-product-info { display: flex; flex: 1; min-width: 0; flex-direction: column; gap: 6px; }
|
||||||
|
.audit-product-info strong { overflow-wrap: anywhere; }
|
||||||
|
.audit-product-info span, .audit-card-footer > span { color: #8793a5; font-size: 12px; }
|
||||||
|
.audit-product-info b { color: #ff6b35; }
|
||||||
|
.audit-quantity { flex-shrink: 0; font-weight: 600; }
|
||||||
|
.image-empty { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background: #f5f7fa; color: #8793a5; font-size: 12px; }
|
||||||
|
.audit-summary { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||||
|
.audit-summary dl { display: grid; grid-template-columns: 36px minmax(0, 1fr); gap: 10px 12px; flex: 1; min-width: 0; margin: 0; font-size: 13px; }
|
||||||
|
.audit-summary dt { color: #8793a5; }
|
||||||
|
.audit-summary dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
|
||||||
|
.audit-card-footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px; margin-top: 20px; padding-top: 16px; border-top: 1px solid #edf0f5; }
|
||||||
|
.audit-detail { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); align-items: start; gap: 20px; }
|
||||||
|
.audit-progress { position: relative; grid-column: 1 / -1; }
|
||||||
|
.detail-result-stamp { position: absolute; z-index: 10; top: -18px; right: 8px; pointer-events: none; width: 128px; height: 128px; margin: 0; border-width: 3px; font-size: 20px; }
|
||||||
|
.detail-result-stamp::after { inset: 7px; border-width: 2px; }
|
||||||
|
.audit-detail-section { min-width: 0; padding: 20px; border: 1px solid #e9eef5; border-radius: 10px; }
|
||||||
|
@media (max-width: 767px) { .audit-detail { grid-template-columns: minmax(0, 1fr); } }
|
||||||
|
.audit-detail-section h3 { margin: 0 0 18px; font-size: 15px; }
|
||||||
|
.audit-progress :deep(.el-step__description) { overflow-wrap: anywhere; }
|
||||||
|
.audit-detail-section p { margin: 6px 0 0; color: #8793a5; font-size: 12px; }
|
||||||
|
.audit-detail :deep(.el-descriptions__label) { width: 100px; white-space: nowrap; }
|
||||||
|
.audit-detail :deep(.el-descriptions__content) { overflow-wrap: anywhere; white-space: pre-wrap; }
|
||||||
|
.audit-evidence { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
|
||||||
|
.audit-evidence .el-image { width: 80px; height: 80px; border-radius: 6px; }
|
||||||
|
.detail-json { max-height: 420px; overflow: auto; padding: 12px; border-radius: 4px; background: #f5f7fa; white-space: pre-wrap; word-break: break-all; }</style>
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<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="#152238" text-color="#afbed3" active-text-color="#ffffff" router>
|
||||||
|
<el-sub-menu index="/billing"><template #title><el-icon><Tickets /></el-icon><span>对账开票</span></template><el-menu-item index="/reconciliation">订单对账</el-menu-item><el-menu-item index="/invoices">发票管理</el-menu-item></el-sub-menu>
|
||||||
|
<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/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 aria-label="切换侧栏" @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"><span class="user-avatar" aria-hidden="true">{{ (currentUser?.nickname || currentUser?.username || '管').slice(0, 1) }}</span><span>{{ currentUser?.nickname || currentUser?.username }}</span><el-button text type="primary" :loading="loggingOut" @click="logout">退出登录</el-button></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'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { postAdmin } from '@/api/admin'
|
||||||
|
import { clearAdminSession, readAdminSession } from '@/utils/auth'
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const currentUser = readAdminSession()?.user
|
||||||
|
const loggingOut = ref(false)
|
||||||
|
const logout = async () => {
|
||||||
|
if (loggingOut.value) return
|
||||||
|
loggingOut.value = true
|
||||||
|
try {
|
||||||
|
await postAdmin('admin-logout', {})
|
||||||
|
clearAdminSession()
|
||||||
|
await router.replace('/login')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '退出失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
loggingOut.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.admin-layout { min-height: 100vh; }
|
||||||
|
.sidebar { position: sticky; top: 0; height: 100vh; flex-shrink: 0; overflow-x: hidden; overflow-y: auto; background: #152238; transition: width .2s ease; }
|
||||||
|
.brand { display: flex; align-items: center; gap: 12px; height: 80px; padding: 0 20px; color: #f5f8ff; font-size: 17px; font-weight: 600; letter-spacing: .5px; white-space: nowrap; }
|
||||||
|
.brand > .el-icon { width: 32px; height: 32px; flex-shrink: 0; border-radius: 9px; background: #2a66d8; color: #fff; font-size: 21px; }
|
||||||
|
.sidebar :deep(.el-menu) { border-right: 0; background: transparent; }
|
||||||
|
.sidebar > :deep(.el-menu) { padding: 8px; }
|
||||||
|
.sidebar :deep(.el-menu-item), .sidebar :deep(.el-sub-menu__title) { height: 46px; margin-bottom: 5px; border-radius: 7px; line-height: 46px; font-size: 14px; }
|
||||||
|
.sidebar :deep(.el-menu-item:hover), .sidebar :deep(.el-sub-menu__title:hover) { color: #fff; background: #21334e; }
|
||||||
|
.sidebar :deep(.el-menu-item.is-active) { background: #2862cb; color: #fff; box-shadow: 0 4px 12px rgba(0, 0, 0, .12); }
|
||||||
|
.sidebar :deep(.el-sub-menu .el-menu-item) { min-width: 0; font-size: 13px; }
|
||||||
|
.sidebar :deep(.el-menu--collapse) { width: 64px; }
|
||||||
|
.sidebar :deep(.el-menu--collapse .el-menu-item), .sidebar :deep(.el-menu--collapse .el-sub-menu__title) { padding: 0 12px !important; }
|
||||||
|
.admin-layout > .el-container { min-width: 0; }
|
||||||
|
.header { position: sticky; z-index: 20; top: 0; display: flex; align-items: center; gap: 18px; height: 64px; padding: 0 28px; border-bottom: 1px solid #e7ecf3; background: #fff; }
|
||||||
|
.header > .el-button { width: 34px; height: 34px; padding: 0; color: #69778e; background: #f4f6fa; }
|
||||||
|
.header :deep(.el-breadcrumb) { font-size: 13px; }
|
||||||
|
.header-user { display: flex; align-items: center; gap: 12px; margin-left: auto; color: #526078; font-size: 13px; white-space: nowrap; }
|
||||||
|
.user-avatar { display: grid; width: 32px; height: 32px; place-items: center; border: 1px solid #dce7fb; border-radius: 50%; background: #edf3fd; color: #2463d4; font-weight: 600; }
|
||||||
|
.header-user > .el-button { margin-left: 4px; font-size: 12px; }
|
||||||
|
.main { min-width: 0; min-height: calc(100vh - 64px); padding: 24px; background: #f3f5f9; }
|
||||||
|
.main :deep(.ele-page) { padding: 0; }
|
||||||
|
@media (max-width: 1100px) { .header { padding: 0 20px; } .main { padding: 18px; } }
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.header { gap: 10px; padding: 0 12px; }
|
||||||
|
.header :deep(.el-breadcrumb) { display: none; }
|
||||||
|
.header-user { gap: 8px; }
|
||||||
|
.main { padding: 12px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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')
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||||
|
import AdminLayout from '@/layouts/AdminLayout.vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { postAdmin } from '@/api/admin'
|
||||||
|
import { clearAdminSession, readAdminSession } from '@/utils/auth'
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [{ path: '/login', name: 'Login', component: () => import('@/views/login/index.vue'), meta: { title: '登录' } }, { path: '/', component: AdminLayout, redirect: '/reconciliation', children: [
|
||||||
|
{ path: 'reconciliation', name: 'Reconciliation', component: () => import('@/views/reconciliation/index.vue'), meta: { title: '订单对账' } },
|
||||||
|
{ path: 'invoices', name: 'Invoices', component: () => import('@/views/invoices/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', redirect: '/orders/after-sales/audit' },
|
||||||
|
{ 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: '商品管理' } }
|
||||||
|
] }]
|
||||||
|
|
||||||
|
const router = createRouter({ history: createWebHistory(), routes })
|
||||||
|
|
||||||
|
router.beforeEach(async to => {
|
||||||
|
if (to.name === 'Login') return true
|
||||||
|
if (readAdminSession()) {
|
||||||
|
try {
|
||||||
|
await postAdmin('admin-session', {})
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
clearAdminSession()
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '登录状态校验失败,请重新登录')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { name: 'Login', query: { redirect: to.fullPath } }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
export const useAppStore = defineStore('app', { state: () => ({ sidebarCollapsed: false }), actions: { toggleSidebar() { this.sidebarCollapsed = !this.sidebarCollapsed } } })
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
export const pinia = createPinia()
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
:root {
|
||||||
|
--el-color-primary: #2463d4;
|
||||||
|
--el-color-primary-light-3: #668fdf;
|
||||||
|
--el-color-primary-light-5: #91b1ea;
|
||||||
|
--el-color-primary-light-7: #bed0f2;
|
||||||
|
--el-color-primary-light-8: #d3e0f7;
|
||||||
|
--el-color-primary-light-9: #edf3fd;
|
||||||
|
--el-color-primary-dark-2: #1d4faa;
|
||||||
|
--el-border-radius-base: 7px;
|
||||||
|
--el-border-color: #dce3ec;
|
||||||
|
--el-border-color-light: #e9eef5;
|
||||||
|
--el-text-color-primary: #202e45;
|
||||||
|
--el-text-color-regular: #526078;
|
||||||
|
--el-text-color-secondary: #78859a;
|
||||||
|
--el-font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #app { min-width: 360px; min-height: 100%; margin: 0; }
|
||||||
|
body { color: var(--el-text-color-primary); font-family: var(--el-font-family); background: #f3f5f9; -webkit-font-smoothing: antialiased; }
|
||||||
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
|
button, input, textarea { font: inherit; }
|
||||||
|
:focus-visible { outline: 2px solid #2463d4; outline-offset: 3px; }
|
||||||
|
|
||||||
|
.page-card {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: calc(100vh - 112px);
|
||||||
|
padding: 28px;
|
||||||
|
border: 1px solid #e5eaf2;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 3px 16px rgba(30, 50, 85, .035);
|
||||||
|
}
|
||||||
|
.page-title { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; margin-bottom: 24px; }
|
||||||
|
.page-title h2 { margin: 0 0 8px; color: #202e45; font-size: 22px; font-weight: 600; letter-spacing: .5px; }
|
||||||
|
.page-title p { margin: 0; color: #78859a; font-size: 13px; line-height: 1.7; }
|
||||||
|
.query-form.el-form--inline { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 16px 20px; padding: 20px; margin-bottom: 24px; border: 1px solid #e9eef5; border-radius: 10px; background: #f8fafc; }
|
||||||
|
.query-form.el-form--inline .el-form-item { display: flex; flex-direction: row; align-items: center; margin: 0; }
|
||||||
|
.query-form .el-form-item__label { justify-content: flex-start; flex-shrink: 0; height: 36px; padding: 0 12px 0 0; color: #526078; font-size: 13px; line-height: 36px; white-space: nowrap; }
|
||||||
|
.query-form .el-form-item__content { min-width: 0; min-height: 36px; }
|
||||||
|
.query-form .el-input, .query-form .el-select { width: 208px; max-width: 100%; }
|
||||||
|
.query-form .el-button { min-width: 76px; height: 36px; }
|
||||||
|
.el-input__wrapper, .el-select__wrapper { min-height: 36px; }
|
||||||
|
.el-textarea__inner { padding: 10px 12px; line-height: 1.6; }
|
||||||
|
.page-title > .el-button { height: 36px; padding: 0 18px; }
|
||||||
|
.el-button { font-weight: 500; }
|
||||||
|
.el-button:not(.is-link):not(.is-text) { border-radius: 7px; }
|
||||||
|
|
||||||
|
.page-card .el-table {
|
||||||
|
--el-table-border-color: #e9eef5;
|
||||||
|
--el-table-header-bg-color: #f7f9fc;
|
||||||
|
--el-table-header-text-color: #526078;
|
||||||
|
--el-table-row-hover-bg-color: #f3f7ff;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 9px;
|
||||||
|
color: #3f4d63;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.page-card .el-table th.el-table__cell { padding: 13px 0; font-size: 12px; font-weight: 600; }
|
||||||
|
.page-card .el-table td.el-table__cell { padding: 15px 0; }
|
||||||
|
.page-card .el-table .cell { padding-right: 16px; padding-left: 16px; line-height: 22px; }
|
||||||
|
.page-card .el-table .el-button.is-link { font-size: 13px; }
|
||||||
|
.page-card .el-table .el-table__empty-block { min-height: 240px; }
|
||||||
|
.page-card .el-table .el-table__empty-text { width: 100%; line-height: normal; }
|
||||||
|
.page-card .el-empty { padding: 36px 0; }
|
||||||
|
.el-empty__description p { color: #8793a5; font-size: 13px; }
|
||||||
|
.page-card .el-tag { height: 25px; padding: 0 9px; border-color: transparent; border-radius: 5px; font-size: 12px; font-weight: 500; }
|
||||||
|
.page-card .el-pagination { justify-content: flex-end; flex-wrap: wrap; gap: 8px; padding-top: 20px; margin-top: 20px; border-top: 1px solid #edf0f5; font-variant-numeric: tabular-nums; }
|
||||||
|
.page-card .el-pagination .el-pager li { border-radius: 6px; }
|
||||||
|
.page-card .el-pagination .el-pager li.is-active { color: #fff; background: var(--el-color-primary); }
|
||||||
|
|
||||||
|
.el-dialog { max-width: calc(100vw - 32px); padding: 0; overflow: hidden; border-radius: 14px; box-shadow: 0 24px 80px rgba(15, 30, 55, .18); }
|
||||||
|
.el-dialog__header { padding: 22px 52px 20px 24px; margin: 0; border-bottom: 1px solid #edf0f5; }
|
||||||
|
.el-dialog__title { color: #202e45; font-size: 17px; font-weight: 600; }
|
||||||
|
.el-dialog__headerbtn { top: 9px; right: 8px; }
|
||||||
|
.el-dialog__body { max-height: 65vh; padding: 24px; overflow-y: auto; }
|
||||||
|
.el-dialog__footer { padding: 16px 24px; border-top: 1px solid #edf0f5; background: #fafbfd; }
|
||||||
|
.el-dialog .el-form-item { margin-bottom: 22px; }
|
||||||
|
.el-dialog .el-form-item:last-child { margin-bottom: 0; }
|
||||||
|
.el-dialog .el-select, .el-dialog .el-input-number { width: 100%; }
|
||||||
|
.el-dialog__footer .el-button { min-width: 80px; height: 36px; }
|
||||||
|
.el-message-box { width: 360px; max-width: calc(100vw - 32px); padding: 20px; border-radius: 12px; }
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.page-card { padding: 22px; }
|
||||||
|
.query-form .el-input, .query-form .el-select { width: 180px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.page-card { min-height: calc(100vh - 88px); padding: 16px; border-radius: 10px; }
|
||||||
|
.page-title h2 { font-size: 19px; }
|
||||||
|
.query-form.el-form--inline { gap: 14px; padding: 14px; }
|
||||||
|
.query-form.el-form--inline .el-form-item { width: 100%; }
|
||||||
|
.query-form .el-input, .query-form .el-select { width: 100% !important; }
|
||||||
|
.el-dialog__body { padding: 18px; }
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0s !important; animation-duration: 0s !important; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export type AdminUser = { id: number; username: string; nickname: string }
|
||||||
|
export type AdminSession = { token: string; expiresAt: string; user: AdminUser }
|
||||||
|
const STORAGE_KEY = 'supplied-admin-session'
|
||||||
|
|
||||||
|
export const clearAdminSession = () => sessionStorage.removeItem(STORAGE_KEY)
|
||||||
|
export const saveAdminSession = (session: AdminSession) => sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session))
|
||||||
|
export const readAdminSession = (): AdminSession | null => {
|
||||||
|
try {
|
||||||
|
const session = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || 'null')
|
||||||
|
if (session?.token && session?.user && Date.parse(session.expiresAt) > Date.now()) return session
|
||||||
|
} catch { /* 忽略已损坏的本地登录信息。 */ }
|
||||||
|
clearAdminSession()
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<section class="page-card">
|
||||||
|
<div class="page-title"><div><h2>发票管理</h2><p>查看开票结果、关联订单及电子发票。</p></div></div>
|
||||||
|
<el-form inline class="query-form" @submit.prevent="search">
|
||||||
|
<el-form-item label="结算单号"><el-input v-model="query.settlementId" placeholder="请输入结算单号" clearable /></el-form-item>
|
||||||
|
<el-form-item label="开票状态"><el-select v-model="query.invoiceStatus" placeholder="全部" clearable><el-option v-for="(name, value) in statuses" :key="value" :label="name" :value="Number(value)" /></el-select></el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" :loading="loading" @click="search">查询</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="settlementId" label="结算单号" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="invoiceIds" label="发票号码" min-width="190" show-overflow-tooltip />
|
||||||
|
<el-table-column label="开票状态" width="110"><template #default="{ row }"><el-tag :type="statusType(row.invoiceStatus)">{{ statuses[row.invoiceStatus] || '-' }}</el-tag></template></el-table-column>
|
||||||
|
<el-table-column label="价税合计" width="120"><template #default="{ row }">¥{{ row.invoiceAmount }}</template></el-table-column>
|
||||||
|
<el-table-column label="开票日期" width="130"><template #default="{ row }">{{ dateText(row.invoiceDate) }}</template></el-table-column>
|
||||||
|
<el-table-column prop="failureMessage" label="说明" min-width="180" show-overflow-tooltip><template #default="{ row }">{{ row.failureMessage || '-' }}</template></el-table-column>
|
||||||
|
<el-table-column label="操作" width="190" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openDetail(row)">详情</el-button><el-button link type="primary" :loading="pushingId === row.id" :disabled="pushingId !== null || ![2, 3].includes(row.invoiceStatus)" @click="pushInvoice(row)">推送发票</el-button></template></el-table-column>
|
||||||
|
<template #empty><el-empty :image-size="80" description="暂无发票记录" /></template>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination v-model:current-page="query.pageNumber" :page-size="query.pageSize" :total="totalCount" layout="total, prev, pager, next, jumper" @current-change="load" />
|
||||||
|
<el-dialog v-model="detailVisible" title="发票详情" width="880px">
|
||||||
|
<template v-if="current"><el-descriptions :column="2" border class="invoice-detail">
|
||||||
|
<el-descriptions-item label="结算单号">{{ current.settlementId }}</el-descriptions-item><el-descriptions-item label="开票状态"><el-tag :type="statusType(current.invoiceStatus)">{{ statuses[current.invoiceStatus] || '-' }}</el-tag></el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发票号码">{{ current.invoiceIds || '-' }}</el-descriptions-item><el-descriptions-item label="发票代码">{{ current.invoiceCode || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="开票日期">{{ dateText(current.invoiceDate) }}</el-descriptions-item><el-descriptions-item label="发票类型">{{ current.invoiceType === 9 ? '全电专票' : current.invoiceType === 10 ? '全电普票' : '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="不含税金额">¥{{ current.invoiceNakeAmount }}</el-descriptions-item><el-descriptions-item label="税率">{{ current.invoiceTaxRate ? `${Number(current.invoiceTaxRate) * 100}%` : '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="税额">¥{{ current.invoiceTaxAmount }}</el-descriptions-item><el-descriptions-item label="价税合计">¥{{ current.invoiceAmount }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="纳税人识别号">{{ current.taxIdNumber || '-' }}</el-descriptions-item><el-descriptions-item label="校验码">{{ current.checkCode || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="说明" :span="2">{{ current.failureMessage || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions><h3 class="orders-title">关联订单</h3><div class="invoice-orders"><el-tag v-for="orderId in current.orderIds.split(',').filter(Boolean)" :key="orderId" type="info">{{ orderId }}</el-tag><span v-if="!current.orderIds">暂无关联订单</span></div></template>
|
||||||
|
<template #footer><el-button @click="detailVisible = false">关闭</el-button><template v-if="current?.hasFile"><el-button :disabled="fileLoading" @click="openFile(current, true)">下载PDF</el-button><el-button type="primary" :disabled="fileLoading" @click="openFile(current)">预览PDF</el-button></template></template>
|
||||||
|
</el-dialog>
|
||||||
|
<el-dialog v-model="previewVisible" title="发票预览" width="1000px" append-to-body @closed="previewUrl = ''"><iframe v-if="previewUrl" :src="previewUrl" title="电子发票" class="invoice-preview" /></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 Invoice = { id: number; settlementId: string; invoiceIds: string; invoiceCode: string; invoiceStatus: number; invoiceDate: string | null; invoiceType: number | null; invoiceAmount: string; invoiceNakeAmount: string; invoiceTaxAmount: string; invoiceTaxRate: string; taxIdNumber: string; checkCode: string; orderIds: string; failureMessage: string; hasFile: boolean }
|
||||||
|
const statuses: Record<number, string> = { 0: '未开票', 1: '开票中', 2: '成功', 3: '失败' }
|
||||||
|
const statusType = (status: number) => status === 2 ? 'success' : status === 3 ? 'danger' : status === 1 ? 'warning' : 'info'
|
||||||
|
const dateText = (value: string | null) => value ? new Date(value).toLocaleDateString('sv-SE') : '-'
|
||||||
|
const query = reactive<{ settlementId: string; invoiceStatus?: number; pageNumber: number; pageSize: number }>({ settlementId: '', pageNumber: 1, pageSize: 10 })
|
||||||
|
const items = ref<Invoice[]>([])
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const current = ref<Invoice>()
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const fileLoading = ref(false)
|
||||||
|
const pushingId = ref<number | null>(null)
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
const previewUrl = ref('')
|
||||||
|
const load = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await postAdmin<{ invoiceItems: Invoice[]; totalCount: number }>('merchant-invoice-list', query)
|
||||||
|
items.value = data.invoiceItems || []
|
||||||
|
totalCount.value = data.totalCount || 0
|
||||||
|
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '发票查询失败') } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
const search = () => { query.pageNumber = 1; return load() }
|
||||||
|
const reset = () => { query.settlementId = ''; query.invoiceStatus = undefined; return search() }
|
||||||
|
const openDetail = (row: Invoice) => { current.value = row; detailVisible.value = true }
|
||||||
|
const pushInvoice = async (row: Invoice) => {
|
||||||
|
if (pushingId.value !== null) return
|
||||||
|
pushingId.value = row.id
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认向工会推送结算单 ${row.settlementId} 的开票结果?`, '推送发票', { confirmButtonText: '确认推送', cancelButtonText: '取消' })
|
||||||
|
await postAdmin('merchant-invoice-push', { id: row.id })
|
||||||
|
ElMessage.success('发票推送成功')
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '发票推送失败')
|
||||||
|
} finally { pushingId.value = null }
|
||||||
|
}
|
||||||
|
const openFile = async (row: Invoice, download = false) => {
|
||||||
|
if (fileLoading.value) return
|
||||||
|
fileLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await postAdmin<{ url: string }>('merchant-invoice-file', { id: row.id, download })
|
||||||
|
if (download) {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = data.url
|
||||||
|
link.download = `${row.invoiceIds || 'invoice'}.pdf`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
} else { previewUrl.value = data.url; previewVisible.value = true }
|
||||||
|
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '获取发票文件失败') } finally { fileLoading.value = false }
|
||||||
|
}
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.invoice-detail :deep(.el-descriptions__content) { overflow-wrap: anywhere; }
|
||||||
|
.invoice-detail :deep(.el-descriptions__label) { width: 110px; }
|
||||||
|
.orders-title { margin: 24px 0 12px; font-size: 15px; }
|
||||||
|
.invoice-orders { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||||
|
.invoice-preview { display: block; width: 100%; height: 60vh; border: 0; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<main class="login-page">
|
||||||
|
<section class="login-intro">
|
||||||
|
<div class="login-brand"><el-icon><ShoppingBag /></el-icon>供应商管理</div>
|
||||||
|
<h1>高效管理,让服务更进一步</h1>
|
||||||
|
<p>商品、订单、售后与对账,集中处理每一项业务。</p>
|
||||||
|
<div class="business-tags"><span>商品管理</span><span>订单处理</span><span>售后服务</span><span>订单对账</span></div>
|
||||||
|
</section>
|
||||||
|
<section class="login-card">
|
||||||
|
<h2>账号登录</h2>
|
||||||
|
<p class="login-hint">欢迎回来,请登录供应商管理平台</p>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" size="large" @submit.prevent="submit">
|
||||||
|
<el-form-item label="账号" prop="username">
|
||||||
|
<el-input v-model="form.username" :prefix-icon="User" placeholder="请输入账号" autocomplete="username" :maxlength="64" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input v-model="form.password" :prefix-icon="Lock" type="password" show-password placeholder="请输入密码" autocomplete="current-password" :maxlength="128" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" class="login-error" />
|
||||||
|
<el-button class="login-submit" type="primary" native-type="submit" :loading="submitting">{{ submitting ? '正在登录' : '登录' }}</el-button>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { Lock, ShoppingBag, User } from '@element-plus/icons-vue'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { postAdmin } from '@/api/admin'
|
||||||
|
import { saveAdminSession, type AdminSession } from '@/utils/auth'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const form = reactive({ username: '', password: '' })
|
||||||
|
const submitting = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const rules: FormRules = {
|
||||||
|
username: [{ required: true, whitespace: true, message: '请输入账号', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
const submit = async () => {
|
||||||
|
if (submitting.value || !await formRef.value?.validate().catch(() => false)) return
|
||||||
|
submitting.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
try {
|
||||||
|
const session = await postAdmin<AdminSession>('login', { username: form.username.trim(), password: form.password })
|
||||||
|
saveAdminSession(session)
|
||||||
|
form.password = ''
|
||||||
|
const target = typeof route.query.redirect === 'string' ? route.query.redirect : ''
|
||||||
|
const resolved = target.startsWith('/') && !target.startsWith('//') && !target.includes(String.fromCharCode(92)) ? router.resolve(target) : null
|
||||||
|
await router.replace(resolved?.matched.length && resolved.name !== 'Login' ? target : '/reconciliation')
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error instanceof Error ? error.message : '登录失败,请稍后重试'
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.login-page { display: flex; align-items: center; justify-content: center; gap: clamp(48px, 7vw, 120px); min-height: 100vh; padding: 60px 40px; background: linear-gradient(120deg, #eaf0fa 0%, #f6f8fc 55%, #eef2f8 100%); }
|
||||||
|
.login-intro { max-width: 480px; color: #1c304e; }
|
||||||
|
.login-brand { display: flex; align-items: center; gap: 12px; margin-bottom: 64px; font-size: 22px; font-weight: 600; letter-spacing: 1px; }
|
||||||
|
.login-brand .el-icon { width: 48px; height: 48px; border-radius: 13px; background: #2463d4; color: #fff; font-size: 27px; box-shadow: 0 8px 20px rgba(36, 99, 212, .18); }
|
||||||
|
.login-intro h1 { max-width: 410px; margin: 0 0 20px; font-size: 38px; font-weight: 600; line-height: 1.55; letter-spacing: 1px; }
|
||||||
|
.login-intro p { color: #738199; font-size: 14px; line-height: 1.9; }
|
||||||
|
.business-tags { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 30px; }
|
||||||
|
.business-tags span { padding: 8px 13px; border: 1px solid #dce5f2; border-radius: 6px; color: #536985; background: rgba(255, 255, 255, .65); font-size: 12px; }
|
||||||
|
.login-card { width: 420px; flex-shrink: 0; padding: 42px 38px; border: 1px solid #e4eaf3; border-radius: 18px; background: #fff; box-shadow: 0 18px 60px rgba(38, 60, 98, .08); }
|
||||||
|
.login-card h2 { margin: 0 0 12px; color: #202e45; font-size: 25px; font-weight: 600; }
|
||||||
|
.login-hint { margin: 0 0 32px; color: #8793a5; font-size: 13px; line-height: 1.7; }
|
||||||
|
.login-card :deep(.el-form-item) { margin-bottom: 24px; }
|
||||||
|
.login-card :deep(.el-form-item__label) { color: #526078; font-size: 13px; }
|
||||||
|
.login-card :deep(.el-input__wrapper) { min-height: 44px; padding: 0 14px; }
|
||||||
|
.login-submit { width: 100%; height: 44px; margin-top: 10px; letter-spacing: 2px; }
|
||||||
|
.login-error { margin-bottom: 16px; }
|
||||||
|
@media (max-width: 960px) { .login-page { gap: 36px; padding: 40px 24px; } .login-intro h1 { font-size: 30px; } .login-card { width: 380px; padding: 34px 28px; } }
|
||||||
|
@media (max-width: 700px) { .login-page { flex-direction: column; gap: 26px; padding: 32px 20px; } .login-intro { width: 100%; max-width: 420px; } .login-brand { margin-bottom: 0; font-size: 20px; } .login-intro h1, .login-intro p, .business-tags { display: none; } .login-card { width: 100%; max-width: 420px; } }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<section class="page-card">
|
||||||
|
<div class="page-title"><div><h2>订单列表</h2><p>查询订单并对待发货订单确认发货。</p></div><el-button type="primary" :loading="batchShipping" :disabled="loading || submitting || shipDialogVisible || Boolean(repushingOrderId) || !pendingShipOrders.length" @click="quickShipAll">一键发货</el-button></div>
|
||||||
|
<el-form :inline="true" class="query-form"><el-form-item label="订单号"><el-input v-model="query.orderId" placeholder="订单编号" clearable /></el-form-item><el-form-item label="用户昵称"><el-input v-model="query.userNickname" placeholder="请输入用户昵称" clearable /></el-form-item><el-form-item label="订单状态"><el-select v-model="query.orderState" placeholder="全部" clearable style="width: 160px"><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="searchOrders">查询</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="200" show-overflow-tooltip /><el-table-column label="用户昵称" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.userNickname || '-' }}</template></el-table-column><el-table-column label="商品信息" min-width="300"><template #default="{ row }">
|
||||||
|
<div v-for="(product, index) in row.products || []" :key="product.id || index" class="order-product">
|
||||||
|
<el-image class="order-product__image" :src="resolveImageUrl(product.image)" fit="cover" :preview-src-list="product.image ? [resolveImageUrl(product.image)] : []" preview-teleported>
|
||||||
|
<template #error><span class="order-product__placeholder">暂无图片</span></template>
|
||||||
|
</el-image>
|
||||||
|
<div class="order-product__info">
|
||||||
|
<div class="order-product__name">{{ product.name || '-' }}</div>
|
||||||
|
<div class="order-product__bottom"><span class="order-product__price">{{ formatPrice(product.price) }}</span><span class="order-product__quantity">×{{ product.number ?? '-' }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-if="!row.products?.length">-</span>
|
||||||
|
</template></el-table-column><el-table-column label="订单金额" width="120" align="right"><template #default="{ row }"><span class="order-amount">{{ formatPrice(row.amount) }}</span></template></el-table-column><el-table-column prop="createTime" label="下单时间" min-width="170" /><el-table-column label="订单状态" width="110"><template #default="{ row }"><el-tag :type="[4, 5].includes(row.orderState) ? 'success' : [0, 1, 6].includes(row.orderState) ? 'warning' : row.orderState === 2 ? 'primary' : 'info'">{{ stateText(row.orderState) }}</el-tag></template></el-table-column><el-table-column label="操作" width="240" fixed="right"><template #default="{ row }"><el-button v-if="row.orderState === 1" link type="primary" :disabled="batchShipping" @click="openShipDialog(row)">确认发货</el-button><span v-else>-</span><el-button v-if="row.orderState === 1" link type="warning" :loading="repushingOrderId === row.orderId" :disabled="batchShipping || Boolean(repushingOrderId)" @click="repushSplit(row)">重新推送拆单</el-button></template></el-table-column><template #empty><el-empty :image-size="80" description="暂无订单" /></template></el-table>
|
||||||
|
<el-pagination v-model:current-page="query.pageNumber" :page-size="query.pageSize" :total="totalCount" layout="total, prev, pager, next, jumper" @current-change="loadOrders" />
|
||||||
|
|
||||||
|
<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 { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { postAdmin, resolveImageUrl } from '@/api/admin'
|
||||||
|
|
||||||
|
type OrderProduct = { id?: number; name?: string; image?: string; price?: string | number; number?: number }
|
||||||
|
type OrderItem = { orderId: string; supplierId: string; orderNumberId?: string; userId?: string; userNickname?: string; orderState: number; createTime?: string; crrgBsnNm?: string; logisticsOrderId?: string; amount?: string | number; products?: OrderProduct[] }
|
||||||
|
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<{ orderId: string; userNickname: string; orderState?: number; pageNumber: number; pageSize: number }>({ orderId: '', userNickname: '', pageNumber: 1, pageSize: 10 })
|
||||||
|
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 totalCount = ref(0)
|
||||||
|
const stateText = (state: number) => orderStates.find(item => item.value === state)?.label || '未知'
|
||||||
|
const loadOrders = async () => { loading.value = true; try { const data = await postAdmin<{ orderItems?: OrderItem[]; totalCount?: number }>('merchant-order-list', query); orders.value = data.orderItems || []; totalCount.value = data.totalCount || 0; queried.value = true } catch (error) { ElMessage.error(error instanceof Error ? error.message : '订单查询失败') } finally { loading.value = false } }
|
||||||
|
const searchOrders = () => { query.pageNumber = 1; return loadOrders() }
|
||||||
|
const reset = () => { query.orderId = ''; query.userNickname = ''; query.orderState = undefined; searchOrders() }
|
||||||
|
const formatPrice = (value?: string | number) => value !== undefined && value !== null && value !== '' && Number.isFinite(Number(value)) ? `¥${Number(value).toFixed(2)}` : '-'
|
||||||
|
const openShipDialog = (order: OrderItem) => { currentOrder.value = order; shipForm.value = { crrgBsnNm: '韵达快递', logisticsOrderId: `YT${Date.now()}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}` }; 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: currentOrder.value.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 } }
|
||||||
|
const batchShipping = ref(false)
|
||||||
|
const pendingShipOrders = computed(() => orders.value.filter(order => order.orderState === 1))
|
||||||
|
const quickShipAll = async () => {
|
||||||
|
if (batchShipping.value || loading.value || submitting.value || shipDialogVisible.value || repushingOrderId.value) return
|
||||||
|
const targets = pendingShipOrders.value.map(order => ({ ...order }))
|
||||||
|
if (!targets.length) return ElMessage.warning('当前页没有待发货订单')
|
||||||
|
batchShipping.value = true
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`将为当前页的 ${targets.length} 个待发货订单提交发货,默认使用韵达快递,并为每笔订单生成独立快递单号。是否继续?`, '确认一键发货', { type: 'warning', confirmButtonText: '确认发货', cancelButtonText: '取消' })
|
||||||
|
const results = await Promise.allSettled(targets.map((order, index) => postAdmin('merchant-order-ship', {
|
||||||
|
supplierId: order.supplierId,
|
||||||
|
userId: order.userId,
|
||||||
|
orderId: order.orderId,
|
||||||
|
crrgBsnNm: order.crrgBsnNm || '韵达快递',
|
||||||
|
logisticsOrderId: order.logisticsOrderId || `YT${Date.now()}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}${index}`
|
||||||
|
})))
|
||||||
|
const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
||||||
|
if (failures.length) {
|
||||||
|
const reason = failures[0].reason instanceof Error ? failures[0].reason.message : '请求失败'
|
||||||
|
ElMessage.warning({ message: `发货成功 ${results.length - failures.length} 笔,失败 ${failures.length} 笔:${reason}`, duration: 6000 })
|
||||||
|
} else {
|
||||||
|
ElMessage.success(`已成功发货 ${results.length} 笔订单`)
|
||||||
|
}
|
||||||
|
await loadOrders()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '批量发货失败')
|
||||||
|
} finally {
|
||||||
|
batchShipping.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const repushingOrderId = ref('')
|
||||||
|
const repushSplit = async (order: OrderItem) => {
|
||||||
|
if (repushingOrderId.value) return
|
||||||
|
repushingOrderId.value = order.orderId
|
||||||
|
try {
|
||||||
|
await postAdmin('merchant-order-repush-split', { supplierId: order.supplierId, userId: order.userId, orderId: order.orderId })
|
||||||
|
ElMessage.success('重新推送拆单成功')
|
||||||
|
await loadOrders()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '重新推送拆单失败')
|
||||||
|
} finally {
|
||||||
|
repushingOrderId.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(loadOrders)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.order-product { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||||
|
.order-product + .order-product { margin-top: 12px; padding-top: 12px; border-top: 1px solid #edf0f5; }
|
||||||
|
.order-product__image { width: 58px; height: 58px; flex-shrink: 0; overflow: hidden; border-radius: 7px; background: #f3f5f9; }
|
||||||
|
.order-product__placeholder { display: grid; height: 100%; place-items: center; color: #8793a5; font-size: 11px; }
|
||||||
|
.order-product__info { min-width: 0; flex: 1; }
|
||||||
|
.order-product__name { color: #202e45; font-size: 13px; font-weight: 500; overflow-wrap: anywhere; }
|
||||||
|
.order-product__bottom { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 6px; }
|
||||||
|
.order-product__price, .order-amount { color: #ed6a2c; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||||
|
.order-product__quantity { color: #526078; font-size: 12px; white-space: nowrap; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<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="search">查询</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><template #empty><el-empty :image-size="80" description="暂无商品" /></template></el-table>
|
||||||
|
<el-pagination v-model:current-page="query.pageNumber" :page-size="query.pageSize" :total="totalCount" layout="total, prev, pager, next, jumper" @current-change="load" />
|
||||||
|
<el-dialog v-model="visible" :title="form.id ? '编辑商品' : '新增商品'" width="760px" destroy-on-close><el-form class="product-form" :model="form" label-width="90px"><el-row :gutter="24"><el-col :xs="24" :sm="12"><el-form-item label="商品SKU" required><el-input v-model="form.sku" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="商品名称" required><el-input v-model="form.name" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="售价" required><el-input-number v-model="form.price" :min="0" :precision="2" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="原价"><el-input-number v-model="form.originPrice" :min="0" :precision="2" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="库存"><el-input-number v-model="form.stock" :min="0" :precision="0" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" :precision="0" /></el-form-item></el-col><el-col :xs="24" :sm="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 :xs="24" :sm="12"><el-form-item label="状态"><el-switch v-model="form.enabled" active-text="上架" inactive-text="下架" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="商品标签"><el-input v-model="form.tag" placeholder="如:赠品、积分兑换" clearable /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="已售数量"><el-input v-model="form.sales" disabled placeholder="0" clearable /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="商品数量"><el-input-number v-model="form.number" :min="0" :precision="0" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="税率"><el-input-number v-model="form.taxRate" :min="0" :max="1" :step="0.01" :precision="4" /><span class="product-field-hint">按小数填写,例如 0.13 表示 13%</span></el-form-item></el-col></el-row><el-form-item label="商品图片"><el-upload class="product-image-upload" list-type="picture-card" accept="image/*" :auto-upload="false" :show-file-list="false" :disabled="uploading" :on-change="uploadProductImage" v-loading="uploading" :aria-label="form.imageUrl ? '更换商品图片' : '上传商品图片'"><template v-if="form.imageUrl"><img :src="resolveImageUrl(form.imageUrl)" alt="商品图片" class="product-image-preview" /><span class="product-image-replace">点击更换</span></template><div v-else class="product-image-placeholder"><span class="product-image-plus" aria-hidden="true">+</span><span>上传图片</span></div></el-upload></el-form-item><el-form-item label="商品描述"><el-input v-model="form.description" type="textarea" :rows="3" /></el-form-item><el-form-item label="商品详情"><el-input v-model="form.detail" type="textarea" :rows="4" /></el-form-item></el-form><template #footer><el-button @click="visible = false">取消</el-button><el-button type="primary" :loading="saving" :disabled="uploading" @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, resolveImageUrl } from '@/api/admin'
|
||||||
|
import type { UploadFile } from 'element-plus'
|
||||||
|
|
||||||
|
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; tag: string; sales: string; detail: string; number: number; taxRate: number }
|
||||||
|
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; tag: string; sales: string; detail: string; number: number; taxRate: number }
|
||||||
|
const query = reactive({ keyword: '', categoryId: '', pageNumber: 1, pageSize: 10 })
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const uploading = ref(false)
|
||||||
|
const uploadProductImage = async (file: UploadFile) => {
|
||||||
|
if (!file.raw || uploading.value) return
|
||||||
|
if (!file.raw.type.startsWith('image/')) return ElMessage.warning('请选择图片文件')
|
||||||
|
uploading.value = true
|
||||||
|
try {
|
||||||
|
const contentBase64 = await new Promise<string>((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => resolve(String(reader.result || ''))
|
||||||
|
reader.onerror = () => reject(new Error('图片读取失败'))
|
||||||
|
reader.readAsDataURL(file.raw!)
|
||||||
|
})
|
||||||
|
const data = await postAdmin<{ fileUrl: string }>('upload', { fileName: file.name, contentType: file.raw.type, contentBase64, folder: 'supplied/product' })
|
||||||
|
form.imageUrl = data.fileUrl
|
||||||
|
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '图片上传失败') } finally { uploading.value = false }
|
||||||
|
}
|
||||||
|
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, tag: '', sales: '0', detail: '', number: 0, taxRate: 0.13 })
|
||||||
|
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[]; totalCount: number }>('merchant-product-list', query); categories.value = data.categories || []; products.value = data.products || []; totalCount.value = data.totalCount || 0; loaded.value = true } catch (error) { ElMessage.error(error instanceof Error ? error.message : '商品查询失败') } finally { loading.value = false } }
|
||||||
|
const search = () => { query.pageNumber = 1; return load() }
|
||||||
|
const reset = () => { query.keyword = ''; query.categoryId = ''; return search() }
|
||||||
|
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, tag: product.tag || '', sales: product.sales?.trim() || '0', detail: product.detail || '', number: product.number ?? 0, taxRate: product.taxRate ?? 0.13 }); 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>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.product-field-hint { margin-top: 6px; color: #8793a5; font-size: 12px; line-height: 18px; }
|
||||||
|
.product-image-upload :deep(.el-upload--picture-card) { position: relative; width: 128px; height: 128px; overflow: hidden; border-radius: 8px; }
|
||||||
|
.product-image-preview { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.product-image-placeholder { display: flex; flex-direction: column; align-items: center; gap: 8px; color: #8793a5; font-size: 13px; }
|
||||||
|
.product-image-plus { font-size: 32px; line-height: 32px; }
|
||||||
|
.product-image-replace { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 12px; background: rgba(0, 0, 0, .4); color: #fff; font-size: 13px; line-height: 20px; opacity: 0; }
|
||||||
|
.product-image-upload :deep(.el-upload--picture-card:hover) .product-image-replace,
|
||||||
|
.product-image-upload :deep(.el-upload--picture-card:focus-within) .product-image-replace { opacity: 1; }
|
||||||
|
@media (hover: none) { .product-image-replace { top: auto; padding: 6px 12px; opacity: 1; } }
|
||||||
|
.product-form :deep(.el-row .el-form-item) { margin-bottom: 24px; }
|
||||||
|
.product-form :deep(.el-form-item__content) { min-width: 0; }
|
||||||
|
.product-form :deep(.el-input),
|
||||||
|
.product-form :deep(.el-select),
|
||||||
|
.product-form :deep(.el-input-number) { width: 100%; }
|
||||||
|
.product-form :deep(.el-form-item__label) { line-height: 36px; }
|
||||||
|
</style>
|
||||||
@@ -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.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="search">查询</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="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" />
|
||||||
|
<template #empty><el-empty :image-size="80" description="暂无对账结果" /></template></el-table>
|
||||||
|
<el-pagination v-model:current-page="query.pageNumber" :page-size="query.pageSize" :total="totalCount" layout="total, prev, pager, next, jumper" @current-change="load" />
|
||||||
|
</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 { onMounted, 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<{ orderId: string; reconciliationStatus?: number; pageNumber: number; pageSize: number }>({ orderId: '', pageNumber: 1, pageSize: 10 })
|
||||||
|
const totalCount = ref(0)
|
||||||
|
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 () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await postAdmin<{ reconciliationItems: ReconciliationItem[]; totalCount: number }>('merchant-reconciliation-list', query)
|
||||||
|
items.value = data.reconciliationItems || []
|
||||||
|
totalCount.value = data.totalCount || 0
|
||||||
|
loaded.value = true
|
||||||
|
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '查询失败') } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
const search = () => { query.pageNumber = 1; return load() }
|
||||||
|
const reset = () => { query.orderId = ''; query.reconciliationStatus = undefined; return search() }
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
onMounted(load)
|
||||||
|
</script>
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -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)) } } })
|
||||||
Generated
+146
-83
@@ -11,16 +11,16 @@
|
|||||||
"@vant/area-data": "^1.5.2",
|
"@vant/area-data": "^1.5.2",
|
||||||
"core-js": "^3.37.1",
|
"core-js": "^3.37.1",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
"vant": "^2.13.9",
|
"vant": "^3.6.16",
|
||||||
"vue": "^2.7.16",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^3.6.5"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-service": "~5.0.8",
|
"@vue/cli-service": "~5.0.8",
|
||||||
|
"@vue/compiler-sfc": "^3.5.13",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0"
|
||||||
"vue-template-compiler": "^2.7.16"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@achrinza/node-ipc": {
|
"node_modules/@achrinza/node-ipc": {
|
||||||
@@ -337,10 +337,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
|
||||||
|
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.29.7"
|
"@babel/types": "^7.29.8"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -1446,6 +1448,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -1482,7 +1485,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
|
||||||
|
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.29.7",
|
"@babel/helper-string-parser": "^7.29.7",
|
||||||
@@ -1550,7 +1555,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
@@ -1859,15 +1863,27 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vant/icons": {
|
"node_modules/@vant/icons": {
|
||||||
"version": "3.0.2",
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vant/icons/-/icons-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-sKfEUo2/CkQFuERxvkuF6mGQZDKu3IQdj5rV9Fm0weJXtchDSSQ+zt8qPCNUEhh9Y8shy5PzxbvAfOOkCwlCXg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vant/popperjs": {
|
"node_modules/@vant/popperjs": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vant/use": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vant/use/-/use-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
"node_modules/@vue/babel-helper-vue-jsx-merge-props": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/babel-helper-vue-transform-on": {
|
"node_modules/@vue/babel-helper-vue-transform-on": {
|
||||||
@@ -2277,49 +2293,53 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.42",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.7",
|
"@babel/parser": "^7.29.8",
|
||||||
"@vue/shared": "3.5.39",
|
"@vue/shared": "3.5.42",
|
||||||
"entities": "^7.0.1",
|
"entities": "^7.0.1",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.42",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.5.39",
|
"@vue/compiler-core": "3.5.42",
|
||||||
"@vue/shared": "3.5.39"
|
"@vue/shared": "3.5.42"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.42",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.7",
|
"@babel/parser": "^7.29.8",
|
||||||
"@vue/compiler-core": "3.5.39",
|
"@vue/compiler-core": "3.5.42",
|
||||||
"@vue/compiler-dom": "3.5.39",
|
"@vue/compiler-dom": "3.5.42",
|
||||||
"@vue/compiler-ssr": "3.5.39",
|
"@vue/compiler-ssr": "3.5.42",
|
||||||
"@vue/shared": "3.5.39",
|
"@vue/shared": "3.5.42",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.19",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.42",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.39",
|
"@vue/compiler-dom": "3.5.42",
|
||||||
"@vue/shared": "3.5.39"
|
"@vue/shared": "3.5.42"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/component-compiler-utils": {
|
"node_modules/@vue/component-compiler-utils": {
|
||||||
@@ -2386,9 +2406,52 @@
|
|||||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/reactivity": {
|
||||||
|
"version": "3.5.42",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/shared": "3.5.42"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-core": {
|
||||||
|
"version": "3.5.42",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.42",
|
||||||
|
"@vue/shared": "3.5.42"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-dom": {
|
||||||
|
"version": "3.5.42",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.42",
|
||||||
|
"@vue/runtime-core": "3.5.42",
|
||||||
|
"@vue/shared": "3.5.42",
|
||||||
|
"csstype": "^3.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/server-renderer": {
|
||||||
|
"version": "3.5.42",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-ssr": "3.5.42",
|
||||||
|
"@vue/runtime-dom": "3.5.42",
|
||||||
|
"@vue/shared": "3.5.42"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.42",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/vue-loader-v15": {
|
"node_modules/@vue/vue-loader-v15": {
|
||||||
@@ -3981,11 +4044,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"license": "MIT"
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
},
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"node_modules/de-indent": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debounce": {
|
"node_modules/debounce": {
|
||||||
@@ -4400,7 +4460,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
@@ -4531,7 +4592,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/esutils": {
|
"node_modules/esutils": {
|
||||||
@@ -6048,7 +6110,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
@@ -6452,7 +6513,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.16",
|
"version": "3.3.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||||
|
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -7020,7 +7083,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.18",
|
"version": "8.5.28",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
|
||||||
|
"integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -7037,7 +7102,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.18",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -7553,6 +7618,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "2.8.8",
|
"version": "2.8.8",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -8397,6 +8463,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -9025,19 +9092,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vant": {
|
"node_modules/vant": {
|
||||||
"version": "2.13.9",
|
"version": "3.6.16",
|
||||||
"resolved": "https://registry.npmmirror.com/vant/-/vant-2.13.9.tgz",
|
"resolved": "https://registry.npmjs.org/vant/-/vant-3.6.16.tgz",
|
||||||
"integrity": "sha512-AIDYTUKOlqEolrTs18vZanLX9Mq6IftzwOnbozaNFmx9+xMrCeJIqRKefbGUPZ7irQvO6t397+VwRXqCSHnedQ==",
|
"integrity": "sha512-9pZao0NEeZQ0ZEb6N7SZxtqcdTp24o8IizhZS1G+FtStlXeKOFzCl+Nf1pIWRneQ9Kn+K+mNrfi2eiIZjVVppw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.x",
|
"@vant/icons": "^1.8.0",
|
||||||
"@vant/icons": "^3.0.2",
|
"@vant/popperjs": "^1.2.1",
|
||||||
"@vant/popperjs": "^1.1.0",
|
"@vant/use": "^1.4.2"
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
|
|
||||||
"vue-lazyload": "1.2.3"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": ">= 2.6.0"
|
"vue": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
@@ -9049,11 +9114,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "2.7.16",
|
"version": "3.5.42",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
|
||||||
|
"integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-sfc": "2.7.16",
|
"@vue/compiler-dom": "3.5.42",
|
||||||
"csstype": "^3.1.0"
|
"@vue/compiler-sfc": "3.5.42",
|
||||||
|
"@vue/runtime-dom": "3.5.42",
|
||||||
|
"@vue/server-renderer": "3.5.42",
|
||||||
|
"@vue/shared": "3.5.42"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-hot-reload-api": {
|
"node_modules/vue-hot-reload-api": {
|
||||||
@@ -9061,10 +9139,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vue-lazyload": {
|
|
||||||
"version": "1.2.3",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/vue-loader": {
|
"node_modules/vue-loader": {
|
||||||
"version": "17.4.2",
|
"version": "17.4.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -9102,10 +9176,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "3.6.5",
|
"version": "4.6.4",
|
||||||
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-3.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
||||||
"integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==",
|
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^6.6.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-style-loader": {
|
"node_modules/vue-style-loader": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
@@ -9121,31 +9204,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vue-template-compiler": {
|
|
||||||
"version": "2.7.16",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"de-indent": "^1.0.2",
|
|
||||||
"he": "^1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-template-es2015-compiler": {
|
"node_modules/vue-template-es2015-compiler": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vue/node_modules/@vue/compiler-sfc": {
|
|
||||||
"version": "2.7.16",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.23.5",
|
|
||||||
"postcss": "^8.4.14",
|
|
||||||
"source-map": "^0.6.1"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"prettier": "^1.18.2 || ^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/watchpack": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -10,15 +10,15 @@
|
|||||||
"@vant/area-data": "^1.5.2",
|
"@vant/area-data": "^1.5.2",
|
||||||
"core-js": "^3.37.1",
|
"core-js": "^3.37.1",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
"vant": "^2.13.9",
|
"vant": "^3.6.16",
|
||||||
"vue": "^2.7.16",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^3.6.5"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~5.0.8",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-service": "~5.0.8",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"vue-template-compiler": "^2.7.16"
|
"@vue/compiler-sfc": "^3.5.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const STORAGE_KEY = 'supplied-h5-merchant-session'
|
||||||
|
export const clearMerchantSession = () => sessionStorage.removeItem(STORAGE_KEY)
|
||||||
|
export const saveMerchantSession = session => sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session))
|
||||||
|
export const readMerchantSession = () => {
|
||||||
|
try {
|
||||||
|
const session = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || 'null')
|
||||||
|
if (session?.token && session?.user && Date.parse(session.expiresAt) > Date.now()) return session
|
||||||
|
} catch { /* 损坏的会话按未登录处理。 */ }
|
||||||
|
clearMerchantSession()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
export const merchantLoginLocation = () => {
|
||||||
|
const redirect = window.location.hash.slice(1) || '/joyful-selection/merchant'
|
||||||
|
return `#/joyful-selection/merchant/login?redirect=${encodeURIComponent(redirect)}`
|
||||||
|
}
|
||||||
@@ -1,7 +1,20 @@
|
|||||||
|
import { readMerchantSession, clearMerchantSession, merchantLoginLocation } from './merchant-session'
|
||||||
|
|
||||||
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/api'
|
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/api'
|
||||||
|
|
||||||
const normalizeBase = base => base.replace(/\/$/, '')
|
const normalizeBase = base => base.replace(/\/$/, '')
|
||||||
|
|
||||||
|
const resolveFileUrls = value => {
|
||||||
|
if (typeof value === 'string' && /^\/api\/(?:file|upload-preview)\?/.test(value)) {
|
||||||
|
return `${normalizeBase(API_BASE).replace(/\/api$/, '')}${value.replace('/api/upload-preview?', '/api/file?')}`
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) return value.map(resolveFileUrls)
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveFileUrls(item)]))
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
export const getFunction = async (name, params = {}) => {
|
export const getFunction = async (name, params = {}) => {
|
||||||
const url = new URL(`${normalizeBase(API_BASE)}/${name}`)
|
const url = new URL(`${normalizeBase(API_BASE)}/${name}`)
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
@@ -18,25 +31,34 @@ export const getFunction = async (name, params = {}) => {
|
|||||||
throw new Error(message)
|
throw new Error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload.data
|
return resolveFileUrls(payload.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const postFunction = async (name, body = {}) => {
|
export const postFunction = async (name, body = {}) => {
|
||||||
const response = await fetch(`${normalizeBase(API_BASE)}/${name}`, {
|
const merchantRequest = name.startsWith('merchant-') || ['login', 'admin-session', 'admin-logout'].includes(name)
|
||||||
|
const token = merchantRequest ? readMerchantSession()?.token : ''
|
||||||
|
const response = await fetch(`${normalizeBase(API_BASE)}${merchantRequest ? '/admin' : ''}/${name}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
})
|
})
|
||||||
|
|
||||||
const payload = await response.json().catch(() => ({}))
|
const payload = await response.json().catch(() => ({}))
|
||||||
|
if (merchantRequest && name !== 'login' && (response.status === 401 || payload.error?.code === 'UNAUTHORIZED')) {
|
||||||
|
clearMerchantSession()
|
||||||
|
if (name !== 'admin-session' && !window.location.hash.startsWith('#/joyful-selection/merchant/login')) {
|
||||||
|
window.location.replace(merchantLoginLocation())
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!response.ok || payload.success === false) {
|
if (!response.ok || payload.success === false) {
|
||||||
const message = payload.error?.message || payload.msg || '请求失败'
|
const message = payload.error?.message || payload.msg || '请求失败'
|
||||||
throw new Error(message)
|
throw new Error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload.data
|
return resolveFileUrls(payload.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const uploadImageFunction = async (file, folder = 'after-sale') => {
|
export const uploadImageFunction = async (file, folder = 'after-sale') => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<van-notice-bar class="notice" left-icon="volume-o" :text="shop.notice" />
|
<van-notice-bar class="notice" left-icon="volume-o" :text="shop.notice" />
|
||||||
<van-search v-model="keyword" placeholder="搜索商品" shape="round" />
|
<van-search v-model="keyword" placeholder="搜索商品" shape="round" />
|
||||||
|
|
||||||
<van-tabs v-model="activeCategory" sticky :color="shop.theme" title-active-color="var(--theme)">
|
<van-tabs v-model:active="activeCategory" sticky :color="shop.theme" title-active-color="var(--theme)">
|
||||||
<van-tab v-for="category in categories" :key="category" :title="category" />
|
<van-tab v-for="category in categories" :key="category" :title="category" />
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<span class="selected">已选 {{ totalCount }} 件</span>
|
<span class="selected">已选 {{ totalCount }} 件</span>
|
||||||
</van-submit-bar>
|
</van-submit-bar>
|
||||||
|
|
||||||
<van-popup v-model="checkoutVisible" round position="bottom" :style="{ minHeight: '68%' }" closeable>
|
<van-popup v-model:show="checkoutVisible" round position="bottom" :style="{ minHeight: '68%' }" closeable>
|
||||||
<section class="checkout">
|
<section class="checkout">
|
||||||
<h2>确认订单</h2>
|
<h2>确认订单</h2>
|
||||||
<van-contact-card type="add" name="商城收货人" tel="138****8888" @click="Toast('当前使用默认收货地址')" />
|
<van-contact-card type="add" name="商城收货人" tel="138****8888" @click="Toast('当前使用默认收货地址')" />
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
|
|
||||||
<van-popup v-model="successVisible" round closeable class="result-popup">
|
<van-popup v-model:show="successVisible" round closeable class="result-popup">
|
||||||
<van-icon name="checked" color="#23b65d" size="62" />
|
<van-icon name="checked" color="#23b65d" size="62" />
|
||||||
<h2>订单提交成功</h2>
|
<h2>订单提交成功</h2>
|
||||||
<p>订单号:{{ submittedOrderId }}</p>
|
<p>订单号:{{ submittedOrderId }}</p>
|
||||||
|
|||||||
+3
-12
@@ -1,20 +1,11 @@
|
|||||||
import Vue from 'vue'
|
import { createApp } from 'vue'
|
||||||
import Vant from 'vant'
|
import Vant from 'vant'
|
||||||
import { Skeleton } from 'vant'
|
import { createPinia } from 'pinia'
|
||||||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
|
||||||
import 'vant/lib/index.css'
|
import 'vant/lib/index.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
Vue.use(Vant)
|
|
||||||
Vue.use(Skeleton)
|
|
||||||
Vue.use(PiniaVuePlugin)
|
|
||||||
Vue.config.productionTip = false
|
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
|
||||||
new Vue({
|
createApp(App).use(pinia).use(router).use(Vant).mount('#app')
|
||||||
router,
|
|
||||||
pinia,
|
|
||||||
render: h => h(App)
|
|
||||||
}).$mount('#app')
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Vue from 'vue'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import Router from 'vue-router'
|
import { Toast } from 'vant'
|
||||||
|
import { postFunction } from '@/api/supplied-service'
|
||||||
|
import { readMerchantSession, clearMerchantSession } from '@/api/merchant-session'
|
||||||
import SupplierHome from '@/view/index.vue'
|
import SupplierHome from '@/view/index.vue'
|
||||||
import JoyfulSelectionLayout from '@/view/joyful-selection/layout.vue'
|
import JoyfulSelectionLayout from '@/view/joyful-selection/layout.vue'
|
||||||
import JoyfulSelectionHome from '@/view/joyful-selection/index.vue'
|
import JoyfulSelectionHome from '@/view/joyful-selection/index.vue'
|
||||||
@@ -29,16 +31,16 @@ import HappyShopping from '@/view/happy-shopping/index.vue'
|
|||||||
import FreshFruit from '@/view/fresh-fruit/index.vue'
|
import FreshFruit from '@/view/fresh-fruit/index.vue'
|
||||||
import CuteSelection from '@/view/cute-selection/index.vue'
|
import CuteSelection from '@/view/cute-selection/index.vue'
|
||||||
|
|
||||||
Vue.use(Router)
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
export default new Router({
|
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: SupplierHome },
|
{ path: '/', component: SupplierHome },
|
||||||
|
{ path: '/joyful-selection/merchant/login', component: () => import('@/view/joyful-selection/merchant/login.vue') },
|
||||||
{
|
{
|
||||||
path: '/joyful-selection',
|
path: '/joyful-selection',
|
||||||
component: JoyfulSelectionLayout,
|
component: JoyfulSelectionLayout,
|
||||||
children: [
|
children: [
|
||||||
{ path: '', redirect: 'home' },
|
{ path: '', redirect: '/joyful-selection/home' },
|
||||||
{ path: 'home', component: JoyfulSelectionHome },
|
{ path: 'home', component: JoyfulSelectionHome },
|
||||||
{ path: 'products', component: JoyfulSelectionProductList },
|
{ path: 'products', component: JoyfulSelectionProductList },
|
||||||
{ path: 'product-detail', component: JoyfulSelectionProductDetail },
|
{ path: 'product-detail', component: JoyfulSelectionProductDetail },
|
||||||
@@ -56,8 +58,8 @@ export default new Router({
|
|||||||
{ path: 'after-sale/select', component: JoyfulSelectionAfterSaleSelect },
|
{ path: 'after-sale/select', component: JoyfulSelectionAfterSaleSelect },
|
||||||
{ path: 'after-sale/return', component: JoyfulSelectionAfterSaleReturn },
|
{ path: 'after-sale/return', component: JoyfulSelectionAfterSaleReturn },
|
||||||
{ path: 'after-sale/exchange', component: JoyfulSelectionAfterSaleExchange },
|
{ path: 'after-sale/exchange', component: JoyfulSelectionAfterSaleExchange },
|
||||||
{ path: 'merchant', component: JoyfulSelectionMerchant },
|
{ path: 'merchant', component: JoyfulSelectionMerchant, meta: { merchantAuth: true } },
|
||||||
{ path: 'merchant/after-sale-detail', component: JoyfulSelectionMerchantAfterSaleDetail },
|
{ path: 'merchant/after-sale-detail', component: JoyfulSelectionMerchantAfterSaleDetail, meta: { merchantAuth: true } },
|
||||||
{ path: 'supplier-logs', component: JoyfulSelectionSupplierLogs },
|
{ path: 'supplier-logs', component: JoyfulSelectionSupplierLogs },
|
||||||
{ path: 'profile', component: JoyfulSelectionProfile },
|
{ path: 'profile', component: JoyfulSelectionProfile },
|
||||||
{ path: 'points', component: JoyfulSelectionPoints }
|
{ path: 'points', component: JoyfulSelectionPoints }
|
||||||
@@ -69,3 +71,19 @@ export default new Router({
|
|||||||
{ path: '/cute-selection', component: CuteSelection }
|
{ path: '/cute-selection', component: CuteSelection }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach(async to => {
|
||||||
|
if (!to.meta.merchantAuth) return true
|
||||||
|
const login = { path: '/joyful-selection/merchant/login', query: { redirect: to.fullPath } }
|
||||||
|
if (!readMerchantSession()) return login
|
||||||
|
try {
|
||||||
|
await postFunction('admin-session')
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
clearMerchantSession()
|
||||||
|
Toast(error.message || '登录已失效,请重新登录')
|
||||||
|
return login
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const saveUser = data => {
|
|||||||
export const useUserStore = defineStore('joyfulSelectionUser', {
|
export const useUserStore = defineStore('joyfulSelectionUser', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
userId: '',
|
userId: '',
|
||||||
|
nickname: '',
|
||||||
supplierIdentification: '',
|
supplierIdentification: '',
|
||||||
supplierId: '',
|
supplierId: '',
|
||||||
supplierName: '',
|
supplierName: '',
|
||||||
@@ -34,6 +35,7 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
|||||||
loadUser() {
|
loadUser() {
|
||||||
const data = readUser()
|
const data = readUser()
|
||||||
this.userId = data.userId || ''
|
this.userId = data.userId || ''
|
||||||
|
this.nickname = data.nickname || ''
|
||||||
const supplierIdentification = DEFAULT_SUPPLIER_IDENTIFICATION || data.supplierIdentification || ''
|
const supplierIdentification = DEFAULT_SUPPLIER_IDENTIFICATION || data.supplierIdentification || ''
|
||||||
const supplierChanged = Boolean(data.supplierIdentification && supplierIdentification !== data.supplierIdentification)
|
const supplierChanged = Boolean(data.supplierIdentification && supplierIdentification !== data.supplierIdentification)
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
|||||||
persistUser() {
|
persistUser() {
|
||||||
saveUser({
|
saveUser({
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
|
nickname: this.nickname,
|
||||||
supplierIdentification: this.supplierIdentification,
|
supplierIdentification: this.supplierIdentification,
|
||||||
supplierId: this.supplierId,
|
supplierId: this.supplierId,
|
||||||
supplierName: this.supplierName,
|
supplierName: this.supplierName,
|
||||||
@@ -53,17 +56,33 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
|||||||
})
|
})
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
},
|
},
|
||||||
hydrateFromRoute(query = {}) {
|
async hydrateFromRoute(query = {}) {
|
||||||
if (!this.loaded) {
|
if (!this.loaded) {
|
||||||
this.loadUser()
|
this.loadUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextUserId = query.userId || ''
|
const keyword = query.keyword || new URLSearchParams(window.location.search).get('keyword') || ''
|
||||||
|
let nextUserId = query.userId || ''
|
||||||
|
let nextNickname
|
||||||
|
|
||||||
|
if (keyword) {
|
||||||
|
const sessionKeyword = String(keyword).replace(/ /g, '+')
|
||||||
|
console.log('[supplier-session] keyword', {
|
||||||
|
routeKeyword: query.keyword,
|
||||||
|
searchKeyword: new URLSearchParams(window.location.search).get('keyword') || '',
|
||||||
|
sessionKeyword
|
||||||
|
})
|
||||||
|
const data = await postFunction('supplier-session', { keyword: sessionKeyword })
|
||||||
|
nextUserId = data?.userId || ''
|
||||||
|
nextNickname = data?.userInfo?.nickname || ''
|
||||||
|
}
|
||||||
|
|
||||||
if (nextUserId && nextUserId !== this.userId) {
|
if (nextUserId && nextUserId !== this.userId) {
|
||||||
this.userId = nextUserId
|
this.userId = nextUserId
|
||||||
|
this.nickname = ''
|
||||||
this.pointsBalance = 0
|
this.pointsBalance = 0
|
||||||
}
|
}
|
||||||
|
if (nextNickname !== undefined) this.nickname = nextNickname
|
||||||
if (!this.supplierIdentification) {
|
if (!this.supplierIdentification) {
|
||||||
this.supplierIdentification = DEFAULT_SUPPLIER_IDENTIFICATION
|
this.supplierIdentification = DEFAULT_SUPPLIER_IDENTIFICATION
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<button v-else type="button" class="primary" @click="contactService">联系客服</button>
|
<button v-else type="button" class="primary" @click="contactService">联系客服</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<van-popup v-model="showLogisticsPopup" round position="bottom">
|
<van-popup v-model:show="showLogisticsPopup" round position="bottom">
|
||||||
<section class="logistics-popup">
|
<section class="logistics-popup">
|
||||||
<h3>填写物流单号</h3>
|
<h3>填写物流单号</h3>
|
||||||
<van-field v-model="logisticsCompany" label="快递公司" placeholder="请输入快递公司" />
|
<van-field v-model="logisticsCompany" label="快递公司" placeholder="请输入快递公司" />
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<van-button v-show="!loading" class="submit-button" block :loading="submitting" @click="submitReturn">提交申请</van-button>
|
<van-button v-show="!loading" class="submit-button" block :loading="submitting" @click="submitReturn">提交申请</van-button>
|
||||||
<van-popup v-model="showReasonPopup" round position="bottom">
|
<van-popup v-model:show="showReasonPopup" round position="bottom">
|
||||||
<section class="reason-popup">
|
<section class="reason-popup">
|
||||||
<h3>请选择换货原因</h3>
|
<h3>请选择换货原因</h3>
|
||||||
<van-radio-group v-model="selectedReason">
|
<van-radio-group v-model="selectedReason">
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
</van-radio-group>
|
</van-radio-group>
|
||||||
</section>
|
</section>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
<van-popup v-model="showExchangeGoodsPopup" round position="bottom" :style="{ height: '80vh' }">
|
<van-popup v-model:show="showExchangeGoodsPopup" round position="bottom" :style="{ height: '80vh' }">
|
||||||
<section class="exchange-popup">
|
<section class="exchange-popup">
|
||||||
<h3>选择换货商品</h3>
|
<h3>选择换货商品</h3>
|
||||||
<p>仅可选择价格为 ¥{{ originalProductPrice }} 的商品</p>
|
<p>仅可选择价格为 ¥{{ originalProductPrice }} 的商品</p>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<van-nav-bar title="退款/售后" left-arrow @click-left="$router.replace('/joyful-selection/profile')" />
|
<van-nav-bar title="退款/售后" left-arrow @click-left="$router.replace('/joyful-selection/profile')" />
|
||||||
|
|
||||||
<van-pull-refresh v-model="refreshing" class="after-sale-list" @refresh="onRefresh">
|
<van-pull-refresh v-model="refreshing" class="after-sale-list" @refresh="onRefresh">
|
||||||
<van-tabs v-model="activeTab" color="#ff5b20">
|
<van-tabs v-model:active="activeTab" color="#ff5b20">
|
||||||
<van-tab title="处理中" name="processing" />
|
<van-tab title="处理中" name="processing" />
|
||||||
<van-tab title="历史" name="history" />
|
<van-tab title="历史" name="history" />
|
||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<van-button v-show="!loading" class="submit-button" block :loading="submitting" @click="submitReturn">提交申请</van-button>
|
<van-button v-show="!loading" class="submit-button" block :loading="submitting" @click="submitReturn">提交申请</van-button>
|
||||||
<van-popup v-model="showReasonPopup" round position="bottom">
|
<van-popup v-model:show="showReasonPopup" round position="bottom">
|
||||||
<section class="reason-popup">
|
<section class="reason-popup">
|
||||||
<h3>请选择退货原因</h3>
|
<h3>请选择退货原因</h3>
|
||||||
<van-radio-group v-model="selectedReason">
|
<van-radio-group v-model="selectedReason">
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
class="cart-item"
|
class="cart-item"
|
||||||
>
|
>
|
||||||
<van-checkbox
|
<van-checkbox
|
||||||
:value="isItemChecked(item)"
|
:model-value="isItemChecked(item)"
|
||||||
class="item-check"
|
class="item-check"
|
||||||
checked-color="#ff5b20"
|
checked-color="#ff5b20"
|
||||||
@input="toggleItem(item, $event)"
|
@update:model-value="toggleItem(item, $event)"
|
||||||
/>
|
/>
|
||||||
<van-image class="item-thumb" :src="item.image" fit="cover" />
|
<van-image class="item-thumb" :src="item.image" fit="cover" />
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
@@ -52,10 +52,10 @@
|
|||||||
@submit="handleCheckout"
|
@submit="handleCheckout"
|
||||||
>
|
>
|
||||||
<van-checkbox
|
<van-checkbox
|
||||||
:value="isAllChecked"
|
:model-value="isAllChecked"
|
||||||
:indeterminate="isPartChecked"
|
:indeterminate="isPartChecked"
|
||||||
checked-color="#ff5b20"
|
checked-color="#ff5b20"
|
||||||
@input="toggleAll"
|
@update:model-value="toggleAll"
|
||||||
>全选</van-checkbox>
|
>全选</van-checkbox>
|
||||||
</van-submit-bar>
|
</van-submit-bar>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
<van-empty v-if="!items.length" description="购物车还是空的" />
|
<van-empty v-if="!items.length" description="购物车还是空的" />
|
||||||
<article v-for="item in items" :key="item.cartId" class="edit-item">
|
<article v-for="item in items" :key="item.cartId" class="edit-item">
|
||||||
<van-checkbox
|
<van-checkbox
|
||||||
:value="isItemChecked(item)"
|
:model-value="isItemChecked(item)"
|
||||||
class="item-check"
|
class="item-check"
|
||||||
checked-color="#ff5b20"
|
checked-color="#ff5b20"
|
||||||
@input="toggleItem(item, $event)"
|
@update:model-value="toggleItem(item, $event)"
|
||||||
/>
|
/>
|
||||||
<van-image class="item-thumb" :src="item.image" fit="cover" />
|
<van-image class="item-thumb" :src="item.image" fit="cover" />
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
@@ -26,10 +26,10 @@
|
|||||||
</section>
|
</section>
|
||||||
<van-submit-bar class="edit-submit" button-text="删除" @submit="remove">
|
<van-submit-bar class="edit-submit" button-text="删除" @submit="remove">
|
||||||
<van-checkbox
|
<van-checkbox
|
||||||
:value="isAllChecked"
|
:model-value="isAllChecked"
|
||||||
:indeterminate="isPartChecked"
|
:indeterminate="isPartChecked"
|
||||||
checked-color="#ff5b20"
|
checked-color="#ff5b20"
|
||||||
@input="toggleAll"
|
@update:model-value="toggleAll"
|
||||||
>全选</van-checkbox>
|
>全选</van-checkbox>
|
||||||
</van-submit-bar>
|
</van-submit-bar>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -49,15 +49,21 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
...mapActions(useCartStore, ['loadCart']),
|
...mapActions(useCartStore, ['loadCart']),
|
||||||
...mapActions(useUserStore, ['hydrateFromRoute', 'fetchBridgeToken']),
|
...mapActions(useUserStore, ['hydrateFromRoute', 'fetchBridgeToken']),
|
||||||
prepareUserSession() {
|
async prepareUserSession() {
|
||||||
|
if (this.$route.meta.merchantAuth) {
|
||||||
|
this.sessionLoading = false
|
||||||
|
return
|
||||||
|
}
|
||||||
this.sessionLoading = true
|
this.sessionLoading = true
|
||||||
this.hydrateFromRoute(this.$route.query)
|
try {
|
||||||
this.fetchBridgeToken().catch(error => {
|
await this.hydrateFromRoute(this.$route.query)
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
} catch (error) {
|
||||||
console.warn('获取平台授权令牌失败', error)
|
console.warn('获取平台授权令牌失败', error)
|
||||||
this.$toast(error.message || '供应商验证失败,请稍后再试')
|
this.$toast(error.message || '供应商验证失败,请稍后再试')
|
||||||
}).finally(() => {
|
} finally {
|
||||||
this.sessionLoading = false
|
this.sessionLoading = false
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<van-popup v-model="showExchangeShipPopup" round position="bottom">
|
<van-popup v-model:show="showExchangeShipPopup" round position="bottom">
|
||||||
<section class="action-popup">
|
<section class="action-popup">
|
||||||
<h3>填写换货发货信息</h3>
|
<h3>填写换货发货信息</h3>
|
||||||
<van-field v-model="exchangeShipForm.logisticsCompany" label="快递公司" placeholder="请输入快递公司" />
|
<van-field v-model="exchangeShipForm.logisticsCompany" label="快递公司" placeholder="请输入快递公司" />
|
||||||
@@ -336,7 +336,7 @@ export default {
|
|||||||
this.loadDetail()
|
this.loadDetail()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
...mapActions(useUserStore, ['fetchSupplierInfo']),
|
||||||
normalizeSteps(steps) {
|
normalizeSteps(steps) {
|
||||||
const serviceState = Number(this.afterSale.serviceState)
|
const serviceState = Number(this.afterSale.serviceState)
|
||||||
return steps.map((step, index) => ({
|
return steps.map((step, index) => ({
|
||||||
@@ -359,9 +359,9 @@ export default {
|
|||||||
async loadDetail() {
|
async loadDetail() {
|
||||||
try {
|
try {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
await this.fetchBridgeToken()
|
if (!this.supplierId && !this.$route.query.supplierId) await this.fetchSupplierInfo()
|
||||||
const data = await postFunction('merchant-after-sale-detail', {
|
const data = await postFunction('merchant-after-sale-detail', {
|
||||||
supplierId: this.supplierId,
|
supplierId: String(this.$route.query.supplierId || this.supplierId),
|
||||||
serviceOrderId: String(this.$route.query.serviceOrderId || '')
|
serviceOrderId: String(this.$route.query.serviceOrderId || '')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -407,7 +407,7 @@ export default {
|
|||||||
message: auditStatus === 1 ? '通过后用户可寄回商品。' : '拒绝后该售后申请将结束。'
|
message: auditStatus === 1 ? '通过后用户可寄回商品。' : '拒绝后该售后申请将结束。'
|
||||||
})
|
})
|
||||||
await postFunction('merchant-after-sale-audit', {
|
await postFunction('merchant-after-sale-audit', {
|
||||||
supplierId: this.supplierId,
|
supplierId: this.afterSale.supplierId || this.supplierId,
|
||||||
serviceOrderId: this.afterSale.serviceOrderId,
|
serviceOrderId: this.afterSale.serviceOrderId,
|
||||||
auditStatus,
|
auditStatus,
|
||||||
auditRemark: auditStatus === 1 ? '审核通过' : '商家拒绝'
|
auditRemark: auditStatus === 1 ? '审核通过' : '商家拒绝'
|
||||||
@@ -421,7 +421,7 @@ export default {
|
|||||||
message: '确认后退货售后会进入完成,换货售后会进入待商家寄货。'
|
message: '确认后退货售后会进入完成,换货售后会进入待商家寄货。'
|
||||||
})
|
})
|
||||||
await postFunction('merchant-after-sale-receive', {
|
await postFunction('merchant-after-sale-receive', {
|
||||||
supplierId: this.supplierId,
|
supplierId: this.afterSale.supplierId || this.supplierId,
|
||||||
serviceOrderId: this.afterSale.serviceOrderId
|
serviceOrderId: this.afterSale.serviceOrderId
|
||||||
})
|
})
|
||||||
Toast('已确认收货')
|
Toast('已确认收货')
|
||||||
@@ -441,7 +441,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await postFunction('merchant-after-sale-ship', {
|
await postFunction('merchant-after-sale-ship', {
|
||||||
supplierId: this.supplierId,
|
supplierId: this.afterSale.supplierId || this.supplierId,
|
||||||
serviceOrderId: this.afterSale.serviceOrderId,
|
serviceOrderId: this.afterSale.serviceOrderId,
|
||||||
logisticsCompany: this.exchangeShipForm.logisticsCompany,
|
logisticsCompany: this.exchangeShipForm.logisticsCompany,
|
||||||
logisticsNo: this.exchangeShipForm.logisticsNo
|
logisticsNo: this.exchangeShipForm.logisticsNo
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
<van-nav-bar title="商家处理台" left-arrow @click-left="$router.back()" />
|
<van-nav-bar title="商家处理台" left-arrow @click-left="$router.back()" />
|
||||||
|
|
||||||
<van-pull-refresh v-model="refreshing" class="merchant-refresh" @refresh="onRefresh">
|
<van-pull-refresh v-model="refreshing" class="merchant-refresh" @refresh="onRefresh">
|
||||||
<van-tabs v-model="activeTab" sticky offset-top="46" color="#ff5b20" @change="loadCurrentTab">
|
<van-tabs v-model:active="activeTab" sticky offset-top="46" color="#ff5b20" @change="loadCurrentTab">
|
||||||
<van-tab title="待发货" name="ship" :badge="tabBadge('ship')">
|
<van-tab title="待发货" name="ship" :badge="tabBadge('ship')">
|
||||||
<section class="list-panel">
|
<section class="list-panel">
|
||||||
|
<button v-if="orderItems.length" type="button" class="batch-ship-button" @click="quickShipAll">一键发货</button>
|
||||||
<section v-if="loading" class="skeleton-card">
|
<section v-if="loading" class="skeleton-card">
|
||||||
<van-skeleton title :row="3" />
|
<van-skeleton title :row="3" />
|
||||||
<van-skeleton title :row="3" />
|
<van-skeleton title :row="3" />
|
||||||
@@ -160,7 +161,7 @@
|
|||||||
</van-tabs>
|
</van-tabs>
|
||||||
</van-pull-refresh>
|
</van-pull-refresh>
|
||||||
|
|
||||||
<van-popup v-model="showShipPopup" round position="bottom">
|
<van-popup v-model:show="showShipPopup" round position="bottom">
|
||||||
<section class="action-popup">
|
<section class="action-popup">
|
||||||
<h3>填写发货信息</h3>
|
<h3>填写发货信息</h3>
|
||||||
<van-field v-model="shipForm.crrgBsnNm" label="快递公司" placeholder="请输入快递公司" />
|
<van-field v-model="shipForm.crrgBsnNm" label="快递公司" placeholder="请输入快递公司" />
|
||||||
@@ -169,7 +170,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</van-popup>
|
</van-popup>
|
||||||
|
|
||||||
<van-popup v-model="showExchangeShipPopup" round position="bottom">
|
<van-popup v-model:show="showExchangeShipPopup" round position="bottom">
|
||||||
<section class="action-popup">
|
<section class="action-popup">
|
||||||
<h3>填写换货发货信息</h3>
|
<h3>填写换货发货信息</h3>
|
||||||
<van-field v-model="exchangeShipForm.logisticsCompany" label="快递公司" placeholder="请输入快递公司" />
|
<van-field v-model="exchangeShipForm.logisticsCompany" label="快递公司" placeholder="请输入快递公司" />
|
||||||
@@ -251,7 +252,7 @@ export default {
|
|||||||
this.prepareSupplier().then(() => this.loadAllTabs())
|
this.prepareSupplier().then(() => this.loadAllTabs())
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useUserStore, ['fetchSupplierInfo', 'fetchBridgeToken']),
|
...mapActions(useUserStore, ['fetchSupplierInfo']),
|
||||||
...mapActions(useUiStore, ['loadUi', 'setMerchantTab']),
|
...mapActions(useUiStore, ['loadUi', 'setMerchantTab']),
|
||||||
skuText(item = {}) {
|
skuText(item = {}) {
|
||||||
const sku = String(item.sku || item.skuText || '').trim()
|
const sku = String(item.sku || item.skuText || '').trim()
|
||||||
@@ -272,7 +273,6 @@ export default {
|
|||||||
return count > 0 ? count : ''
|
return count > 0 ? count : ''
|
||||||
},
|
},
|
||||||
async prepareSupplier() {
|
async prepareSupplier() {
|
||||||
await this.fetchBridgeToken()
|
|
||||||
if (!this.supplierId) {
|
if (!this.supplierId) {
|
||||||
await this.fetchSupplierInfo()
|
await this.fetchSupplierInfo()
|
||||||
}
|
}
|
||||||
@@ -352,18 +352,34 @@ export default {
|
|||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/joyful-selection/merchant/after-sale-detail',
|
path: '/joyful-selection/merchant/after-sale-detail',
|
||||||
query: {
|
query: {
|
||||||
serviceOrderId: item.serviceOrderId
|
serviceOrderId: item.serviceOrderId,
|
||||||
|
supplierId: item.supplierId || this.supplierId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
openShipPopup(item) {
|
openShipPopup(item) {
|
||||||
this.currentOrder = item
|
this.currentOrder = item
|
||||||
this.shipForm = {
|
this.shipForm = {
|
||||||
crrgBsnNm: item.crrgBsnNm || '',
|
crrgBsnNm: item.crrgBsnNm || '韵达快递',
|
||||||
logisticsOrderId: item.logisticsOrderId || ''
|
logisticsOrderId: item.logisticsOrderId || `YT${Date.now()}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`
|
||||||
}
|
}
|
||||||
this.showShipPopup = true
|
this.showShipPopup = true
|
||||||
},
|
},
|
||||||
|
async quickShipAll() {
|
||||||
|
await Dialog.confirm({
|
||||||
|
title: '确认一键发货?',
|
||||||
|
message: `将为当前 ${this.orderItems.length} 个待发货订单提交发货。`
|
||||||
|
})
|
||||||
|
await Promise.all(this.orderItems.map(item => postFunction('merchant-order-ship', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: item.userId,
|
||||||
|
orderId: item.orderId,
|
||||||
|
crrgBsnNm: item.crrgBsnNm || '韵达快递',
|
||||||
|
logisticsOrderId: item.logisticsOrderId || `YT${Date.now()}${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`
|
||||||
|
})))
|
||||||
|
Toast('批量发货成功')
|
||||||
|
await this.loadOrders()
|
||||||
|
},
|
||||||
async submitShip() {
|
async submitShip() {
|
||||||
if (!this.shipForm.crrgBsnNm || !this.shipForm.logisticsOrderId) {
|
if (!this.shipForm.crrgBsnNm || !this.shipForm.logisticsOrderId) {
|
||||||
Toast('请填写完整发货信息')
|
Toast('请填写完整发货信息')
|
||||||
@@ -467,6 +483,18 @@ export default {
|
|||||||
padding: 12px 10px 18px;
|
padding: 12px 10px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.batch-ship-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(90deg, #ff6a2a 0%, #ff4b18 100%);
|
||||||
|
}
|
||||||
|
|
||||||
.work-card {
|
.work-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<main class="merchant-login-page">
|
||||||
|
<van-nav-bar title="商家登录" left-arrow @click-left="$router.replace('/joyful-selection/home')" />
|
||||||
|
<section class="merchant-login-card">
|
||||||
|
<div class="login-icon"><van-icon name="shop-o" size="36" /></div>
|
||||||
|
<h1>商家处理台</h1><p>使用供应商管理账号登录</p>
|
||||||
|
<van-form @submit="submit">
|
||||||
|
<van-cell-group inset>
|
||||||
|
<van-field v-model="username" name="username" label="账号" placeholder="请输入账号" autocomplete="username" :rules="[{ required: true, message: '请输入账号' }]" />
|
||||||
|
<van-field v-model="password" name="password" type="password" label="密码" placeholder="请输入密码" autocomplete="current-password" :rules="[{ required: true, message: '请输入密码' }]" />
|
||||||
|
</van-cell-group>
|
||||||
|
<van-button block round type="primary" native-type="submit" :loading="submitting" :disabled="submitting" color="#ff5b20" class="login-submit">登录</van-button>
|
||||||
|
</van-form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Toast } from 'vant'
|
||||||
|
import { postFunction } from '@/api/supplied-service'
|
||||||
|
import { saveMerchantSession, clearMerchantSession } from '@/api/merchant-session'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MerchantLogin',
|
||||||
|
data: () => ({ username: '', password: '', submitting: false }),
|
||||||
|
methods: {
|
||||||
|
async submit() {
|
||||||
|
if (this.submitting) return
|
||||||
|
this.submitting = true
|
||||||
|
try {
|
||||||
|
const session = await postFunction('login', { username: this.username.trim(), password: this.password })
|
||||||
|
saveMerchantSession(session)
|
||||||
|
await postFunction('admin-session')
|
||||||
|
this.password = ''
|
||||||
|
const target = typeof this.$route.query.redirect === 'string' ? this.$route.query.redirect : ''
|
||||||
|
const resolved = this.$router.resolve(target || '/joyful-selection/merchant')
|
||||||
|
await this.$router.replace(resolved.meta.merchantAuth ? resolved.fullPath : '/joyful-selection/merchant')
|
||||||
|
} catch (error) {
|
||||||
|
clearMerchantSession()
|
||||||
|
Toast(error.message || '登录失败')
|
||||||
|
} finally { this.submitting = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.merchant-login-page { min-height: 100vh; background: #f7f8fa; }
|
||||||
|
.merchant-login-card { max-width: 420px; margin: 48px auto; padding: 28px 20px; background: #fff; border-radius: 16px; }
|
||||||
|
.login-icon { display: flex; align-items: center; justify-content: center; width: 72px; height: 72px; margin: 0 auto 20px; border-radius: 20px; color: #ff5b20; background: #fff1e9; }
|
||||||
|
.merchant-login-card h1 { margin: 0; text-align: center; font-size: 23px; }
|
||||||
|
.merchant-login-card > p { margin: 12px 0 28px; text-align: center; color: #8793a5; font-size: 14px; }
|
||||||
|
.merchant-login-card :deep(.van-cell-group--inset) { margin: 0; }
|
||||||
|
.login-submit { margin-top: 28px; }
|
||||||
|
@media (max-width: 480px) { .merchant-login-card { margin: 32px 16px; } }
|
||||||
|
</style>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
立即支付 ¥{{ amountText }}
|
立即支付 ¥{{ amountText }}
|
||||||
</van-button>
|
</van-button>
|
||||||
|
|
||||||
<van-popup v-model="showConfirm" class="pay-popup" round position="bottom">
|
<van-popup v-model:show="showConfirm" class="pay-popup" round position="bottom">
|
||||||
<article class="pay-panel">
|
<article class="pay-panel">
|
||||||
<header>
|
<header>
|
||||||
<van-icon name="cross" @click="showConfirm = false" />
|
<van-icon name="cross" @click="showConfirm = false" />
|
||||||
|
|||||||
@@ -59,16 +59,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<van-goods-action>
|
<van-action-bar>
|
||||||
<van-goods-action-icon icon="chat-o" text="客服" dot />
|
<van-action-bar-icon icon="chat-o" text="客服" dot />
|
||||||
<van-goods-action-icon icon="cart-o" text="购物车" :badge="cartBadge" @click="openCartPopup" />
|
<van-action-bar-icon icon="cart-o" text="购物车" :badge="cartBadge" @click="openCartPopup" />
|
||||||
<van-goods-action-icon icon="star" text="已收藏" color="#ff5000" />
|
<van-action-bar-icon icon="star" text="已收藏" color="#ff5000" />
|
||||||
<van-goods-action-button type="warning" text="加入购物车" @click="openSku('cart')" />
|
<van-action-bar-button type="warning" text="加入购物车" @click="openSku('cart')" />
|
||||||
<van-goods-action-button type="danger" text="立即购买" @click="openSku('buy')" />
|
<van-action-bar-button type="danger" text="立即购买" @click="openSku('buy')" />
|
||||||
</van-goods-action>
|
</van-action-bar>
|
||||||
|
|
||||||
<joyful-sku-popup ref="skuPopup" @add-cart="loadCart" />
|
<joyful-sku-popup ref="skuPopup" @add-cart="loadCart" />
|
||||||
<van-popup v-model="showCartPopup" class="cart-popup" position="bottom" round>
|
<van-popup v-model:show="showCartPopup" class="cart-popup" position="bottom" round>
|
||||||
<header class="cart-popup-head">
|
<header class="cart-popup-head">
|
||||||
<b>购物车</b>
|
<b>购物车</b>
|
||||||
<van-icon name="cross" @click="showCartPopup = false" />
|
<van-icon name="cross" @click="showCartPopup = false" />
|
||||||
@@ -251,7 +251,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.product-info {
|
.product-info {
|
||||||
margin-top: -4px;
|
margin-top: 12px;
|
||||||
padding: 18px 16px 16px;
|
padding: 18px 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<van-sku
|
<van-popup
|
||||||
:key="skuKey"
|
v-model:show="show"
|
||||||
v-model="show"
|
|
||||||
:class="`sku-popup--${mode}`"
|
:class="`sku-popup--${mode}`"
|
||||||
:sku="sku"
|
class="van-sku-container"
|
||||||
:goods="skuGoods"
|
position="bottom"
|
||||||
:goods-id="currentGoods.id"
|
round
|
||||||
:initial-sku="initialSku"
|
closeable
|
||||||
:hide-stock="sku.hide_stock"
|
safe-area-inset-bottom
|
||||||
:show-stepper="false"
|
>
|
||||||
:show-add-cart-btn="mode !== 'buy'"
|
<div class="sku-content">
|
||||||
:show-buy-btn="true"
|
<div class="van-sku-header">
|
||||||
:preview-on-click-image="true"
|
<div class="van-sku-header__goods-info">
|
||||||
add-cart-text="加入购物车"
|
<div class="van-sku__goods-price">
|
||||||
buy-text="立即购买"
|
<span class="van-sku__price-symbol">¥</span>
|
||||||
@add-cart="onAddCartClicked"
|
<span class="van-sku__price-num">{{ sku.price }}</span>
|
||||||
@buy-clicked="onBuyClicked"
|
</div>
|
||||||
|
<div class="van-sku__goods-name">{{ skuGoods.title }}</div>
|
||||||
|
<div class="van-sku__stock">库存 {{ sku.stock_num }} 件</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<van-cell-group class="van-sku-body" :border="false">
|
||||||
|
<div v-for="group in sku.tree" :key="group.k_s" class="van-sku-row">
|
||||||
|
<div class="van-sku-row__title">{{ group.k }}</div>
|
||||||
|
<div class="sku-options" :class="{ 'sku-options--colors': group.k_s === 's1' }">
|
||||||
|
<van-button
|
||||||
|
v-for="option in group.v"
|
||||||
|
:key="option.id"
|
||||||
|
size="small"
|
||||||
|
class="van-sku-row__item"
|
||||||
|
:class="{ 'van-sku-row__item--active': selectedSku[group.k_s] === option.id, 'sku-color-card': group.k_s === 's1' }"
|
||||||
|
@click="selectedSku[group.k_s] = option.id"
|
||||||
|
>
|
||||||
|
<span v-if="group.k_s === 's1'" class="sku-color-card__picture">
|
||||||
|
<van-image class="sku-color-card__image" :src="option.imgUrl" width="100%" height="100%" fit="cover" />
|
||||||
|
<van-icon
|
||||||
|
v-if="option.previewImgUrl"
|
||||||
|
name="expand-o"
|
||||||
|
class="sku-color-card__preview"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="预览商品图片"
|
||||||
|
@click.stop="previewImage(option.previewImgUrl)"
|
||||||
|
@keydown.enter.stop.prevent="previewImage(option.previewImgUrl)"
|
||||||
|
@keydown.space.stop.prevent="previewImage(option.previewImgUrl)"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
|
<span :class="{ 'sku-color-card__name': group.k_s === 's1' }">{{ option.name }}</span>
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<van-cell title="购买数量" class="sku-quantity" center>
|
||||||
|
<template #right-icon>
|
||||||
|
<van-stepper v-model="selectedNum" integer :min="1" :max="sku.stock_num" />
|
||||||
|
</template>
|
||||||
|
</van-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
<div class="sku-actions">
|
||||||
|
<van-button v-if="mode !== 'buy'" round type="warning" @click="onAddCartClicked(selectionData())">加入购物车</van-button>
|
||||||
|
<van-button round type="danger" @click="onBuyClicked(selectionData())">立即购买</van-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Toast } from 'vant'
|
import { ImagePreview, Toast } from 'vant'
|
||||||
import { useCartStore } from '@/store/cart'
|
import { useCartStore } from '@/store/cart'
|
||||||
|
|
||||||
const colorOptions = [
|
const colorOptions = [
|
||||||
@@ -47,7 +91,8 @@ export default {
|
|||||||
data: () => ({
|
data: () => ({
|
||||||
show: false,
|
show: false,
|
||||||
mode: 'cart',
|
mode: 'cart',
|
||||||
skuKey: 0,
|
selectedSku: {},
|
||||||
|
selectedNum: 1,
|
||||||
currentGoods: emptyGoods
|
currentGoods: emptyGoods
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
@@ -105,9 +150,19 @@ 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.selectedSku = { ...this.initialSku }
|
||||||
|
this.selectedNum = 1
|
||||||
this.show = true
|
this.show = true
|
||||||
},
|
},
|
||||||
|
previewImage(image) {
|
||||||
|
if (image) ImagePreview([image])
|
||||||
|
},
|
||||||
|
selectionData() {
|
||||||
|
return {
|
||||||
|
selectedSkuComb: this.sku.list.find(item => item.s1 === this.selectedSku.s1 && item.s2 === this.selectedSku.s2),
|
||||||
|
selectedNum: Math.min(this.sku.stock_num, Math.max(1, Math.floor(Number(this.selectedNum) || 1)))
|
||||||
|
}
|
||||||
|
},
|
||||||
getCartPayload(data) {
|
getCartPayload(data) {
|
||||||
const selectedSku = data.selectedSkuComb || {}
|
const selectedSku = data.selectedSkuComb || {}
|
||||||
const selectedColor = colorOptions.find(item => item.id === selectedSku.s1) || colorOptions[0]
|
const selectedColor = colorOptions.find(item => item.id === selectedSku.s1) || colorOptions[0]
|
||||||
@@ -153,14 +208,110 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
::v-deep .van-sku-container {
|
.sku-content {
|
||||||
|
padding: 15px 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-options .van-sku-row__item.sku-color-card {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #f6f7f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-color-card :deep(.van-button__content),
|
||||||
|
.sku-color-card :deep(.van-button__text) {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-color-card__picture {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 0;
|
||||||
|
background: #f1f1f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-color-card__image {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-color-card__preview {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(0, 0, 0, .4);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-color-card__name {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 4px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-color-card.van-sku-row__item--active .sku-color-card__name {
|
||||||
|
color: #ff5b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-options,
|
||||||
|
.sku-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-options.sku-options--colors {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-options .van-sku-row__item {
|
||||||
|
height: auto;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-quantity {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-actions {
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-actions .van-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-sku-container {
|
||||||
|
max-height: 85vh;
|
||||||
|
overflow-y: auto;
|
||||||
border-radius: 14px 14px 0 0;
|
border-radius: 14px 14px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.van-sku-container.van-popup--safe-area-inset-bottom {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
::v-deep .van-sku-header {
|
::v-deep .van-sku-header {
|
||||||
min-height: 96px;
|
min-height: 96px;
|
||||||
padding: 18px 42px 12px 16px;
|
padding: 0 30px 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .van-sku-header__img-wrap {
|
::v-deep .van-sku-header__img-wrap {
|
||||||
@@ -210,7 +361,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .van-sku-body {
|
::v-deep .van-sku-body {
|
||||||
padding: 0 16px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .van-sku-group-container {
|
::v-deep .van-sku-group-container {
|
||||||
@@ -236,7 +387,7 @@ export default {
|
|||||||
|
|
||||||
::v-deep .van-sku-row:not(:first-child) .van-sku-row__item {
|
::v-deep .van-sku-row:not(:first-child) .van-sku-row__item {
|
||||||
height: 31px;
|
height: 31px;
|
||||||
margin: 0 10px 8px 0;
|
margin: 0;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<div class="goods-bottom">
|
<div class="goods-bottom">
|
||||||
<div class="price-stack">
|
<div class="price-stack">
|
||||||
<div><b>¥{{ item.price }}</b></div>
|
<div><b>¥{{ item.price }}</b></div>
|
||||||
<div><span>已售{{ item.sales }}</span></div>
|
<div><span>已售{{ formatSales(item.sales) }}件</span></div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="cart-button" @click.stop="openSku(item)">
|
<button type="button" class="cart-button" @click.stop="openSku(item)">
|
||||||
<van-icon name="cart-o" />
|
<van-icon name="cart-o" />
|
||||||
@@ -96,6 +96,11 @@ export default {
|
|||||||
this.loadProducts()
|
this.loadProducts()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatSales(value) {
|
||||||
|
const number = Number(value)
|
||||||
|
const count = Number.isFinite(number) ? Math.max(0, Math.floor(number)) : 0
|
||||||
|
return count >= 10000 ? `${Math.floor(count / 10000)}万` : String(count)
|
||||||
|
},
|
||||||
async loadProducts() {
|
async loadProducts() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
|
src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<b>张三</b>
|
<b>{{ nickname || '-' }}</b>
|
||||||
<p>欢迎来到悦享优选</p>
|
<p>欢迎来到悦享优选</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +103,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useUserStore, ['pointsBalance', 'supplierId', 'userId']),
|
...mapState(useUserStore, ['pointsBalance', 'supplierId', 'userId', 'nickname']),
|
||||||
formattedPoints() {
|
formattedPoints() {
|
||||||
return Number(this.pointsBalance || 0).toLocaleString('zh-CN', {
|
return Number(this.pointsBalance || 0).toLocaleString('zh-CN', {
|
||||||
maximumFractionDigits: 2
|
maximumFractionDigits: 2
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<main v-else class="orders-page">
|
<main v-else class="orders-page">
|
||||||
<van-nav-bar title="我的订单" left-arrow @click-left="$router.replace('/joyful-selection/profile')" />
|
<van-nav-bar title="我的订单" left-arrow @click-left="$router.replace('/joyful-selection/profile')" />
|
||||||
<van-tabs
|
<van-tabs
|
||||||
v-model="active"
|
v-model:active="active"
|
||||||
class="order-tabs"
|
class="order-tabs"
|
||||||
line-width="18"
|
line-width="18"
|
||||||
line-height="2"
|
line-height="2"
|
||||||
@@ -15,6 +15,15 @@
|
|||||||
</van-tabs>
|
</van-tabs>
|
||||||
|
|
||||||
<van-pull-refresh v-model="refreshing" class="order-list" @refresh="onRefresh">
|
<van-pull-refresh v-model="refreshing" class="order-list" @refresh="onRefresh">
|
||||||
|
<van-button
|
||||||
|
v-if="receivableOrders.length"
|
||||||
|
block
|
||||||
|
round
|
||||||
|
class="batch-receive-button"
|
||||||
|
@click="confirmReceiveAll"
|
||||||
|
>
|
||||||
|
一键收货({{ receivableOrders.length }})
|
||||||
|
</van-button>
|
||||||
<van-empty v-if="visibleOrders.length === 0" description="暂无订单" />
|
<van-empty v-if="visibleOrders.length === 0" description="暂无订单" />
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -164,6 +173,11 @@ export default {
|
|||||||
activeTab() {
|
activeTab() {
|
||||||
return this.tabs[this.active] || this.tabs[0]
|
return this.tabs[this.active] || this.tabs[0]
|
||||||
},
|
},
|
||||||
|
receivableOrders() {
|
||||||
|
return this.visibleOrders.filter(order => {
|
||||||
|
return order.displayType === 'sub' && Number(order.orderState) === ORDER_STATE.IN_PROGRESS
|
||||||
|
})
|
||||||
|
},
|
||||||
visibleOrders() {
|
visibleOrders() {
|
||||||
const tab = this.activeTab
|
const tab = this.activeTab
|
||||||
|
|
||||||
@@ -321,6 +335,44 @@ export default {
|
|||||||
this.refreshing = false
|
this.refreshing = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async confirmReceiveAll() {
|
||||||
|
try {
|
||||||
|
await Dialog.confirm({
|
||||||
|
title: '一键收货',
|
||||||
|
message: `确定将当前 ${this.receivableOrders.length} 个订单全部确认收货吗?`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.operationKey = 'batch-confirm-receive'
|
||||||
|
try {
|
||||||
|
await this.fetchBridgeToken()
|
||||||
|
if (!this.supplierId || !this.userId) {
|
||||||
|
Toast('用户信息不完整,请重新进入商城')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(this.receivableOrders.map(order => postFunction('order-push-event', {
|
||||||
|
supplierId: this.supplierId,
|
||||||
|
userId: this.userId,
|
||||||
|
orderType: ORDER_EVENT_TYPE.CONFIRM_RECEIVE,
|
||||||
|
orderId: order.orderId,
|
||||||
|
mainOrderId: order.mainOrderId,
|
||||||
|
subOrderId: order.orderId,
|
||||||
|
amount: Number(order.amount || 0),
|
||||||
|
reason: '确认收货',
|
||||||
|
time: Date.now()
|
||||||
|
})))
|
||||||
|
|
||||||
|
Toast('批量收货成功')
|
||||||
|
await this.loadOrders()
|
||||||
|
} catch (error) {
|
||||||
|
Toast(error.message || '批量收货失败,请稍后再试')
|
||||||
|
} finally {
|
||||||
|
this.operationKey = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
orderActions(order) {
|
orderActions(order) {
|
||||||
if (order.displayType === 'main') {
|
if (order.displayType === 'main') {
|
||||||
return Number(order.orderState) === ORDER_STATE.PENDING_PAY
|
return Number(order.orderState) === ORDER_STATE.PENDING_PAY
|
||||||
@@ -583,6 +635,12 @@ export default {
|
|||||||
padding: 12px 10px 18px;
|
padding: 12px 10px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.batch-receive-button {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #ff5b20;
|
||||||
|
background: #fff1e8;
|
||||||
|
}
|
||||||
|
|
||||||
.order-card {
|
.order-card {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default {
|
|||||||
this.loadLogs()
|
this.loadLogs()
|
||||||
this.startListening()
|
this.startListening()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeUnmount() {
|
||||||
this.stopListening()
|
this.stopListening()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
+107
-42
@@ -1,61 +1,126 @@
|
|||||||
# supplied-service
|
# supplied-service
|
||||||
|
|
||||||
Netlify Functions backend for the supplied mall H5 project.
|
供应商业务后端,为 supplied-h5、supplied-admin 和 zhgh 平台对接提供接口。当前 HTTP 入口为 `src/server.ts`,默认端口为 `8888`。
|
||||||
|
|
||||||
## Stack
|
## 技术栈
|
||||||
|
|
||||||
- Netlify Functions
|
- Koa、TypeScript:HTTP 服务与业务逻辑。
|
||||||
- TypeScript
|
- Prisma、MySQL:数据库访问与模型定义。
|
||||||
- Prisma ORM
|
- Redis:平台桥接 Token 缓存。
|
||||||
- Neon Postgres
|
- MinIO:文件对象存储。
|
||||||
- `@prisma/adapter-neon` + `@neondatabase/serverless`
|
- Node.js crypto:管理端密码哈希与随机登录 Token。
|
||||||
|
|
||||||
## Directory
|
## 目录结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
netlify/
|
src/
|
||||||
functions/ # API entrypoints
|
server.ts # Koa 入口、路由分发、管理端登录校验
|
||||||
utils/ # shared function utilities
|
controllers/ # 请求解析与响应封装
|
||||||
types/ # shared API types
|
services/ # 业务逻辑
|
||||||
|
dao/ # 数据访问
|
||||||
|
functions/ # 导出各接口处理器
|
||||||
|
utils/ # 数据库、日志、平台桥接等工具
|
||||||
|
types/ # 公共类型
|
||||||
prisma/
|
prisma/
|
||||||
schema.prisma # Prisma models
|
schema.prisma # 数据模型与表映射
|
||||||
|
sql/ # 专项表结构脚本
|
||||||
|
script/supplier_db.sql # 数据库结构及开发数据快照
|
||||||
|
init-supplied-user.mjs # 管理账号初始化与旧表处理
|
||||||
|
tools/ # 发票等辅助工具
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment
|
`src/functions` 保留接口处理器导出,当前本地服务由 Koa 的 `/api` 路由分发。
|
||||||
|
|
||||||
Copy `.env.example` to `.env` and set:
|
## 环境配置
|
||||||
|
|
||||||
- `DATABASE_URL`: Neon Postgres connection string for Netlify Functions runtime and Prisma CLI
|
配置文件为本目录 `.env`,可参考 `.env.example`。数据库及存储服务的账号信息不在本文重复列出。
|
||||||
- `JWT_SECRET`: auth signing secret
|
|
||||||
- `UPLOAD_TOKEN`: upload guard token
|
|
||||||
- `POINTS_MALL_OPEN_API_BASE`: main platform bridge base URL, for example `http://localhost/open`
|
|
||||||
|
|
||||||
Supplier H5 uses `VUE_APP_SUPPLIER_IDENTIFICATION` to request supplier config from this service. The supplier `clientSecret` stays in the database and is only used by the Netlify Function when requesting the platform bridge token.
|
| 配置项 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `DATABASE_URL` | MySQL 连接地址 |
|
||||||
|
| `PORT` | Koa 服务端口,默认 `8888` |
|
||||||
|
| `PRISMA_CLIENT_ENGINE_TYPE` | Prisma 引擎设置,示例为 `binary` |
|
||||||
|
| `POINTS_MALL_OPEN_API_BASE` | zhgh 平台接口根地址,例如 `http://localhost/open` |
|
||||||
|
| `SUPPLIER_OPEN_SM4_KEY_HEX`、`SUPPLIER_OPEN_SM4_IV_HEX` | 供应商开放接口 SM4 配置 |
|
||||||
|
| `REDIS_HOST`、`REDIS_PORT`、`REDIS_PASSWORD`、`REDIS_DB` | 平台 Token 缓存连接配置 |
|
||||||
|
| `MINIO_ENDPOINT`、`MINIO_ACCESS_KEY`、`MINIO_SECRET_KEY` | MinIO 地址与访问凭据 |
|
||||||
|
| `MINIO_BUCKET`、`MINIO_REGION` | 存储桶与区域;本项目配置示例使用 `njupt` |
|
||||||
|
|
||||||
## Scripts
|
`.env.example` 中的 `JWT_SECRET`、`UPLOAD_TOKEN` 不参与当前管理端登录校验;管理端采用数据库保存 Token 哈希的会话机制。
|
||||||
|
|
||||||
```bash
|
H5 通过供应商标识获取配置。供应商 `clientSecret` 保存在后端数据库中,用于请求平台桥接 Token,与管理端登录密码、登录 Token 分开处理。
|
||||||
npm run dev
|
|
||||||
npm run build
|
|
||||||
npm run typecheck
|
|
||||||
npm run prisma:generate
|
|
||||||
npm run prisma:migrate
|
|
||||||
npm run prisma:studio
|
|
||||||
```
|
|
||||||
|
|
||||||
## Function Routes
|
## 已有脚本说明
|
||||||
|
|
||||||
Netlify exposes functions under `/.netlify/functions/<name>`.
|
以下列出 `package.json` 中已有脚本的用途。
|
||||||
|
|
||||||
Current placeholders:
|
| 脚本名 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `dev`、`start` | 通过 tsx 运行 Koa 服务 |
|
||||||
|
| `build`、`prisma:generate` | 生成 Prisma Client;`build` 不生成后端应用打包产物 |
|
||||||
|
| `typecheck` | TypeScript 静态类型检查 |
|
||||||
|
| `prisma:migrate` | Prisma 开发迁移 |
|
||||||
|
| `prisma:studio` | Prisma 数据查看界面 |
|
||||||
|
| `seed:products` | 商品初始化数据脚本 |
|
||||||
|
|
||||||
- `login`
|
## 管理账号与表结构
|
||||||
- `product-list`
|
|
||||||
- `product-detail`
|
| 表名 | 用途 |
|
||||||
- `cart-add`
|
| --- | --- |
|
||||||
- `order-create`
|
| `supplied_user` | 管理端账号、密码哈希、会话及失败登录状态 |
|
||||||
- `order-pay`
|
| `supplied_cart_item` | 购物车条目,Prisma 模型为 `CartItem` |
|
||||||
- `user-info`
|
|
||||||
- `upload`
|
管理端密码使用随机盐和 scrypt 哈希,格式为 `scrypt$盐$哈希`。登录时校验密码后签发随机 Token,数据库仅保存 Token 的 SHA-256 哈希及有效期。
|
||||||
- `supplier-info`
|
|
||||||
- `supplier-token`
|
- 会话有效期:8 小时,同一账号仅保留最新一次登录。
|
||||||
|
- 连续输错密码:启用账号累计 5 次后锁定 15 分钟。
|
||||||
|
- 退出登录:清除当前 Token 哈希与有效期。
|
||||||
|
- 登录请求密码不会写入供应商调试日志。
|
||||||
|
- 购物车 `userId` 仍为业务用户 ID,不关联 `supplied_user` 中的管理员。
|
||||||
|
|
||||||
|
### 账号初始化脚本
|
||||||
|
|
||||||
|
`prisma/init-supplied-user.mjs` 读取本目录 `.env`,创建账号表并插入管理账号:
|
||||||
|
|
||||||
|
- `SUPPLIED_ADMIN_USERNAME`:初始化账号,默认 `admin`。
|
||||||
|
- `SUPPLIED_ADMIN_PASSWORD`:初始化密码;未指定时随机生成,长度要求为 12–128 位。
|
||||||
|
- 账号创建成功后输出账号和密码,已存在账号不会覆盖密码。
|
||||||
|
- 可将旧 `cartitem` 重命名为 `supplied_cart_item`;新旧表同时存在时停止,避免覆盖数据。
|
||||||
|
- 旧 `user` 表有数据时停止;为空时移除。表结构处理发生在账号是否已存在的判断之前。
|
||||||
|
- 此脚本不会创建缺失的购物车表,也不处理曾使用过的 `supplied_cartitem` 名称。
|
||||||
|
|
||||||
|
当前开发账号见 [项目 README](../README.md#管理端登录)。
|
||||||
|
|
||||||
|
## 接口入口
|
||||||
|
|
||||||
|
| 路径 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `/api/:name` | H5 及通用业务接口 |
|
||||||
|
| `/api/admin/:name` | 管理端入口,除 `login` 外校验 Bearer Token |
|
||||||
|
| `/supplierOpenApi/pointOrder/*` | zhgh 调用的供应商开放接口 |
|
||||||
|
| `/api/supplier-logs` | 查询或清除当前进程保存的调试日志 |
|
||||||
|
|
||||||
|
### 管理端登录接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 请求内容 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| POST | `/api/admin/login` | JSON:`username`、`password` |
|
||||||
|
| POST | `/api/admin/admin-session` | Bearer Token,返回用户信息及过期时间 |
|
||||||
|
| POST | `/api/admin/admin-logout` | Bearer Token,使当前登录失效 |
|
||||||
|
|
||||||
|
登录成功返回 `data.token`、`data.expiresAt` 和 `data.user`。管理端后续请求通过 `Authorization: Bearer <token>` 携带凭据。
|
||||||
|
|
||||||
|
### 业务接口分组
|
||||||
|
|
||||||
|
- 商品:`product-list`、`product-detail`、`merchant-product-*`。
|
||||||
|
- 订单:`order-create`、`order-pay`、`order-list`、`order-detail`、取消及事件推送接口。
|
||||||
|
- 商家处理:`merchant-order-*`、`merchant-after-sale-*`。
|
||||||
|
- 对账:`merchant-reconciliation-*`、`supplier-reconciliation-*`。
|
||||||
|
- 发票:`supplier-invoice-apply`、`supplier-invoice-info`、`supplier-file-base64-info`。
|
||||||
|
- 平台授权:`supplier-info`、`supplier-session`、`supplier-token`。
|
||||||
|
|
||||||
|
完整路由以 `src/server.ts` 为准。`cart-add`、`user-info` 仍为占位实现。
|
||||||
|
|
||||||
|
## 调试日志
|
||||||
|
|
||||||
|
`GET /api/supplier-logs` 返回当前后端接收请求、向 zhgh 发送请求、接收 zhgh 请求的记录;`DELETE /api/supplier-logs` 清除记录。日志保存在进程内存中,最多保留 200 条,服务重启后清空。
|
||||||
|
|||||||
Generated
+334
@@ -17,6 +17,7 @@
|
|||||||
"ioredis": "^5.4.2",
|
"ioredis": "^5.4.2",
|
||||||
"koa": "^2.15.3",
|
"koa": "^2.15.3",
|
||||||
"koa-bodyparser": "^4.4.1",
|
"koa-bodyparser": "^4.4.1",
|
||||||
|
"minio": "^8.0.5",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -510,6 +511,18 @@
|
|||||||
"node": ">=19.0.0"
|
"node": ">=19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nodable/entities": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@prisma/adapter-neon": {
|
"node_modules/@prisma/adapter-neon": {
|
||||||
"version": "6.19.3",
|
"version": "6.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/adapter-neon/-/adapter-neon-6.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/adapter-neon/-/adapter-neon-6.19.3.tgz",
|
||||||
@@ -840,6 +853,48 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/anynum": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/block-stream2": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browser-or-node": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/buffer-crc32": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -1074,6 +1129,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decode-uri-component": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deep-equal": {
|
"node_modules/deep-equal": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
||||||
@@ -1278,6 +1342,12 @@
|
|||||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz",
|
||||||
@@ -1308,6 +1378,54 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-xml-builder": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-expression-matcher": "^1.6.2",
|
||||||
|
"xml-naming": "^0.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-xml-parser": {
|
||||||
|
"version": "5.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.1.tgz",
|
||||||
|
"integrity": "sha512-TBw6K/fxoQGGjCmZDw9w/ZwP3uDcnTM4YH/g+PFRWr8sbe5idXtxNN6vITh4+1ruCZaho6uBFurElsA7F0zzgw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nodable/entities": "^3.0.0",
|
||||||
|
"fast-xml-builder": "^1.2.0",
|
||||||
|
"is-unsafe": "^2.0.0",
|
||||||
|
"path-expression-matcher": "^1.6.2",
|
||||||
|
"strnum": "^2.4.2",
|
||||||
|
"xml-naming": "^0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"fxparser": "src/cli/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/filter-obj": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
@@ -1572,6 +1690,15 @@
|
|||||||
"url": "https://opencollective.com/ioredis"
|
"url": "https://opencollective.com/ioredis"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-generator-function": {
|
"node_modules/is-generator-function": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
|
||||||
@@ -1609,6 +1736,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-unsafe": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||||
@@ -1732,6 +1871,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -1771,6 +1916,30 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minio": {
|
||||||
|
"version": "8.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/minio/-/minio-8.0.7.tgz",
|
||||||
|
"integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^3.2.4",
|
||||||
|
"block-stream2": "^2.1.0",
|
||||||
|
"browser-or-node": "^2.1.1",
|
||||||
|
"buffer-crc32": "^1.0.0",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"fast-xml-parser": "^5.3.4",
|
||||||
|
"ipaddr.js": "^2.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"query-string": "^7.1.3",
|
||||||
|
"stream-json": "^1.8.0",
|
||||||
|
"through2": "^4.0.2",
|
||||||
|
"xml2js": "^0.5.0 || ^0.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16 || ^18 || >=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -1863,6 +2032,21 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-expression-matcher": {
|
||||||
|
"version": "1.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz",
|
||||||
|
"integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
||||||
@@ -1963,6 +2147,24 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/query-string": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decode-uri-component": "^0.2.2",
|
||||||
|
"filter-obj": "^1.1.0",
|
||||||
|
"split-on-first": "^1.0.0",
|
||||||
|
"strict-uri-encode": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/raw-body": {
|
"node_modules/raw-body": {
|
||||||
"version": "2.5.3",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||||
@@ -1989,6 +2191,20 @@
|
|||||||
"destr": "^2.0.3"
|
"destr": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -2067,6 +2283,15 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sax": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
|
||||||
|
"integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
@@ -2145,6 +2370,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split-on-first": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/standard-as-callback": {
|
"node_modules/standard-as-callback": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||||
@@ -2160,6 +2394,63 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stream-chain": {
|
||||||
|
"version": "2.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
|
||||||
|
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/stream-json": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"stream-chain": "^2.2.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strict-uri-encode": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strnum": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anynum": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/through2": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyexec": {
|
"node_modules/tinyexec": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
||||||
@@ -2250,6 +2541,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vary": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
@@ -2280,6 +2577,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xml-naming": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xml2js": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sax": ">=0.6.0",
|
||||||
|
"xmlbuilder": "~11.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlbuilder": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ylru": {
|
"node_modules/ylru": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@neondatabase/serverless": "^1.0.1",
|
"@neondatabase/serverless": "^1.0.1",
|
||||||
"@prisma/adapter-neon": "6.19.3",
|
"@prisma/adapter-neon": "6.19.3",
|
||||||
"@prisma/client": "6.19.3",
|
"@prisma/client": "6.19.3",
|
||||||
|
"minio": "^8.0.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"ioredis": "^5.4.2",
|
"ioredis": "^5.4.2",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { config } from 'dotenv'
|
||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import { randomBytes, scryptSync } from 'node:crypto'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
config({ path: fileURLToPath(new URL('../.env', import.meta.url)), quiet: true })
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
try {
|
||||||
|
const username = (process.env.SUPPLIED_ADMIN_USERNAME || 'admin').trim()
|
||||||
|
const password = process.env.SUPPLIED_ADMIN_PASSWORD || `Sp!${randomBytes(12).toString('base64url')}9a`
|
||||||
|
if (!username || username.length > 64 || password.length < 12 || password.length > 128) {
|
||||||
|
throw new Error('账号长度需为 1-64,密码长度需为 12-128')
|
||||||
|
}
|
||||||
|
const tables = await prisma.$queryRaw`SELECT TABLE_NAME AS name FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND LOWER(TABLE_NAME) IN ('user', 'cartitem', 'supplied_cart_item')`
|
||||||
|
const legacyUser = tables.find(row => row.name.toLowerCase() === 'user')
|
||||||
|
const legacyCart = tables.find(row => row.name.toLowerCase() === 'cartitem')
|
||||||
|
if (legacyUser) {
|
||||||
|
const rows = await prisma.$queryRawUnsafe('SELECT COUNT(*) AS count FROM `' + legacyUser.name.replace(/`/g, '``') + '`')
|
||||||
|
if (Number(rows[0].count) > 0) throw new Error('旧 user 表存在数据,未删除,请先处理原有数据')
|
||||||
|
}
|
||||||
|
if (legacyCart) {
|
||||||
|
if (tables.some(row => row.name.toLowerCase() === 'supplied_cart_item')) throw new Error('新旧购物车表同时存在,未覆盖数据')
|
||||||
|
await prisma.$executeRawUnsafe('RENAME TABLE `' + legacyCart.name.replace(/`/g, '``') + '` TO `supplied_cart_item`')
|
||||||
|
}
|
||||||
|
if (legacyUser) await prisma.$executeRawUnsafe('DROP TABLE `' + legacyUser.name.replace(/`/g, '``') + '`')
|
||||||
|
await prisma.$executeRawUnsafe(readFileSync(new URL('./sql/supplied-user.sql', import.meta.url), 'utf8').trim().replace(/;$/, ''))
|
||||||
|
const existing = await prisma.$queryRaw`SELECT id FROM supplied_user WHERE username = ${username} LIMIT 1`
|
||||||
|
if (existing.length) throw new Error('账号已存在,未覆盖原有密码')
|
||||||
|
const salt = randomBytes(16).toString('hex')
|
||||||
|
const hash = scryptSync(password, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }).toString('hex')
|
||||||
|
const passwordHash = `scrypt$${salt}$${hash}`
|
||||||
|
await prisma.$executeRaw`INSERT INTO supplied_user (username, passwordHash, nickname) VALUES (${username}, ${passwordHash}, ${'管理员'})`
|
||||||
|
console.log(JSON.stringify({ username, password, table: 'supplied_user' }))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error instanceof Error ? error.message : '初始化账号失败')
|
||||||
|
process.exitCode = 1
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
}
|
||||||
@@ -7,16 +7,21 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model SuppliedUser {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
mobile String @unique
|
username String? @unique @db.VarChar(64)
|
||||||
nickname String?
|
virtualId String? @unique @db.VarChar(191)
|
||||||
avatarUrl String?
|
passwordHash String? @db.VarChar(255)
|
||||||
points Int @default(0)
|
nickname String @default("管理员") @db.VarChar(64)
|
||||||
|
enabled Boolean @default(true)
|
||||||
|
tokenHash String? @unique @db.Char(64)
|
||||||
|
tokenExpiresAt DateTime?
|
||||||
|
failedLoginCount Int @default(0)
|
||||||
|
lockedUntil DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
carts CartItem[]
|
@@map("supplied_user")
|
||||||
}
|
}
|
||||||
|
|
||||||
model SupplierProduct {
|
model SupplierProduct {
|
||||||
@@ -75,10 +80,10 @@ model CartItem {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
product SupplierProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
|
product SupplierProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([userId, productId, skuText])
|
@@unique([userId, productId, skuText], map: "CartItem_userId_productId_skuText_key")
|
||||||
|
@@map("supplied_cart_item")
|
||||||
}
|
}
|
||||||
|
|
||||||
model SupplierOrderMain {
|
model SupplierOrderMain {
|
||||||
@@ -159,6 +164,37 @@ model SupplierOrderSub {
|
|||||||
@@map("supplier_order_sub")
|
@@map("supplier_order_sub")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model SupplierReconciliationOrder {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
supplierId String
|
||||||
|
orderId String
|
||||||
|
mainOrderId String?
|
||||||
|
userId String?
|
||||||
|
reconciliationStatus Int @default(0)
|
||||||
|
diffType String? @db.VarChar(64)
|
||||||
|
diffReason String? @db.VarChar(1000)
|
||||||
|
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 {
|
model SupplierOrderProduct {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
subOrderId String
|
subOrderId String
|
||||||
@@ -342,6 +378,23 @@ model SupplierAfterSale {
|
|||||||
@@map("supplier_after_sale")
|
@@map("supplier_after_sale")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model SupplierFile {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
fileId String @unique
|
||||||
|
supplierId String?
|
||||||
|
fileName String
|
||||||
|
objectName String
|
||||||
|
bucketName String
|
||||||
|
contentType String?
|
||||||
|
fileSize Int?
|
||||||
|
createTime DateTime @default(now())
|
||||||
|
updateTime DateTime @updatedAt
|
||||||
|
deleted Int @default(0)
|
||||||
|
|
||||||
|
@@index([supplierId, deleted])
|
||||||
|
@@map("supplier_file")
|
||||||
|
}
|
||||||
|
|
||||||
model SupplierInvoiceInfo {
|
model SupplierInvoiceInfo {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
invoiceBizId String @unique
|
invoiceBizId String @unique
|
||||||
@@ -351,11 +404,21 @@ model SupplierInvoiceInfo {
|
|||||||
subOrderId String?
|
subOrderId String?
|
||||||
settlementId String?
|
settlementId String?
|
||||||
invoiceCode String?
|
invoiceCode String?
|
||||||
|
taxIdNumber String?
|
||||||
invoiceIds String? @db.VarChar(1000)
|
invoiceIds String? @db.VarChar(1000)
|
||||||
orderIds String? @db.VarChar(1000)
|
orderIds String? @db.VarChar(1000)
|
||||||
invoiceStatus Int @default(0)
|
invoiceStatus Int @default(0)
|
||||||
|
invoiceDate DateTime?
|
||||||
|
invoiceType Int?
|
||||||
|
invoiceNakeAmount Decimal? @db.Decimal(12, 2)
|
||||||
|
invoiceTaxRate Decimal? @db.Decimal(8, 4)
|
||||||
|
invoiceTaxAmount Decimal? @db.Decimal(12, 2)
|
||||||
|
invoiceAmount Decimal? @db.Decimal(12, 2)
|
||||||
|
checkCode String?
|
||||||
|
fileType String? @db.VarChar(16)
|
||||||
fileId String?
|
fileId String?
|
||||||
filePath String?
|
filePath String?
|
||||||
|
failureMessage String? @db.Text
|
||||||
requestBody Json?
|
requestBody Json?
|
||||||
createTime DateTime @default(now())
|
createTime DateTime @default(now())
|
||||||
updateTime DateTime @updatedAt
|
updateTime DateTime @updatedAt
|
||||||
|
|||||||
@@ -0,0 +1,550 @@
|
|||||||
|
/*
|
||||||
|
Navicat Premium Dump SQL
|
||||||
|
|
||||||
|
Source Server : localhost
|
||||||
|
Source Server Type : MySQL
|
||||||
|
Source Server Version : 80046 (8.0.46)
|
||||||
|
Source Host : 127.0.0.1:10086
|
||||||
|
Source Schema : supplier_db
|
||||||
|
|
||||||
|
Target Server Type : MySQL
|
||||||
|
Target Server Version : 80046 (8.0.46)
|
||||||
|
File Encoding : 65001
|
||||||
|
|
||||||
|
Date: 02/09/2026 11:08:42
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplied_cart_item
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplied_cart_item`;
|
||||||
|
CREATE TABLE `supplied_cart_item` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`userId` int NOT NULL,
|
||||||
|
`productId` int NOT NULL,
|
||||||
|
`skuText` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
|
||||||
|
`quantity` int NOT NULL DEFAULT 1,
|
||||||
|
`createdAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` datetime(3) NOT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `CartItem_userId_productId_skuText_key`(`userId` ASC, `productId` ASC, `skuText` ASC) USING BTREE,
|
||||||
|
INDEX `CartItem_productId_fkey`(`productId` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplied_cart_item
|
||||||
|
-- ----------------------------
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_after_sale
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_after_sale`;
|
||||||
|
CREATE TABLE `supplier_after_sale` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '供应商售后单主键ID',
|
||||||
|
`serviceOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '售后业务单号',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户ID',
|
||||||
|
`mainOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联主单ID',
|
||||||
|
`subOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联子单ID',
|
||||||
|
`serviceType` int NOT NULL COMMENT '售后类型:4退货,5换货',
|
||||||
|
`serviceState` int NOT NULL DEFAULT 0 COMMENT '售后状态:0商家待审核,1审核通过,2审核拒绝,3用户已寄回,4商家已收货,5已完成,6已取消',
|
||||||
|
`auditStatus` int NOT NULL DEFAULT 0 COMMENT '审核状态:0待审核,1审核通过,2审核拒绝',
|
||||||
|
`logisticsState` int NOT NULL DEFAULT 0 COMMENT '物流状态:0无物流,1待用户寄回,2用户已寄回,3商家已收货,4商家已发货,5用户已收货',
|
||||||
|
`amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '售后金额',
|
||||||
|
`reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '售后原因',
|
||||||
|
`auditRemark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '审核备注',
|
||||||
|
`userLogisticsNo` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户寄回物流单号',
|
||||||
|
`userLogisticsCompany` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户寄回物流公司',
|
||||||
|
`merchantLogisticsNo` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商家换货物流单号',
|
||||||
|
`merchantLogisticsCompany` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商家换货物流公司',
|
||||||
|
`applyTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '申请时间',
|
||||||
|
`auditTime` datetime(3) NULL DEFAULT NULL COMMENT '审核时间',
|
||||||
|
`userShipTime` datetime(3) NULL DEFAULT NULL COMMENT '用户寄回时间',
|
||||||
|
`merchantReceiveTime` datetime(3) NULL DEFAULT NULL COMMENT '商家收货时间',
|
||||||
|
`merchantShipTime` datetime(3) NULL DEFAULT NULL COMMENT '商家发货时间',
|
||||||
|
`finishTime` datetime(3) NULL DEFAULT NULL COMMENT '完成时间',
|
||||||
|
`requestBody` json NULL COMMENT '原始业务请求体',
|
||||||
|
`createTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updateTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` int NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||||
|
`reasonType` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
|
||||||
|
`problemDescription` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
|
||||||
|
`pickupMethod` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
|
||||||
|
`pickupName` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
|
||||||
|
`pickupMobile` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
|
||||||
|
`pickupAddress` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
|
||||||
|
`images` json NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_after_sale_serviceOrderId_key`(`serviceOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_after_sale_supplierId_mainOrderId_idx`(`supplierId` ASC, `mainOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_after_sale_supplierId_userId_idx`(`supplierId` ASC, `userId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_after_sale_subOrderId_idx`(`subOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_after_sale_serviceType_serviceState_idx`(`serviceType` ASC, `serviceState` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_after_sale_auditStatus_logisticsState_idx`(`auditStatus` ASC, `logisticsState` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商退款售后流程表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_after_sale
|
||||||
|
-- ----------------------------
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_info
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_info`;
|
||||||
|
CREATE TABLE `supplier_info` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`supplierIdentification` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商唯一标识',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`supplierName` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商名称',
|
||||||
|
`clientId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '平台分配的客户端ID',
|
||||||
|
`clientSecret` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '平台分配的客户端密钥',
|
||||||
|
`supplierApiUrl` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '供应商开放接口地址',
|
||||||
|
`sroNo` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'BAB7服务商编号',
|
||||||
|
`createTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updateTime` datetime(3) NOT NULL COMMENT '更新时间',
|
||||||
|
`creator` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
|
||||||
|
`updator` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
|
||||||
|
`version` int NOT NULL DEFAULT 0 COMMENT '版本号',
|
||||||
|
`deleted` int NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_info_supplierIdentification_key`(`supplierIdentification` ASC) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_info_supplierId_key`(`supplierId` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商信息表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_info
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `supplier_info` VALUES (1, 'joyful-selection', '99999999', '悦享优选', 'pmall_5f8c2d91a7b34e3db1c9f4e6a8d2b7c1', 'pmsec_8d7f4c1a9b3e6f2d5c8a1e7b9f4d6c2a3b8e1f5d9c7a6b4e2f1a8c3d6b9e7f', NULL, NULL, '2026-07-28 15:16:35.000', '2026-07-28 15:16:35.000', 'system', 'system', 0, 0);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_invoice_info
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_invoice_info`;
|
||||||
|
CREATE TABLE `supplier_invoice_info` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '发票信息主键ID',
|
||||||
|
`invoiceBizId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发票业务唯一ID,优先使用发票号或事件订单号',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户ID',
|
||||||
|
`mainOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联主单ID',
|
||||||
|
`subOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联子单ID',
|
||||||
|
`settlementId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '结算单号',
|
||||||
|
`invoiceCode` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发票代码或纳税识别号',
|
||||||
|
`invoiceIds` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发票号码列表,多个用英文逗号分隔',
|
||||||
|
`orderIds` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联订单号列表,多个用英文逗号分隔',
|
||||||
|
`invoiceStatus` int NOT NULL DEFAULT 0 COMMENT '发票状态:0已记录,1已申请,2已开票,3失败',
|
||||||
|
`fileId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发票文件ID',
|
||||||
|
`filePath` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发票文件路径',
|
||||||
|
`requestBody` json NULL COMMENT '原始业务请求体',
|
||||||
|
`createTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updateTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` int NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_invoice_info_invoiceBizId_key`(`invoiceBizId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_invoice_info_supplierId_mainOrderId_idx`(`supplierId` ASC, `mainOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_invoice_info_settlementId_idx`(`settlementId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_invoice_info_invoiceCode_idx`(`invoiceCode` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商发票信息表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_invoice_info
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `supplier_invoice_info` VALUES (1, 'INV_ORD20260729134300_001', '99999999', '17a7f8ad3ee947b4a26175049a9c253d', 'ORD20260729134300', 'ORD20260729134300_001', NULL, NULL, 'INV_ORD20260729134300_001', 'ORD20260729134300_001', 2, NULL, NULL, '{\"time\": 1785303898394, \"amount\": 20.02, \"reason\": \"发票信息\", \"userId\": \"17a7f8ad3ee947b4a26175049a9c253d\", \"orderId\": \"ORD20260729134300_001\", \"orderIds\": \"ORD20260729134300_001\", \"orderType\": 11, \"invoiceIds\": \"INV_ORD20260729134300_001\", \"subOrderId\": \"ORD20260729134300_001\", \"supplierId\": \"99999999\", \"mainOrderId\": \"ORD20260729134300\", \"invoiceBizId\": \"INV_ORD20260729134300_001\"}', '2026-07-29 05:45:01.657', '2026-07-29 05:45:01.657', 0);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_order_main
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_order_main`;
|
||||||
|
CREATE TABLE `supplier_order_main` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`orderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主订单ID',
|
||||||
|
`orderNumberId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主订单编号',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '平台用户ID',
|
||||||
|
`aplName` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '应用名称',
|
||||||
|
`addressId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收货地址ID',
|
||||||
|
`remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '订单备注',
|
||||||
|
`createTime` datetime(3) NULL DEFAULT NULL COMMENT '订单创建时间',
|
||||||
|
`updateTime` datetime(3) NULL DEFAULT NULL COMMENT '订单更新时间',
|
||||||
|
`deliveryTime` datetime(3) NULL DEFAULT NULL COMMENT '发货时间',
|
||||||
|
`orderFinishTime` datetime(3) NULL DEFAULT NULL COMMENT '订单完成时间',
|
||||||
|
`completeTime` datetime(3) NULL DEFAULT NULL COMMENT '交易完成时间',
|
||||||
|
`deliveryStatus` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '配送状态',
|
||||||
|
`freightCost` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '运费积分金额',
|
||||||
|
`wPayOrAPayFreightCost` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付运费金额',
|
||||||
|
`refund` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分退款金额',
|
||||||
|
`wPayOrAPayRefund` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝退款金额',
|
||||||
|
`logisticsOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '物流单号',
|
||||||
|
`crrgBsnNm` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '承运商名称',
|
||||||
|
`orderState` int NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1待发货,2待收货,3已取消,4已完成兼容旧值,5已完成,6换货,7退货,8废除',
|
||||||
|
`isSplit` tinyint NOT NULL DEFAULT 0 COMMENT '主单是否拆单:0未拆单,1已拆单',
|
||||||
|
`splitTime` datetime(3) NULL DEFAULT NULL COMMENT '主单拆单时间',
|
||||||
|
`totalPrice` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额',
|
||||||
|
`wPayOrAPay` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付金额',
|
||||||
|
`pointsPrice` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分支付金额',
|
||||||
|
`invoiceCode` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 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已删除',
|
||||||
|
`payTime` datetime NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_order_main_orderId_key`(`orderId` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商主订单表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_order_main
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `supplier_order_main` VALUES (1, 'ORD20260901165221', 'TR20260901165221752', '99999999', 'c281e124321c8db3a14f0a7c86046c3c', '积分商城', 'addr-1788252731181', '', '2026-09-01 08:52:21.752', '2026-09-01 08:55:56.487', '2026-09-01 08:55:23.144', NULL, '2026-09-01 08:55:56.487', '1', 0.00, 0.00, 0.00, 0.00, '20260901', '韵达快递', 5, 1, '2026-09-01 08:52:25.891', 10.01, 0.00, 10.01, NULL, '2026-09-01 08:52:21.766', '2026-09-01 08:55:56.526', 0, '2026-09-01 08:52:26');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_order_operate_log
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_order_operate_log`;
|
||||||
|
CREATE TABLE `supplier_order_operate_log` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '订单操作日志主键ID',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户ID',
|
||||||
|
`mainOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主单ID',
|
||||||
|
`subOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '子单ID',
|
||||||
|
`operateType` int NOT NULL COMMENT '操作类型:1创建待支付,2支付成功,3拆单,4取消主单,5取消子单,6取消已支付未拆单整单,7确认收货,8已完成,9退款,10换货,11废除子单,12发票信息',
|
||||||
|
`beforeState` int NULL DEFAULT NULL COMMENT '操作前订单状态',
|
||||||
|
`afterState` int NULL DEFAULT NULL COMMENT '操作后订单状态',
|
||||||
|
`beforeSplit` int NULL DEFAULT NULL COMMENT '操作前拆单状态:0未拆单,1已拆单',
|
||||||
|
`afterSplit` int NULL DEFAULT NULL COMMENT '操作后拆单状态:0未拆单,1已拆单',
|
||||||
|
`remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '操作备注',
|
||||||
|
`operator` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '操作人或系统标识',
|
||||||
|
`createTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`deleted` int NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `supplier_order_operate_log_supplierId_mainOrderId_idx`(`supplierId` ASC, `mainOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_order_operate_log_subOrderId_idx`(`subOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_order_operate_log_operateType_idx`(`operateType` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商订单操作日志表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_order_operate_log
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `supplier_order_operate_log` VALUES (1, '99999999', 'c281e124321c8db3a14f0a7c86046c3c', 'ORD20260901165221', 'ORD20260901165221_001', 7, 1, 2, NULL, NULL, NULL, 'api', '2026-09-01 08:55:23.165', 0);
|
||||||
|
INSERT INTO `supplier_order_operate_log` VALUES (2, '99999999', 'c281e124321c8db3a14f0a7c86046c3c', 'ORD20260901165221', 'ORD20260901165221_001', 8, 2, 5, NULL, NULL, '确认收货', 'system', '2026-09-01 08:55:56.515', 0);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_order_product
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_order_product`;
|
||||||
|
CREATE TABLE `supplier_order_product` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`subOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '所属子订单ID',
|
||||||
|
`sku` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '商品SKU编码',
|
||||||
|
`number` int NOT NULL COMMENT '商品购买数量',
|
||||||
|
`price` decimal(10, 2) NOT NULL COMMENT '商品成交单价',
|
||||||
|
`name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '商品名称快照',
|
||||||
|
`taxRate` decimal(5, 4) NOT NULL DEFAULT 0.1300 COMMENT '商品税率快照',
|
||||||
|
`productImgUrl` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品图片地址',
|
||||||
|
`fileId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '平台文件ID或图片ID',
|
||||||
|
`filePath` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件存储路径',
|
||||||
|
`picInfo` json NULL COMMENT '商品图片列表快照,JSON数组',
|
||||||
|
`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`) USING BTREE,
|
||||||
|
INDEX `supplier_order_product_subOrderId_idx`(`subOrderId` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商子订单商品表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_order_product
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `supplier_order_product` VALUES (1, 'ORD20260901165221_001', '2-cream-standard', 1, 10.01, '手机壳', 0.1300, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1', 'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1', 'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1', '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\"]', '2026-09-01 08:52:21.780', '2026-09-01 08:52:21.780', 0);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_order_push_log
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_order_push_log`;
|
||||||
|
CREATE TABLE `supplier_order_push_log` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '订单推送日志主键ID',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户ID',
|
||||||
|
`mainOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主单ID',
|
||||||
|
`orderId` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '推送订单ID,多个子单用英文逗号分隔',
|
||||||
|
`orderType` int NULL DEFAULT NULL COMMENT '推送事件类型:1取消主单,2拆单子单,3取消子单,7取消已支付未拆单整单',
|
||||||
|
`apiPath` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '推送接口路径',
|
||||||
|
`requestBody` json NULL COMMENT '推送请求体',
|
||||||
|
`responseBody` json NULL COMMENT '推送响应体',
|
||||||
|
`httpStatus` int NULL DEFAULT NULL COMMENT 'HTTP状态码',
|
||||||
|
`resultCode` int NULL DEFAULT NULL COMMENT '远程业务返回码',
|
||||||
|
`pushStatus` int NOT NULL DEFAULT 0 COMMENT '推送状态:0待推送,1推送成功,2推送失败',
|
||||||
|
`retryCount` int NOT NULL DEFAULT 0 COMMENT '重试次数',
|
||||||
|
`errorMessage` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '失败原因',
|
||||||
|
`lastPushTime` datetime(3) NULL DEFAULT NULL COMMENT '最后推送时间',
|
||||||
|
`nextRetryTime` datetime(3) NULL DEFAULT NULL COMMENT '下次重试时间',
|
||||||
|
`createTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updateTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` int NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `supplier_order_push_log_supplierId_mainOrderId_idx`(`supplierId` ASC, `mainOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_order_push_log_orderType_pushStatus_idx`(`orderType` ASC, `pushStatus` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_order_push_log_nextRetryTime_idx`(`nextRetryTime` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商订单远程推送日志表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_order_push_log
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `supplier_order_push_log` VALUES (1, '99999999', 'c281e124321c8db3a14f0a7c86046c3c', 'ORD20260901165221', 'ORD20260901165221_001', 6, '/pointOrder/pushOrderInfoMsg', '{\"time\": 1788252956480, \"userId\": \"c281e124321c8db3a14f0a7c86046c3c\", \"orderId\": \"ORD20260901165221_001\", \"orderType\": 6, \"supplierId\": \"99999999\", \"mainOrderId\": \"ORD20260901165221\"}', '{\"message\": \"成功\", \"resultCode\": 0}', 200, 0, 1, 0, NULL, '2026-09-01 08:55:56.601', NULL, '2026-09-01 08:55:56.534', '2026-09-01 08:55:56.604', 0);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_order_sub
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_order_sub`;
|
||||||
|
CREATE TABLE `supplier_order_sub` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`orderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '子订单ID',
|
||||||
|
`orderNumberId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '子订单编号',
|
||||||
|
`mainOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '所属主订单ID',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`token` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '平台桥接访问令牌快照',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '平台用户ID',
|
||||||
|
`aplName` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '应用名称',
|
||||||
|
`addressId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收货地址ID',
|
||||||
|
`remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '订单备注',
|
||||||
|
`createTime` datetime(3) NULL DEFAULT NULL COMMENT '订单创建时间',
|
||||||
|
`updateTime` datetime(3) NULL DEFAULT NULL COMMENT '订单更新时间',
|
||||||
|
`deliveryTime` datetime(3) NULL DEFAULT NULL COMMENT '发货时间',
|
||||||
|
`orderFinishTime` datetime(3) NULL DEFAULT NULL COMMENT '订单完成时间',
|
||||||
|
`completeTime` datetime(3) NULL DEFAULT NULL COMMENT '交易完成时间',
|
||||||
|
`deliveryStatus` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '配送状态',
|
||||||
|
`freightCost` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '运费积分金额',
|
||||||
|
`wPayOrAPayFreightCost` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付运费金额',
|
||||||
|
`refund` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分退款金额',
|
||||||
|
`wPayOrAPayRefund` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝退款金额',
|
||||||
|
`logisticsOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '物流单号',
|
||||||
|
`crrgBsnNm` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '承运商名称',
|
||||||
|
`orderState` int NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1待发货,2待收货,3已取消,4已完成兼容旧值,5已完成,6换货,7退货,8废除',
|
||||||
|
`totalPrice` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '子订单总金额',
|
||||||
|
`wPayOrAPay` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付金额',
|
||||||
|
`pointsPrice` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分支付金额',
|
||||||
|
`invoiceCode` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 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已删除',
|
||||||
|
`payTime` datetime NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_order_sub_orderId_key`(`orderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_order_sub_mainOrderId_idx`(`mainOrderId` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商子订单表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_order_sub
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `supplier_order_sub` VALUES (1, 'ORD20260901165221_001', 'TR20260901165221752_001', 'ORD20260901165221', '99999999', NULL, 'c281e124321c8db3a14f0a7c86046c3c', '积分商城', 'addr-1788252731181', '', '2026-09-01 08:52:21.752', '2026-09-01 08:55:56.487', '2026-09-01 08:55:23.144', '2026-09-01 08:55:56.487', '2026-09-01 08:55:56.487', '1', 0.00, 0.00, 0.00, 0.00, '20260901', '韵达快递', 5, 10.01, 0.00, 10.01, NULL, '2026-09-01 08:52:21.776', '2026-09-01 08:55:56.509', 0, '2026-09-01 08:52:26');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_points_ledger
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_points_ledger`;
|
||||||
|
CREATE TABLE `supplier_points_ledger` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '积分流水主键ID',
|
||||||
|
`ledgerNo` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '流水号,业务唯一',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户ID',
|
||||||
|
`mainOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联主单ID',
|
||||||
|
`subOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联子单ID',
|
||||||
|
`bizType` int NOT NULL COMMENT '业务类型:1支付消费,2取消返还,3退货退款,4人工调整',
|
||||||
|
`direction` int NOT NULL COMMENT '流水方向:1增加,2扣减',
|
||||||
|
`points` decimal(10, 2) NOT NULL COMMENT '积分变动金额',
|
||||||
|
`beforePoints` decimal(10, 2) NULL DEFAULT NULL COMMENT '变动前积分余额',
|
||||||
|
`afterPoints` decimal(10, 2) NULL DEFAULT NULL COMMENT '变动后积分余额',
|
||||||
|
`remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '流水备注',
|
||||||
|
`createTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updateTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` int NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_points_ledger_ledgerNo_key`(`ledgerNo` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_points_ledger_supplierId_userId_idx`(`supplierId` ASC, `userId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_points_ledger_mainOrderId_idx`(`mainOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_points_ledger_subOrderId_idx`(`subOrderId` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商用户积分消费流水表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_points_ledger
|
||||||
|
-- ----------------------------
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_product
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_product`;
|
||||||
|
CREATE TABLE `supplier_product` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '商品主键ID',
|
||||||
|
`sku` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '商品SKU编码',
|
||||||
|
`number` int NOT NULL DEFAULT 0 COMMENT '默认购买数量或导入数量',
|
||||||
|
`price` decimal(10, 2) NOT NULL COMMENT '商品销售价格',
|
||||||
|
`name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '商品名称',
|
||||||
|
`taxRate` decimal(5, 4) NOT NULL DEFAULT 0.1300 COMMENT '商品税率',
|
||||||
|
`picInfo` json NULL COMMENT '商品图片列表,JSON数组',
|
||||||
|
`description` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品简短描述',
|
||||||
|
`detail` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品详情描述',
|
||||||
|
`originPrice` decimal(10, 2) NULL DEFAULT NULL COMMENT '商品原价',
|
||||||
|
`imageUrl` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品主图地址',
|
||||||
|
`categoryId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品分类ID',
|
||||||
|
`category` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品分类名称',
|
||||||
|
`tag` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品标签',
|
||||||
|
`sales` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '商品销量展示文案',
|
||||||
|
`stock` int NOT NULL DEFAULT 0 COMMENT '商品库存',
|
||||||
|
`sortOrder` int NOT NULL DEFAULT 0 COMMENT '排序值,越小越靠前',
|
||||||
|
`enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||||
|
`deleted` int NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||||
|
`createdAt` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updatedAt` datetime(3) NOT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_product_sku_key`(`sku` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商商品信息表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_product
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `supplier_product` VALUES (1, 'SKU001', 2, 149.99, '蓝牙耳机', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\"]', '轻盈佩戴,稳定连接,通勤运动都好用', '轻盈佩戴,稳定连接,通勤运动都好用。精选品质商品,适合积分商城兑换和日常福利采购场景。', 199.99, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1', 'digitalAppliance', '数码家电', '积分兑换', '1.2万件', 180, 1, 1, 0, '2026-07-28 09:43:03.406', '2026-07-28 09:43:03.406');
|
||||||
|
INSERT INTO `supplier_product` VALUES (2, 'SKU002', 1, 10.01, '手机壳', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\"]', '防滑耐磨,贴合机身,日常保护更安心', '防滑耐磨,贴合机身,日常保护更安心。精选品质商品,适合积分商城兑换和日常福利采购场景。', 19.90, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1', 'digitalAppliance', '数码家电', '赠品', '5632件', 187, 2, 1, 0, '2026-07-28 09:43:03.427', '2026-07-28 09:43:03.427');
|
||||||
|
INSERT INTO `supplier_product` VALUES (3, 'SKU003', 1, 99.00, '智能音箱 Pro', 0.1300, '[\"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\", \"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '语音唤醒,沉浸音效,适合卧室和书房', '语音唤醒,沉浸音效,适合卧室和书房。精选品质商品,适合积分商城兑换和日常福利采购场景。', 139.00, 'https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1', 'digitalAppliance', '数码家电', '热门推荐', '2.1万件', 194, 3, 1, 0, '2026-07-28 09:43:03.431', '2026-07-28 09:43:03.431');
|
||||||
|
INSERT INTO `supplier_product` VALUES (4, 'SKU004', 1, 129.00, '奶油白迷你烤箱', 0.1300, '[\"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\", \"https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '小巧不占地,早餐烘焙好帮手', '小巧不占地,早餐烘焙好帮手。精选品质商品,适合积分商城兑换和日常福利采购场景。', 169.00, 'https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', 'homeLiving', '居家生活', '新品上架', '1.2万件', 201, 4, 1, 0, '2026-07-28 09:43:03.435', '2026-07-28 09:43:03.435');
|
||||||
|
INSERT INTO `supplier_product` VALUES (5, 'SKU005', 1, 69.90, '北欧陶瓷餐具套装', 0.1300, '[\"https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\", \"https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1\"]', '简约釉色,日常用餐更有仪式感', '简约釉色,日常用餐更有仪式感。精选品质商品,适合积分商城兑换和日常福利采购场景。', 99.90, 'https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', 'homeLiving', '居家生活', '品质精选', '6453件', 208, 5, 1, 0, '2026-07-28 09:43:03.438', '2026-07-28 09:43:03.438');
|
||||||
|
INSERT INTO `supplier_product` VALUES (6, 'SKU006', 1, 179.00, '纯棉舒适四件套', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\"]', '柔软亲肤,安心睡眠,四季可用', '柔软亲肤,安心睡眠,四季可用。精选品质商品,适合积分商城兑换和日常福利采购场景。', 229.00, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1', 'homeLiving', '居家生活', '积分兑换', '8321件', 215, 6, 1, 0, '2026-07-28 09:43:03.441', '2026-07-28 09:43:03.441');
|
||||||
|
INSERT INTO `supplier_product` VALUES (7, 'SKU007', 1, 99.90, '植萃保湿护肤礼盒', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\"]', '补水保湿,清爽不黏腻', '补水保湿,清爽不黏腻。精选品质商品,适合积分商城兑换和日常福利采购场景。', 139.90, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1', 'beautyCare', '美妆个护', '礼盒精选', '4820件', 222, 7, 1, 0, '2026-07-28 09:43:03.443', '2026-07-28 09:43:03.443');
|
||||||
|
INSERT INTO `supplier_product` VALUES (8, 'SKU008', 1, 59.90, '香氛洗护旅行套装', 0.1300, '[\"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\", \"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '温和清洁,清新留香,出行便携', '温和清洁,清新留香,出行便携。精选品质商品,适合积分商城兑换和日常福利采购场景。', 89.90, 'https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1', 'beautyCare', '美妆个护', '新品上架', '7932件', 229, 8, 1, 0, '2026-07-28 09:43:03.446', '2026-07-28 09:43:03.446');
|
||||||
|
INSERT INTO `supplier_product` VALUES (9, 'SKU009', 1, 49.90, '便携补光化妆镜', 0.1300, '[\"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\", \"https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '高清镜面,柔光补妆,随身携带', '高清镜面,柔光补妆,随身携带。精选品质商品,适合积分商城兑换和日常福利采购场景。', 79.90, 'https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', 'beautyCare', '美妆个护', '热门推荐', '3651件', 236, 9, 1, 0, '2026-07-28 09:43:03.450', '2026-07-28 09:43:03.450');
|
||||||
|
INSERT INTO `supplier_product` VALUES (10, 'SKU010', 1, 59.90, '橙意零食礼盒', 0.1300, '[\"https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\", \"https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1\"]', '精选零食组合,办公室分享刚刚好', '精选零食组合,办公室分享刚刚好。精选品质商品,适合积分商城兑换和日常福利采购场景。', 89.90, 'https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', 'foodDrink', '食品饮料', '礼盒精选', '6324件', 243, 10, 1, 0, '2026-07-28 09:43:03.453', '2026-07-28 09:43:03.453');
|
||||||
|
INSERT INTO `supplier_product` VALUES (11, 'SKU011', 1, 49.90, '咖啡随行杯组合', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\"]', '锁温防漏,通勤随行更方便', '锁温防漏,通勤随行更方便。精选品质商品,适合积分商城兑换和日常福利采购场景。', 69.90, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1', 'foodDrink', '食品饮料', '积分兑换', '3520件', 250, 11, 1, 0, '2026-07-28 09:43:03.456', '2026-07-28 09:43:03.456');
|
||||||
|
INSERT INTO `supplier_product` VALUES (12, 'SKU012', 1, 69.90, '下午茶点心礼盒', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\"]', '多种风味,一盒满足甜咸口味', '多种风味,一盒满足甜咸口味。精选品质商品,适合积分商城兑换和日常福利采购场景。', 99.90, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1', 'foodDrink', '食品饮料', '品质精选', '4891件', 257, 12, 1, 0, '2026-07-28 09:43:03.459', '2026-07-28 09:43:03.459');
|
||||||
|
INSERT INTO `supplier_product` VALUES (13, 'SKU013', 1, 39.90, '安心儿童水杯', 0.1300, '[\"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\", \"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '轻巧易握,防摔耐用,出门好携带', '轻巧易握,防摔耐用,出门好携带。精选品质商品,适合积分商城兑换和日常福利采购场景。', 59.90, 'https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1', 'motherBaby', '母婴玩具', '新品上架', '4250件', 264, 13, 1, 0, '2026-07-28 09:43:03.462', '2026-07-28 09:43:03.462');
|
||||||
|
INSERT INTO `supplier_product` VALUES (14, 'SKU014', 1, 89.00, '益智积木套装', 0.1300, '[\"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\", \"https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '色彩柔和,锻炼动手能力', '色彩柔和,锻炼动手能力。精选品质商品,适合积分商城兑换和日常福利采购场景。', 129.00, 'https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', 'motherBaby', '母婴玩具', '热门推荐', '2186件', 271, 14, 1, 0, '2026-07-28 09:43:03.465', '2026-07-28 09:43:03.465');
|
||||||
|
INSERT INTO `supplier_product` VALUES (15, 'SKU015', 1, 69.90, '柔光儿童夜灯', 0.1300, '[\"https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\", \"https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1\"]', '温和光线,陪伴睡前时光', '温和光线,陪伴睡前时光。精选品质商品,适合积分商城兑换和日常福利采购场景。', 99.90, 'https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', 'motherBaby', '母婴玩具', '品质精选', '3548件', 278, 15, 1, 0, '2026-07-28 09:43:03.469', '2026-07-28 09:43:03.469');
|
||||||
|
INSERT INTO `supplier_product` VALUES (16, 'SKU016', 1, 149.00, '大容量电脑背包', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\"]', '分区收纳,通勤出行更轻松', '分区收纳,通勤出行更轻松。精选品质商品,适合积分商城兑换和日常福利采购场景。', 199.00, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1', 'fashionBag', '服饰鞋包', '积分兑换', '1.6万件', 285, 16, 1, 0, '2026-07-28 09:43:03.472', '2026-07-28 09:43:03.472');
|
||||||
|
INSERT INTO `supplier_product` VALUES (17, 'SKU017', 1, 79.90, '轻巧通勤斜挎包', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\"]', '简洁百搭,日常出门更实用', '简洁百搭,日常出门更实用。精选品质商品,适合积分商城兑换和日常福利采购场景。', 109.90, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1', 'fashionBag', '服饰鞋包', '新品上架', '5321件', 292, 17, 1, 0, '2026-07-28 09:43:03.475', '2026-07-28 09:43:03.475');
|
||||||
|
INSERT INTO `supplier_product` VALUES (18, 'SKU018', 1, 69.90, '柔软撞色丝巾礼盒', 0.1300, '[\"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\", \"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '细腻亲肤,点亮日常穿搭', '细腻亲肤,点亮日常穿搭。精选品质商品,适合积分商城兑换和日常福利采购场景。', 99.90, 'https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1', 'fashionBag', '服饰鞋包', '礼盒精选', '2680件', 299, 18, 1, 0, '2026-07-28 09:43:03.479', '2026-07-28 09:43:03.479');
|
||||||
|
INSERT INTO `supplier_product` VALUES (19, 'SKU019', 1, 49.90, '运动保温水杯', 0.1300, '[\"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\", \"https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '长效锁温,户外运动随行', '长效锁温,户外运动随行。精选品质商品,适合积分商城兑换和日常福利采购场景。', 79.90, 'https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', 'sportsOutdoor', '运动户外', '热门推荐', '7342件', 306, 19, 1, 0, '2026-07-28 09:43:03.483', '2026-07-28 09:43:03.483');
|
||||||
|
INSERT INTO `supplier_product` VALUES (20, 'SKU020', 1, 99.00, '筋膜放松按摩仪 Mini', 0.1300, '[\"https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\", \"https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1\"]', '轻巧便携,缓解久坐酸胀', '轻巧便携,缓解久坐酸胀。精选品质商品,适合积分商城兑换和日常福利采购场景。', 149.00, 'https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', 'sportsOutdoor', '运动户外', '积分兑换', '3921件', 313, 20, 1, 0, '2026-07-28 09:43:03.486', '2026-07-28 09:43:03.486');
|
||||||
|
INSERT INTO `supplier_product` VALUES (21, 'SKU021', 1, 79.00, '户外露营灯', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\"]', '多档照明,露营夜晚更安心', '多档照明,露营夜晚更安心。精选品质商品,适合积分商城兑换和日常福利采购场景。', 119.00, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1', 'sportsOutdoor', '运动户外', '新品上架', '2568件', 320, 21, 1, 0, '2026-07-28 09:43:03.489', '2026-07-28 09:43:03.489');
|
||||||
|
INSERT INTO `supplier_product` VALUES (22, 'SKU022', 1, 59.90, '悦享精选礼包', 0.1300, '[\"https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1\", \"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\"]', '多场景精选好物,福利兑换优选', '多场景精选好物,福利兑换优选。精选品质商品,适合积分商城兑换和日常福利采购场景。', 89.90, 'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1', 'recommend', '精选推荐', '精选推荐', '8820件', 327, 22, 1, 0, '2026-07-28 09:43:03.492', '2026-07-28 09:43:03.492');
|
||||||
|
INSERT INTO `supplier_product` VALUES (23, 'SKU023', 1, 119.00, '品质生活组合装', 0.1300, '[\"https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1\", \"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '居家、通勤、日用好物组合', '居家、通勤、日用好物组合。精选品质商品,适合积分商城兑换和日常福利采购场景。', 159.00, 'https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1', 'recommend', '精选推荐', '热门推荐', '6902件', 334, 23, 1, 0, '2026-07-28 09:43:03.495', '2026-07-28 09:43:03.495');
|
||||||
|
INSERT INTO `supplier_product` VALUES (24, 'SKU024', 1, 39.90, '积分兑换惊喜盒', 0.1300, '[\"https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\", \"https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3\"]', '随机精选商品,兑换更有新鲜感', '随机精选商品,兑换更有新鲜感。精选品质商品,适合积分商城兑换和日常福利采购场景。', 69.90, 'https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', 'recommend', '精选推荐', '限时秒杀', '5310件', 341, 24, 1, 0, '2026-07-28 09:43:03.498', '2026-07-28 09:43:03.498');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_reconciliation_order
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_reconciliation_order`;
|
||||||
|
CREATE TABLE `supplier_reconciliation_order` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`orderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '子订单ID',
|
||||||
|
`mainOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '主订单ID',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户ID',
|
||||||
|
`reconciliationStatus` tinyint NOT NULL DEFAULT 0 COMMENT '对账状态:0未对账,1对账成功,2对账异常',
|
||||||
|
`diffType` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '差异类型',
|
||||||
|
`diffReason` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '差异说明',
|
||||||
|
`platformTotalPrice` decimal(12, 2) NULL DEFAULT NULL COMMENT '平台订单总金额',
|
||||||
|
`platformCashAmount` decimal(12, 2) NULL DEFAULT NULL COMMENT '平台现金支付金额',
|
||||||
|
`platformPointsPrice` decimal(12, 2) NULL DEFAULT NULL COMMENT '平台积分支付金额',
|
||||||
|
`platformRefundPrice` decimal(12, 2) NULL DEFAULT NULL COMMENT '平台退款金额',
|
||||||
|
`supplierTotalPrice` decimal(12, 2) NULL DEFAULT NULL COMMENT '供应商订单总金额',
|
||||||
|
`supplierCashAmount` decimal(12, 2) NULL DEFAULT NULL COMMENT '供应商现金支付金额',
|
||||||
|
`supplierPointsPrice` decimal(12, 2) NULL DEFAULT NULL COMMENT '供应商积分支付金额',
|
||||||
|
`supplierRefundPrice` decimal(12, 2) NULL DEFAULT NULL COMMENT '供应商退款金额',
|
||||||
|
`reconciliationBatchNo` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '对账批次号',
|
||||||
|
`reconciliationTime` datetime(3) NULL DEFAULT NULL COMMENT '对账完成时间',
|
||||||
|
`resultPayload` json 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`) USING BTREE,
|
||||||
|
UNIQUE INDEX `uk_supplier_order`(`supplierId` ASC, `orderId` ASC) USING BTREE,
|
||||||
|
INDEX `idx_supplier_status`(`supplierId` ASC, `reconciliationStatus` ASC) USING BTREE,
|
||||||
|
INDEX `idx_batch_no`(`reconciliationBatchNo` ASC) USING BTREE,
|
||||||
|
INDEX `idx_main_order`(`mainOrderId` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商子订单对账结果表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_reconciliation_order
|
||||||
|
-- ----------------------------
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_refund_exchange
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_refund_exchange`;
|
||||||
|
CREATE TABLE `supplier_refund_exchange` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '售后记录主键ID',
|
||||||
|
`serviceOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '退货/换货业务单号',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户ID',
|
||||||
|
`mainOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联主单ID',
|
||||||
|
`subOrderId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联子单ID',
|
||||||
|
`serviceType` int NOT NULL COMMENT '售后类型:4退货,5换货',
|
||||||
|
`serviceState` int NOT NULL DEFAULT 0 COMMENT '售后状态:0已创建,1处理中,2已完成,3已取消',
|
||||||
|
`amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '售后金额',
|
||||||
|
`reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '售后原因',
|
||||||
|
`requestBody` json NULL COMMENT '原始业务请求体',
|
||||||
|
`createTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updateTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` int NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_refund_exchange_serviceOrderId_key`(`serviceOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_refund_exchange_supplierId_mainOrderId_idx`(`supplierId` ASC, `mainOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_refund_exchange_subOrderId_idx`(`subOrderId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_refund_exchange_serviceType_serviceState_idx`(`serviceType` ASC, `serviceState` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商退货换货售后记录表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_refund_exchange
|
||||||
|
-- ----------------------------
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for supplier_user_address
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `supplier_user_address`;
|
||||||
|
CREATE TABLE `supplier_user_address` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT COMMENT '地址主键ID',
|
||||||
|
`addressId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '地址业务ID',
|
||||||
|
`supplierId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '供应商ID',
|
||||||
|
`userId` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户ID',
|
||||||
|
`name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '收货人姓名',
|
||||||
|
`tel` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '收货人手机号',
|
||||||
|
`province` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '省',
|
||||||
|
`city` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '市',
|
||||||
|
`county` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '区县',
|
||||||
|
`addressDetail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '详细地址',
|
||||||
|
`areaCode` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地区编码',
|
||||||
|
`postalCode` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮政编码',
|
||||||
|
`isDefault` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否默认地址',
|
||||||
|
`createTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
`updateTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
`deleted` int NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `supplier_user_address_addressId_key`(`addressId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_user_address_supplierId_userId_idx`(`supplierId` ASC, `userId` ASC) USING BTREE,
|
||||||
|
INDEX `supplier_user_address_supplierId_userId_isDefault_idx`(`supplierId` ASC, `userId` ASC, `isDefault` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '供应商用户收货地址表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Records of supplier_user_address
|
||||||
|
-- ----------------------------
|
||||||
|
INSERT INTO `supplier_user_address` VALUES (1, 'addr-1785395646503', '99999999', 'b2397ac9a67f476c9833471d27709db1', '谌盼兮', '13545478393', '吉林省', '白山市', '临江市', '未来科技城', '220681', '430213', 1, '2026-07-30 07:14:06.541', '2026-07-30 07:29:34.829', 0);
|
||||||
|
INSERT INTO `supplier_user_address` VALUES (2, 'addr-1788252731181', '99999999', 'c281e124321c8db3a14f0a7c86046c3c', '谌盼兮', '19090230905', '湖北省', '武汉市', '洪山区', '中国地质大学', '420111', '430074', 1, '2026-09-01 08:52:11.226', '2026-09-01 08:52:11.226', 0);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `supplied_user` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` VARCHAR(64) NULL,
|
||||||
|
`virtualId` VARCHAR(191) COLLATE utf8mb4_bin NULL,
|
||||||
|
`passwordHash` VARCHAR(255) NULL,
|
||||||
|
`nickname` VARCHAR(64) NOT NULL DEFAULT '管理员',
|
||||||
|
`enabled` BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
`tokenHash` CHAR(64) NULL,
|
||||||
|
`tokenExpiresAt` DATETIME(3) NULL,
|
||||||
|
`failedLoginCount` INT NOT NULL DEFAULT 0,
|
||||||
|
`lockedUntil` DATETIME(3) NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `supplied_user_username_key` (`username`),
|
||||||
|
UNIQUE KEY `supplied_user_virtualId_key` (`virtualId`),
|
||||||
|
UNIQUE KEY `supplied_user_tokenHash_key` (`tokenHash`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
@@ -113,3 +113,33 @@ CREATE TABLE IF NOT EXISTS `supplier_order_product` (
|
|||||||
FOREIGN KEY (`subOrderId`) REFERENCES `supplier_order_sub` (`orderId`)
|
FOREIGN KEY (`subOrderId`) REFERENCES `supplier_order_sub` (`orderId`)
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商子订单商品表';
|
) 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='供应商子订单对账结果表';
|
||||||
|
|||||||
@@ -28,12 +28,22 @@ CREATE TABLE IF NOT EXISTS `supplier_invoice_info` (
|
|||||||
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
||||||
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
||||||
`settlementId` VARCHAR(191) NULL COMMENT '结算单号',
|
`settlementId` VARCHAR(191) NULL COMMENT '结算单号',
|
||||||
`invoiceCode` VARCHAR(191) NULL COMMENT '发票代码或纳税识别号',
|
`invoiceCode` VARCHAR(191) NULL COMMENT '发票代码',
|
||||||
|
`taxIdNumber` VARCHAR(191) NULL COMMENT '购买方纳税人识别号',
|
||||||
`invoiceIds` VARCHAR(1000) NULL COMMENT '发票号码列表,多个用英文逗号分隔',
|
`invoiceIds` VARCHAR(1000) NULL COMMENT '发票号码列表,多个用英文逗号分隔',
|
||||||
`orderIds` VARCHAR(1000) NULL COMMENT '关联订单号列表,多个用英文逗号分隔',
|
`orderIds` VARCHAR(1000) NULL COMMENT '关联订单号列表,多个用英文逗号分隔',
|
||||||
`invoiceStatus` INT NOT NULL DEFAULT 0 COMMENT '发票状态:0已记录,1已申请,2已开票,3失败',
|
`invoiceStatus` INT NOT NULL DEFAULT 0 COMMENT '发票状态:0已记录,1已申请,2已开票,3失败',
|
||||||
|
`invoiceDate` DATETIME(3) NULL COMMENT '开票日期',
|
||||||
|
`invoiceType` INT NULL COMMENT '发票类型',
|
||||||
|
`invoiceNakeAmount` DECIMAL(12, 2) NULL COMMENT '不含税金额',
|
||||||
|
`invoiceTaxRate` DECIMAL(8, 4) NULL COMMENT '发票税率',
|
||||||
|
`invoiceTaxAmount` DECIMAL(12, 2) NULL COMMENT '发票税额',
|
||||||
|
`invoiceAmount` DECIMAL(12, 2) NULL COMMENT '价税合计',
|
||||||
|
`checkCode` VARCHAR(191) NULL COMMENT '发票校验码',
|
||||||
|
`fileType` VARCHAR(16) NULL COMMENT '发票文件类型',
|
||||||
`fileId` VARCHAR(191) NULL COMMENT '发票文件ID',
|
`fileId` VARCHAR(191) NULL COMMENT '发票文件ID',
|
||||||
`filePath` VARCHAR(191) NULL COMMENT '发票文件路径',
|
`filePath` VARCHAR(191) NULL COMMENT '发票文件路径',
|
||||||
|
`failureMessage` TEXT NULL COMMENT '开票失败原因',
|
||||||
`requestBody` JSON NULL COMMENT '原始业务请求体',
|
`requestBody` JSON NULL COMMENT '原始业务请求体',
|
||||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE `supplied_user`
|
||||||
|
ADD COLUMN `virtualId` VARCHAR(191) COLLATE utf8mb4_bin NULL AFTER `username`,
|
||||||
|
ADD UNIQUE KEY `supplied_user_virtualId_key` (`virtualId`),
|
||||||
|
MODIFY COLUMN `username` VARCHAR(64) NULL,
|
||||||
|
MODIFY COLUMN `passwordHash` VARCHAR(255) NULL;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `supplied_user` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` VARCHAR(64) NULL,
|
||||||
|
`virtualId` VARCHAR(191) COLLATE utf8mb4_bin NULL,
|
||||||
|
`passwordHash` VARCHAR(255) NULL,
|
||||||
|
`nickname` VARCHAR(64) NOT NULL DEFAULT '管理员',
|
||||||
|
`enabled` BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
`tokenHash` CHAR(64) NULL,
|
||||||
|
`tokenExpiresAt` DATETIME(3) NULL,
|
||||||
|
`failedLoginCount` INT NOT NULL DEFAULT 0,
|
||||||
|
`lockedUntil` DATETIME(3) NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `supplied_user_username_key` (`username`),
|
||||||
|
UNIQUE KEY `supplied_user_virtualId_key` (`virtualId`),
|
||||||
|
UNIQUE KEY `supplied_user_tokenHash_key` (`tokenHash`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE `supplier_file` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`fileId` varchar(191) NOT NULL,
|
||||||
|
`supplierId` varchar(191) DEFAULT NULL,
|
||||||
|
`fileName` varchar(191) NOT NULL,
|
||||||
|
`objectName` varchar(500) NOT NULL,
|
||||||
|
`bucketName` varchar(191) NOT NULL,
|
||||||
|
`contentType` varchar(100) DEFAULT NULL,
|
||||||
|
`fileSize` int DEFAULT NULL,
|
||||||
|
`createTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updateTime` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
`deleted` int NOT NULL DEFAULT '0',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `supplier_file_fileId_key` (`fileId`),
|
||||||
|
KEY `supplier_file_supplierId_deleted_idx` (`supplierId`,`deleted`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='供应商文件存储记录';
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
ALTER TABLE `supplier_invoice_info`
|
||||||
|
MODIFY COLUMN `invoiceCode` VARCHAR(191) NULL COMMENT '发票代码',
|
||||||
|
ADD COLUMN `taxIdNumber` VARCHAR(191) NULL COMMENT '购买方纳税人识别号' AFTER `invoiceCode`,
|
||||||
|
ADD COLUMN `invoiceDate` DATETIME(3) NULL COMMENT '开票日期' AFTER `invoiceStatus`,
|
||||||
|
ADD COLUMN `invoiceType` INT NULL COMMENT '发票类型' AFTER `invoiceDate`,
|
||||||
|
ADD COLUMN `invoiceNakeAmount` DECIMAL(12, 2) NULL COMMENT '不含税金额' AFTER `invoiceType`,
|
||||||
|
ADD COLUMN `invoiceTaxRate` DECIMAL(8, 4) NULL COMMENT '发票税率' AFTER `invoiceNakeAmount`,
|
||||||
|
ADD COLUMN `invoiceTaxAmount` DECIMAL(12, 2) NULL COMMENT '发票税额' AFTER `invoiceTaxRate`,
|
||||||
|
ADD COLUMN `invoiceAmount` DECIMAL(12, 2) NULL COMMENT '价税合计' AFTER `invoiceTaxAmount`,
|
||||||
|
ADD COLUMN `checkCode` VARCHAR(191) NULL COMMENT '发票校验码' AFTER `invoiceAmount`,
|
||||||
|
ADD COLUMN `fileType` VARCHAR(16) NULL COMMENT '发票文件类型' AFTER `checkCode`,
|
||||||
|
ADD COLUMN `failureMessage` TEXT NULL COMMENT '开票失败原因' AFTER `filePath`;
|
||||||
@@ -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 ='供应商子订单对账结果表';
|
||||||
@@ -3,14 +3,27 @@ import { guardMethod } from '../utils/method.js'
|
|||||||
import { withErrorBoundary } from '../utils/handler.js'
|
import { withErrorBoundary } from '../utils/handler.js'
|
||||||
import { basicService } from '../services/basic.service.js'
|
import { basicService } from '../services/basic.service.js'
|
||||||
import { minioUploadService, type PreviewInput, type UploadInput } from '../services/minio-upload.service.js'
|
import { minioUploadService, type PreviewInput, type UploadInput } from '../services/minio-upload.service.js'
|
||||||
import { readJson } from '../utils/request.js'
|
import { getBearerToken, readJson } from '../utils/request.js'
|
||||||
|
import { adminAuthService, type AdminLoginInput } from '../services/admin-auth.service.js'
|
||||||
import { toHttpResponse } from './controller-response.js'
|
import { toHttpResponse } from './controller-response.js'
|
||||||
|
|
||||||
export const loginController = withErrorBoundary(async (request: Request, _context: Context) => {
|
export const loginController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
const methodResponse = guardMethod(request, ['POST'])
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
if (methodResponse) return methodResponse
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
return toHttpResponse(basicService.login())
|
return toHttpResponse(await adminAuthService.login(await readJson<AdminLoginInput>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const adminSessionController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
return toHttpResponse(await adminAuthService.session(getBearerToken(request)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const adminLogoutController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
return toHttpResponse(await adminAuthService.logout(getBearerToken(request)))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const cartAddController = withErrorBoundary(async (request: Request, _context: Context) => {
|
export const cartAddController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
@@ -27,7 +40,19 @@ export const uploadController = withErrorBoundary(async (request: Request, _cont
|
|||||||
return toHttpResponse(await minioUploadService.upload(await readJson<UploadInput>(request)))
|
return toHttpResponse(await minioUploadService.upload(await readJson<UploadInput>(request)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const fileController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['GET'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
const result = await minioUploadService.imageUrl(new URL(request.url).searchParams.get('fid')?.trim() || '')
|
||||||
|
if (!result.success) return toHttpResponse(result)
|
||||||
|
return new Response(null, { status: 302, headers: { Location: result.data.url, 'Cache-Control': 'no-store' } })
|
||||||
|
})
|
||||||
|
|
||||||
export const uploadPreviewController = withErrorBoundary(async (request: Request, _context: Context) => {
|
export const uploadPreviewController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
if (request.method === 'GET') {
|
||||||
|
const fid = new URL(request.url).searchParams.get('fid') || ''
|
||||||
|
return new Response(null, { status: 302, headers: { Location: `/api/file?fid=${encodeURIComponent(fid)}`, 'Cache-Control': 'no-store' } })
|
||||||
|
}
|
||||||
const methodResponse = guardMethod(request, ['POST'])
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
if (methodResponse) return methodResponse
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { withErrorBoundary } from '../utils/handler.js'
|
||||||
|
import { guardMethod } from '../utils/method.js'
|
||||||
|
import { getBearerToken, readJson } from '../utils/request.js'
|
||||||
|
import { adminAuthService } from '../services/admin-auth.service.js'
|
||||||
|
import { serviceFail } from '../types/service-result.js'
|
||||||
|
import { merchantInvoiceService, type MerchantInvoiceListInput, type MerchantInvoiceFileInput } from '../services/merchant-invoice.service.js'
|
||||||
|
import { toHttpResponse } from './controller-response.js'
|
||||||
|
|
||||||
|
export const merchantInvoiceListController = withErrorBoundary(async request => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
if (!await adminAuthService.findSession(getBearerToken(request))) return toHttpResponse(serviceFail('登录已失效,请重新登录', 401, 'UNAUTHORIZED'))
|
||||||
|
return toHttpResponse(await merchantInvoiceService.list(await readJson<MerchantInvoiceListInput>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const merchantInvoicePushController = withErrorBoundary(async request => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
if (!await adminAuthService.findSession(getBearerToken(request))) return toHttpResponse(serviceFail('登录已失效,请重新登录', 401, 'UNAUTHORIZED'))
|
||||||
|
return toHttpResponse(await merchantInvoiceService.push(await readJson<{ id?: number }>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const merchantInvoiceFileController = withErrorBoundary(async request => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
if (!await adminAuthService.findSession(getBearerToken(request))) return toHttpResponse(serviceFail('登录已失效,请重新登录', 401, 'UNAUTHORIZED'))
|
||||||
|
return toHttpResponse(await merchantInvoiceService.file(await readJson<MerchantInvoiceFileInput>(request)))
|
||||||
|
})
|
||||||
@@ -23,6 +23,20 @@ export const merchantOrderListController = withErrorBoundary(async (request: Req
|
|||||||
return toHttpResponse(await merchantService.listOrders(await readJson<MerchantOrderListInput>(request)))
|
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 merchantOrderRepushSplitController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
return toHttpResponse(await merchantService.repushSplitOrder(await readJson<Pick<MerchantShipInput, 'supplierId' | 'userId' | 'orderId'>>(request)))
|
||||||
|
})
|
||||||
|
|
||||||
export const merchantOrderShipController = withErrorBoundary(async (request: Request, _context: Context) => {
|
export const merchantOrderShipController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
const methodResponse = guardMethod(request, ['POST'])
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
if (methodResponse) return methodResponse
|
if (methodResponse) return methodResponse
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ import { orderActionService } from '../services/order-action.service.js'
|
|||||||
import { orderCreateService, type CheckoutGoods } from '../services/order-create.service.js'
|
import { orderCreateService, type CheckoutGoods } from '../services/order-create.service.js'
|
||||||
import { orderListService } from '../services/order-list.service.js'
|
import { orderListService } from '../services/order-list.service.js'
|
||||||
import { orderPushEventService, type OrderPushEventInput } from '../services/order-push-event.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 SupplierFileBase64QueryRequest,
|
||||||
|
type SupplierInvoiceApplyRequest,
|
||||||
|
type SupplierInvoiceQueryRequest,
|
||||||
|
type SupplierOrderQueryRequest,
|
||||||
|
type SupplierReconciliationQueryRequest
|
||||||
|
} from '../services/supplier-open-order.service.js'
|
||||||
import { toHttpResponse } from './controller-response.js'
|
import { toHttpResponse } from './controller-response.js'
|
||||||
|
|
||||||
type OrderListRequest = {
|
type OrderListRequest = {
|
||||||
@@ -239,3 +246,74 @@ export const supplierOrderInfoController = withErrorBoundary(async (request: Req
|
|||||||
|
|
||||||
return Response.json(await supplierOpenOrderService.queryOrderInfo(body))
|
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))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const supplierInvoiceApplyController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
let body: SupplierInvoiceApplyRequest
|
||||||
|
try {
|
||||||
|
body = supplierOpenOrderService.parseBody(await readJson<SupplierInvoiceApplyRequest>(request))
|
||||||
|
} catch (error) {
|
||||||
|
return Response.json({
|
||||||
|
resultCode: 400,
|
||||||
|
message: error instanceof Error ? error.message : '请求参数解密失败'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json(await supplierOpenOrderService.applyForInvoices(body))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const supplierInvoiceInfoController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
let body: SupplierInvoiceQueryRequest
|
||||||
|
try {
|
||||||
|
body = supplierOpenOrderService.parseBody(await readJson<SupplierInvoiceQueryRequest>(request))
|
||||||
|
} catch (error) {
|
||||||
|
return Response.json({
|
||||||
|
resultCode: 400,
|
||||||
|
message: error instanceof Error ? error.message : '请求参数解密失败'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json(await supplierOpenOrderService.queryInvoiceInfo(body))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const supplierFileBase64InfoController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
let body: SupplierFileBase64QueryRequest
|
||||||
|
try {
|
||||||
|
body = supplierOpenOrderService.parseBody(await readJson<SupplierFileBase64QueryRequest>(request))
|
||||||
|
} catch (error) {
|
||||||
|
return Response.json({
|
||||||
|
resultCode: 400,
|
||||||
|
message: error instanceof Error ? error.message : '请求参数解密失败',
|
||||||
|
fileType: 'JPG',
|
||||||
|
imageEncodeMap: {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json(await supplierOpenOrderService.queryFileBase64Info(body))
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Context } from '../utils/handler.js'
|
import type { Context } from '../utils/handler.js'
|
||||||
import { guardMethod } from '../utils/method.js'
|
import { guardMethod } from '../utils/method.js'
|
||||||
|
import { readJson } from '../utils/request.js'
|
||||||
import { withErrorBoundary } from '../utils/handler.js'
|
import { withErrorBoundary } from '../utils/handler.js'
|
||||||
import { productService } from '../services/product.service.js'
|
import { productService } from '../services/product.service.js'
|
||||||
import { toHttpResponse } from './controller-response.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))
|
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; pageNumber?: number; pageSize?: number }>(request)
|
||||||
|
return toHttpResponse(await productService.listManageProducts(String(body.keyword || '').trim(), String(body.categoryId || '').trim(), body.pageNumber, body.pageSize))
|
||||||
|
})
|
||||||
|
|
||||||
|
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)))
|
||||||
|
})
|
||||||
@@ -9,6 +9,10 @@ type SupplierInfoRequest = {
|
|||||||
supplierIdentification?: string
|
supplierIdentification?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SupplierSessionRequest = {
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
type SupplierTokenRequest = {
|
type SupplierTokenRequest = {
|
||||||
userId?: string
|
userId?: string
|
||||||
supplierIdentification?: string
|
supplierIdentification?: string
|
||||||
@@ -34,6 +38,16 @@ export const supplierInfoController = withErrorBoundary(async (request: Request,
|
|||||||
return toHttpResponse(await supplierService.getSupplierInfo(supplierIdentification))
|
return toHttpResponse(await supplierService.getSupplierInfo(supplierIdentification))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const supplierSessionController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
|
if (methodResponse) return methodResponse
|
||||||
|
|
||||||
|
const body = await readJson<SupplierSessionRequest>(request)
|
||||||
|
const keyword = String(body.keyword || '')
|
||||||
|
|
||||||
|
return toHttpResponse(await supplierService.decryptSupplierSession(keyword))
|
||||||
|
})
|
||||||
|
|
||||||
export const supplierTokenController = withErrorBoundary(async (request: Request, _context: Context) => {
|
export const supplierTokenController = withErrorBoundary(async (request: Request, _context: Context) => {
|
||||||
const methodResponse = guardMethod(request, ['POST'])
|
const methodResponse = guardMethod(request, ['POST'])
|
||||||
if (methodResponse) return methodResponse
|
if (methodResponse) return methodResponse
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export type PrismaTransaction = Prisma.TransactionClient
|
|||||||
|
|
||||||
export const orderDao = {
|
export const orderDao = {
|
||||||
transaction<T>(handler: (tx: PrismaTransaction) => Promise<T>) {
|
transaction<T>(handler: (tx: PrismaTransaction) => Promise<T>) {
|
||||||
return prisma.$transaction(handler)
|
return prisma.$transaction(handler, { maxWait: 10000, timeout: 20000 })
|
||||||
},
|
},
|
||||||
|
|
||||||
findMainOrdersWithSubs(supplierId: string, userId: string) {
|
findMainOrdersWithSubs(supplierId: string, userId: string) {
|
||||||
@@ -137,12 +137,29 @@ export const orderDao = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
findMerchantSubOrders(supplierId: string, orderState?: number) {
|
findInvoiceBySettlementId(supplierId: string, settlementId: string) {
|
||||||
|
return prisma.supplierInvoiceInfo.findFirst({
|
||||||
|
where: {
|
||||||
|
supplierId,
|
||||||
|
settlementId,
|
||||||
|
deleted: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
findOpenApiReconciliationSubOrders(supplierId: string, orderIds: string[], startDate?: Date, endDate?: Date, orderState?: number) {
|
||||||
return prisma.supplierOrderSub.findMany({
|
return prisma.supplierOrderSub.findMany({
|
||||||
where: {
|
where: {
|
||||||
supplierId,
|
supplierId,
|
||||||
deleted: 0,
|
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: {
|
include: {
|
||||||
products: {
|
products: {
|
||||||
@@ -150,19 +167,48 @@ export const orderDao = {
|
|||||||
orderBy: { createAt: 'asc' }
|
orderBy: { createAt: 'asc' }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
orderBy: { createAt: 'asc' }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async findMerchantSubOrders(supplierId: string, orderState?: number, orderId?: string, userId?: string, excludedOrderIds: string[] = [], pagination?: { pageNumber: number; pageSize: number }, matchingUserIds?: string[]) {
|
||||||
|
const where: Prisma.SupplierOrderSubWhereInput = {
|
||||||
|
...(supplierId ? { supplierId } : {}),
|
||||||
|
deleted: 0,
|
||||||
|
...(Number.isInteger(orderState) ? { orderState } : {}),
|
||||||
|
...(orderId ? { OR: [{ orderId: { contains: orderId } }, { orderNumberId: { contains: orderId } }] } : {}),
|
||||||
|
...(userId ? { userId: { contains: userId } } : {}),
|
||||||
|
...(matchingUserIds !== undefined ? { AND: [{ userId: { in: matchingUserIds } }] } : {}),
|
||||||
|
...(excludedOrderIds.length ? { orderId: { notIn: excludedOrderIds } } : {})
|
||||||
|
}
|
||||||
|
const [orders, totalCount] = await prisma.$transaction([
|
||||||
|
prisma.supplierOrderSub.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
products: {
|
||||||
|
where: { deleted: 0 },
|
||||||
|
orderBy: { createAt: 'asc' }
|
||||||
|
}
|
||||||
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{ createTime: 'desc' },
|
{ createTime: 'desc' },
|
||||||
{ createAt: 'desc' }
|
{ createAt: 'desc' },
|
||||||
]
|
{ orderId: 'desc' }
|
||||||
})
|
],
|
||||||
|
...(pagination ? { skip: (pagination.pageNumber - 1) * pagination.pageSize, take: pagination.pageSize } : {})
|
||||||
|
}),
|
||||||
|
prisma.supplierOrderSub.count({ where })
|
||||||
|
])
|
||||||
|
return { orders, totalCount }
|
||||||
},
|
},
|
||||||
|
|
||||||
findMerchantAfterSales(supplierId: string, serviceStates?: number[]) {
|
findMerchantAfterSales(supplierId: string, serviceStates?: number[], serviceOrderId = '') {
|
||||||
return prisma.supplierAfterSale.findMany({
|
return prisma.supplierAfterSale.findMany({
|
||||||
where: {
|
where: {
|
||||||
supplierId,
|
...(supplierId ? { supplierId } : {}),
|
||||||
deleted: 0,
|
deleted: 0,
|
||||||
...(serviceStates?.length ? { serviceState: { in: serviceStates } } : {})
|
...(serviceStates?.length ? { serviceState: { in: serviceStates } } : {}),
|
||||||
|
...(serviceOrderId ? { serviceOrderId: { contains: serviceOrderId } } : {})
|
||||||
},
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{ createTime: 'desc' },
|
{ createTime: 'desc' },
|
||||||
|
|||||||
@@ -33,5 +33,35 @@ export const productDao = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async findManageProducts(keyword: string, categoryId: string, pageNumber = 1, pageSize = 10) {
|
||||||
|
const where = {
|
||||||
|
deleted: 0,
|
||||||
|
...(keyword ? { OR: [{ name: { contains: keyword } }, { sku: { contains: keyword } }] } : {}),
|
||||||
|
...(categoryId ? { categoryId } : {})
|
||||||
|
}
|
||||||
|
const [products, totalCount] = await prisma.$transaction([
|
||||||
|
prisma.supplierProduct.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }, { id: 'desc' }],
|
||||||
|
skip: (pageNumber - 1) * pageSize,
|
||||||
|
take: pageSize
|
||||||
|
}),
|
||||||
|
prisma.supplierProduct.count({ where })
|
||||||
|
])
|
||||||
|
return { products, totalCount }
|
||||||
|
},
|
||||||
|
|
||||||
|
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 } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Prisma } from '@prisma/client'
|
||||||
|
import { prisma } from '../utils/prisma.js'
|
||||||
|
|
||||||
|
export type SuppliedUser = {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
passwordHash: string
|
||||||
|
nickname: string
|
||||||
|
enabled: boolean
|
||||||
|
tokenExpiresAt: Date | null
|
||||||
|
lockedUntil: Date | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SuppliedCustomer = { id: number; virtualId: string; nickname: string; enabled: boolean }
|
||||||
|
|
||||||
|
export const suppliedUserDao = {
|
||||||
|
async findByVirtualId(virtualId: string) {
|
||||||
|
const rows = await prisma.$queryRaw<SuppliedCustomer[]>`
|
||||||
|
SELECT id, virtualId, nickname, enabled FROM supplied_user WHERE virtualId = ${virtualId} LIMIT 1`
|
||||||
|
return rows[0]
|
||||||
|
},
|
||||||
|
async findOrCreateByVirtualId(virtualId: string, nickname: string) {
|
||||||
|
const existing = await this.findByVirtualId(virtualId)
|
||||||
|
if (existing) return existing
|
||||||
|
// 并发进入时由虚拟 ID 唯一索引去重,不覆盖已有昵称。
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
INSERT INTO supplied_user (virtualId, nickname, username, passwordHash)
|
||||||
|
VALUES (${virtualId}, ${nickname}, NULL, NULL)
|
||||||
|
ON DUPLICATE KEY UPDATE virtualId = VALUES(virtualId)`
|
||||||
|
const user = await this.findByVirtualId(virtualId)
|
||||||
|
if (!user) throw new Error('获取供应商用户信息失败')
|
||||||
|
return user
|
||||||
|
},
|
||||||
|
async findVirtualIdsByNickname(nickname: string) {
|
||||||
|
return prisma.$queryRaw<Array<{ virtualId: string }>>`
|
||||||
|
SELECT virtualId FROM supplied_user
|
||||||
|
WHERE virtualId IS NOT NULL AND LOCATE(${nickname}, nickname) > 0`
|
||||||
|
},
|
||||||
|
async findByVirtualIds(virtualIds: string[]) {
|
||||||
|
if (!virtualIds.length) return []
|
||||||
|
return prisma.$queryRaw<SuppliedCustomer[]>`
|
||||||
|
SELECT id, virtualId, nickname, enabled FROM supplied_user
|
||||||
|
WHERE virtualId IN (${Prisma.join(virtualIds)})`
|
||||||
|
},
|
||||||
|
async findByUsername(username: string) {
|
||||||
|
const rows = await prisma.$queryRaw<SuppliedUser[]>`
|
||||||
|
SELECT id, username, passwordHash, nickname, enabled, tokenExpiresAt, lockedUntil
|
||||||
|
FROM supplied_user WHERE username = ${username} AND virtualId IS NULL LIMIT 1`
|
||||||
|
return rows[0]
|
||||||
|
},
|
||||||
|
async findSession(tokenHash: string) {
|
||||||
|
const now = new Date()
|
||||||
|
const rows = await prisma.$queryRaw<SuppliedUser[]>`
|
||||||
|
SELECT id, username, nickname, tokenExpiresAt FROM supplied_user
|
||||||
|
WHERE tokenHash = ${tokenHash} AND virtualId IS NULL AND enabled = TRUE AND tokenExpiresAt > ${now} LIMIT 1`
|
||||||
|
return rows[0]
|
||||||
|
},
|
||||||
|
async recordFailure(id: number) {
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE supplied_user SET
|
||||||
|
failedLoginCount = IF(lockedUntil IS NOT NULL AND lockedUntil <= NOW(3), 1, failedLoginCount + 1),
|
||||||
|
lockedUntil = IF(failedLoginCount >= 5, DATE_ADD(NOW(3), INTERVAL 15 MINUTE), NULL),
|
||||||
|
updatedAt = NOW(3) WHERE id = ${id}`
|
||||||
|
},
|
||||||
|
async saveSession(id: number, tokenHash: string, expiresAt: Date) {
|
||||||
|
return prisma.$executeRaw`
|
||||||
|
UPDATE supplied_user SET tokenHash = ${tokenHash}, tokenExpiresAt = ${expiresAt},
|
||||||
|
failedLoginCount = 0, lockedUntil = NULL, updatedAt = NOW(3)
|
||||||
|
WHERE id = ${id} AND virtualId IS NULL AND enabled = TRUE AND (lockedUntil IS NULL OR lockedUntil <= NOW(3))`
|
||||||
|
},
|
||||||
|
async clearSession(tokenHash: string) {
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE supplied_user SET tokenHash = NULL, tokenExpiresAt = NULL, updatedAt = NOW(3)
|
||||||
|
WHERE tokenHash = ${tokenHash}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,5 +11,13 @@ export const supplierDao = {
|
|||||||
return prisma.supplierInfo.findFirst({
|
return prisma.supplierInfo.findFirst({
|
||||||
where: { supplierId, deleted: 0 }
|
where: { supplierId, deleted: 0 }
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
findActiveSuppliers() {
|
||||||
|
return prisma.supplierInfo.findMany({
|
||||||
|
where: { deleted: 0 },
|
||||||
|
select: { supplierId: true, supplierName: true },
|
||||||
|
orderBy: { supplierName: 'asc' }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { fileController as default } from '../controllers/basic.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantInvoiceFileController as default } from '../controllers/merchant-invoice.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantInvoiceListController as default } from '../controllers/merchant-invoice.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantInvoicePushController as default } from '../controllers/merchant-invoice.controller.js'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { merchantOrderRepushSplitController as default } from '../controllers/merchant.controller.js'
|
||||||
@@ -4,6 +4,7 @@ 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 { logger } from './utils/logger.js'
|
import { logger } from './utils/logger.js'
|
||||||
|
import { adminAuthService } from './services/admin-auth.service.js'
|
||||||
import {
|
import {
|
||||||
addressDefaultController,
|
addressDefaultController,
|
||||||
addressDeleteController,
|
addressDeleteController,
|
||||||
@@ -11,10 +12,13 @@ import {
|
|||||||
addressSaveController
|
addressSaveController
|
||||||
} from './controllers/address.controller.js'
|
} from './controllers/address.controller.js'
|
||||||
import {
|
import {
|
||||||
|
adminSessionController,
|
||||||
|
adminLogoutController,
|
||||||
cartAddController,
|
cartAddController,
|
||||||
loginController,
|
loginController,
|
||||||
uploadController,
|
uploadController,
|
||||||
uploadPreviewController,
|
uploadPreviewController,
|
||||||
|
fileController,
|
||||||
userInfoController
|
userInfoController
|
||||||
} from './controllers/basic.controller.js'
|
} from './controllers/basic.controller.js'
|
||||||
import {
|
import {
|
||||||
@@ -33,8 +37,18 @@ import {
|
|||||||
orderPayController,
|
orderPayController,
|
||||||
orderPushEventController,
|
orderPushEventController,
|
||||||
orderShipController,
|
orderShipController,
|
||||||
supplierOrderInfoController
|
supplierFileBase64InfoController,
|
||||||
|
supplierInvoiceApplyController,
|
||||||
|
supplierInvoiceInfoController,
|
||||||
|
supplierOrderInfoController,
|
||||||
|
supplierReconciliationInfoController
|
||||||
} from './controllers/order.controller.js'
|
} from './controllers/order.controller.js'
|
||||||
|
import {
|
||||||
|
merchantReconciliationListController,
|
||||||
|
merchantReconciliationUpdateController,
|
||||||
|
supplierReconciliationResultController,
|
||||||
|
supplierUnReconciledMsgController
|
||||||
|
} from './controllers/reconciliation.controller.js'
|
||||||
import {
|
import {
|
||||||
merchantAfterSaleAuditController,
|
merchantAfterSaleAuditController,
|
||||||
merchantAfterSaleDetailController,
|
merchantAfterSaleDetailController,
|
||||||
@@ -42,13 +56,22 @@ import {
|
|||||||
merchantAfterSaleReceiveController,
|
merchantAfterSaleReceiveController,
|
||||||
merchantAfterSaleShipController,
|
merchantAfterSaleShipController,
|
||||||
merchantOrderListController,
|
merchantOrderListController,
|
||||||
merchantOrderShipController
|
merchantOrderShipController,
|
||||||
|
merchantOrderRepushSplitController,
|
||||||
|
merchantSupplierListController
|
||||||
} from './controllers/merchant.controller.js'
|
} 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 {
|
import {
|
||||||
supplierDebugTokensController,
|
supplierDebugTokensController,
|
||||||
supplierInfoController,
|
supplierInfoController,
|
||||||
supplierPointsController,
|
supplierPointsController,
|
||||||
|
supplierSessionController,
|
||||||
supplierTokenController
|
supplierTokenController
|
||||||
} from './controllers/supplier.controller.js'
|
} from './controllers/supplier.controller.js'
|
||||||
|
|
||||||
@@ -56,7 +79,12 @@ 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)
|
||||||
|
|
||||||
|
import { merchantInvoiceListController, merchantInvoiceFileController, merchantInvoicePushController } from './controllers/merchant-invoice.controller.js'
|
||||||
|
|
||||||
const handlers: Record<string, FunctionHandler> = {
|
const handlers: Record<string, FunctionHandler> = {
|
||||||
|
'merchant-invoice-list': merchantInvoiceListController,
|
||||||
|
'merchant-invoice-file': merchantInvoiceFileController,
|
||||||
|
'merchant-invoice-push': merchantInvoicePushController,
|
||||||
'address-default': addressDefaultController,
|
'address-default': addressDefaultController,
|
||||||
'address-delete': addressDeleteController,
|
'address-delete': addressDeleteController,
|
||||||
'address-list': addressListController,
|
'address-list': addressListController,
|
||||||
@@ -68,6 +96,8 @@ const handlers: Record<string, FunctionHandler> = {
|
|||||||
'after-sale-logistics': afterSaleLogisticsController,
|
'after-sale-logistics': afterSaleLogisticsController,
|
||||||
'cart-add': cartAddController,
|
'cart-add': cartAddController,
|
||||||
login: loginController,
|
login: loginController,
|
||||||
|
'admin-session': adminSessionController,
|
||||||
|
'admin-logout': adminLogoutController,
|
||||||
'merchant-after-sale-audit': merchantAfterSaleAuditController,
|
'merchant-after-sale-audit': merchantAfterSaleAuditController,
|
||||||
'merchant-after-sale-detail': merchantAfterSaleDetailController,
|
'merchant-after-sale-detail': merchantAfterSaleDetailController,
|
||||||
'merchant-after-sale-list': merchantAfterSaleListController,
|
'merchant-after-sale-list': merchantAfterSaleListController,
|
||||||
@@ -75,6 +105,8 @@ const handlers: Record<string, FunctionHandler> = {
|
|||||||
'merchant-after-sale-ship': merchantAfterSaleShipController,
|
'merchant-after-sale-ship': merchantAfterSaleShipController,
|
||||||
'merchant-order-list': merchantOrderListController,
|
'merchant-order-list': merchantOrderListController,
|
||||||
'merchant-order-ship': merchantOrderShipController,
|
'merchant-order-ship': merchantOrderShipController,
|
||||||
|
'merchant-order-repush-split': merchantOrderRepushSplitController,
|
||||||
|
'merchant-supplier-list': merchantSupplierListController,
|
||||||
'order-cancel-main': orderCancelMainController,
|
'order-cancel-main': orderCancelMainController,
|
||||||
'order-cancel-paid-main': orderCancelPaidMainController,
|
'order-cancel-paid-main': orderCancelPaidMainController,
|
||||||
'order-cancel-sub': orderCancelSubController,
|
'order-cancel-sub': orderCancelSubController,
|
||||||
@@ -87,13 +119,26 @@ const handlers: Record<string, FunctionHandler> = {
|
|||||||
'order-ship': orderShipController,
|
'order-ship': orderShipController,
|
||||||
'product-detail': productDetailController,
|
'product-detail': productDetailController,
|
||||||
'product-list': productListController,
|
'product-list': productListController,
|
||||||
|
'merchant-product-list': merchantProductListController,
|
||||||
|
'merchant-product-save': merchantProductSaveController,
|
||||||
|
'merchant-product-delete': merchantProductDeleteController,
|
||||||
'supplier-debug-tokens': supplierDebugTokensController,
|
'supplier-debug-tokens': supplierDebugTokensController,
|
||||||
|
'supplier-file-base64-info': supplierFileBase64InfoController,
|
||||||
|
'supplier-invoice-apply': supplierInvoiceApplyController,
|
||||||
|
'supplier-invoice-info': supplierInvoiceInfoController,
|
||||||
'supplier-info': supplierInfoController,
|
'supplier-info': supplierInfoController,
|
||||||
'supplier-order-info': supplierOrderInfoController,
|
'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-points': supplierPointsController,
|
||||||
|
'supplier-session': supplierSessionController,
|
||||||
'supplier-token': supplierTokenController,
|
'supplier-token': supplierTokenController,
|
||||||
upload: uploadController,
|
upload: uploadController,
|
||||||
'upload-preview': uploadPreviewController,
|
'upload-preview': uploadPreviewController,
|
||||||
|
file: fileController,
|
||||||
'user-info': userInfoController
|
'user-info': userInfoController
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,11 +162,34 @@ const toRequest = (ctx: Koa.Context) =>
|
|||||||
body: requestBody(ctx)
|
body: requestBody(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
const writeResponse = async (ctx: Koa.Context, response: Response) => {
|
const ba7ResponseBody = (content: string) => ({
|
||||||
|
status: 200,
|
||||||
|
msg: 'success',
|
||||||
|
data: {
|
||||||
|
Body: {
|
||||||
|
RspSvcHeader: {
|
||||||
|
ReturnCode: '000000000000',
|
||||||
|
ReturnMsg: '交易成功'
|
||||||
|
},
|
||||||
|
RspSvcBody: {
|
||||||
|
ReturnStCd: '200',
|
||||||
|
ReturnCntnt: content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const writeResponse = async (ctx: Koa.Context, response: Response, wrapBa7 = false) => {
|
||||||
ctx.status = response.status
|
ctx.status = response.status
|
||||||
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
||||||
response.headers.forEach((value, key) => ctx.set(key, value))
|
response.headers.forEach((value, key) => ctx.set(key, value))
|
||||||
ctx.body = response.body ? await response.text() : undefined
|
if (response.status >= 300 && response.status < 400 && response.headers.has('Location')) {
|
||||||
|
ctx.body = ''
|
||||||
|
ctx.status = response.status
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const responseText = response.body ? await response.text() : ''
|
||||||
|
ctx.body = wrapBa7 ? ba7ResponseBody(responseText) : responseText || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatch = (name: string) => async (ctx: Koa.Context) => {
|
const dispatch = (name: string) => async (ctx: Koa.Context) => {
|
||||||
@@ -134,16 +202,25 @@ const dispatch = (name: string) => async (ctx: Koa.Context) => {
|
|||||||
path: ctx.path,
|
path: ctx.path,
|
||||||
url: ctx.originalUrl,
|
url: ctx.originalUrl,
|
||||||
query: ctx.query,
|
query: ctx.query,
|
||||||
body: ctx.request.body || {}
|
body: name === 'login' ? { password: '[REDACTED]' } : ctx.request.body || {}
|
||||||
})
|
})
|
||||||
|
if (name === 'login' || name === 'admin-session' || name === 'admin-logout') ctx.set('Cache-Control', 'no-store')
|
||||||
const handler = handlers[name]
|
const handler = handlers[name]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
ctx.status = 404
|
ctx.status = 404
|
||||||
ctx.body = { success: false, error: { code: 'NOT_FOUND', message: '接口不存在' } }
|
ctx.body = { success: false, error: { code: 'NOT_FOUND', message: '接口不存在' } }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (name.startsWith('merchant-')) {
|
||||||
|
const token = ctx.get('Authorization').match(/^Bearer\s+(.+)$/i)?.[1] || ''
|
||||||
|
if (!await adminAuthService.findSession(token)) {
|
||||||
|
ctx.status = 401
|
||||||
|
ctx.body = { success: false, error: { code: 'UNAUTHORIZED', message: '登录已失效,请重新登录' } }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
const response = await handler(toRequest(ctx), {})
|
const response = await handler(toRequest(ctx), {})
|
||||||
await writeResponse(ctx, response)
|
await writeResponse(ctx, response, ctx.path.startsWith('/supplierOpenApi/'))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(bodyParser({ jsonLimit: '10mb' }))
|
app.use(bodyParser({ jsonLimit: '10mb' }))
|
||||||
@@ -174,7 +251,30 @@ router.delete('/api/supplier-logs', ctx => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
router.all('/api/:name', async ctx => dispatch(ctx.params.name)(ctx))
|
router.all('/api/:name', async ctx => dispatch(ctx.params.name)(ctx))
|
||||||
|
router.all('/api/admin/:name', async ctx => {
|
||||||
|
if (ctx.params.name !== 'login') {
|
||||||
|
try {
|
||||||
|
const token = ctx.get('Authorization').match(/^Bearer\s+(.+)$/i)?.[1] || ''
|
||||||
|
if (!await adminAuthService.findSession(token)) {
|
||||||
|
ctx.status = 401
|
||||||
|
ctx.body = { success: false, error: { code: 'UNAUTHORIZED', message: '登录已失效,请重新登录' } }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ctx.status = 500
|
||||||
|
ctx.body = { success: false, error: { code: 'AUTH_SERVICE_ERROR', message: '登录状态校验失败,请稍后重试' } }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await dispatch(ctx.params.name)(ctx)
|
||||||
|
})
|
||||||
router.post('/supplierOpenApi/pointOrder/queryOrderInfo', dispatch('supplier-order-info'))
|
router.post('/supplierOpenApi/pointOrder/queryOrderInfo', dispatch('supplier-order-info'))
|
||||||
|
router.post('/supplierOpenApi/pointOrder/queryReconciliationInfo', dispatch('supplier-reconciliation-info'))
|
||||||
|
router.post('/supplierOpenApi/pointOrder/applyForInvoices', dispatch('supplier-invoice-apply'))
|
||||||
|
router.post('/supplierOpenApi/pointOrder/queryInvoicesInfo', dispatch('supplier-invoice-info'))
|
||||||
|
router.post('/supplierOpenApi/pointOrder/queryFileBase64Info', dispatch('supplier-file-base64-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'))
|
router.post('/supplierOpenApi/debug/tokens', dispatch('supplier-debug-tokens'))
|
||||||
|
|
||||||
app.use(router.routes())
|
app.use(router.routes())
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { createHash, randomBytes, scrypt, timingSafeEqual } from 'node:crypto'
|
||||||
|
import { suppliedUserDao, type SuppliedUser } from '../dao/supplied-user.dao.js'
|
||||||
|
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||||
|
|
||||||
|
export type AdminLoginInput = { username?: unknown; password?: unknown }
|
||||||
|
const digest = (token: string) => createHash('sha256').update(token).digest('hex')
|
||||||
|
const publicUser = (user: SuppliedUser) => ({ id: user.id, username: user.username, nickname: user.nickname })
|
||||||
|
const derivePassword = (password: string, salt: string) => new Promise<Buffer>((resolve, reject) => {
|
||||||
|
scrypt(password, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }, (error, key) => {
|
||||||
|
if (error) reject(error)
|
||||||
|
else resolve(key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const adminAuthService = {
|
||||||
|
async login(input: AdminLoginInput) {
|
||||||
|
const username = typeof input?.username === 'string' ? input.username.trim() : ''
|
||||||
|
const password = typeof input?.password === 'string' ? input.password : ''
|
||||||
|
if (!username || username.length > 64 || !password || password.length > 128) {
|
||||||
|
return serviceFail('请输入有效的账号和密码', 400, 'INVALID_LOGIN_INPUT')
|
||||||
|
}
|
||||||
|
const user = await suppliedUserDao.findByUsername(username)
|
||||||
|
const [algorithm, salt, hash] = (user?.passwordHash || '').split('$')
|
||||||
|
const validHash = algorithm === 'scrypt' && /^[a-f0-9]{32}$/.test(salt || '') && /^[a-f0-9]{128}$/.test(hash || '')
|
||||||
|
const actual = await derivePassword(password, validHash ? salt : '00000000000000000000000000000000')
|
||||||
|
const expected = validHash ? Buffer.from(hash, 'hex') : Buffer.alloc(64)
|
||||||
|
const passwordMatches = timingSafeEqual(actual, expected) && validHash
|
||||||
|
if (user?.lockedUntil && user.lockedUntil.getTime() > Date.now()) {
|
||||||
|
return serviceFail('登录尝试过多,请稍后重试', 429, 'LOGIN_LOCKED')
|
||||||
|
}
|
||||||
|
if (!user || !user.enabled || !passwordMatches) {
|
||||||
|
if (user?.enabled) await suppliedUserDao.recordFailure(user.id)
|
||||||
|
return serviceFail('账号或密码错误', 401, 'INVALID_CREDENTIALS')
|
||||||
|
}
|
||||||
|
const token = randomBytes(32).toString('hex')
|
||||||
|
const expiresAt = new Date(Date.now() + 8 * 60 * 60 * 1000)
|
||||||
|
if (!await suppliedUserDao.saveSession(user.id, digest(token), expiresAt)) {
|
||||||
|
return serviceFail('账号暂时无法登录,请稍后重试', 401, 'LOGIN_UNAVAILABLE')
|
||||||
|
}
|
||||||
|
return serviceOk({ token, expiresAt: expiresAt.toISOString(), user: publicUser(user) })
|
||||||
|
},
|
||||||
|
async findSession(token: string) {
|
||||||
|
if (!/^[a-f0-9]{64}$/.test(token)) return undefined
|
||||||
|
return suppliedUserDao.findSession(digest(token))
|
||||||
|
},
|
||||||
|
async session(token: string) {
|
||||||
|
const user = await this.findSession(token)
|
||||||
|
return user ? serviceOk({ user: publicUser(user), expiresAt: user.tokenExpiresAt?.toISOString() })
|
||||||
|
: serviceFail('登录已失效,请重新登录', 401, 'UNAUTHORIZED')
|
||||||
|
},
|
||||||
|
async logout(token: string) {
|
||||||
|
if (/^[a-f0-9]{64}$/.test(token)) await suppliedUserDao.clearSession(digest(token))
|
||||||
|
return serviceOk({})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { randomBytes } from 'node:crypto'
|
||||||
import {
|
import {
|
||||||
AFTER_SALE_LOGISTICS_STATE,
|
AFTER_SALE_LOGISTICS_STATE,
|
||||||
AFTER_SALE_SERVICE_STATE,
|
AFTER_SALE_SERVICE_STATE,
|
||||||
@@ -132,13 +133,13 @@ const toDateTimeText = (date?: Date | null) => {
|
|||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const createServiceOrderId = (subOrderId: string, serviceType: number) => {
|
const createServiceOrderId = (serviceType: number) => {
|
||||||
return `AS${serviceType}_${subOrderId}_${Date.now()}`
|
return `AS${serviceType}_${randomBytes(8).toString('hex')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOrderSigned = (orderState?: number | null) => signedOrderStates.has(Number(orderState))
|
const isOrderSigned = (orderState?: number | null) => signedOrderStates.has(Number(orderState))
|
||||||
|
|
||||||
const pushReturnOrder = async (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>) => {
|
const pushReturnOrder = async (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>, operationTime: Date) => {
|
||||||
if (!record.supplierId || !record.userId || !record.subOrderId || !record.mainOrderId) {
|
if (!record.supplierId || !record.userId || !record.subOrderId || !record.mainOrderId) {
|
||||||
return serviceFail('售后订单信息不完整,无法推送退货单')
|
return serviceFail('售后订单信息不完整,无法推送退货单')
|
||||||
}
|
}
|
||||||
@@ -149,6 +150,7 @@ const pushReturnOrder = async (record: NonNullable<Awaited<ReturnType<typeof ord
|
|||||||
orderId: record.serviceOrderId,
|
orderId: record.serviceOrderId,
|
||||||
mainOrderId: record.subOrderId,
|
mainOrderId: record.subOrderId,
|
||||||
userId: record.userId,
|
userId: record.userId,
|
||||||
|
operationTime: toDateTimeText(operationTime),
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -159,7 +161,7 @@ const pushReturnOrder = async (record: NonNullable<Awaited<ReturnType<typeof ord
|
|||||||
return serviceOk({ serviceOrderId: record.serviceOrderId })
|
return serviceOk({ serviceOrderId: record.serviceOrderId })
|
||||||
}
|
}
|
||||||
|
|
||||||
const pushExchangeOrder = async (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>) => {
|
const pushExchangeOrder = async (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>, operationTime: Date) => {
|
||||||
if (!record.supplierId || !record.userId || !record.subOrderId || !record.mainOrderId) {
|
if (!record.supplierId || !record.userId || !record.subOrderId || !record.mainOrderId) {
|
||||||
return serviceFail('售后订单信息不完整,无法推送换货单')
|
return serviceFail('售后订单信息不完整,无法推送换货单')
|
||||||
}
|
}
|
||||||
@@ -170,6 +172,7 @@ const pushExchangeOrder = async (record: NonNullable<Awaited<ReturnType<typeof o
|
|||||||
orderId: record.serviceOrderId,
|
orderId: record.serviceOrderId,
|
||||||
mainOrderId: record.subOrderId,
|
mainOrderId: record.subOrderId,
|
||||||
userId: record.userId,
|
userId: record.userId,
|
||||||
|
operationTime: toDateTimeText(operationTime),
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -180,7 +183,7 @@ const pushExchangeOrder = async (record: NonNullable<Awaited<ReturnType<typeof o
|
|||||||
return serviceOk({ serviceOrderId: record.serviceOrderId })
|
return serviceOk({ serviceOrderId: record.serviceOrderId })
|
||||||
}
|
}
|
||||||
|
|
||||||
const pushCancelSubOrder = async (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>) => {
|
const pushCancelSubOrder = async (record: NonNullable<Awaited<ReturnType<typeof orderDao.findAfterSale>>>, operationTime: Date) => {
|
||||||
if (!record.supplierId || !record.userId || !record.subOrderId || !record.mainOrderId) {
|
if (!record.supplierId || !record.userId || !record.subOrderId || !record.mainOrderId) {
|
||||||
return serviceFail('售后订单信息不完整,无法取消子单')
|
return serviceFail('售后订单信息不完整,无法取消子单')
|
||||||
}
|
}
|
||||||
@@ -191,6 +194,7 @@ const pushCancelSubOrder = async (record: NonNullable<Awaited<ReturnType<typeof
|
|||||||
orderId: record.subOrderId,
|
orderId: record.subOrderId,
|
||||||
mainOrderId: record.mainOrderId,
|
mainOrderId: record.mainOrderId,
|
||||||
userId: record.userId,
|
userId: record.userId,
|
||||||
|
operationTime: toDateTimeText(operationTime),
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -320,7 +324,7 @@ export const afterSaleService = {
|
|||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const amount = Number(input.amount || subOrder.totalPrice || 0)
|
const amount = Number(input.amount || subOrder.totalPrice || 0)
|
||||||
const serviceOrderId = createServiceOrderId(subOrderId, serviceType)
|
const serviceOrderId = createServiceOrderId(serviceType)
|
||||||
const { isSigned: _isSigned, ...requestBody } = input
|
const { isSigned: _isSigned, ...requestBody } = input
|
||||||
|
|
||||||
await orderDao.transaction(async tx => {
|
await orderDao.transaction(async tx => {
|
||||||
@@ -487,7 +491,7 @@ export const afterSaleService = {
|
|||||||
const subOrders = record.subOrderId
|
const subOrders = record.subOrderId
|
||||||
? await orderDao.findOpenApiSubOrders(supplierId, [record.subOrderId])
|
? await orderDao.findOpenApiSubOrders(supplierId, [record.subOrderId])
|
||||||
: []
|
: []
|
||||||
const isSigned = isOrderSigned(subOrders[0]?.orderState)
|
const isSigned = Boolean(record.pickupMethod || record.pickupName || record.pickupMobile || record.pickupAddress) || isOrderSigned(subOrders[0]?.orderState)
|
||||||
const approved = auditStatus === 1
|
const approved = auditStatus === 1
|
||||||
const isExchange = record.serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER
|
const isExchange = record.serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER
|
||||||
const finishUnsignedReturn = approved && !isSigned && record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER
|
const finishUnsignedReturn = approved && !isSigned && record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER
|
||||||
@@ -498,14 +502,14 @@ export const afterSaleService = {
|
|||||||
? (isSigned ? AFTER_SALE_LOGISTICS_STATE.WAIT_USER_SHIP : AFTER_SALE_LOGISTICS_STATE.NONE)
|
? (isSigned ? AFTER_SALE_LOGISTICS_STATE.WAIT_USER_SHIP : AFTER_SALE_LOGISTICS_STATE.NONE)
|
||||||
: AFTER_SALE_LOGISTICS_STATE.NONE
|
: AFTER_SALE_LOGISTICS_STATE.NONE
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
if (finishUnsignedReturn) {
|
if (finishUnsignedReturn) {
|
||||||
const cancelSubOrder = await pushCancelSubOrder(record)
|
const cancelSubOrder = await pushCancelSubOrder(record, now)
|
||||||
if (!cancelSubOrder.success) {
|
if (!cancelSubOrder.success) {
|
||||||
return cancelSubOrder
|
return cancelSubOrder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
await orderDao.transaction(async tx => {
|
await orderDao.transaction(async tx => {
|
||||||
await tx.supplierAfterSale.update({
|
await tx.supplierAfterSale.update({
|
||||||
where: { serviceOrderId },
|
where: { serviceOrderId },
|
||||||
@@ -550,20 +554,20 @@ export const afterSaleService = {
|
|||||||
return serviceFail('退款/售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
|
return serviceFail('退款/售后申请不存在', 404, 'AFTER_SALE_NOT_FOUND')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
if (record.serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_SHIPPED) {
|
if (record.serviceType === ORDER_EVENT_TYPE.EXCHANGE_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_SHIPPED) {
|
||||||
const exchangeOrder = await pushExchangeOrder(record)
|
const exchangeOrder = await pushExchangeOrder(record, now)
|
||||||
if (!exchangeOrder.success) {
|
if (!exchangeOrder.success) {
|
||||||
return exchangeOrder
|
return exchangeOrder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED) {
|
if (record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED) {
|
||||||
const returnOrder = await pushReturnOrder(record)
|
const returnOrder = await pushReturnOrder(record, now)
|
||||||
if (!returnOrder.success) {
|
if (!returnOrder.success) {
|
||||||
return returnOrder
|
return returnOrder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
const finish = logisticsState === AFTER_SALE_LOGISTICS_STATE.USER_RECEIVED ||
|
const finish = logisticsState === AFTER_SALE_LOGISTICS_STATE.USER_RECEIVED ||
|
||||||
(record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED)
|
(record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER && logisticsState === AFTER_SALE_LOGISTICS_STATE.MERCHANT_RECEIVED)
|
||||||
const finishReturn = record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER &&
|
const finishReturn = record.serviceType === ORDER_EVENT_TYPE.RETURN_ORDER &&
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import { logger } from '../utils/logger.js'
|
|||||||
import { serviceOk } from '../types/service-result.js'
|
import { serviceOk } from '../types/service-result.js'
|
||||||
|
|
||||||
export const basicService = {
|
export const basicService = {
|
||||||
login() {
|
|
||||||
logger.info('login placeholder called')
|
|
||||||
return serviceOk({ message: '登录接口待实现' })
|
|
||||||
},
|
|
||||||
|
|
||||||
addCart() {
|
addCart() {
|
||||||
logger.info('cart add placeholder called')
|
logger.info('cart add placeholder called')
|
||||||
return serviceOk({ message: '加入购物车接口待实现' })
|
return serviceOk({ message: '加入购物车接口待实现' })
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||||
|
import { ORDER_EVENT_TYPE, ORDER_PUSH_STATUS } from '../constants/order.js'
|
||||||
|
import { prisma } from '../utils/prisma.js'
|
||||||
|
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||||
|
import { supplierFileService } from './supplier-file.service.js'
|
||||||
|
|
||||||
|
export type MerchantInvoiceListInput = { settlementId?: string; invoiceStatus?: number; pageNumber?: number; pageSize?: number }
|
||||||
|
export type MerchantInvoiceFileInput = { id?: number; download?: boolean }
|
||||||
|
|
||||||
|
export const merchantInvoiceService = {
|
||||||
|
async list(input: MerchantInvoiceListInput) {
|
||||||
|
const settlementId = String(input.settlementId || '').trim()
|
||||||
|
const invoiceStatus = input.invoiceStatus === undefined || input.invoiceStatus === null ? undefined : Number(input.invoiceStatus)
|
||||||
|
if (invoiceStatus !== undefined && ![0, 1, 2, 3].includes(invoiceStatus)) return serviceFail('开票状态不正确')
|
||||||
|
const pageNumber = Number.isInteger(Number(input.pageNumber)) && Number(input.pageNumber) > 0 ? Number(input.pageNumber) : 1
|
||||||
|
const pageSize = Number.isInteger(Number(input.pageSize)) && Number(input.pageSize) > 0 ? Math.min(Number(input.pageSize), 100) : 10
|
||||||
|
const where = { deleted: 0, ...(settlementId ? { settlementId: { contains: settlementId } } : {}), ...(invoiceStatus === undefined ? {} : { invoiceStatus }) }
|
||||||
|
const [records, totalCount] = await prisma.$transaction([
|
||||||
|
prisma.supplierInvoiceInfo.findMany({ where, orderBy: [{ createTime: 'desc' }, { id: 'desc' }], skip: (pageNumber - 1) * pageSize, take: pageSize }),
|
||||||
|
prisma.supplierInvoiceInfo.count({ where })
|
||||||
|
])
|
||||||
|
const invoiceItems = records.map(record => ({
|
||||||
|
id: record.id, settlementId: record.settlementId || record.invoiceBizId, invoiceIds: record.invoiceIds || '',
|
||||||
|
invoiceCode: record.invoiceCode || '', invoiceStatus: record.invoiceStatus, invoiceDate: record.invoiceDate,
|
||||||
|
invoiceType: record.invoiceType, invoiceAmount: record.invoiceAmount?.toFixed(2) || '0.00',
|
||||||
|
invoiceNakeAmount: record.invoiceNakeAmount?.toFixed(2) || '0.00', invoiceTaxAmount: record.invoiceTaxAmount?.toFixed(2) || '0.00',
|
||||||
|
invoiceTaxRate: record.invoiceTaxRate?.toString() || '', taxIdNumber: record.taxIdNumber || '', checkCode: record.checkCode || '',
|
||||||
|
orderIds: record.orderIds || '', failureMessage: record.failureMessage || '', createTime: record.createTime,
|
||||||
|
hasFile: record.invoiceStatus === 2 && Boolean(record.fileId)
|
||||||
|
}))
|
||||||
|
return serviceOk({ invoiceItems, totalCount, pageNumber, pageSize })
|
||||||
|
},
|
||||||
|
async push(input: { id?: number }) {
|
||||||
|
const id = Number(input.id)
|
||||||
|
if (!Number.isInteger(id) || id <= 0) return serviceFail('发票参数不正确')
|
||||||
|
const invoice = await prisma.supplierInvoiceInfo.findFirst({ where: { id, deleted: 0 } })
|
||||||
|
if (!invoice) return serviceFail('发票记录不存在', 404, 'INVOICE_NOT_FOUND')
|
||||||
|
if (![2, 3].includes(invoice.invoiceStatus)) return serviceFail('开票尚未结束,暂不能推送发票')
|
||||||
|
const now = new Date()
|
||||||
|
const pad = (value: number) => String(value).padStart(2, '0')
|
||||||
|
const body = {
|
||||||
|
supplierId: invoice.supplierId,
|
||||||
|
userId: invoice.userId || 'SYSTEM',
|
||||||
|
orderType: ORDER_EVENT_TYPE.INVOICE_INFO,
|
||||||
|
orderId: invoice.settlementId || invoice.invoiceBizId,
|
||||||
|
mainOrderId: invoice.mainOrderId || '',
|
||||||
|
operationTime: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`,
|
||||||
|
time: now.getTime()
|
||||||
|
}
|
||||||
|
const apiPath = '/pointOrder/pushOrderInfoMsg'
|
||||||
|
const log = await prisma.supplierOrderPushLog.create({ data: {
|
||||||
|
supplierId: body.supplierId, userId: body.userId, mainOrderId: invoice.mainOrderId,
|
||||||
|
orderId: body.orderId, orderType: body.orderType, apiPath, requestBody: body, pushStatus: ORDER_PUSH_STATUS.PENDING
|
||||||
|
} })
|
||||||
|
try {
|
||||||
|
const result = await bridgeFetchWithAuth<{ resultCode?: number; message?: string }>(invoice.supplierId, apiPath, body)
|
||||||
|
const success = result.response.ok && result.payload.resultCode === 0
|
||||||
|
const message = result.payload.message || '发票推送失败'
|
||||||
|
await prisma.supplierOrderPushLog.update({ where: { id: log.id }, data: {
|
||||||
|
responseBody: result.payload, httpStatus: result.response.status, resultCode: result.payload.resultCode,
|
||||||
|
pushStatus: success ? ORDER_PUSH_STATUS.SUCCESS : ORDER_PUSH_STATUS.FAILED,
|
||||||
|
errorMessage: success ? null : message, lastPushTime: new Date()
|
||||||
|
} })
|
||||||
|
return success ? serviceOk({ message: '发票推送成功' }) : serviceFail(message, 502, 'INVOICE_PUSH_FAILED')
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : '发票推送失败'
|
||||||
|
await prisma.supplierOrderPushLog.update({ where: { id: log.id }, data: {
|
||||||
|
pushStatus: ORDER_PUSH_STATUS.FAILED, errorMessage: message, lastPushTime: new Date()
|
||||||
|
} })
|
||||||
|
return serviceFail(message, 502, 'INVOICE_PUSH_FAILED')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async file(input: MerchantInvoiceFileInput) {
|
||||||
|
const id = Number(input.id)
|
||||||
|
if (!Number.isInteger(id) || id <= 0) return serviceFail('发票参数不正确')
|
||||||
|
const invoice = await prisma.supplierInvoiceInfo.findFirst({ where: { id, deleted: 0, invoiceStatus: 2 } })
|
||||||
|
if (!invoice?.fileId) return serviceFail('发票文件不存在', 404, 'INVOICE_FILE_NOT_FOUND')
|
||||||
|
const url = await supplierFileService.pdfUrl(invoice.fileId, invoice.supplierId, input.download === true)
|
||||||
|
if (!url) return serviceFail('发票文件不存在', 404, 'INVOICE_FILE_NOT_FOUND')
|
||||||
|
return serviceOk({ url })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,17 +2,26 @@ import {
|
|||||||
AFTER_SALE_LOGISTICS_STATE,
|
AFTER_SALE_LOGISTICS_STATE,
|
||||||
AFTER_SALE_SERVICE_STATE,
|
AFTER_SALE_SERVICE_STATE,
|
||||||
ORDER_EVENT_TYPE,
|
ORDER_EVENT_TYPE,
|
||||||
|
ORDER_SPLIT_STATE,
|
||||||
ORDER_STATE
|
ORDER_STATE
|
||||||
} from '../constants/order.js'
|
} from '../constants/order.js'
|
||||||
import { orderDao } from '../dao/order.dao.js'
|
import { orderDao } from '../dao/order.dao.js'
|
||||||
|
import { supplierDao } from '../dao/supplier.dao.js'
|
||||||
|
import { suppliedUserDao } from '../dao/supplied-user.dao.js'
|
||||||
import { afterSaleService, type AfterSaleAuditInput, type AfterSaleLogisticsInput } from './after-sale.service.js'
|
import { afterSaleService, type AfterSaleAuditInput, type AfterSaleLogisticsInput } from './after-sale.service.js'
|
||||||
import { orderActionService } from './order-action.service.js'
|
import { orderActionService } from './order-action.service.js'
|
||||||
import { logger } from '../utils/logger.js'
|
import { logger } from '../utils/logger.js'
|
||||||
|
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||||
import { serviceFail, serviceOk } from '../types/service-result.js'
|
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||||
|
|
||||||
export type MerchantOrderListInput = {
|
export type MerchantOrderListInput = {
|
||||||
supplierId?: string
|
supplierId?: string
|
||||||
orderState?: number
|
orderState?: number
|
||||||
|
pageNumber?: number
|
||||||
|
pageSize?: number
|
||||||
|
userNickname?: string
|
||||||
|
orderId?: string
|
||||||
|
userId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MerchantShipInput = {
|
export type MerchantShipInput = {
|
||||||
@@ -26,6 +35,7 @@ export type MerchantShipInput = {
|
|||||||
export type MerchantAfterSaleListInput = {
|
export type MerchantAfterSaleListInput = {
|
||||||
supplierId?: string
|
supplierId?: string
|
||||||
scene?: string
|
scene?: string
|
||||||
|
serviceOrderId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceStateText = (serviceState: number) => {
|
const serviceStateText = (serviceState: number) => {
|
||||||
@@ -68,7 +78,7 @@ const firstImage = (picInfo: unknown) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const afterSaleStatesByScene = (scene: string) => {
|
const afterSaleStatesByScene = (scene: string) => {
|
||||||
if (scene === 'audit') return [AFTER_SALE_SERVICE_STATE.PENDING_AUDIT]
|
if (scene === 'audit') return [AFTER_SALE_SERVICE_STATE.PENDING_AUDIT, AFTER_SALE_SERVICE_STATE.APPROVED, AFTER_SALE_SERVICE_STATE.USER_SHIPPED, AFTER_SALE_SERVICE_STATE.RECEIVED]
|
||||||
if (scene === 'process') {
|
if (scene === 'process') {
|
||||||
return [
|
return [
|
||||||
AFTER_SALE_SERVICE_STATE.APPROVED,
|
AFTER_SALE_SERVICE_STATE.APPROVED,
|
||||||
@@ -105,20 +115,27 @@ const isOrderSigned = (orderState?: number | null) => signedOrderStates.has(Numb
|
|||||||
export const merchantService = {
|
export const merchantService = {
|
||||||
async listOrders(input: MerchantOrderListInput) {
|
async listOrders(input: MerchantOrderListInput) {
|
||||||
const supplierId = String(input.supplierId || '').trim()
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
const orderState = Number(input.orderState ?? ORDER_STATE.FINISH_PAY)
|
const orderState = input.orderState == null || String(input.orderState) === '' ? undefined : Number(input.orderState)
|
||||||
|
const orderId = String(input.orderId || '').trim()
|
||||||
|
const userId = String(input.userId || '').trim()
|
||||||
logger.info('merchant order list query', { supplierId, orderState })
|
logger.info('merchant order list query', { supplierId, orderState })
|
||||||
|
|
||||||
if (!supplierId) {
|
const pageNumber = Number.isInteger(Number(input.pageNumber)) && Number(input.pageNumber) > 0 ? Number(input.pageNumber) : 1
|
||||||
return serviceFail('供应商不能为空')
|
const pageSize = Number.isInteger(Number(input.pageSize)) && Number(input.pageSize) > 0 ? Number(input.pageSize) : 10
|
||||||
}
|
const pagination = input.pageNumber !== undefined || input.pageSize !== undefined ? { pageNumber, pageSize } : undefined
|
||||||
|
|
||||||
const orders = await orderDao.findMerchantSubOrders(supplierId, orderState)
|
|
||||||
const afterSales = await orderDao.findMerchantAfterSales(supplierId, pendingShipExcludeAfterSaleStates)
|
const afterSales = await orderDao.findMerchantAfterSales(supplierId, pendingShipExcludeAfterSaleStates)
|
||||||
const afterSaleSubOrderIds = new Set(afterSales.map(item => item.subOrderId).filter(Boolean))
|
const afterSaleSubOrderIds = afterSales.map(item => item.subOrderId).filter((id): id is string => Boolean(id))
|
||||||
const orderItems = orders.filter(order => !afterSaleSubOrderIds.has(order.orderId)).map(order => ({
|
const userNickname = String(input.userNickname || '').trim()
|
||||||
|
const matchingUserIds = userNickname ? (await suppliedUserDao.findVirtualIdsByNickname(userNickname)).map(user => user.virtualId) : undefined
|
||||||
|
const { orders, totalCount } = await orderDao.findMerchantSubOrders(supplierId, orderState, orderId, userId, afterSaleSubOrderIds, pagination, matchingUserIds)
|
||||||
|
const virtualIds = [...new Set(orders.map(order => order.userId).filter((id): id is string => Boolean(id)))]
|
||||||
|
const users = await suppliedUserDao.findByVirtualIds(virtualIds)
|
||||||
|
const nicknames = new Map(users.map(user => [user.virtualId, user.nickname]))
|
||||||
|
const orderItems = orders.map(order => ({
|
||||||
orderId: order.orderId,
|
orderId: order.orderId,
|
||||||
mainOrderId: order.mainOrderId,
|
mainOrderId: order.mainOrderId,
|
||||||
userId: order.userId || '',
|
userId: order.userId || '',
|
||||||
|
userNickname: nicknames.get(order.userId || '') || '',
|
||||||
supplierId: order.supplierId,
|
supplierId: order.supplierId,
|
||||||
orderState: order.orderState,
|
orderState: order.orderState,
|
||||||
orderNumberId: order.orderNumberId || '',
|
orderNumberId: order.orderNumberId || '',
|
||||||
@@ -136,7 +153,63 @@ export const merchantService = {
|
|||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return serviceOk({ orderItems })
|
return serviceOk({ orderItems, totalCount, pageNumber, pageSize })
|
||||||
|
},
|
||||||
|
|
||||||
|
async listSuppliers() {
|
||||||
|
const suppliers = await supplierDao.findActiveSuppliers()
|
||||||
|
return serviceOk({ suppliers })
|
||||||
|
},
|
||||||
|
|
||||||
|
async repushSplitOrder(input: Pick<MerchantShipInput, 'supplierId' | 'userId' | 'orderId'>) {
|
||||||
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
|
const userId = String(input.userId || '').trim()
|
||||||
|
const orderId = String(input.orderId || '').trim()
|
||||||
|
if (!supplierId || !userId || !orderId) {
|
||||||
|
return serviceFail('供应商、用户或子单信息不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
const subOrder = await orderDao.findSubOrderWithProducts(supplierId, userId, orderId)
|
||||||
|
if (!subOrder) {
|
||||||
|
return serviceFail('子单不存在', 404, 'SUB_ORDER_NOT_FOUND')
|
||||||
|
}
|
||||||
|
const mainOrder = await orderDao.findMainOrder(supplierId, userId, subOrder.mainOrderId)
|
||||||
|
if (!mainOrder) {
|
||||||
|
return serviceFail('主单不存在', 404, 'MAIN_ORDER_NOT_FOUND')
|
||||||
|
}
|
||||||
|
if (mainOrder.orderState !== ORDER_STATE.FINISH_PAY || subOrder.orderState !== ORDER_STATE.FINISH_PAY) {
|
||||||
|
return serviceFail('仅支持已支付且待发货的订单重新推送拆单')
|
||||||
|
}
|
||||||
|
if (!subOrder.products.length) {
|
||||||
|
return serviceFail('子单缺少商品信息,无法推送拆单')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { response, payload } = await bridgeFetchWithAuth<{ resultCode?: number; message?: string }>(
|
||||||
|
supplierId,
|
||||||
|
'/pointOrder/pushOrderInfoMsg',
|
||||||
|
{
|
||||||
|
orderType: ORDER_EVENT_TYPE.SPLIT_SUB_ORDER,
|
||||||
|
orderId: subOrder.orderId,
|
||||||
|
mainOrderId: mainOrder.orderId,
|
||||||
|
userId: mainOrder.userId,
|
||||||
|
time: Date.now()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (!response.ok || payload.resultCode !== 0) {
|
||||||
|
return serviceFail(payload.message || '重新推送拆单失败', 502, 'BRIDGE_SPLIT_ORDER_FAILED')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return serviceFail(error instanceof Error ? error.message : '重新推送拆单失败', 502, 'BRIDGE_SPLIT_ORDER_FAILED')
|
||||||
|
}
|
||||||
|
|
||||||
|
await orderDao.transaction(async tx => {
|
||||||
|
await tx.supplierOrderMain.update({
|
||||||
|
where: { orderId: mainOrder.orderId },
|
||||||
|
data: { isSplit: ORDER_SPLIT_STATE.SPLIT, splitTime: mainOrder.splitTime || new Date(), updateTime: new Date() }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return serviceOk({ orderId, mainOrderId: mainOrder.orderId })
|
||||||
},
|
},
|
||||||
|
|
||||||
shipOrder(input: MerchantShipInput) {
|
shipOrder(input: MerchantShipInput) {
|
||||||
@@ -155,13 +228,12 @@ export const merchantService = {
|
|||||||
const scene = String(input.scene || '').trim()
|
const scene = String(input.scene || '').trim()
|
||||||
logger.info('merchant after sale list query', { supplierId, scene })
|
logger.info('merchant after sale list query', { supplierId, scene })
|
||||||
|
|
||||||
if (!supplierId) {
|
const records = await orderDao.findMerchantAfterSales(supplierId, afterSaleStatesByScene(scene), String(input.serviceOrderId || '').trim())
|
||||||
return serviceFail('供应商不能为空')
|
const supplierIds = [...new Set(records.map(item => item.supplierId))]
|
||||||
}
|
const subOrders = (await Promise.all(supplierIds.map(recordSupplierId => {
|
||||||
|
const subOrderIds = records.filter(item => item.supplierId === recordSupplierId).map(item => item.subOrderId).filter(Boolean) as string[]
|
||||||
const records = await orderDao.findMerchantAfterSales(supplierId, afterSaleStatesByScene(scene))
|
return subOrderIds.length ? orderDao.findOpenApiSubOrders(recordSupplierId, subOrderIds) : []
|
||||||
const subOrderIds = records.map(item => item.subOrderId).filter(Boolean) as string[]
|
}))).flat()
|
||||||
const subOrders = subOrderIds.length ? await orderDao.findOpenApiSubOrders(supplierId, subOrderIds) : []
|
|
||||||
const subOrderMap = new Map(subOrders.map(item => [item.orderId, item]))
|
const subOrderMap = new Map(subOrders.map(item => [item.orderId, item]))
|
||||||
const afterSaleItems = records.map(record => {
|
const afterSaleItems = records.map(record => {
|
||||||
const subOrder = record.subOrderId ? subOrderMap.get(record.subOrderId) : null
|
const subOrder = record.subOrderId ? subOrderMap.get(record.subOrderId) : null
|
||||||
@@ -177,7 +249,7 @@ export const merchantService = {
|
|||||||
serviceState: record.serviceState,
|
serviceState: record.serviceState,
|
||||||
serviceStateText: serviceStateText(record.serviceState),
|
serviceStateText: serviceStateText(record.serviceState),
|
||||||
logisticsState: record.logisticsState,
|
logisticsState: record.logisticsState,
|
||||||
isSigned: isOrderSigned(subOrder?.orderState),
|
isSigned: Boolean(record.pickupMethod || record.pickupName || record.pickupMobile || record.pickupAddress) || isOrderSigned(subOrder?.orderState),
|
||||||
reason: record.reason || '',
|
reason: record.reason || '',
|
||||||
problemDescription: record.problemDescription || '',
|
problemDescription: record.problemDescription || '',
|
||||||
pickupMethod: record.pickupMethod || '',
|
pickupMethod: record.pickupMethod || '',
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import crypto from 'node:crypto'
|
import crypto from 'node:crypto'
|
||||||
|
import { prisma } from '../utils/prisma.js'
|
||||||
import { serviceFail, serviceOk } from '../types/service-result.js'
|
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||||
|
|
||||||
export type UploadInput = {
|
export type UploadInput = {
|
||||||
@@ -36,9 +37,9 @@ const signingKey = (date: string) => {
|
|||||||
return hmac(serviceKey, 'aws4_request')
|
return hmac(serviceKey, 'aws4_request')
|
||||||
}
|
}
|
||||||
|
|
||||||
const objectUrl = (objectName: string) => {
|
const objectUrl = (objectName: string, bucket = minioConfig.bucket) => {
|
||||||
const endpoint = new URL(minioConfig.endPoint)
|
const endpoint = new URL(minioConfig.endPoint)
|
||||||
endpoint.pathname = `${endpoint.pathname.replace(/\/$/, '')}/${minioConfig.bucket}/${encodePath(objectName)}`
|
endpoint.pathname = `${endpoint.pathname.replace(/\/$/, '')}/${bucket}/${encodePath(objectName)}`
|
||||||
return endpoint
|
return endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,12 +50,12 @@ const extName = (fileName: string) => {
|
|||||||
|
|
||||||
const safeFolder = (folder?: string) => String(folder || 'after-sale').replace(/[^a-zA-Z0-9/_-]/g, '').replace(/^\/+|\/+$/g, '') || 'after-sale'
|
const safeFolder = (folder?: string) => String(folder || 'after-sale').replace(/[^a-zA-Z0-9/_-]/g, '').replace(/^\/+|\/+$/g, '') || 'after-sale'
|
||||||
|
|
||||||
const presign = (method: 'GET', objectName: string, expires = 3600) => {
|
const presign = (method: 'GET', objectName: string, expires = 3600, bucket = minioConfig.bucket) => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const date = shortDate(now)
|
const date = shortDate(now)
|
||||||
const fullDate = amzDate(now)
|
const fullDate = amzDate(now)
|
||||||
const credentialScope = `${date}/${minioConfig.region}/s3/aws4_request`
|
const credentialScope = `${date}/${minioConfig.region}/s3/aws4_request`
|
||||||
const endpoint = objectUrl(objectName)
|
const endpoint = objectUrl(objectName, bucket)
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||||
'X-Amz-Credential': `${minioConfig.accessKey}/${credentialScope}`,
|
'X-Amz-Credential': `${minioConfig.accessKey}/${credentialScope}`,
|
||||||
@@ -147,8 +148,15 @@ export const minioUploadService = {
|
|||||||
return serviceFail(`图片上传失败:${response.status}`, 500, 'MINIO_UPLOAD_FAILED')
|
return serviceFail(`图片上传失败:${response.status}`, 500, 'MINIO_UPLOAD_FAILED')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileId = crypto.randomUUID()
|
||||||
|
await prisma.supplierFile.create({
|
||||||
|
data: { fileId, fileName, objectName, bucketName: minioConfig.bucket, contentType, fileSize: body.length }
|
||||||
|
})
|
||||||
|
const fileUrl = `/api/file?fid=${encodeURIComponent(fileId)}`
|
||||||
const previewUrl = presign('GET', objectName)
|
const previewUrl = presign('GET', objectName)
|
||||||
return serviceOk({
|
return serviceOk({
|
||||||
|
fileId,
|
||||||
|
fileUrl,
|
||||||
objectName,
|
objectName,
|
||||||
fileName,
|
fileName,
|
||||||
contentType,
|
contentType,
|
||||||
@@ -158,6 +166,16 @@ export const minioUploadService = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async imageUrl(fileId: string) {
|
||||||
|
if (!ensureConfig()) return serviceFail('MinIO配置不完整', 500, 'MINIO_CONFIG_MISSING')
|
||||||
|
if (!fileId) return serviceFail('文件ID不能为空', 400, 'FILE_ID_REQUIRED')
|
||||||
|
const file = await prisma.supplierFile.findFirst({ where: { fileId, deleted: 0 } })
|
||||||
|
if (!file || !file.contentType?.startsWith('image/')) {
|
||||||
|
return serviceFail('图片不存在', 404, 'IMAGE_NOT_FOUND')
|
||||||
|
}
|
||||||
|
return serviceOk({ url: presign('GET', file.objectName, 3600, file.bucketName) })
|
||||||
|
},
|
||||||
|
|
||||||
preview(input: PreviewInput) {
|
preview(input: PreviewInput) {
|
||||||
if (!ensureConfig()) {
|
if (!ensureConfig()) {
|
||||||
return serviceFail('MinIO配置不完整', 500, 'MINIO_CONFIG_MISSING')
|
return serviceFail('MinIO配置不完整', 500, 'MINIO_CONFIG_MISSING')
|
||||||
|
|||||||
@@ -64,14 +64,15 @@ export const orderActionService = {
|
|||||||
return serviceFail('已支付主单不能通过未支付取消接口取消', 400, 'MAIN_ORDER_ALREADY_PAID')
|
return serviceFail('已支付主单不能通过未支付取消接口取消', 400, 'MAIN_ORDER_ALREADY_PAID')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const operationTime = new Date()
|
||||||
await orderDao.transaction(async tx => {
|
await orderDao.transaction(async tx => {
|
||||||
await tx.supplierOrderMain.update({
|
await tx.supplierOrderMain.update({
|
||||||
where: { orderId: mainOrderId },
|
where: { orderId: mainOrderId },
|
||||||
data: { orderState: ORDER_STATE.CANCELLED, updateTime: new Date() }
|
data: { orderState: ORDER_STATE.CANCELLED, updateTime: operationTime }
|
||||||
})
|
})
|
||||||
await tx.supplierOrderSub.updateMany({
|
await tx.supplierOrderSub.updateMany({
|
||||||
where: { mainOrderId, supplierId, userId, deleted: 0 },
|
where: { mainOrderId, supplierId, userId, deleted: 0 },
|
||||||
data: { orderState: ORDER_STATE.CANCELLED, updateTime: new Date() }
|
data: { orderState: ORDER_STATE.CANCELLED, updateTime: operationTime }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ export const orderActionService = {
|
|||||||
orderId,
|
orderId,
|
||||||
mainOrderId,
|
mainOrderId,
|
||||||
userId,
|
userId,
|
||||||
|
operationTime: toDateTimeText(operationTime),
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -112,14 +114,15 @@ export const orderActionService = {
|
|||||||
return serviceFail('该主单已拆单,不能按已支付未拆单整单取消', 400, 'MAIN_ORDER_ALREADY_SPLIT')
|
return serviceFail('该主单已拆单,不能按已支付未拆单整单取消', 400, 'MAIN_ORDER_ALREADY_SPLIT')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const operationTime = new Date()
|
||||||
await orderDao.transaction(async tx => {
|
await orderDao.transaction(async tx => {
|
||||||
await tx.supplierOrderMain.update({
|
await tx.supplierOrderMain.update({
|
||||||
where: { orderId: mainOrderId },
|
where: { orderId: mainOrderId },
|
||||||
data: { orderState: ORDER_STATE.CANCELLED, updateTime: new Date() }
|
data: { orderState: ORDER_STATE.CANCELLED, updateTime: operationTime }
|
||||||
})
|
})
|
||||||
await tx.supplierOrderSub.updateMany({
|
await tx.supplierOrderSub.updateMany({
|
||||||
where: { mainOrderId, supplierId, userId, deleted: 0 },
|
where: { mainOrderId, supplierId, userId, deleted: 0 },
|
||||||
data: { orderState: ORDER_STATE.CANCELLED, updateTime: new Date() }
|
data: { orderState: ORDER_STATE.CANCELLED, updateTime: operationTime }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -128,6 +131,7 @@ export const orderActionService = {
|
|||||||
orderId,
|
orderId,
|
||||||
mainOrderId,
|
mainOrderId,
|
||||||
userId,
|
userId,
|
||||||
|
operationTime: toDateTimeText(operationTime),
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -153,10 +157,11 @@ export const orderActionService = {
|
|||||||
|
|
||||||
const mainOrderId = input.mainOrderId || subOrder.mainOrderId
|
const mainOrderId = input.mainOrderId || subOrder.mainOrderId
|
||||||
|
|
||||||
|
const operationTime = new Date()
|
||||||
await orderDao.transaction(async tx => {
|
await orderDao.transaction(async tx => {
|
||||||
await tx.supplierOrderSub.update({
|
await tx.supplierOrderSub.update({
|
||||||
where: { orderId },
|
where: { orderId },
|
||||||
data: { orderState: ORDER_STATE.CANCELLED, updateTime: new Date() }
|
data: { orderState: ORDER_STATE.CANCELLED, updateTime: operationTime }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -165,6 +170,7 @@ export const orderActionService = {
|
|||||||
orderId,
|
orderId,
|
||||||
mainOrderId,
|
mainOrderId,
|
||||||
userId,
|
userId,
|
||||||
|
operationTime: toDateTimeText(operationTime),
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -198,9 +204,9 @@ export const orderActionService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payTime = new Date()
|
const payTime = new Date()
|
||||||
await orderDao.transaction(async tx => {
|
const paid = await orderDao.transaction(async tx => {
|
||||||
await tx.supplierOrderMain.update({
|
const changed = await tx.supplierOrderMain.updateMany({
|
||||||
where: { orderId },
|
where: { orderId, supplierId, deleted: 0, orderState: ORDER_STATE.PENDING_PAY },
|
||||||
data: {
|
data: {
|
||||||
orderState: ORDER_STATE.FINISH_PAY,
|
orderState: ORDER_STATE.FINISH_PAY,
|
||||||
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
|
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
|
||||||
@@ -211,11 +217,38 @@ export const orderActionService = {
|
|||||||
updateTime: payTime
|
updateTime: payTime
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (changed.count !== 1) return false
|
||||||
await tx.supplierOrderSub.updateMany({
|
await tx.supplierOrderSub.updateMany({
|
||||||
where: { mainOrderId: orderId, deleted: 0 },
|
where: { mainOrderId: orderId, deleted: 0 },
|
||||||
data: { orderState: ORDER_STATE.FINISH_PAY, payTime, updateTime: payTime }
|
data: { orderState: ORDER_STATE.FINISH_PAY, payTime, updateTime: payTime }
|
||||||
})
|
})
|
||||||
|
const products = await tx.supplierOrderProduct.findMany({
|
||||||
|
where: { deleted: 0, subOrder: { mainOrderId: orderId, supplierId, deleted: 0 } },
|
||||||
|
select: { sku: true, number: true }
|
||||||
})
|
})
|
||||||
|
const quantities = new Map<string, number>()
|
||||||
|
products.forEach(product => {
|
||||||
|
if (product.number > 0) quantities.set(product.sku, (quantities.get(product.sku) || 0) + product.number)
|
||||||
|
})
|
||||||
|
// 锁定商品行后累加销量,与首次支付状态变更一起提交,避免并发丢失或重复累计。
|
||||||
|
for (const sku of [...quantities.keys()].sort()) {
|
||||||
|
const rows = await tx.$queryRaw<{ id: number; sales: string | null }[]>`
|
||||||
|
SELECT id, sales FROM supplier_product WHERE sku = ${sku} FOR UPDATE
|
||||||
|
`
|
||||||
|
const product = rows[0]
|
||||||
|
if (!product) continue
|
||||||
|
const text = String(product.sales || '').trim().replace(/,/g, '')
|
||||||
|
const match = text.match(/^(\d+(?:\.\d+)?)\s*([万千])?(?:件)?\+?$/)
|
||||||
|
if (text && !match) throw new Error(`商品销量格式不正确:${sku}`)
|
||||||
|
const sales = match ? Math.floor(Number(match[1]) * (match[2] === '万' ? 10000 : match[2] === '千' ? 1000 : 1)) : 0
|
||||||
|
await tx.supplierProduct.update({
|
||||||
|
where: { id: product.id },
|
||||||
|
data: { sales: String(sales + quantities.get(sku)!) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if (!paid) return serviceFail('当前订单状态不可支付,请刷新订单状态', 400, 'ORDER_NOT_PAYABLE')
|
||||||
|
|
||||||
const confirm = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/confirmPurchase', {
|
const confirm = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, '/pointOrder/confirmPurchase', {
|
||||||
orderId,
|
orderId,
|
||||||
@@ -259,6 +292,7 @@ export const orderActionService = {
|
|||||||
orderId: subOrderIds,
|
orderId: subOrderIds,
|
||||||
mainOrderId: orderId,
|
mainOrderId: orderId,
|
||||||
userId: order.userId,
|
userId: order.userId,
|
||||||
|
operationTime: toDateTimeText(payTime),
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
})
|
})
|
||||||
if (!split.response.ok || split.payload.resultCode !== 0) {
|
if (!split.response.ok || split.payload.resultCode !== 0) {
|
||||||
@@ -376,6 +410,7 @@ export const orderActionService = {
|
|||||||
deliveryTime: toDateTimeText(now),
|
deliveryTime: toDateTimeText(now),
|
||||||
logisticsOrderId,
|
logisticsOrderId,
|
||||||
crrgBsnNm,
|
crrgBsnNm,
|
||||||
|
operationTime: toDateTimeText(now),
|
||||||
time: Date.now()
|
time: Date.now()
|
||||||
})
|
})
|
||||||
if (!ship.response.ok || ship.payload.resultCode !== 0) {
|
if (!ship.response.ok || ship.payload.resultCode !== 0) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type OrderPushEventInput = {
|
|||||||
mainOrderId?: string
|
mainOrderId?: string
|
||||||
subOrderId?: string
|
subOrderId?: string
|
||||||
time?: number
|
time?: number
|
||||||
|
operationTime?: string
|
||||||
amount?: number
|
amount?: number
|
||||||
reason?: string
|
reason?: string
|
||||||
settlementId?: string
|
settlementId?: string
|
||||||
@@ -61,12 +62,18 @@ const operateType = (orderType: number) => {
|
|||||||
return orderType
|
return orderType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toDateTimeText = (date: Date) => {
|
||||||
|
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 buildBridgeBody = (body: OrderPushEventInput) => ({
|
const buildBridgeBody = (body: OrderPushEventInput) => ({
|
||||||
supplierId: String(body.supplierId || '').trim(),
|
supplierId: String(body.supplierId || '').trim(),
|
||||||
orderType: Number(body.orderType),
|
orderType: Number(body.orderType),
|
||||||
orderId: String(body.orderId || '').trim(),
|
orderId: String(body.orderId || '').trim(),
|
||||||
mainOrderId: String(body.mainOrderId || '').trim(),
|
mainOrderId: String(body.mainOrderId || '').trim(),
|
||||||
userId: String(body.userId || '').trim(),
|
userId: String(body.userId || '').trim(),
|
||||||
|
operationTime: body.operationTime,
|
||||||
time: Number(body.time || Date.now())
|
time: Number(body.time || Date.now())
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -87,8 +94,17 @@ export const orderPushEventService = {
|
|||||||
return serviceFail('暂不支持该订单事件类型', 400, 'ORDER_EVENT_NOT_SUPPORTED')
|
return serviceFail('暂不支持该订单事件类型', 400, 'ORDER_EVENT_NOT_SUPPORTED')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 操作时间在业务处理时确定,写入事件及推送记录;不得由接口 time 推导。
|
||||||
|
const operationTime = body.operationTime ?? toDateTimeText(new Date())
|
||||||
|
if (typeof operationTime !== 'string' || !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(operationTime)) {
|
||||||
|
return serviceFail('业务操作时间格式必须为 yyyy-MM-dd HH:mm:ss')
|
||||||
|
}
|
||||||
|
const now = new Date(operationTime.replace(' ', 'T'))
|
||||||
|
if (Number.isNaN(now.getTime()) || toDateTimeText(now) !== operationTime) {
|
||||||
|
return serviceFail('业务操作时间无效')
|
||||||
|
}
|
||||||
|
body = { ...body, operationTime }
|
||||||
const bridgeBody = buildBridgeBody(body)
|
const bridgeBody = buildBridgeBody(body)
|
||||||
const now = new Date()
|
|
||||||
let pushLogId = 0
|
let pushLogId = 0
|
||||||
let affectedSubOrderIds: string[] = []
|
let affectedSubOrderIds: string[] = []
|
||||||
|
|
||||||
@@ -190,7 +206,6 @@ export const orderPushEventService = {
|
|||||||
invoiceStatus: 2,
|
invoiceStatus: 2,
|
||||||
fileId: body.fileId || null,
|
fileId: body.fileId || null,
|
||||||
filePath: body.filePath || null,
|
filePath: body.filePath || null,
|
||||||
requestBody: body,
|
|
||||||
deleted: 0
|
deleted: 0
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ const toProductItem = (product: ProductRecord) => {
|
|||||||
categoryId: product.categoryId || '',
|
categoryId: product.categoryId || '',
|
||||||
category: product.category || '',
|
category: product.category || '',
|
||||||
tag: product.tag || '',
|
tag: product.tag || '',
|
||||||
sales: product.sales || '',
|
sales: product.sales?.trim() || '0',
|
||||||
stock: product.stock
|
stock: product.stock,
|
||||||
|
sortOrder: product.sortOrder,
|
||||||
|
enabled: product.enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,5 +66,81 @@ export const productService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return serviceOk(toProductItem(product))
|
return serviceOk(toProductItem(product))
|
||||||
|
},
|
||||||
|
|
||||||
|
async listManageProducts(keyword: string, categoryId: string, requestedPageNumber = 1, requestedPageSize = 10) {
|
||||||
|
const pageNumber = Number.isInteger(Number(requestedPageNumber)) && Number(requestedPageNumber) > 0 ? Number(requestedPageNumber) : 1
|
||||||
|
const pageSize = Number.isInteger(Number(requestedPageSize)) && Number(requestedPageSize) > 0 ? Math.min(Number(requestedPageSize), 100) : 10
|
||||||
|
const { products, totalCount } = await productDao.findManageProducts(keyword, categoryId, pageNumber, pageSize)
|
||||||
|
return serviceOk({ categories, products: products.map(product => ({ ...toProductItem(product), description: product.description || '', detail: product.detail || '' })), totalCount, pageNumber, pageSize })
|
||||||
|
},
|
||||||
|
|
||||||
|
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、名称和价格不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageUrl = String(input.imageUrl || '').trim()
|
||||||
|
if (imageUrl) {
|
||||||
|
const image = new URL(imageUrl, 'http://supplier.local')
|
||||||
|
if ((image.pathname.endsWith('/api/file') || image.pathname.endsWith('/api/upload-preview')) && image.searchParams.get('fid')) {
|
||||||
|
imageUrl = `/api/file?fid=${encodeURIComponent(image.searchParams.get('fid')!)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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: imageUrl || 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: '0',
|
||||||
|
sortOrder: Math.max(Number(input.sortOrder || 0), 0),
|
||||||
|
enabled: input.enabled !== false
|
||||||
|
}
|
||||||
|
const id = Number(input.id || 0)
|
||||||
|
const updateData: Partial<typeof data> = { ...data }
|
||||||
|
for (const field of Object.keys(data) as Array<keyof typeof data>) {
|
||||||
|
if (input[field] === undefined) delete updateData[field]
|
||||||
|
}
|
||||||
|
delete updateData.sales
|
||||||
|
const product = id > 0 ? await productDao.updateProduct(id, updateData) : 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,258 @@
|
|||||||
|
import { 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?: string | 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 = {
|
||||||
|
pageNumber?: number
|
||||||
|
pageSize?: number
|
||||||
|
supplierId?: string
|
||||||
|
reconciliationStatus?: number
|
||||||
|
orderId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReconciliationResultUpdateInput = {
|
||||||
|
supplierId?: string
|
||||||
|
orderId?: string
|
||||||
|
reconciliationStatus?: number
|
||||||
|
diffType?: string | 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
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecimalValue = { toFixed: (fractionDigits?: number) => string } | number | string
|
||||||
|
|
||||||
|
type SupplierReconciliationOrderRecord = {
|
||||||
|
supplierId: string
|
||||||
|
orderId: string
|
||||||
|
mainOrderId: string | null
|
||||||
|
userId: string | null
|
||||||
|
reconciliationStatus: number
|
||||||
|
diffType: string | null
|
||||||
|
diffReason: string | null
|
||||||
|
platformTotalPrice: DecimalValue | null
|
||||||
|
platformCashAmount: DecimalValue | null
|
||||||
|
platformPointsPrice: DecimalValue | null
|
||||||
|
platformRefundPrice: DecimalValue | null
|
||||||
|
supplierTotalPrice: DecimalValue | null
|
||||||
|
supplierCashAmount: DecimalValue | null
|
||||||
|
supplierPointsPrice: DecimalValue | null
|
||||||
|
supplierRefundPrice: DecimalValue | null
|
||||||
|
reconciliationBatchNo: string | null
|
||||||
|
reconciliationTime: Date | null
|
||||||
|
updateAt: Date
|
||||||
|
deleted?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatAmount = (value: DecimalValue | null) => {
|
||||||
|
if (value === undefined || value === null) return ''
|
||||||
|
if (typeof value === 'object') return value.toFixed(2)
|
||||||
|
const numberValue = Number(value)
|
||||||
|
return Number.isNaN(numberValue) ? '' : numberValue.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultItem = (record: SupplierReconciliationOrderRecord) => ({
|
||||||
|
supplierId: record.supplierId,
|
||||||
|
orderId: record.orderId,
|
||||||
|
mainOrderId: record.mainOrderId || '',
|
||||||
|
userId: record.userId || '',
|
||||||
|
reconciliationStatus: record.reconciliationStatus,
|
||||||
|
diffType: record.diffType,
|
||||||
|
diffReason: record.diffReason || '',
|
||||||
|
platformTotalPrice: formatAmount(record.platformTotalPrice),
|
||||||
|
platformCashAmount: formatAmount(record.platformCashAmount),
|
||||||
|
platformPointsPrice: formatAmount(record.platformPointsPrice),
|
||||||
|
platformRefundPrice: formatAmount(record.platformRefundPrice),
|
||||||
|
supplierTotalPrice: formatAmount(record.supplierTotalPrice),
|
||||||
|
supplierCashAmount: formatAmount(record.supplierCashAmount),
|
||||||
|
supplierPointsPrice: formatAmount(record.supplierPointsPrice),
|
||||||
|
supplierRefundPrice: formatAmount(record.supplierRefundPrice),
|
||||||
|
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 : String(record.diffType).trim() || null,
|
||||||
|
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: JSON.stringify(record),
|
||||||
|
deleted: 0
|
||||||
|
}
|
||||||
|
return prisma.$executeRaw`
|
||||||
|
INSERT INTO supplier_reconciliation_order
|
||||||
|
(supplierId, orderId, mainOrderId, userId, reconciliationStatus, diffType, diffReason,
|
||||||
|
platformTotalPrice, platformCashAmount, platformPointsPrice, platformRefundPrice,
|
||||||
|
supplierTotalPrice, supplierCashAmount, supplierPointsPrice, supplierRefundPrice,
|
||||||
|
reconciliationBatchNo, reconciliationTime, resultPayload, deleted, createAt, updateAt)
|
||||||
|
VALUES
|
||||||
|
(${supplierId}, ${orderId}, ${data.mainOrderId}, ${data.userId}, ${data.reconciliationStatus}, ${data.diffType}, ${data.diffReason},
|
||||||
|
${data.platformTotalPrice}, ${data.platformCashAmount}, ${data.platformPointsPrice}, ${data.platformRefundPrice},
|
||||||
|
${data.supplierTotalPrice}, ${data.supplierCashAmount}, ${data.supplierPointsPrice}, ${data.supplierRefundPrice},
|
||||||
|
${data.reconciliationBatchNo}, ${data.reconciliationTime}, ${data.resultPayload}, ${data.deleted}, CURRENT_TIMESTAMP(3), CURRENT_TIMESTAMP(3))
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
mainOrderId = VALUES(mainOrderId),
|
||||||
|
userId = VALUES(userId),
|
||||||
|
reconciliationStatus = VALUES(reconciliationStatus),
|
||||||
|
diffType = VALUES(diffType),
|
||||||
|
diffReason = VALUES(diffReason),
|
||||||
|
platformTotalPrice = VALUES(platformTotalPrice),
|
||||||
|
platformCashAmount = VALUES(platformCashAmount),
|
||||||
|
platformPointsPrice = VALUES(platformPointsPrice),
|
||||||
|
platformRefundPrice = VALUES(platformRefundPrice),
|
||||||
|
supplierTotalPrice = VALUES(supplierTotalPrice),
|
||||||
|
supplierCashAmount = VALUES(supplierCashAmount),
|
||||||
|
supplierPointsPrice = VALUES(supplierPointsPrice),
|
||||||
|
supplierRefundPrice = VALUES(supplierRefundPrice),
|
||||||
|
reconciliationBatchNo = VALUES(reconciliationBatchNo),
|
||||||
|
reconciliationTime = VALUES(reconciliationTime),
|
||||||
|
resultPayload = VALUES(resultPayload),
|
||||||
|
deleted = VALUES(deleted),
|
||||||
|
updateAt = CURRENT_TIMESTAMP(3)
|
||||||
|
`
|
||||||
|
}))
|
||||||
|
|
||||||
|
return { resultCode: 0, message: '成功' }
|
||||||
|
},
|
||||||
|
|
||||||
|
async list(input: ReconciliationResultListInput) {
|
||||||
|
const supplierId = String(input.supplierId || '').trim()
|
||||||
|
const reconciliationStatus = input.reconciliationStatus === undefined ? undefined : Number(input.reconciliationStatus)
|
||||||
|
if (reconciliationStatus !== undefined && ![0, 1, 2].includes(reconciliationStatus)) {
|
||||||
|
return serviceFail('对账状态不正确')
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderId = String(input.orderId || '').trim()
|
||||||
|
const pageNumber = Number.isInteger(Number(input.pageNumber)) && Number(input.pageNumber) > 0 ? Number(input.pageNumber) : 1
|
||||||
|
const pageSize = Number.isInteger(Number(input.pageSize)) && Number(input.pageSize) > 0 ? Math.min(Number(input.pageSize), 100) : 10
|
||||||
|
const condition = Prisma.sql`
|
||||||
|
WHERE deleted = 0
|
||||||
|
${supplierId ? Prisma.sql`AND supplierId = ${supplierId}` : Prisma.empty}
|
||||||
|
${reconciliationStatus === undefined ? Prisma.empty : Prisma.sql`AND reconciliationStatus = ${reconciliationStatus}`}
|
||||||
|
${orderId ? Prisma.sql`AND orderId LIKE ${`%${orderId}%`}` : Prisma.empty}
|
||||||
|
`
|
||||||
|
const totals = await prisma.$queryRaw<{ totalCount: bigint }[]>`
|
||||||
|
SELECT COUNT(*) AS totalCount FROM supplier_reconciliation_order ${condition}
|
||||||
|
`
|
||||||
|
const records = await prisma.$queryRaw<SupplierReconciliationOrderRecord[]>`
|
||||||
|
SELECT supplierId, orderId, mainOrderId, userId, reconciliationStatus, diffType, diffReason,
|
||||||
|
platformTotalPrice, platformCashAmount, platformPointsPrice, platformRefundPrice,
|
||||||
|
supplierTotalPrice, supplierCashAmount, supplierPointsPrice, supplierRefundPrice,
|
||||||
|
reconciliationBatchNo, reconciliationTime, updateAt
|
||||||
|
FROM supplier_reconciliation_order
|
||||||
|
${condition}
|
||||||
|
ORDER BY updateAt DESC, supplierId, orderId
|
||||||
|
LIMIT ${pageSize} OFFSET ${(pageNumber - 1) * pageSize}
|
||||||
|
`
|
||||||
|
return serviceOk({ reconciliationItems: records.map(resultItem), totalCount: Number(totals[0]?.totalCount || 0), pageNumber, pageSize })
|
||||||
|
},
|
||||||
|
|
||||||
|
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 records = await prisma.$queryRaw<SupplierReconciliationOrderRecord[]>`
|
||||||
|
SELECT supplierId, orderId, mainOrderId, userId, reconciliationStatus, diffType, diffReason,
|
||||||
|
platformTotalPrice, platformCashAmount, platformPointsPrice, platformRefundPrice,
|
||||||
|
supplierTotalPrice, supplierCashAmount, supplierPointsPrice, supplierRefundPrice,
|
||||||
|
reconciliationBatchNo, reconciliationTime, updateAt, deleted
|
||||||
|
FROM supplier_reconciliation_order
|
||||||
|
WHERE supplierId = ${supplierId}
|
||||||
|
AND orderId = ${orderId}
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
const record = records[0]
|
||||||
|
if (!record || record.deleted !== 0) return serviceFail('对账结果不存在')
|
||||||
|
|
||||||
|
const diffType = input.diffType === undefined || input.diffType === null ? null : String(input.diffType).trim() || null
|
||||||
|
const diffReason = String(input.diffReason || '').trim() || null
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE supplier_reconciliation_order
|
||||||
|
SET reconciliationStatus = ${reconciliationStatus},
|
||||||
|
diffType = ${diffType},
|
||||||
|
diffReason = ${diffReason},
|
||||||
|
updateAt = CURRENT_TIMESTAMP(3)
|
||||||
|
WHERE supplierId = ${supplierId}
|
||||||
|
AND orderId = ${orderId}
|
||||||
|
`
|
||||||
|
return serviceOk({})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { randomBytes, randomInt } from 'node:crypto'
|
||||||
|
import { suppliedUserDao } from '../dao/supplied-user.dao.js'
|
||||||
|
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||||
|
|
||||||
|
export const suppliedUserService = {
|
||||||
|
async getSessionUser(userId: string) {
|
||||||
|
const virtualId = userId.trim()
|
||||||
|
if (!virtualId || virtualId.length > 191) return serviceFail('用户虚拟 ID 无效')
|
||||||
|
const letters = 'abcdefghijklmnopqrstuvwxyz'
|
||||||
|
const nickname = `用户${randomInt(10)}${letters[randomInt(letters.length)]}${randomBytes(4).toString('hex')}`
|
||||||
|
const user = await suppliedUserDao.findOrCreateByVirtualId(virtualId, nickname)
|
||||||
|
if (!user.enabled) return serviceFail('当前用户已停用', 403, 'USER_DISABLED')
|
||||||
|
return serviceOk({
|
||||||
|
userId: user.virtualId,
|
||||||
|
userInfo: { id: user.id, virtualId: user.virtualId, nickname: user.nickname }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Client } from 'minio'
|
||||||
|
import { statSync } from 'node:fs'
|
||||||
|
import { prisma } from '../utils/prisma.js'
|
||||||
|
|
||||||
|
const endpoint = new URL(process.env.MINIO_ENDPOINT || 'http://localhost:9000')
|
||||||
|
const bucketName = process.env.MINIO_BUCKET || 'unions'
|
||||||
|
const client = new Client({
|
||||||
|
endPoint: endpoint.hostname,
|
||||||
|
port: Number(endpoint.port || (endpoint.protocol === 'https:' ? 443 : 80)),
|
||||||
|
useSSL: endpoint.protocol === 'https:',
|
||||||
|
accessKey: process.env.MINIO_ACCESS_KEY || '',
|
||||||
|
secretKey: process.env.MINIO_SECRET_KEY || '',
|
||||||
|
region: process.env.MINIO_REGION || 'us-east-1'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ensureBucket = async () => {
|
||||||
|
if (!(await client.bucketExists(bucketName))) {
|
||||||
|
await client.makeBucket(bucketName, process.env.MINIO_REGION || 'us-east-1')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamToBuffer = async (stream: NodeJS.ReadableStream) => {
|
||||||
|
const chunks: Buffer[] = []
|
||||||
|
for await (const chunk of stream as AsyncIterable<Buffer>) chunks.push(Buffer.from(chunk))
|
||||||
|
return Buffer.concat(chunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supplierFileService = {
|
||||||
|
async uploadPdf(fileId: string, supplierId: string, fileName: string, filePath: string) {
|
||||||
|
await ensureBucket()
|
||||||
|
const now = new Date()
|
||||||
|
const pad = (value: number) => String(value).padStart(2, '0')
|
||||||
|
const datePath = `${now.getFullYear()}/${pad(now.getMonth() + 1)}/${pad(now.getDate())}`
|
||||||
|
const objectName = `supplied/invoices/${datePath}/${supplierId}/${fileName}`
|
||||||
|
const stat = statSync(filePath)
|
||||||
|
await client.fPutObject(bucketName, objectName, filePath, {
|
||||||
|
'Content-Type': 'application/pdf'
|
||||||
|
})
|
||||||
|
await prisma.supplierFile.upsert({
|
||||||
|
where: { fileId },
|
||||||
|
update: { supplierId, fileName, objectName, bucketName, contentType: 'application/pdf', fileSize: stat.size, deleted: 0 },
|
||||||
|
create: { fileId, supplierId, fileName, objectName, bucketName, contentType: 'application/pdf', fileSize: stat.size }
|
||||||
|
})
|
||||||
|
return { fileId, objectName }
|
||||||
|
},
|
||||||
|
|
||||||
|
async pdfUrl(fileId: string, supplierId: string, download = false) {
|
||||||
|
const file = await prisma.supplierFile.findFirst({ where: { fileId, supplierId, deleted: 0, contentType: 'application/pdf' } })
|
||||||
|
if (!file) return null
|
||||||
|
return client.presignedGetObject(file.bucketName, file.objectName, 3600, {
|
||||||
|
'response-content-type': 'application/pdf',
|
||||||
|
'response-content-disposition': `${download ? 'attachment' : 'inline'}; filename*=UTF-8''${encodeURIComponent(file.fileName)}`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async read(fileId: string, supplierId: string) {
|
||||||
|
const file = await prisma.supplierFile.findFirst({ where: { fileId, supplierId, deleted: 0 } })
|
||||||
|
if (!file) return null
|
||||||
|
const stream = await client.getObject(file.bucketName, file.objectName)
|
||||||
|
return streamToBuffer(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { existsSync, readFileSync, unlinkSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
import { supplierDao } from '../dao/supplier.dao.js'
|
import { supplierDao } from '../dao/supplier.dao.js'
|
||||||
import { orderDao } from '../dao/order.dao.js'
|
import { orderDao } from '../dao/order.dao.js'
|
||||||
import { ORDER_EVENT_TYPE, ORDER_STATE } from '../constants/order.js'
|
import { ORDER_EVENT_TYPE, ORDER_STATE } from '../constants/order.js'
|
||||||
|
import { orderPushEventService } from './order-push-event.service.js'
|
||||||
import { logger } from '../utils/logger.js'
|
import { logger } from '../utils/logger.js'
|
||||||
import { sm4CbcDecrypt, verifySupplierOpenSignature } from '../utils/supplier-open-crypto.js'
|
import { sm4CbcDecrypt, verifySupplierOpenSignature } from '../utils/supplier-open-crypto.js'
|
||||||
|
import { generateMockInvoicePdf } from '../../tools/mock-invoice-pdf.js'
|
||||||
|
import { supplierFileService } from './supplier-file.service.js'
|
||||||
|
import { minioUploadService } from './minio-upload.service.js'
|
||||||
|
|
||||||
export type SupplierOrderQueryRequest = {
|
export type SupplierOrderQueryRequest = {
|
||||||
supplierId?: string
|
supplierId?: string
|
||||||
@@ -11,18 +18,90 @@ export type SupplierOrderQueryRequest = {
|
|||||||
orderId?: string
|
orderId?: string
|
||||||
timestamp?: number
|
timestamp?: number
|
||||||
AplParmObjct?: string
|
AplParmObjct?: string
|
||||||
|
RsrvFld1?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseBody = (body: SupplierOrderQueryRequest) => {
|
export type SupplierReconciliationQueryRequest = {
|
||||||
if (!body.AplParmObjct) return body
|
supplierId?: string
|
||||||
|
token?: string
|
||||||
|
reconciliationType?: number
|
||||||
|
orderId?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
timestamp?: number
|
||||||
|
AplParmObjct?: string
|
||||||
|
RsrvFld1?: string
|
||||||
|
}
|
||||||
|
|
||||||
const keyHex = process.env.SUPPLIER_OPEN_SM4_KEY_HEX || process.env.SM4CBC_KEY_HEX || ''
|
export type SupplierInvoiceApplyRequest = {
|
||||||
const ivHex = process.env.SUPPLIER_OPEN_SM4_IV_HEX || process.env.SM4CBC_IV_HEX || ''
|
supplierId?: string
|
||||||
|
token?: string
|
||||||
|
settlementId?: string
|
||||||
|
orderIds?: string
|
||||||
|
orderNum?: number
|
||||||
|
orderTotalPrice?: number | string
|
||||||
|
invoiceDate?: string | number
|
||||||
|
invoiceTitle?: string
|
||||||
|
taxIdNumber?: string
|
||||||
|
taxIDNumber?: string
|
||||||
|
invoiceAddress?: string
|
||||||
|
invoiceContact?: string
|
||||||
|
invoiceNakedPrice?: number | string
|
||||||
|
invoiceTaxPrice?: number | string
|
||||||
|
invoiceType?: string
|
||||||
|
invoiceContent?: string
|
||||||
|
remark?: string
|
||||||
|
bankAccount?: string
|
||||||
|
registeredAddress?: string
|
||||||
|
bankName?: string
|
||||||
|
timestamp?: number
|
||||||
|
AplParmObjct?: string
|
||||||
|
RsrvFld1?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SupplierFileBase64QueryRequest = {
|
||||||
|
supplierId?: string
|
||||||
|
token?: string
|
||||||
|
queryType?: string
|
||||||
|
idNumber?: string
|
||||||
|
fileIdList?: string[]
|
||||||
|
timestamp?: number
|
||||||
|
ApiParamObject?: string
|
||||||
|
RsrvFld1?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SupplierInvoiceQueryRequest = {
|
||||||
|
retry?: boolean
|
||||||
|
supplierId?: string
|
||||||
|
token?: string
|
||||||
|
settlementId?: string
|
||||||
|
timestamp?: number
|
||||||
|
AplParmObjct?: string
|
||||||
|
RsrvFld1?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupplierOpenEncryptedRequest = {
|
||||||
|
AplParmObjct?: string
|
||||||
|
ApiParamObject?: string
|
||||||
|
RsrvFld1?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseBody = <T extends SupplierOpenEncryptedRequest>(body: T) => {
|
||||||
|
const encryptedData = body.AplParmObjct || body.ApiParamObject
|
||||||
|
if (!encryptedData) return body
|
||||||
|
|
||||||
|
const keyHex = process.env.SM4CBC_KEY_HEX || process.env.SUPPLIER_OPEN_SM4_KEY_HEX || ''
|
||||||
|
const ivHex = process.env.SM4CBC_IV_HEX || process.env.SUPPLIER_OPEN_SM4_IV_HEX || ''
|
||||||
if (!keyHex || !ivHex) {
|
if (!keyHex || !ivHex) {
|
||||||
throw new Error('供应商开放接口SM4配置缺失')
|
throw new Error('供应商开放接口SM4配置缺失')
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(sm4CbcDecrypt(body.AplParmObjct, keyHex, ivHex)) as SupplierOrderQueryRequest
|
return {
|
||||||
|
...JSON.parse(sm4CbcDecrypt(encryptedData, keyHex, ivHex)),
|
||||||
|
RsrvFld1: body.RsrvFld1
|
||||||
|
} as T
|
||||||
}
|
}
|
||||||
|
|
||||||
const toText = (date?: Date | null) => {
|
const toText = (date?: Date | null) => {
|
||||||
@@ -33,6 +112,42 @@ const toText = (date?: Date | null) => {
|
|||||||
|
|
||||||
const toMoney = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => Number(value || 0).toFixed(2)
|
const toMoney = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => Number(value || 0).toFixed(2)
|
||||||
|
|
||||||
|
const numericId = (seed: string, length: number) => BigInt(`0x${createHash('sha256').update(seed).digest('hex')}`)
|
||||||
|
.toString()
|
||||||
|
.padStart(length, '0')
|
||||||
|
.slice(0, length)
|
||||||
|
|
||||||
|
const invoiceOutputRoot = () => path.resolve(process.env.INVOICE_PDF_OUTPUT_DIR || path.join(process.cwd(), 'temp', 'pdf', 'invoices'))
|
||||||
|
|
||||||
|
const storedRequest = (value: unknown) => value && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
? value as Record<string, unknown>
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const encodeImage = async (source?: string | null) => {
|
||||||
|
let imageSource = String(source || '').trim()
|
||||||
|
if (/^\/api\/(?:file|upload-preview)\?/.test(imageSource)) {
|
||||||
|
const fileId = new URL(imageSource, 'http://supplier.local').searchParams.get('fid') || ''
|
||||||
|
const result = await minioUploadService.imageUrl(fileId)
|
||||||
|
if (!result.success) return ''
|
||||||
|
imageSource = result.data.url
|
||||||
|
}
|
||||||
|
if (!imageSource) return ''
|
||||||
|
if (imageSource.startsWith('data:')) {
|
||||||
|
return imageSource.split(',')[1] || ''
|
||||||
|
}
|
||||||
|
if (!/^https?:\/\//i.test(imageSource)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(imageSource)
|
||||||
|
if (!response.ok) return ''
|
||||||
|
return Buffer.from(await response.arrayBuffer()).toString('base64')
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('query file base64 fetch image failed', { imageSource, message: error instanceof Error ? error.message : String(error) })
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const productSku = (product: { sku: string; subOrderId: string; name: string }) => {
|
const productSku = (product: { sku: string; subOrderId: string; name: string }) => {
|
||||||
if (product.sku && product.sku !== '[object Object]') {
|
if (product.sku && product.sku !== '[object Object]') {
|
||||||
return product.sku
|
return product.sku
|
||||||
@@ -52,9 +167,392 @@ const toOrderProductInfo = (order: Awaited<ReturnType<typeof orderDao.findOpenAp
|
|||||||
filePath: product.filePath
|
filePath: product.filePath
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const productImageSource = (product?: Awaited<ReturnType<typeof orderDao.findOpenApiSubOrders>>[number]['products'][number]) => {
|
||||||
|
if (!product) return ''
|
||||||
|
if (product.productImgUrl) return product.productImgUrl
|
||||||
|
if (product.filePath) return product.filePath
|
||||||
|
if (Array.isArray(product.picInfo)) return String(product.picInfo[0] || '')
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
export const supplierOpenOrderService = {
|
export const supplierOpenOrderService = {
|
||||||
parseBody,
|
parseBody,
|
||||||
|
|
||||||
|
async applyForInvoices(body: SupplierInvoiceApplyRequest, notifyPlatform = true) {
|
||||||
|
const supplierId = String(body.supplierId || '').trim()
|
||||||
|
const token = String(body.token || '').trim()
|
||||||
|
const settlementId = String(body.settlementId || '').trim()
|
||||||
|
const orderIds = [...new Set(String(body.orderIds || '')
|
||||||
|
.split(',')
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean))]
|
||||||
|
const orderNum = Number(body.orderNum || 0)
|
||||||
|
const orderTotalPrice = Number(body.orderTotalPrice)
|
||||||
|
const timestamp = Number(body.timestamp || 0)
|
||||||
|
const taxIdNumber = String(body.taxIdNumber || body.taxIDNumber || '').trim()
|
||||||
|
const invoiceDateValue = body.invoiceDate
|
||||||
|
const invoiceDate = typeof invoiceDateValue === 'number' || /^\d+$/.test(String(invoiceDateValue || ''))
|
||||||
|
? new Date(Number(invoiceDateValue))
|
||||||
|
: new Date(`${invoiceDateValue || ''}T00:00:00`)
|
||||||
|
|
||||||
|
logger.info('supplier open invoice apply', { supplierId, settlementId, orderCount: orderIds.length, timestamp })
|
||||||
|
|
||||||
|
if (!supplierId || !token || !timestamp || !settlementId || !orderIds.length) {
|
||||||
|
return { resultCode: 400, message: '供应商、结算单、订单或签名信息不能为空' }
|
||||||
|
}
|
||||||
|
if (!body.invoiceDate || !body.invoiceTitle || !taxIdNumber || !body.invoiceAddress || !body.invoiceContact) {
|
||||||
|
return { resultCode: 400, message: '开票日期、抬头、纳税人识别号、地址和联系电话不能为空' }
|
||||||
|
}
|
||||||
|
if (Number.isNaN(invoiceDate.getTime())) {
|
||||||
|
return { resultCode: 400, message: '开票日期格式不正确' }
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(orderNum) || orderNum <= 0 || !Number.isFinite(orderTotalPrice) || orderTotalPrice < 0) {
|
||||||
|
return { resultCode: 400, message: '订单数量或订单总金额不正确' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const supplier = await supplierDao.findActiveBySupplierId(supplierId)
|
||||||
|
if (!supplier) {
|
||||||
|
return { resultCode: 404, message: '供应商信息不存在' }
|
||||||
|
}
|
||||||
|
if (!verifySupplierOpenSignature(supplier.supplierId, String(body.RsrvFld1 || supplier.clientId), timestamp, token)) {
|
||||||
|
return { resultCode: 401, message: '签名无效或已过期' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const orders = await orderDao.findOpenApiSubOrders(supplierId, orderIds)
|
||||||
|
if (orders.length !== orderIds.length || orderNum !== orderIds.length) {
|
||||||
|
return { resultCode: 400, message: '订单数量校验不通过' }
|
||||||
|
}
|
||||||
|
const actualTotalPrice = orders.reduce((total, order) => total + Number(order.totalPrice || 0), 0)
|
||||||
|
if (Math.abs(actualTotalPrice - orderTotalPrice) >= 0.01) {
|
||||||
|
return { resultCode: 400, message: '订单总金额校验不通过' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderIdText = orderIds.join(',')
|
||||||
|
const firstOrder = orders[0]
|
||||||
|
const requestBody = JSON.parse(JSON.stringify(body))
|
||||||
|
await orderDao.transaction(async tx => {
|
||||||
|
await tx.supplierInvoiceInfo.upsert({
|
||||||
|
where: { invoiceBizId: settlementId },
|
||||||
|
update: {
|
||||||
|
supplierId,
|
||||||
|
userId: firstOrder?.userId || null,
|
||||||
|
mainOrderId: firstOrder?.mainOrderId || null,
|
||||||
|
subOrderId: firstOrder?.orderId || null,
|
||||||
|
settlementId,
|
||||||
|
invoiceCode: null,
|
||||||
|
taxIdNumber,
|
||||||
|
orderIds: orderIdText,
|
||||||
|
invoiceStatus: 1,
|
||||||
|
invoiceDate,
|
||||||
|
failureMessage: null,
|
||||||
|
requestBody,
|
||||||
|
deleted: 0
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
invoiceBizId: settlementId,
|
||||||
|
supplierId,
|
||||||
|
userId: firstOrder?.userId || null,
|
||||||
|
mainOrderId: firstOrder?.mainOrderId || null,
|
||||||
|
subOrderId: firstOrder?.orderId || null,
|
||||||
|
settlementId,
|
||||||
|
taxIdNumber,
|
||||||
|
orderIds: orderIdText,
|
||||||
|
invoiceStatus: 1,
|
||||||
|
invoiceDate,
|
||||||
|
requestBody
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const invoiceId = numericId(`invoice-id:${supplierId}:${settlementId}`, 20)
|
||||||
|
const invoiceCode = numericId(`invoice-code:${supplierId}:${settlementId}`, 12)
|
||||||
|
const checkCode = numericId(`check-code:${supplierId}:${settlementId}`, 20)
|
||||||
|
const taxRate = Number(orders.flatMap(order => order.products)[0]?.taxRate || 0.13)
|
||||||
|
const invoiceNakeAmount = Number(body.invoiceNakedPrice ?? orderTotalPrice / (1 + taxRate))
|
||||||
|
const invoiceTaxAmount = Number(body.invoiceTaxPrice ?? orderTotalPrice - invoiceNakeAmount)
|
||||||
|
const requestedInvoiceType = Number(body.invoiceType || 10)
|
||||||
|
const invoiceType = Number.isInteger(requestedInvoiceType) ? requestedInvoiceType : 10
|
||||||
|
const absoluteFilePath = path.join(invoiceOutputRoot(), supplierId, `${invoiceId}.pdf`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
generateMockInvoicePdf({
|
||||||
|
...body,
|
||||||
|
taxIdNumber,
|
||||||
|
settlementId,
|
||||||
|
orderIds: orderIdText,
|
||||||
|
orderNum: orders.length,
|
||||||
|
orderTotalPrice: actualTotalPrice,
|
||||||
|
invoiceContent: orders.flatMap(order => order.products.map(product => product.name)).filter(Boolean).join('、'),
|
||||||
|
orderProducts: orders.flatMap(order => order.products.map(product => ({
|
||||||
|
orderId: order.orderId,
|
||||||
|
sku: product.sku,
|
||||||
|
name: product.name,
|
||||||
|
number: product.number,
|
||||||
|
price: Number(product.price || 0)
|
||||||
|
}))),
|
||||||
|
documentInvoiceCode: invoiceCode,
|
||||||
|
invoiceNumber: invoiceId,
|
||||||
|
checkCode,
|
||||||
|
sellerName: supplier.supplierName,
|
||||||
|
sellerTaxId: supplier.supplierId,
|
||||||
|
invoiceNakedPrice: invoiceNakeAmount,
|
||||||
|
invoiceTaxPrice: invoiceTaxAmount,
|
||||||
|
taxRate
|
||||||
|
}, absoluteFilePath)
|
||||||
|
await supplierFileService.uploadPdf(invoiceId, supplierId, `${invoiceId}.pdf`, absoluteFilePath)
|
||||||
|
try {
|
||||||
|
unlinkSync(absoluteFilePath)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('supplier invoice temp file cleanup failed', { supplierId, settlementId, absoluteFilePath, error })
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainOrderIds = [...new Set(orders.map(order => order.mainOrderId).filter(Boolean))]
|
||||||
|
await orderDao.transaction(async tx => {
|
||||||
|
await tx.supplierInvoiceInfo.update({
|
||||||
|
where: { invoiceBizId: settlementId },
|
||||||
|
data: {
|
||||||
|
invoiceCode,
|
||||||
|
invoiceIds: invoiceId,
|
||||||
|
invoiceStatus: 2,
|
||||||
|
invoiceDate,
|
||||||
|
invoiceType,
|
||||||
|
invoiceNakeAmount,
|
||||||
|
invoiceTaxRate: taxRate,
|
||||||
|
invoiceTaxAmount,
|
||||||
|
invoiceAmount: orderTotalPrice,
|
||||||
|
checkCode,
|
||||||
|
fileType: 'PDF',
|
||||||
|
fileId: invoiceId,
|
||||||
|
filePath: null,
|
||||||
|
failureMessage: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await tx.supplierOrderSub.updateMany({
|
||||||
|
where: { supplierId, orderId: { in: orderIds }, deleted: 0 },
|
||||||
|
data: { invoiceCode }
|
||||||
|
})
|
||||||
|
if (mainOrderIds.length) {
|
||||||
|
await tx.supplierOrderMain.updateMany({
|
||||||
|
where: { supplierId, orderId: { in: mainOrderIds }, deleted: 0 },
|
||||||
|
data: { invoiceCode }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const failureMessage = error instanceof Error ? error.message : '模拟发票生成失败'
|
||||||
|
await orderDao.transaction(async tx => {
|
||||||
|
await tx.supplierInvoiceInfo.update({
|
||||||
|
where: { invoiceBizId: settlementId },
|
||||||
|
data: {
|
||||||
|
invoiceStatus: 3, failureMessage, invoiceIds: invoiceId, invoiceCode,
|
||||||
|
invoiceDate, invoiceType, invoiceNakeAmount, invoiceTaxRate: taxRate,
|
||||||
|
invoiceTaxAmount, invoiceAmount: orderTotalPrice, checkCode, fileType: 'PDF',
|
||||||
|
fileId: null, filePath: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
logger.error('supplier mock invoice generate failed', { supplierId, settlementId, failureMessage })
|
||||||
|
if (notifyPlatform) {
|
||||||
|
await orderPushEventService.pushEvent({
|
||||||
|
supplierId, userId: String(firstOrder?.userId || 'SYSTEM'),
|
||||||
|
orderType: ORDER_EVENT_TYPE.INVOICE_INFO, orderId: settlementId,
|
||||||
|
mainOrderId: firstOrder?.mainOrderId, settlementId, invoiceBizId: settlementId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { resultCode: 500, message: failureMessage }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notifyPlatform) {
|
||||||
|
return { resultCode: 0, message: '发票重新生成成功' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushResult = await orderPushEventService.pushEvent({
|
||||||
|
supplierId,
|
||||||
|
userId: String(firstOrder?.userId || 'SYSTEM'),
|
||||||
|
orderType: ORDER_EVENT_TYPE.INVOICE_INFO,
|
||||||
|
orderId: settlementId,
|
||||||
|
mainOrderId: firstOrder?.mainOrderId,
|
||||||
|
settlementId,
|
||||||
|
invoiceBizId: settlementId,
|
||||||
|
invoiceCode,
|
||||||
|
invoiceIds: invoiceId,
|
||||||
|
orderIds: orderIdText,
|
||||||
|
fileId: invoiceId
|
||||||
|
})
|
||||||
|
const pushSuccess = pushResult.success && pushResult.data.pushSuccess
|
||||||
|
|
||||||
|
return {
|
||||||
|
resultCode: 0,
|
||||||
|
message: pushSuccess ? '开票完成并已通知平台' : '开票完成,平台通知已记录待重试'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async queryInvoiceInfo(body: SupplierInvoiceQueryRequest) {
|
||||||
|
const supplierId = String(body.supplierId || '').trim()
|
||||||
|
const token = String(body.token || '').trim()
|
||||||
|
const settlementId = String(body.settlementId || '').trim()
|
||||||
|
const timestamp = Number(body.timestamp || 0)
|
||||||
|
|
||||||
|
if (!supplierId || !token || !timestamp || !settlementId) {
|
||||||
|
return { resultCode: 400, message: '供应商、结算单或签名信息不能为空' }
|
||||||
|
}
|
||||||
|
const supplier = await supplierDao.findActiveBySupplierId(supplierId)
|
||||||
|
if (!supplier) {
|
||||||
|
return { resultCode: 404, message: '供应商信息不存在' }
|
||||||
|
}
|
||||||
|
if (!verifySupplierOpenSignature(supplier.supplierId, String(body.RsrvFld1 || supplier.clientId), timestamp, token)) {
|
||||||
|
return { resultCode: 401, message: '签名无效或已过期' }
|
||||||
|
}
|
||||||
|
|
||||||
|
let invoice = await orderDao.findInvoiceBySettlementId(supplierId, settlementId)
|
||||||
|
if (!invoice) {
|
||||||
|
return { resultCode: 404, message: '未找到对应的开票记录' }
|
||||||
|
}
|
||||||
|
if (invoice.invoiceStatus === 1) {
|
||||||
|
return { resultCode: 409, message: '发票正在开具中' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.retry === true && invoice.invoiceStatus === 3) {
|
||||||
|
const previous = invoice
|
||||||
|
const claimed = await orderDao.transaction(tx => tx.supplierInvoiceInfo.updateMany({
|
||||||
|
where: { invoiceBizId: settlementId, supplierId, invoiceStatus: 3, deleted: 0 },
|
||||||
|
data: { invoiceStatus: 1 }
|
||||||
|
}))
|
||||||
|
if (!claimed.count) {
|
||||||
|
return { resultCode: 409, message: '发票正在重新生成,请勿重复操作' }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await this.applyForInvoices({
|
||||||
|
...storedRequest(previous.requestBody),
|
||||||
|
supplierId, settlementId, token, timestamp, RsrvFld1: body.RsrvFld1
|
||||||
|
}, false)
|
||||||
|
if (result.resultCode !== 0) {
|
||||||
|
await orderDao.transaction(tx => tx.supplierInvoiceInfo.updateMany({
|
||||||
|
where: { invoiceBizId: settlementId, supplierId, invoiceStatus: 1 },
|
||||||
|
data: { invoiceStatus: 3, failureMessage: result.message }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const failureMessage = error instanceof Error ? error.message : '重新生成发票失败'
|
||||||
|
await orderDao.transaction(tx => tx.supplierInvoiceInfo.updateMany({
|
||||||
|
where: { invoiceBizId: settlementId, supplierId, invoiceStatus: 1 },
|
||||||
|
data: { invoiceStatus: 3, failureMessage }
|
||||||
|
}))
|
||||||
|
return { resultCode: 500, message: failureMessage }
|
||||||
|
}
|
||||||
|
invoice = await orderDao.findInvoiceBySettlementId(supplierId, settlementId)
|
||||||
|
if (!invoice) return { resultCode: 404, message: '未找到对应的开票记录' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = storedRequest(invoice.requestBody)
|
||||||
|
const success = invoice.invoiceStatus === 2
|
||||||
|
return {
|
||||||
|
resultCode: 0,
|
||||||
|
message: '成功',
|
||||||
|
getInvoiceInfo: {
|
||||||
|
bSuccess: success ? 0 : 1,
|
||||||
|
settlementId,
|
||||||
|
sucOrderIds: success ? invoice.orderIds || '' : '',
|
||||||
|
failOrderIds: success ? '' : invoice.orderIds || '',
|
||||||
|
failMsg: success ? '' : invoice.failureMessage || '开票失败',
|
||||||
|
invoiceInfos: [{
|
||||||
|
status: success ? 1 : 2,
|
||||||
|
remark: String(request.remark || ''),
|
||||||
|
invoiceId: invoice.invoiceIds || numericId(`invoice-id:${supplierId}:${settlementId}`, 20),
|
||||||
|
invoiceCode: invoice.invoiceCode,
|
||||||
|
invoiceDate: invoice.invoiceDate?.toISOString().slice(0, 10),
|
||||||
|
invoiceNakeAmount: Number(invoice.invoiceNakeAmount || 0),
|
||||||
|
invoiceTaxRate: Number(invoice.invoiceTaxRate || 0),
|
||||||
|
invoiceTaxAmount: Number(invoice.invoiceTaxAmount || 0),
|
||||||
|
invoiceAmount: Number(invoice.invoiceAmount || 0),
|
||||||
|
invoiceType: invoice.invoiceType,
|
||||||
|
url: '',
|
||||||
|
imageEncode: '',
|
||||||
|
fileType: invoice.fileType || 'PDF',
|
||||||
|
checkCode: invoice.checkCode,
|
||||||
|
invoiceAddress: String(request.invoiceAddress || '')
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async queryFileBase64Info(body: SupplierFileBase64QueryRequest) {
|
||||||
|
const supplierId = String(body.supplierId || '').trim()
|
||||||
|
const token = String(body.token || '').trim()
|
||||||
|
const queryType = String(body.queryType || '').trim()
|
||||||
|
const idNumber = String(body.idNumber || '').trim()
|
||||||
|
const fileIdList = Array.isArray(body.fileIdList) ? body.fileIdList.map(item => String(item || '').trim()).filter(Boolean) : []
|
||||||
|
const timestamp = Number(body.timestamp || 0)
|
||||||
|
|
||||||
|
logger.info('supplier open file base64 query', { supplierId, queryType, idNumber, fileCount: fileIdList.length })
|
||||||
|
|
||||||
|
if (!supplierId || !token || !timestamp || !queryType || !idNumber || !fileIdList.length) {
|
||||||
|
return { resultCode: 400, message: '供应商、查询类型、单号、文件ID或签名信息不能为空', fileType: 'JPG', imageEncodeMap: {} }
|
||||||
|
}
|
||||||
|
if (!['1', '2'].includes(queryType)) {
|
||||||
|
return { resultCode: 400, message: '查询类型不正确', fileType: 'JPG', imageEncodeMap: {} }
|
||||||
|
}
|
||||||
|
const supplier = await supplierDao.findActiveBySupplierId(supplierId)
|
||||||
|
if (!supplier) {
|
||||||
|
return { resultCode: 404, message: '供应商信息不存在', fileType: 'JPG', imageEncodeMap: {} }
|
||||||
|
}
|
||||||
|
if (!verifySupplierOpenSignature(supplier.supplierId, String(body.RsrvFld1 || supplier.clientId), timestamp, token)) {
|
||||||
|
return { resultCode: 401, message: '签名无效或已过期', fileType: 'JPG', imageEncodeMap: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageEncodeMap: Record<string, string> = {}
|
||||||
|
fileIdList.forEach(fileId => {
|
||||||
|
imageEncodeMap[fileId] = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
if (queryType === '1') {
|
||||||
|
const orders = await orderDao.findOpenApiSubOrders(supplierId, [idNumber])
|
||||||
|
const products = orders.flatMap(order => order.products)
|
||||||
|
for (const fileId of fileIdList) {
|
||||||
|
const product = products.find(item => item.sku === fileId || item.fileId === fileId || item.filePath === fileId)
|
||||||
|
imageEncodeMap[fileId] = await encodeImage(productImageSource(product))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const invoice = await orderDao.findInvoiceBySettlementId(supplierId, idNumber)
|
||||||
|
const invoiceIds = String(invoice?.invoiceIds || '').split(',').map(item => item.trim()).filter(Boolean)
|
||||||
|
if (!invoice || invoice.invoiceStatus !== 2 || (!invoice.fileId && !invoice.filePath)) {
|
||||||
|
return { resultCode: 404, message: '未找到已开具的发票文件', fileType: 'PDF', imageEncodeMap }
|
||||||
|
}
|
||||||
|
if (!fileIdList.length || invoiceIds.length !== 1 || fileIdList.some(fileId => fileId !== invoiceIds[0]) || (invoice.fileId && invoice.fileId !== invoiceIds[0])) {
|
||||||
|
return { resultCode: 404, message: '发票号码与结算单不匹配', fileType: 'PDF', imageEncodeMap }
|
||||||
|
}
|
||||||
|
const pdf = invoice.fileId
|
||||||
|
? await supplierFileService.read(invoice.fileId, supplierId)
|
||||||
|
: null
|
||||||
|
if (!pdf && invoice.filePath) {
|
||||||
|
const absoluteFilePath = path.resolve(process.cwd(), invoice.filePath)
|
||||||
|
const outputRoot = invoiceOutputRoot()
|
||||||
|
const relativePath = path.relative(outputRoot, absoluteFilePath)
|
||||||
|
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath) && path.basename(absoluteFilePath) === `${invoiceIds[0]}.pdf` && existsSync(absoluteFilePath)) {
|
||||||
|
const localPdf = readFileSync(absoluteFilePath)
|
||||||
|
if (localPdf.length <= 3.5 * 1024 * 1024) {
|
||||||
|
fileIdList.forEach(fileId => {
|
||||||
|
imageEncodeMap[fileId] = localPdf.toString('base64')
|
||||||
|
})
|
||||||
|
return { resultCode: 0, message: '成功', fileType: 'PDF', imageEncodeMap }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!pdf) {
|
||||||
|
return { resultCode: 404, message: '发票文件不存在', fileType: 'PDF', imageEncodeMap }
|
||||||
|
}
|
||||||
|
if (pdf.length > 3.5 * 1024 * 1024) {
|
||||||
|
return { resultCode: 413, message: '发票文件超过Base64接口大小限制', fileType: 'PDF', imageEncodeMap }
|
||||||
|
}
|
||||||
|
const base64 = pdf.toString('base64')
|
||||||
|
fileIdList.forEach(fileId => {
|
||||||
|
imageEncodeMap[fileId] = base64
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { resultCode: 0, message: '成功', fileType: queryType === '2' ? 'PDF' : 'JPG', imageEncodeMap }
|
||||||
|
},
|
||||||
|
|
||||||
async queryOrderInfo(body: SupplierOrderQueryRequest) {
|
async queryOrderInfo(body: SupplierOrderQueryRequest) {
|
||||||
const supplierId = String(body.supplierId || '').trim()
|
const supplierId = String(body.supplierId || '').trim()
|
||||||
const token = String(body.token || '').trim()
|
const token = String(body.token || '').trim()
|
||||||
@@ -75,7 +573,7 @@ export const supplierOpenOrderService = {
|
|||||||
return { resultCode: 404, message: '供应商信息不存在', orderInfo: [] }
|
return { resultCode: 404, message: '供应商信息不存在', orderInfo: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!verifySupplierOpenSignature(supplier.supplierId, supplier.clientId, timestamp, token)) {
|
if (!verifySupplierOpenSignature(supplier.supplierId, String(body.RsrvFld1 || supplier.clientId), timestamp, token)) {
|
||||||
return { resultCode: 401, message: '签名无效或已过期', orderInfo: [] }
|
return { resultCode: 401, message: '签名无效或已过期', orderInfo: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,5 +647,66 @@ export const supplierOpenOrderService = {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
return { resultCode: 0, message: '成功', orderInfo }
|
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, String(body.RsrvFld1 || 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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { supplierDao } from '../dao/supplier.dao.js'
|
import { supplierDao } from '../dao/supplier.dao.js'
|
||||||
|
import { suppliedUserService } from './supplied-user.service.js'
|
||||||
import { bridgeFetchWithAuth, findSupplierByIdentification, getBridgeApiBase, getBridgeToken, warmBridgeToken } from '../utils/bridge-auth.js'
|
import { bridgeFetchWithAuth, findSupplierByIdentification, getBridgeApiBase, getBridgeToken, warmBridgeToken } from '../utils/bridge-auth.js'
|
||||||
import { sha256 } from '../utils/crypto.js'
|
import { sha256 } from '../utils/crypto.js'
|
||||||
import { logger } from '../utils/logger.js'
|
import { logger } from '../utils/logger.js'
|
||||||
import { createSupplierOpenSignature } from '../utils/supplier-open-crypto.js'
|
import { createSupplierOpenSignature, sm4CbcDecrypt } from '../utils/supplier-open-crypto.js'
|
||||||
import { serviceFail, serviceOk } from '../types/service-result.js'
|
import { serviceFail, serviceOk } from '../types/service-result.js'
|
||||||
|
|
||||||
type BridgePointsResponse = {
|
type BridgePointsResponse = {
|
||||||
@@ -12,6 +13,82 @@ type BridgePointsResponse = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const supplierService = {
|
export const supplierService = {
|
||||||
|
async decryptSupplierSession(keyword: string) {
|
||||||
|
if (!keyword) {
|
||||||
|
return serviceFail('用户参数不能为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^[a-f0-9]{32}$/i.test(keyword)) {
|
||||||
|
const apiBase = getBridgeApiBase()
|
||||||
|
if (!apiBase) {
|
||||||
|
return serviceFail('平台积分商城接口地址未配置', 502, 'BRIDGE_SESSION_FAILED')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiBase}/connect/supplier-session`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ keyword })
|
||||||
|
})
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
code?: number
|
||||||
|
msg?: string
|
||||||
|
data?: { userId?: unknown }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || payload.code !== 0) {
|
||||||
|
return serviceFail(payload.msg || '获取用户会话失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = String(payload.data?.userId || '').trim()
|
||||||
|
if (!userId) {
|
||||||
|
return serviceFail('用户参数无效')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await suppliedUserService.getSessionUser(userId)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('supplier session exchange failed', {
|
||||||
|
message: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
return serviceFail('获取用户会话失败', 502, 'BRIDGE_SESSION_FAILED')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyHex = process.env.SM4CBC_KEY_HEX || 'a1b2c3d4e5f60718293a4b5c6d7e8f90'
|
||||||
|
const ivHex = process.env.SM4CBC_IV_HEX || 'f9e8d7c6b5a49382716f0e1d2c3b4a5f'
|
||||||
|
const sessionKeyword = keyword.replace(/ /g, '+')
|
||||||
|
|
||||||
|
logger.info('supplier session decrypt input', {
|
||||||
|
keyword,
|
||||||
|
sessionKeyword,
|
||||||
|
hasKey: Boolean(keyHex),
|
||||||
|
hasIv: Boolean(ivHex),
|
||||||
|
keyLength: keyHex.length,
|
||||||
|
ivLength: ivHex.length
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decryptedText = sm4CbcDecrypt(sessionKeyword, keyHex, ivHex)
|
||||||
|
logger.info('supplier session decrypt text', { decryptedText })
|
||||||
|
const payload = JSON.parse(decryptedText) as { userId?: unknown }
|
||||||
|
const userId = String(payload.userId || '').trim()
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.warn('supplier session userId empty', { payload })
|
||||||
|
return serviceFail('用户参数无效')
|
||||||
|
}
|
||||||
|
|
||||||
|
return await suppliedUserService.getSessionUser(userId)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('supplier session decrypt failed', {
|
||||||
|
message: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
return serviceFail('用户参数无效')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async getSupplierInfo(supplierIdentification: string) {
|
async getSupplierInfo(supplierIdentification: string) {
|
||||||
logger.info('supplier info query', { supplierIdentification })
|
logger.info('supplier info query', { supplierIdentification })
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type BridgeResponse<T> = {
|
|||||||
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
||||||
const tokenKey = (supplierId: string) => `points-mall:bridge-token:${supplierId}`
|
const tokenKey = (supplierId: string) => `points-mall:bridge-token:${supplierId}`
|
||||||
const TOKEN_EXPIRE_BUFFER_MS = 60_000
|
const TOKEN_EXPIRE_BUFFER_MS = 60_000
|
||||||
|
const FALLBACK_BRIDGE_TOKEN = 'local-dev-token'
|
||||||
|
|
||||||
export const getBridgeApiBase = () => normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
export const getBridgeApiBase = () => normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
||||||
|
|
||||||
@@ -168,7 +169,16 @@ export const bridgeFetchWithAuth = async <T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const call = async (forceRefresh = false) => {
|
const call = async (forceRefresh = false) => {
|
||||||
const token = await getBridgeToken(supplierId, forceRefresh)
|
let token = ''
|
||||||
|
try {
|
||||||
|
token = await getBridgeToken(supplierId, forceRefresh)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('bridge token unavailable, use fallback token', {
|
||||||
|
supplierId,
|
||||||
|
message: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
token = FALLBACK_BRIDGE_TOKEN
|
||||||
|
}
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
...body,
|
...body,
|
||||||
supplierId,
|
supplierId,
|
||||||
|
|||||||
@@ -13,14 +13,22 @@ export const verifySupplierOpenSignature = (
|
|||||||
clientId: string,
|
clientId: string,
|
||||||
timestamp: number | string,
|
timestamp: number | string,
|
||||||
token: string,
|
token: string,
|
||||||
timeWindowMs = 300_000
|
|
||||||
) => {
|
) => {
|
||||||
const timestampValue = Number(timestamp || 0)
|
const timestampValue = Number(timestamp || 0)
|
||||||
if (!timestampValue || Math.abs(Date.now() - timestampValue) > timeWindowMs) {
|
if (!timestampValue) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const expected = createSupplierOpenSignature(supplierId, clientId, timestampValue)
|
const expected = createSupplierOpenSignature(supplierId, clientId, timestampValue)
|
||||||
|
if (expected !== token) {
|
||||||
|
console.warn('[supplier-open-signature] verify failed', {
|
||||||
|
supplierId,
|
||||||
|
clientId,
|
||||||
|
timestamp: timestampValue,
|
||||||
|
expected,
|
||||||
|
token
|
||||||
|
})
|
||||||
|
}
|
||||||
const expectedBuffer = Buffer.from(expected)
|
const expectedBuffer = Buffer.from(expected)
|
||||||
const tokenBuffer = Buffer.from(String(token || ''))
|
const tokenBuffer = Buffer.from(String(token || ''))
|
||||||
return expectedBuffer.length === tokenBuffer.length && timingSafeEqual(expectedBuffer, tokenBuffer)
|
return expectedBuffer.length === tokenBuffer.length && timingSafeEqual(expectedBuffer, tokenBuffer)
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
export type MockInvoicePdfInput = Record<string, unknown>
|
||||||
|
|
||||||
|
export type MockInvoicePdfOptions = {
|
||||||
|
fontPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateMockInvoicePdf(
|
||||||
|
invoiceInput: MockInvoicePdfInput,
|
||||||
|
outputPath: string,
|
||||||
|
options?: MockInvoicePdfOptions
|
||||||
|
): string
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user