feat: 基本实现拆单收货
This commit is contained in:
Generated
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "points-mall",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
VUE_APP_SUPPLIED_API_BASE="http://localhost:8888/.netlify/functions"
|
||||
VUE_APP_SUPPLIED_API_BASE="http://localhost:8888/api"
|
||||
VUE_APP_SUPPLIER_IDENTIFICATION="joyful-selection"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/.netlify/functions'
|
||||
const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888/api'
|
||||
|
||||
const normalizeBase = base => base.replace(/\/$/, '')
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import Vue from 'vue'
|
||||
import Vant from 'vant'
|
||||
import { Skeleton } from 'vant'
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
||||
import 'vant/lib/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
Vue.use(Vant)
|
||||
Vue.use(Skeleton)
|
||||
Vue.use(PiniaVuePlugin)
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import JoyfulSelectionPaymentSuccess from '@/view/joyful-selection/order/payment
|
||||
import JoyfulSelectionAddressList from '@/view/joyful-selection/address/index.vue'
|
||||
import JoyfulSelectionAddressEdit from '@/view/joyful-selection/address/edit/index.vue'
|
||||
import JoyfulSelectionOrders from '@/view/joyful-selection/profile/orders/index.vue'
|
||||
import JoyfulSelectionOrderDetail from '@/view/joyful-selection/profile/order-detail/index.vue'
|
||||
import JoyfulSelectionProfile from '@/view/joyful-selection/profile/index.vue'
|
||||
import JoyfulSelectionPoints from '@/view/joyful-selection/profile/points/index.vue'
|
||||
import HomeLiving from '@/view/home-living/index.vue'
|
||||
@@ -41,6 +42,7 @@ export default new Router({
|
||||
{ path: 'address', component: JoyfulSelectionAddressList },
|
||||
{ path: 'address/edit', component: JoyfulSelectionAddressEdit },
|
||||
{ path: 'orders', component: JoyfulSelectionOrders },
|
||||
{ path: 'order-detail', component: JoyfulSelectionOrderDetail },
|
||||
{ path: 'profile', component: JoyfulSelectionProfile },
|
||||
{ path: 'points', component: JoyfulSelectionPoints }
|
||||
]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<main class="card-skeleton">
|
||||
<div v-if="showNav" class="nav-line"></div>
|
||||
<section v-for="item in cards" :key="item" class="card-line">
|
||||
<van-skeleton title :row="rows" />
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CardSkeleton',
|
||||
props: {
|
||||
cards: {
|
||||
type: Number,
|
||||
default: 4
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
showNav: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.card-skeleton {
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.nav-line {
|
||||
height: 46px;
|
||||
margin: -12px -12px 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-line {
|
||||
margin-bottom: 10px;
|
||||
padding: 16px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<main class="detail-skeleton">
|
||||
<div class="nav-line"></div>
|
||||
<div class="image-line"></div>
|
||||
<section class="card-line">
|
||||
<van-skeleton title :row="3" />
|
||||
</section>
|
||||
<section class="card-line">
|
||||
<van-skeleton title :row="2" />
|
||||
</section>
|
||||
<section class="grid-line">
|
||||
<span v-for="item in 4" :key="item"></span>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DetailSkeleton'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.detail-skeleton {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.nav-line {
|
||||
height: 54px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.image-line {
|
||||
height: 365px;
|
||||
background: #eef0f4;
|
||||
}
|
||||
|
||||
.card-line,
|
||||
.grid-line {
|
||||
margin: 10px 12px 0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-line {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.grid-line {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.grid-line span {
|
||||
height: 70px;
|
||||
border-radius: 8px;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<main class="list-skeleton">
|
||||
<div v-if="showNav" class="nav-line"></div>
|
||||
<div v-if="showSearch" class="search-line"></div>
|
||||
<section v-if="showBanner" class="banner-line"></section>
|
||||
<div v-if="showTabs" class="tabs-line">
|
||||
<span v-for="item in 5" :key="item"></span>
|
||||
</div>
|
||||
<article v-for="item in rows" :key="item" class="item-line">
|
||||
<div class="thumb-line"></div>
|
||||
<div class="content-line">
|
||||
<van-skeleton title :row="3" />
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ListSkeleton',
|
||||
props: {
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
showNav: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showBanner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showTabs: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.list-skeleton {
|
||||
min-height: 100vh;
|
||||
padding: 12px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.nav-line,
|
||||
.search-line,
|
||||
.banner-line,
|
||||
.tabs-line,
|
||||
.item-line {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.nav-line {
|
||||
height: 46px;
|
||||
margin: -12px -12px 12px;
|
||||
}
|
||||
|
||||
.search-line {
|
||||
height: 42px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 21px;
|
||||
}
|
||||
|
||||
.banner-line {
|
||||
height: 108px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tabs-line {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: 44px;
|
||||
align-items: center;
|
||||
margin: -12px -12px 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.tabs-line span {
|
||||
width: 44px;
|
||||
height: 14px;
|
||||
border-radius: 8px;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
|
||||
.item-line {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.thumb-line {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
flex: none;
|
||||
border-radius: 6px;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
|
||||
.content-line {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<main class="profile-skeleton">
|
||||
<section class="hero-line">
|
||||
<div class="avatar-line"></div>
|
||||
<van-skeleton title :row="1" />
|
||||
</section>
|
||||
<section class="asset-line">
|
||||
<div class="coin-line"></div>
|
||||
<van-skeleton title :row="1" />
|
||||
</section>
|
||||
<section v-for="item in 2" :key="item" class="panel-line">
|
||||
<van-skeleton title :row="2" />
|
||||
<div class="icon-row">
|
||||
<span v-for="icon in 5" :key="icon"></span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ProfileSkeleton'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.profile-skeleton {
|
||||
min-height: 100vh;
|
||||
padding: 18px 10px 92px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.hero-line {
|
||||
min-height: 164px;
|
||||
padding: 68px 18px 28px;
|
||||
border-radius: 0 0 16px 16px;
|
||||
background: #f2eefc;
|
||||
}
|
||||
|
||||
.avatar-line {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
float: left;
|
||||
margin-right: 12px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.asset-line,
|
||||
.panel-line {
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.asset-line {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.coin-line {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
background: #fff0e8;
|
||||
}
|
||||
|
||||
.icon-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.icon-row span {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
background: #f2f3f5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="5" show-nav show-search show-banner />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from './components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'SupplierHomeSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="4" :rows="2" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionAddressEditSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="4" show-nav />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionAddressListSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="5" show-nav />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCartEditSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="4" show-nav />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCartSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="4" :show-nav="false" show-search show-banner />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionHomeSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="5" :rows="3" :show-nav="false" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionLayoutSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="5" :rows="2" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCheckoutSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="3" :rows="2" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPaymentSuccessSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="4" :rows="3" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPaymentSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<detail-skeleton />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DetailSkeleton from '../../components/DetailSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProductDetailSkeleton',
|
||||
components: { DetailSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="6" show-nav show-search />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProductListSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<profile-skeleton />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProfileSkeleton from '../../components/ProfileSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProfileSkeleton',
|
||||
components: { ProfileSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="4" :rows="3" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionOrderDetailSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<list-skeleton :rows="4" show-nav show-tabs />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListSkeleton from '../../../components/ListSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionOrdersSkeleton',
|
||||
components: { ListSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="4" :rows="2" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../../../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPointsSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<card-skeleton :cards="5" :rows="3" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CardSkeleton from '../components/CardSkeleton.vue'
|
||||
|
||||
export default {
|
||||
name: 'SupplierShopSkeleton',
|
||||
components: { CardSkeleton }
|
||||
}
|
||||
</script>
|
||||
@@ -23,15 +23,12 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
||||
supplierIdentification: '',
|
||||
supplierId: '',
|
||||
supplierName: '',
|
||||
token: '',
|
||||
refreshToken: '',
|
||||
tokenExpiresAt: 0,
|
||||
pointsBalance: 0,
|
||||
loaded: false,
|
||||
loadingToken: false
|
||||
}),
|
||||
getters: {
|
||||
hasToken: state => Boolean(state.token && state.tokenExpiresAt > Date.now())
|
||||
hasToken: state => Boolean(state.supplierId && state.userId)
|
||||
},
|
||||
actions: {
|
||||
loadUser() {
|
||||
@@ -43,9 +40,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
||||
this.supplierIdentification = supplierIdentification
|
||||
this.supplierId = supplierChanged ? '' : data.supplierId || ''
|
||||
this.supplierName = supplierChanged ? '' : data.supplierName || ''
|
||||
this.token = supplierChanged ? '' : data.token || ''
|
||||
this.refreshToken = supplierChanged ? '' : data.refreshToken || ''
|
||||
this.tokenExpiresAt = supplierChanged ? 0 : Number(data.tokenExpiresAt || 0)
|
||||
this.pointsBalance = supplierChanged ? 0 : Number(data.pointsBalance || 0)
|
||||
this.loaded = true
|
||||
},
|
||||
@@ -55,9 +49,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
||||
supplierIdentification: this.supplierIdentification,
|
||||
supplierId: this.supplierId,
|
||||
supplierName: this.supplierName,
|
||||
token: this.token,
|
||||
refreshToken: this.refreshToken,
|
||||
tokenExpiresAt: this.tokenExpiresAt,
|
||||
pointsBalance: this.pointsBalance
|
||||
})
|
||||
this.loaded = true
|
||||
@@ -71,9 +62,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
||||
|
||||
if (nextUserId && nextUserId !== this.userId) {
|
||||
this.userId = nextUserId
|
||||
this.token = ''
|
||||
this.refreshToken = ''
|
||||
this.tokenExpiresAt = 0
|
||||
this.pointsBalance = 0
|
||||
}
|
||||
if (!this.supplierIdentification) {
|
||||
@@ -94,12 +82,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
||||
supplierIdentification: this.supplierIdentification
|
||||
})
|
||||
|
||||
if (data?.supplierId && data.supplierId !== this.supplierId) {
|
||||
this.token = ''
|
||||
this.refreshToken = ''
|
||||
this.tokenExpiresAt = 0
|
||||
}
|
||||
|
||||
this.supplierId = data?.supplierId || ''
|
||||
this.supplierName = data?.supplierName || ''
|
||||
this.persistUser()
|
||||
@@ -128,10 +110,6 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
||||
|
||||
this.supplierId = data?.supplierId || this.supplierId
|
||||
this.supplierName = data?.supplierName || this.supplierName
|
||||
this.token = data?.token || data?.accessToken || ''
|
||||
this.refreshToken = data?.refreshToken || ''
|
||||
const expiresIn = Number(data?.expiresIn || 0)
|
||||
this.tokenExpiresAt = this.token && expiresIn > 0 ? Date.now() + expiresIn * 1000 : 0
|
||||
this.persistUser()
|
||||
})()
|
||||
|
||||
@@ -148,14 +126,13 @@ export const useUserStore = defineStore('joyfulSelectionUser', {
|
||||
}
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.userId || !this.supplierId || !this.token) {
|
||||
if (!this.userId || !this.supplierId) {
|
||||
return this.pointsBalance
|
||||
}
|
||||
|
||||
const data = await postFunction('supplier-points', {
|
||||
userId: this.userId,
|
||||
supplierId: this.supplierId,
|
||||
token: this.token
|
||||
supplierId: this.supplierId
|
||||
})
|
||||
|
||||
this.pointsBalance = Number(data?.userPoints || 0)
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<supplier-shop :shop="shop" />
|
||||
<div>
|
||||
<supplier-shop-skeleton v-if="loading" />
|
||||
<supplier-shop v-else :shop="shop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SupplierShop from '@/components/SupplierShop.vue'
|
||||
import SupplierShopSkeleton from '@/skeleton-view/shop/index.vue'
|
||||
import { shops } from '../shop-data'
|
||||
|
||||
export default {
|
||||
name: 'CuteSelection',
|
||||
components: { SupplierShop },
|
||||
data: () => ({ shop: shops.cuteSelection })
|
||||
components: { SupplierShop, SupplierShopSkeleton },
|
||||
data: () => ({
|
||||
loading: true,
|
||||
shop: shops.cuteSelection
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<supplier-shop :shop="shop" />
|
||||
<div>
|
||||
<supplier-shop-skeleton v-if="loading" />
|
||||
<supplier-shop v-else :shop="shop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SupplierShop from '@/components/SupplierShop.vue'
|
||||
import SupplierShopSkeleton from '@/skeleton-view/shop/index.vue'
|
||||
import { shops } from '../shop-data'
|
||||
|
||||
export default {
|
||||
name: 'FreshFruit',
|
||||
components: { SupplierShop },
|
||||
data: () => ({ shop: shops.freshFruit })
|
||||
components: { SupplierShop, SupplierShopSkeleton },
|
||||
data: () => ({
|
||||
loading: true,
|
||||
shop: shops.freshFruit
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<supplier-shop :shop="shop" />
|
||||
<div>
|
||||
<supplier-shop-skeleton v-if="loading" />
|
||||
<supplier-shop v-else :shop="shop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SupplierShop from '@/components/SupplierShop.vue'
|
||||
import SupplierShopSkeleton from '@/skeleton-view/shop/index.vue'
|
||||
import { shops } from '../shop-data'
|
||||
|
||||
export default {
|
||||
name: 'HappyShopping',
|
||||
components: { SupplierShop },
|
||||
data: () => ({ shop: shops.happyShopping })
|
||||
components: { SupplierShop, SupplierShopSkeleton },
|
||||
data: () => ({
|
||||
loading: true,
|
||||
shop: shops.happyShopping
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<template>
|
||||
<supplier-shop :shop="shop" />
|
||||
<div>
|
||||
<supplier-shop-skeleton v-if="loading" />
|
||||
<supplier-shop v-else :shop="shop" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SupplierShop from '@/components/SupplierShop.vue'
|
||||
import SupplierShopSkeleton from '@/skeleton-view/shop/index.vue'
|
||||
import { shops } from '../shop-data'
|
||||
|
||||
export default {
|
||||
name: 'HomeLiving',
|
||||
components: { SupplierShop },
|
||||
data: () => ({ shop: shops.homeLiving })
|
||||
components: { SupplierShop, SupplierShopSkeleton },
|
||||
data: () => ({
|
||||
loading: true,
|
||||
shop: shops.homeLiving
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="page">
|
||||
<div>
|
||||
<supplier-home-skeleton v-if="loading" />
|
||||
<main v-else class="page">
|
||||
<van-nav-bar fixed placeholder title="首页" :border="false" />
|
||||
<van-search v-model="keyword" class="fixed-search" shape="round" placeholder="搜索供应商名称或商品" />
|
||||
|
||||
@@ -45,7 +47,8 @@
|
||||
<span><b>{{ service.title }}</b><small>{{ service.description }}</small></span>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -57,10 +60,15 @@ import supplierHome from '@/assets/supplier-icons/home-living.png'
|
||||
import supplierHappy from '@/assets/supplier-icons/happy-shopping.png'
|
||||
import supplierFruit from '@/assets/supplier-icons/fresh-fruit.png'
|
||||
import supplierCute from '@/assets/supplier-icons/cute-selection.png'
|
||||
import SupplierHomeSkeleton from '@/skeleton-view/index.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SupplierHomeSkeleton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
keyword: '',
|
||||
banners: [
|
||||
{ id: 'welfare', image: bannerWelfare, alt: '优质供应商精选' },
|
||||
@@ -81,6 +89,11 @@ export default {
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
filteredSuppliers() {
|
||||
const keyword = this.keyword.trim()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="address-edit-page">
|
||||
<div>
|
||||
<joyful-selection-address-edit-skeleton v-if="loading" />
|
||||
<main v-else class="address-edit-page">
|
||||
<van-nav-bar :title="pageTitle" left-arrow @click-left="$router.back()" />
|
||||
<van-address-edit
|
||||
:area-list="areaList"
|
||||
@@ -14,17 +16,23 @@
|
||||
@delete="onDelete"
|
||||
@change-detail="onChangeDetail"
|
||||
/>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Toast } from 'vant'
|
||||
import { areaList } from '@vant/area-data'
|
||||
import { createAddressId, readAddresses, saveAddresses, saveChosenAddressId } from '../address-data'
|
||||
import JoyfulSelectionAddressEditSkeleton from '@/skeleton-view/joyful-selection/address/edit/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionAddressEdit',
|
||||
components: {
|
||||
JoyfulSelectionAddressEditSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
areaList,
|
||||
searchResult: [],
|
||||
addressInfo: {}
|
||||
@@ -43,6 +51,9 @@ export default {
|
||||
if (address) {
|
||||
this.addressInfo = { ...address }
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onSave(content) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="address-page">
|
||||
<div>
|
||||
<joyful-selection-address-list-skeleton v-if="loading" />
|
||||
<main v-else class="address-page">
|
||||
<van-nav-bar title="地址管理" left-arrow @click-left="$router.back()" />
|
||||
<van-address-list
|
||||
v-model="chosenAddressId"
|
||||
@@ -11,15 +13,21 @@
|
||||
@edit="onEdit"
|
||||
@select="onSelect"
|
||||
/>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { formatAddress, readAddresses, readChosenAddressId, saveChosenAddressId } from './address-data'
|
||||
import JoyfulSelectionAddressListSkeleton from '@/skeleton-view/joyful-selection/address/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionAddressList',
|
||||
components: {
|
||||
JoyfulSelectionAddressListSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
chosenAddressId: '',
|
||||
addresses: [],
|
||||
disabledList: []
|
||||
@@ -38,6 +46,9 @@ export default {
|
||||
created() {
|
||||
this.addresses = readAddresses()
|
||||
this.chosenAddressId = readChosenAddressId()
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onSelect(item) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="edit-page">
|
||||
<div>
|
||||
<joyful-selection-cart-edit-skeleton v-if="loading" />
|
||||
<main v-else class="edit-page">
|
||||
<van-nav-bar title="购物车" right-text="完成" @click-right="$router.push('/joyful-selection/cart')" />
|
||||
<section class="edit-list">
|
||||
<van-empty v-if="!items.length" description="购物车还是空的" />
|
||||
@@ -30,17 +32,23 @@
|
||||
@input="toggleAll"
|
||||
>全选</van-checkbox>
|
||||
</van-submit-bar>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Toast } from 'vant'
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import JoyfulSelectionCartEditSkeleton from '@/skeleton-view/joyful-selection/cart/edit/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCartEdit',
|
||||
components: {
|
||||
JoyfulSelectionCartEditSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
selectedIds: []
|
||||
}),
|
||||
computed: {
|
||||
@@ -55,6 +63,9 @@ export default {
|
||||
created() {
|
||||
this.loadCart()
|
||||
this.selectedIds = this.items.map(item => item.cartId)
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useCartStore, ['loadCart', 'removeItems']),
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
<template>
|
||||
<main class="cart-page">
|
||||
<div>
|
||||
<joyful-selection-cart-skeleton v-if="loading" />
|
||||
<main v-else class="cart-page">
|
||||
<van-nav-bar title="购物车" right-text="编辑" @click-right="$router.push('/joyful-selection/cart/edit')" />
|
||||
<joyful-cart-panel fixed-submit page-mode @checkout="goCheckout" />
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import JoyfulCartPanel from './components/JoyfulCartPanel.vue'
|
||||
import JoyfulSelectionCartSkeleton from '@/skeleton-view/joyful-selection/cart/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCart',
|
||||
components: {
|
||||
JoyfulCartPanel
|
||||
JoyfulCartPanel,
|
||||
JoyfulSelectionCartSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true
|
||||
}),
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
goCheckout(items) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="joy-home">
|
||||
<div>
|
||||
<joyful-selection-home-skeleton v-if="loading" />
|
||||
<main v-else class="joy-home">
|
||||
<van-search class="fixed-search" v-model="keyword" shape="round" placeholder="搜索商品名称" />
|
||||
|
||||
<section class="hero">
|
||||
@@ -44,16 +46,22 @@
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getFunction } from '@/api/supplied-service'
|
||||
import JoyfulSelectionHomeSkeleton from '@/skeleton-view/joyful-selection/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionHome',
|
||||
components: {
|
||||
JoyfulSelectionHomeSkeleton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
keyword: '',
|
||||
categories: [
|
||||
{ name: '精选推荐', icon: 'like', color: '#ff6e8a', background: '#fff0f3' },
|
||||
@@ -83,12 +91,15 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async loadProducts() {
|
||||
this.loading = true
|
||||
try {
|
||||
const data = await getFunction('product-list')
|
||||
this.products = data?.products || []
|
||||
} catch (error) {
|
||||
console.warn('查询商品列表失败', error)
|
||||
this.$toast(error.message || '查询商品列表失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
goProductDetail(item) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<template>
|
||||
<main class="joyful-layout">
|
||||
<router-view />
|
||||
<joyful-selection-layout-skeleton v-if="sessionLoading" />
|
||||
<router-view v-else />
|
||||
<van-tabbar v-if="showTabbar" route active-color="#ff5b20" inactive-color="#777">
|
||||
<van-tabbar-item icon="wap-home-o" to="/joyful-selection/home">首页</van-tabbar-item>
|
||||
<van-tabbar-item icon="apps-o" to="/joyful-selection/products">分类</van-tabbar-item>
|
||||
@@ -14,9 +15,16 @@
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import JoyfulSelectionLayoutSkeleton from '@/skeleton-view/joyful-selection/layout.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionLayout',
|
||||
components: {
|
||||
JoyfulSelectionLayoutSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
sessionLoading: true
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useCartStore, ['totalCount']),
|
||||
cartBadge() {
|
||||
@@ -42,10 +50,13 @@ export default {
|
||||
...mapActions(useCartStore, ['loadCart']),
|
||||
...mapActions(useUserStore, ['hydrateFromRoute', 'fetchBridgeToken']),
|
||||
prepareUserSession() {
|
||||
this.sessionLoading = true
|
||||
this.hydrateFromRoute(this.$route.query)
|
||||
this.fetchBridgeToken().catch(error => {
|
||||
console.warn('获取平台授权令牌失败', error)
|
||||
this.$toast(error.message || '供应商验证失败,请稍后再试')
|
||||
}).finally(() => {
|
||||
this.sessionLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="checkout-page">
|
||||
<div>
|
||||
<joyful-selection-checkout-skeleton v-if="loading" />
|
||||
<main v-else class="checkout-page">
|
||||
<van-nav-bar title="确认订单" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<section class="address-card" @click="$router.push({ path: '/joyful-selection/address', query: { from: 'checkout' } })">
|
||||
@@ -50,7 +52,8 @@
|
||||
</section>
|
||||
|
||||
<van-submit-bar :price="goodsAmount" button-text="提交订单" @submit="onSubmit" />
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -60,12 +63,17 @@ import { useCartStore } from '@/store/cart'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { readAddresses, readChosenAddressId } from '../../address/address-data'
|
||||
import { createOrderNo, savePaymentInfo } from '../payment/payment-data'
|
||||
import JoyfulSelectionCheckoutSkeleton from '@/skeleton-view/joyful-selection/order/checkout/index.vue'
|
||||
|
||||
const CHECKOUT_ITEMS_KEY = 'joyful-selection-checkout-items'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionCheckout',
|
||||
components: {
|
||||
JoyfulSelectionCheckoutSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
remark: '',
|
||||
goods: [],
|
||||
showAllGoods: false,
|
||||
@@ -74,7 +82,7 @@ export default {
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useCartStore, ['items']),
|
||||
...mapState(useUserStore, ['supplierId', 'token', 'userId']),
|
||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||
visibleGoods() {
|
||||
return this.showAllGoods ? this.goods : this.goods.slice(0, 3)
|
||||
},
|
||||
@@ -89,12 +97,15 @@ export default {
|
||||
this.loadCart()
|
||||
this.goods = this.getCheckoutGoods()
|
||||
this.loadAddress()
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
activated() {
|
||||
this.loadAddress()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useCartStore, ['loadCart']),
|
||||
...mapActions(useCartStore, ['loadCart', 'removeItems']),
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
getCheckoutGoods() {
|
||||
const storedItems = this.readCheckoutItems()
|
||||
@@ -136,7 +147,7 @@ export default {
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.token || !this.userId) {
|
||||
if (!this.supplierId || !this.userId) {
|
||||
throw new Error('供应商授权信息不完整,请重新进入商城')
|
||||
}
|
||||
|
||||
@@ -144,7 +155,6 @@ export default {
|
||||
const data = await postFunction('order-create', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
token: this.token,
|
||||
orderNo,
|
||||
amount: this.goodsAmount,
|
||||
goods: this.goods
|
||||
@@ -158,6 +168,8 @@ export default {
|
||||
receiverTel: this.maskTel(this.chosenAddress.tel || ''),
|
||||
goods: this.goods
|
||||
})
|
||||
|
||||
this.clearSubmittedCartItems()
|
||||
this.$router.push('/joyful-selection/payment')
|
||||
} catch (error) {
|
||||
console.warn('提交待支付订单失败', error)
|
||||
@@ -165,6 +177,19 @@ export default {
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
},
|
||||
clearSubmittedCartItems() {
|
||||
if (this.$route.query.source !== 'cart') {
|
||||
return
|
||||
}
|
||||
|
||||
const cartIds = this.goods.map(item => item.cartId).filter(Boolean)
|
||||
|
||||
if (cartIds.length) {
|
||||
this.removeItems(cartIds)
|
||||
}
|
||||
|
||||
sessionStorage.removeItem(CHECKOUT_ITEMS_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="success-page">
|
||||
<div>
|
||||
<joyful-selection-payment-success-skeleton v-if="loading" />
|
||||
<main v-else class="success-page">
|
||||
<van-nav-bar title="支付成功" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<section class="success-card">
|
||||
@@ -20,15 +22,21 @@
|
||||
<van-button round type="danger" @click="$router.push('/joyful-selection/home')">继续购物</van-button>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { readPaymentInfo, formatYuan } from '../payment/payment-data'
|
||||
import JoyfulSelectionPaymentSuccessSkeleton from '@/skeleton-view/joyful-selection/order/payment-success/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPaymentSuccess',
|
||||
components: {
|
||||
JoyfulSelectionPaymentSuccessSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
paymentInfo: {},
|
||||
paidAt: ''
|
||||
}),
|
||||
@@ -49,6 +57,9 @@ export default {
|
||||
created() {
|
||||
this.paymentInfo = readPaymentInfo()
|
||||
this.paidAt = this.formatTime(new Date())
|
||||
this.$nextTick(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
formatTime(date) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="payment-page">
|
||||
<div>
|
||||
<joyful-selection-payment-skeleton v-if="loading" />
|
||||
<main v-else class="payment-page">
|
||||
<van-nav-bar title="确认支付" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<section class="amount-card">
|
||||
@@ -61,7 +63,8 @@
|
||||
</footer>
|
||||
</article>
|
||||
</van-popup>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -69,17 +72,22 @@ import { mapActions, mapState } from 'pinia'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { readPaymentInfo, formatYuan } from './payment-data'
|
||||
import JoyfulSelectionPaymentSkeleton from '@/skeleton-view/joyful-selection/order/payment/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPayment',
|
||||
components: {
|
||||
JoyfulSelectionPaymentSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
payType: 'points',
|
||||
showConfirm: false,
|
||||
submitting: false,
|
||||
paymentInfo: {}
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['pointsBalance', 'supplierId', 'token']),
|
||||
...mapState(useUserStore, ['pointsBalance', 'supplierId']),
|
||||
amountText() {
|
||||
return formatYuan(this.paymentInfo.amount)
|
||||
},
|
||||
@@ -110,9 +118,12 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.paymentInfo = readPaymentInfo()
|
||||
this.loading = true
|
||||
this.fetchUserPoints().catch(error => {
|
||||
console.warn('查询用户积分失败', error)
|
||||
this.$toast(error.message || '查询用户积分失败')
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
@@ -129,13 +140,12 @@ export default {
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.token || !this.paymentInfo.orderNo) {
|
||||
if (!this.supplierId || !this.paymentInfo.orderNo) {
|
||||
throw new Error('订单支付信息不完整,请重新提交订单')
|
||||
}
|
||||
|
||||
await postFunction('order-pay', {
|
||||
supplierId: this.supplierId,
|
||||
token: this.token,
|
||||
orderId: this.paymentInfo.orderNo,
|
||||
amount: this.paymentInfo.amount,
|
||||
totalPrice: Number(this.amountText),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="detail-page">
|
||||
<div>
|
||||
<joyful-selection-product-detail-skeleton v-if="loading" />
|
||||
<main v-else class="detail-page">
|
||||
<header class="detail-top">
|
||||
<van-icon name="arrow-left" @click="$router.back()" />
|
||||
<h1>商品详情</h1>
|
||||
@@ -77,7 +79,8 @@
|
||||
@checkout="goCheckout"
|
||||
/>
|
||||
</van-popup>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -86,6 +89,7 @@ import { getFunction } from '@/api/supplied-service'
|
||||
import { useCartStore } from '@/store/cart'
|
||||
import JoyfulSkuPopup from '../products/components/JoyfulSkuPopup.vue'
|
||||
import JoyfulCartPanel from '../cart/components/JoyfulCartPanel.vue'
|
||||
import JoyfulSelectionProductDetailSkeleton from '@/skeleton-view/joyful-selection/product-detail/index.vue'
|
||||
|
||||
const emptyProduct = {
|
||||
id: '',
|
||||
@@ -102,9 +106,11 @@ export default {
|
||||
name: 'JoyfulSelectionProductDetail',
|
||||
components: {
|
||||
JoyfulSkuPopup,
|
||||
JoyfulCartPanel
|
||||
JoyfulCartPanel,
|
||||
JoyfulSelectionProductDetailSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
product: emptyProduct,
|
||||
showCartPopup: false,
|
||||
count: 1,
|
||||
@@ -131,12 +137,15 @@ export default {
|
||||
methods: {
|
||||
...mapActions(useCartStore, ['loadCart']),
|
||||
async loadProduct() {
|
||||
this.loading = true
|
||||
try {
|
||||
const data = await getFunction('product-detail', { id: this.$route.query.id })
|
||||
this.product = data || emptyProduct
|
||||
} catch (error) {
|
||||
console.warn('查询商品详情失败', error)
|
||||
this.$toast(error.message || '查询商品详情失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
openCartPopup() {
|
||||
|
||||
@@ -110,20 +110,20 @@ export default {
|
||||
const selectedColor = colorOptions.find(item => item.id === selectedSku.s1) || colorOptions[0]
|
||||
const selectedSize = sizeOptions.find(item => item.id === selectedSku.s2) || sizeOptions[0]
|
||||
const count = data.selectedNum || 1
|
||||
const skuId = selectedSku.id || `${this.currentGoods.id}-${selectedColor.id}-${selectedSize.id}`
|
||||
const skuDesc = `${selectedColor.name} / ${selectedSize.name}`
|
||||
|
||||
return {
|
||||
cartId: selectedSku.id || `${this.currentGoods.id}-${selectedColor.id}-${selectedSize.id}`,
|
||||
cartId: skuId,
|
||||
id: this.currentGoods.id,
|
||||
name: this.currentGoods.name,
|
||||
desc: `${selectedColor.name} / ${selectedSize.name}`,
|
||||
desc: skuDesc,
|
||||
price: this.currentGoods.price,
|
||||
image: this.currentGoods.image,
|
||||
tag: this.currentGoods.tag,
|
||||
count,
|
||||
sku: {
|
||||
color: selectedColor.name,
|
||||
size: selectedSize.name
|
||||
}
|
||||
sku: skuId,
|
||||
skuText: skuDesc
|
||||
}
|
||||
},
|
||||
addCart(data) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="product-page">
|
||||
<div>
|
||||
<joyful-selection-product-list-skeleton v-if="loading" />
|
||||
<main v-else class="product-page">
|
||||
<div class="fixed-top">
|
||||
<van-nav-bar title="商品列表" left-arrow @click-left="$router.back()" />
|
||||
<div class="search-row">
|
||||
@@ -53,12 +55,14 @@
|
||||
</section>
|
||||
|
||||
<joyful-sku-popup ref="skuPopup" />
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getFunction } from '@/api/supplied-service'
|
||||
import JoyfulSkuPopup from './components/JoyfulSkuPopup.vue'
|
||||
import JoyfulSelectionProductListSkeleton from '@/skeleton-view/joyful-selection/products/index.vue'
|
||||
|
||||
const defaultCategories = [
|
||||
{ id: 'recommend', name: '推荐' }
|
||||
@@ -67,9 +71,11 @@ const defaultCategories = [
|
||||
export default {
|
||||
name: 'JoyfulSelectionProductList',
|
||||
components: {
|
||||
JoyfulSkuPopup
|
||||
JoyfulSkuPopup,
|
||||
JoyfulSelectionProductListSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
selectedCategoryId: 'recommend',
|
||||
keyword: '',
|
||||
categories: defaultCategories,
|
||||
@@ -91,6 +97,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async loadProducts() {
|
||||
this.loading = true
|
||||
try {
|
||||
const data = await getFunction('product-list')
|
||||
this.categories = data?.categories?.length ? data.categories : defaultCategories
|
||||
@@ -98,6 +105,8 @@ export default {
|
||||
} catch (error) {
|
||||
console.warn('查询商品列表失败', error)
|
||||
this.$toast(error.message || '查询商品列表失败')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
goProductDetail(item) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="profile-page">
|
||||
<div>
|
||||
<joyful-selection-profile-skeleton v-if="loading" />
|
||||
<main v-else class="profile-page">
|
||||
<section class="profile-hero">
|
||||
<div class="profile-actions">
|
||||
<van-icon name="setting-o" />
|
||||
@@ -69,16 +71,22 @@
|
||||
<p>敬请期待</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import JoyfulSelectionProfileSkeleton from '@/skeleton-view/joyful-selection/profile/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionProfile',
|
||||
components: {
|
||||
JoyfulSelectionProfileSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
orderItems: [
|
||||
{ icon: 'balance-pay', text: '待付款' },
|
||||
{ icon: 'logistics', text: '待发货' },
|
||||
@@ -96,9 +104,12 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loading = true
|
||||
this.fetchUserPoints().catch(error => {
|
||||
console.warn('查询用户积分失败', error)
|
||||
this.$toast(error.message || '查询用户积分失败')
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div>
|
||||
<joyful-selection-order-detail-skeleton v-if="loading" />
|
||||
<main v-else class="detail-page">
|
||||
<van-nav-bar title="订单详情" left-arrow @click-left="$router.back()" />
|
||||
|
||||
<van-empty v-if="!currentSubOrder" description="未找到订单" />
|
||||
|
||||
<template v-else>
|
||||
<section class="status-card">
|
||||
<h2>{{ currentSubOrder.status }}</h2>
|
||||
<p>主单号:{{ currentOrder.mainOrderId || '-' }}</p>
|
||||
<p>子单号:{{ currentSubOrder.orderId || '-' }}</p>
|
||||
</section>
|
||||
|
||||
<section class="goods-card">
|
||||
<article v-for="item in currentSubOrder.products" :key="item.id" class="goods-item">
|
||||
<van-image class="goods-thumb" :src="item.image" fit="cover" />
|
||||
<div class="goods-info">
|
||||
<h3>{{ item.name }}</h3>
|
||||
<p>SKU:{{ item.sku || '-' }}</p>
|
||||
<b>¥{{ item.price }}</b>
|
||||
</div>
|
||||
<span>x{{ item.number || 1 }}</span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="info-card">
|
||||
<p><span>订单编号</span><b>{{ currentSubOrder.orderNumberId || '-' }}</b></p>
|
||||
<p><span>供应商</span><b>{{ currentSubOrder.supplierId || '-' }}</b></p>
|
||||
<p><span>商品数量</span><b>{{ currentSubOrder.count || 0 }}</b></p>
|
||||
<p><span>订单金额</span><b class="price">¥{{ currentSubOrder.amount }}</b></p>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { Toast } from 'vant'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import JoyfulSelectionOrderDetailSkeleton from '@/skeleton-view/joyful-selection/profile/order-detail/index.vue'
|
||||
|
||||
const ORDER_STATE_VIEW = {
|
||||
0: { text: '待付款', statusClass: 'pending' },
|
||||
1: { text: '待发货', statusClass: 'pending' },
|
||||
2: { text: '待收货', statusClass: 'pending' },
|
||||
3: { text: '已取消', statusClass: 'muted' },
|
||||
5: { text: '已完成', statusClass: 'done' },
|
||||
6: { text: '换货', statusClass: 'muted' },
|
||||
7: { text: '退货', statusClass: 'muted' }
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionOrderDetail',
|
||||
components: {
|
||||
JoyfulSelectionOrderDetailSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
currentOrder: null,
|
||||
currentSubOrder: null
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['supplierId', 'userId'])
|
||||
},
|
||||
created() {
|
||||
this.loadOrderDetail()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
normalizeSubOrder(order, subOrder) {
|
||||
const view = ORDER_STATE_VIEW[Number(subOrder.orderState)] || ORDER_STATE_VIEW[Number(order.orderState)] || ORDER_STATE_VIEW[2]
|
||||
|
||||
return {
|
||||
...subOrder,
|
||||
status: view.text,
|
||||
statusClass: view.statusClass
|
||||
}
|
||||
},
|
||||
async loadOrderDetail() {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId) {
|
||||
Toast('用户信息不完整,请重新进入商城')
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('order-list', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId
|
||||
})
|
||||
|
||||
const mainOrderId = String(this.$route.query.mainOrderId || '')
|
||||
const orderId = String(this.$route.query.orderId || '')
|
||||
const order = (data?.orders || []).find(item => item.mainOrderId === mainOrderId)
|
||||
const subOrder = order?.subOrders?.find(item => item.orderId === orderId)
|
||||
|
||||
this.currentOrder = order || null
|
||||
this.currentSubOrder = order && subOrder ? this.normalizeSubOrder(order, subOrder) : null
|
||||
} catch (error) {
|
||||
Toast(error.message || '查询订单详情失败,请稍后再试')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
padding: 46px 0 16px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.detail-page ::v-deep .van-nav-bar {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, .04);
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.goods-card,
|
||||
.info-card {
|
||||
margin: 10px 12px 0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
padding: 16px 14px;
|
||||
}
|
||||
|
||||
.status-card h2 {
|
||||
margin: 0 0 10px;
|
||||
color: #ff5b20;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.status-card p {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.goods-card {
|
||||
padding: 2px 12px;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 86px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.goods-item + .goods-item {
|
||||
border-top: 1px solid #f4f4f4;
|
||||
}
|
||||
|
||||
.goods-thumb {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.goods-info h3 {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
margin: 0 0 6px;
|
||||
color: #222;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.goods-info p {
|
||||
overflow: hidden;
|
||||
margin: 0 0 6px;
|
||||
color: #999;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-info b,
|
||||
.price {
|
||||
color: #ff5b20;
|
||||
}
|
||||
|
||||
.goods-item > span {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
color: #777;
|
||||
font-size: 13px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.info-card b {
|
||||
max-width: 66%;
|
||||
overflow: hidden;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,74 +1,184 @@
|
||||
<template>
|
||||
<main class="orders-page">
|
||||
<van-nav-bar title="我的订单" left-arrow @click-left="$router.back()" />
|
||||
<van-tabs
|
||||
v-model="active"
|
||||
class="order-tabs"
|
||||
line-width="18"
|
||||
line-height="2"
|
||||
color="#ff5b20"
|
||||
title-active-color="#ff5b20"
|
||||
>
|
||||
<van-tab v-for="tab in tabs" :key="tab" :title="tab" />
|
||||
</van-tabs>
|
||||
<div>
|
||||
<joyful-selection-orders-skeleton v-if="loading" />
|
||||
<main v-else class="orders-page">
|
||||
<van-nav-bar title="我的订单" left-arrow @click-left="$router.back()" />
|
||||
<van-tabs
|
||||
v-model="active"
|
||||
class="order-tabs"
|
||||
line-width="18"
|
||||
line-height="2"
|
||||
color="#ff5b20"
|
||||
title-active-color="#ff5b20"
|
||||
>
|
||||
<van-tab v-for="tab in tabs" :key="tab.label" :title="tab.label" />
|
||||
</van-tabs>
|
||||
|
||||
<section class="order-list">
|
||||
<van-loading v-if="loading" class="order-loading" color="#ff5b20" size="24px">加载中...</van-loading>
|
||||
<van-empty v-else-if="visibleOrders.length === 0" description="暂无订单" />
|
||||
<section class="order-list">
|
||||
<van-empty v-if="visibleOrders.length === 0" description="暂无订单" />
|
||||
|
||||
<template v-else>
|
||||
<article v-for="order in visibleOrders" :key="order.id" class="order-card">
|
||||
<header>
|
||||
<div class="order-nos">
|
||||
<template v-else>
|
||||
<article v-for="order in visibleOrders" :key="order.viewId" class="order-card">
|
||||
<header class="main-order-header">
|
||||
<span>主单号:{{ order.mainOrderId || '-' }}</span>
|
||||
<span>子单号:{{ order.orderId || '-' }}</span>
|
||||
<van-button
|
||||
v-if="order.canCancel"
|
||||
class="cancel-main-btn"
|
||||
size="mini"
|
||||
round
|
||||
plain
|
||||
:loading="isOperating('cancel-main', order.mainOrderId)"
|
||||
@click.stop="cancelMainOrder(order)"
|
||||
>
|
||||
取消订单
|
||||
</van-button>
|
||||
</header>
|
||||
|
||||
<div class="sub-order-list">
|
||||
<section
|
||||
v-for="subOrder in order.subOrders"
|
||||
:key="subOrder.id"
|
||||
class="sub-order-row"
|
||||
@click="goOrderDetail(order, subOrder)"
|
||||
>
|
||||
<div class="sub-order-title">
|
||||
<span>子单号:{{ subOrder.orderId || '-' }}</span>
|
||||
<b :class="subOrder.statusClass">{{ subOrder.status }}</b>
|
||||
</div>
|
||||
<div class="order-body">
|
||||
<div class="thumb-row">
|
||||
<van-image
|
||||
v-for="item in previewProducts(subOrder.products)"
|
||||
:key="item.id"
|
||||
class="order-thumb"
|
||||
:src="item.image"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="order-summary">
|
||||
<p>共{{ subOrder.count }}件商品</p>
|
||||
<p>合计:<b>¥{{ subOrder.amount }}</b></p>
|
||||
</div>
|
||||
</div>
|
||||
<footer v-if="subActions(subOrder).length">
|
||||
<van-button
|
||||
v-for="action in subActions(subOrder)"
|
||||
:key="action.orderType"
|
||||
size="small"
|
||||
round
|
||||
plain
|
||||
:class="action.buttonClass"
|
||||
:loading="isOperating(action.orderType, subOrder.orderId)"
|
||||
@click.stop="submitSubOrderEvent(order, subOrder, action)"
|
||||
>
|
||||
{{ action.text }}
|
||||
</van-button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
<b :class="order.statusClass">{{ order.status }}</b>
|
||||
</header>
|
||||
<div class="order-body">
|
||||
<div class="thumb-row">
|
||||
<van-image
|
||||
v-for="item in previewProducts(order.products)"
|
||||
:key="item.id"
|
||||
class="order-thumb"
|
||||
:src="item.image"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="order-summary">
|
||||
<p>共{{ order.count }}件商品</p>
|
||||
<p>合计:<b>¥{{ order.amount }}</b></p>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<van-button size="small" round plain :class="order.buttonClass">{{ order.actionText }}</van-button>
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
</section>
|
||||
</main>
|
||||
</article>
|
||||
</template>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { Toast } from 'vant'
|
||||
import { Dialog, Toast } from 'vant'
|
||||
import { postFunction } from '@/api/supplied-service'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import JoyfulSelectionOrdersSkeleton from '@/skeleton-view/joyful-selection/profile/orders/index.vue'
|
||||
|
||||
const ORDER_STATE = {
|
||||
PENDING_PAY: 0,
|
||||
FINISH_PAY: 1,
|
||||
IN_PROGRESS: 2,
|
||||
CANCELLED: 3,
|
||||
COMPLETED: 5,
|
||||
EXCHANGE: 6,
|
||||
REFUNDED: 7
|
||||
}
|
||||
|
||||
const ORDER_SPLIT_STATE = {
|
||||
NOT_SPLIT: 0,
|
||||
SPLIT: 1
|
||||
}
|
||||
|
||||
const ORDER_EVENT_TYPE = {
|
||||
RETURN_ORDER: 4,
|
||||
EXCHANGE_ORDER: 5,
|
||||
CONFIRM_RECEIVE: 6,
|
||||
INVOICE_INFO: 11
|
||||
}
|
||||
|
||||
const ORDER_STATE_VIEW = {
|
||||
[ORDER_STATE.PENDING_PAY]: { text: '待付款', statusClass: 'pending' },
|
||||
[ORDER_STATE.FINISH_PAY]: { text: '待发货', statusClass: 'pending' },
|
||||
[ORDER_STATE.IN_PROGRESS]: { text: '待收货', statusClass: 'pending' },
|
||||
[ORDER_STATE.CANCELLED]: { text: '已取消', statusClass: 'muted' },
|
||||
[ORDER_STATE.COMPLETED]: { text: '已完成', statusClass: 'done' },
|
||||
[ORDER_STATE.EXCHANGE]: { text: '换货', statusClass: 'muted' },
|
||||
[ORDER_STATE.REFUNDED]: { text: '退货', statusClass: 'muted' }
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionOrders',
|
||||
components: {
|
||||
JoyfulSelectionOrdersSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
active: 0,
|
||||
tabs: ['全部', '待付款', '待发货', '待收货', '已完成', '售后'],
|
||||
tabs: [
|
||||
{ label: '全部', type: 'all' },
|
||||
{ label: '待付款', type: 'pending-main' },
|
||||
{ label: '待发货', type: 'sub-state', state: ORDER_STATE.FINISH_PAY },
|
||||
{ label: '待收货', type: 'sub-state', state: ORDER_STATE.IN_PROGRESS },
|
||||
{ label: '已取消', type: 'cancelled' },
|
||||
{ label: '已完成', type: 'sub-state', state: ORDER_STATE.COMPLETED },
|
||||
{ label: '售后', type: 'after-sale' }
|
||||
],
|
||||
orders: [],
|
||||
loading: false
|
||||
loading: false,
|
||||
operationKey: ''
|
||||
}),
|
||||
computed: {
|
||||
...mapState(useUserStore, ['supplierId', 'userId']),
|
||||
activeTab() {
|
||||
return this.tabs[this.active] || this.tabs[0]
|
||||
},
|
||||
visibleOrders() {
|
||||
const tab = this.tabs[this.active]
|
||||
const tab = this.activeTab
|
||||
|
||||
return tab === '全部' ? this.orders : this.orders.filter(order => order.status === tab)
|
||||
if (tab.type === 'all') {
|
||||
return this.orders.map(order => this.normalizeOrder(order, order.subOrders))
|
||||
}
|
||||
|
||||
if (tab.type === 'pending-main') {
|
||||
return this.orders
|
||||
.filter(order => Number(order.orderState) === ORDER_STATE.PENDING_PAY)
|
||||
.map(order => this.normalizeOrder(order, order.subOrders))
|
||||
}
|
||||
|
||||
if (tab.type === 'sub-state') {
|
||||
return this.splitSubOrdersByState(tab.state)
|
||||
}
|
||||
|
||||
if (tab.type === 'cancelled') {
|
||||
return this.cancelledOrders()
|
||||
}
|
||||
|
||||
if (tab.type === 'after-sale') {
|
||||
return this.orders.flatMap(order => {
|
||||
const subOrders = this.normalizedSubOrders(order).filter(subOrder => {
|
||||
return [ORDER_STATE.EXCHANGE, ORDER_STATE.REFUNDED].includes(Number(subOrder.orderState))
|
||||
})
|
||||
|
||||
return subOrders.map(subOrder => this.normalizeOrder(order, [subOrder]))
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@@ -76,9 +186,91 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useUserStore, ['fetchBridgeToken']),
|
||||
normalizeOrder(order, subOrders = []) {
|
||||
const sourceSubOrders = subOrders.length ? subOrders : order.subOrders
|
||||
const normalizedSubOrders = (sourceSubOrders || []).map(subOrder => this.normalizeSubOrder(order, subOrder))
|
||||
|
||||
return {
|
||||
...order,
|
||||
viewId: `${order.mainOrderId || order.id}-${normalizedSubOrders.map(item => item.orderId).join('-') || 'main'}`,
|
||||
canCancel: Number(order.orderState) === ORDER_STATE.PENDING_PAY,
|
||||
subOrders: normalizedSubOrders
|
||||
}
|
||||
},
|
||||
normalizeSubOrder(order, subOrder) {
|
||||
const view = ORDER_STATE_VIEW[Number(subOrder.orderState)] || ORDER_STATE_VIEW[Number(order.orderState)] || ORDER_STATE_VIEW[ORDER_STATE.IN_PROGRESS]
|
||||
|
||||
return {
|
||||
...subOrder,
|
||||
status: view.text,
|
||||
statusClass: view.statusClass,
|
||||
count: Number(subOrder.count || 0)
|
||||
}
|
||||
},
|
||||
normalizedSubOrders(order) {
|
||||
return (order.subOrders || []).map(subOrder => this.normalizeSubOrder(order, subOrder))
|
||||
},
|
||||
splitSubOrdersByState(state) {
|
||||
return this.orders.flatMap(order => {
|
||||
if (Number(order.isSplit) !== ORDER_SPLIT_STATE.SPLIT) {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.normalizedSubOrders(order)
|
||||
.filter(subOrder => Number(subOrder.orderState) === state)
|
||||
.map(subOrder => this.normalizeOrder(order, [subOrder]))
|
||||
})
|
||||
},
|
||||
cancelledOrders() {
|
||||
return this.orders.flatMap(order => {
|
||||
const mainCancelled = Number(order.orderState) === ORDER_STATE.CANCELLED
|
||||
|
||||
if (mainCancelled && Number(order.isSplit) !== ORDER_SPLIT_STATE.SPLIT) {
|
||||
return [this.normalizeOrder(order, order.subOrders)]
|
||||
}
|
||||
|
||||
return this.normalizedSubOrders(order)
|
||||
.filter(subOrder => Number(subOrder.orderState) === ORDER_STATE.CANCELLED)
|
||||
.map(subOrder => this.normalizeOrder(order, [subOrder]))
|
||||
})
|
||||
},
|
||||
previewProducts(products = []) {
|
||||
return products.slice(0, 3)
|
||||
},
|
||||
actionKey(type, orderId) {
|
||||
return `${type}-${orderId}`
|
||||
},
|
||||
isOperating(type, orderId) {
|
||||
return this.operationKey === this.actionKey(type, orderId)
|
||||
},
|
||||
subActions(subOrder) {
|
||||
const state = Number(subOrder.orderState)
|
||||
const actions = []
|
||||
|
||||
if (state === ORDER_STATE.IN_PROGRESS) {
|
||||
actions.push({ orderType: ORDER_EVENT_TYPE.CONFIRM_RECEIVE, text: '确认收货', buttonClass: 'primary-action' })
|
||||
}
|
||||
|
||||
if ([ORDER_STATE.IN_PROGRESS, ORDER_STATE.COMPLETED].includes(state)) {
|
||||
actions.push({ orderType: ORDER_EVENT_TYPE.RETURN_ORDER, text: '退货', buttonClass: 'dark-action' })
|
||||
actions.push({ orderType: ORDER_EVENT_TYPE.EXCHANGE_ORDER, text: '换货', buttonClass: 'dark-action' })
|
||||
}
|
||||
|
||||
if (![ORDER_STATE.PENDING_PAY, ORDER_STATE.CANCELLED].includes(state)) {
|
||||
actions.push({ orderType: ORDER_EVENT_TYPE.INVOICE_INFO, text: '发票信息', buttonClass: 'dark-action' })
|
||||
}
|
||||
|
||||
return actions
|
||||
},
|
||||
goOrderDetail(order, subOrder) {
|
||||
this.$router.push({
|
||||
path: '/joyful-selection/order-detail',
|
||||
query: {
|
||||
mainOrderId: order.mainOrderId,
|
||||
orderId: subOrder.orderId
|
||||
}
|
||||
})
|
||||
},
|
||||
async loadOrders() {
|
||||
this.loading = true
|
||||
|
||||
@@ -103,6 +295,84 @@ export default {
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async cancelMainOrder(order) {
|
||||
try {
|
||||
await Dialog.confirm({
|
||||
title: '取消订单',
|
||||
message: '确定取消该未支付订单吗?'
|
||||
})
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.operationKey = this.actionKey('cancel-main', order.mainOrderId)
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId || !order.mainOrderId) {
|
||||
Toast('订单信息不完整,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
await postFunction('order-cancel-main', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
orderId: order.mainOrderId,
|
||||
mainOrderId: order.mainOrderId
|
||||
})
|
||||
|
||||
Toast('订单已取消')
|
||||
await this.loadOrders()
|
||||
} catch (error) {
|
||||
Toast(error.message || '取消订单失败,请稍后再试')
|
||||
} finally {
|
||||
this.operationKey = ''
|
||||
}
|
||||
},
|
||||
async submitSubOrderEvent(order, subOrder, action) {
|
||||
try {
|
||||
await Dialog.confirm({
|
||||
title: action.text,
|
||||
message: `确定对该子单进行“${action.text}”操作吗?`
|
||||
})
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.operationKey = this.actionKey(action.orderType, subOrder.orderId)
|
||||
|
||||
try {
|
||||
await this.fetchBridgeToken()
|
||||
|
||||
if (!this.supplierId || !this.userId || !order.mainOrderId || !subOrder.orderId) {
|
||||
Toast('订单信息不完整,请稍后再试')
|
||||
return
|
||||
}
|
||||
|
||||
const data = await postFunction('order-push-event', {
|
||||
supplierId: this.supplierId,
|
||||
userId: this.userId,
|
||||
orderType: action.orderType,
|
||||
orderId: subOrder.orderId,
|
||||
mainOrderId: order.mainOrderId,
|
||||
subOrderId: subOrder.orderId,
|
||||
amount: Number(subOrder.amount || 0),
|
||||
reason: action.text,
|
||||
invoiceBizId: action.orderType === ORDER_EVENT_TYPE.INVOICE_INFO ? `INV_${subOrder.orderId}` : undefined,
|
||||
invoiceIds: action.orderType === ORDER_EVENT_TYPE.INVOICE_INFO ? `INV_${subOrder.orderId}` : undefined,
|
||||
orderIds: action.orderType === ORDER_EVENT_TYPE.INVOICE_INFO ? subOrder.orderId : undefined,
|
||||
time: Date.now()
|
||||
})
|
||||
|
||||
Toast(data?.pushSuccess === false ? '本地已保存,远程推送失败' : `${action.text}成功`)
|
||||
await this.loadOrders()
|
||||
} catch (error) {
|
||||
Toast(error.message || `${action.text}失败,请稍后再试`)
|
||||
} finally {
|
||||
this.operationKey = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,12 +416,6 @@ export default {
|
||||
padding: 8px 8px 14px;
|
||||
}
|
||||
|
||||
.order-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 42px 0;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
@@ -159,48 +423,78 @@ export default {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.order-card header {
|
||||
.main-order-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 12px 8px;
|
||||
padding: 12px 12px 10px;
|
||||
border-bottom: 1px solid #f4f4f4;
|
||||
}
|
||||
|
||||
.order-nos {
|
||||
.main-order-header span {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-nos span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
color: #555;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.order-card header b {
|
||||
.cancel-main-btn {
|
||||
flex: none;
|
||||
margin-left: 10px;
|
||||
color: #ff5b20;
|
||||
border-color: #ff5b20;
|
||||
}
|
||||
|
||||
.sub-order-row {
|
||||
padding: 10px 12px 12px;
|
||||
}
|
||||
|
||||
.sub-order-row + .sub-order-row {
|
||||
border-top: 1px solid #f4f4f4;
|
||||
}
|
||||
|
||||
.sub-order-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sub-order-title span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sub-order-title b {
|
||||
flex: none;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.order-card header .pending {
|
||||
.sub-order-title .pending {
|
||||
color: #ff5b20;
|
||||
}
|
||||
|
||||
.order-card header .done {
|
||||
.sub-order-title .done {
|
||||
color: #21b36b;
|
||||
}
|
||||
|
||||
.sub-order-title .muted {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.order-body {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px 10px;
|
||||
}
|
||||
|
||||
.thumb-row {
|
||||
@@ -238,16 +532,18 @@ export default {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.order-card footer {
|
||||
.sub-order-row footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
padding: 0 12px 12px;
|
||||
gap: 8px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.order-card footer .van-button {
|
||||
min-width: 78px;
|
||||
.sub-order-row footer .van-button {
|
||||
min-width: 72px;
|
||||
height: 28px;
|
||||
padding: 0 14px;
|
||||
padding: 0 12px;
|
||||
border-radius: 15px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<main class="points-page">
|
||||
<div>
|
||||
<joyful-selection-points-skeleton v-if="loading" />
|
||||
<main v-else class="points-page">
|
||||
<van-nav-bar title="我的积分" left-arrow @click-left="$router.back()" />
|
||||
<section class="score">
|
||||
<small>可用积分</small>
|
||||
@@ -24,16 +26,22 @@
|
||||
/>
|
||||
<p>没有更多了~</p>
|
||||
</section>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState } from 'pinia'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import JoyfulSelectionPointsSkeleton from '@/skeleton-view/joyful-selection/profile/points/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'JoyfulSelectionPoints',
|
||||
components: {
|
||||
JoyfulSelectionPointsSkeleton
|
||||
},
|
||||
data: () => ({
|
||||
loading: true,
|
||||
items: [
|
||||
{ title: '订单兑换', time: '2024-05-18 14:35', value: '-287.9' },
|
||||
{ title: '积分发放', time: '2024-05-10 09:20', value: '+300' }
|
||||
@@ -48,9 +56,12 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loading = true
|
||||
this.fetchUserPoints().catch(error => {
|
||||
console.warn('查询用户积分失败', error)
|
||||
this.$toast(error.message || '查询用户积分失败')
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# MySQL connection string for Netlify Functions runtime and Prisma CLI.
|
||||
# MySQL connection string for Koa runtime and Prisma CLI.
|
||||
DATABASE_URL="mysql://supper:Supper%40123456@localhost:10086/supplier_db"
|
||||
PRISMA_CLIENT_ENGINE_TYPE="binary"
|
||||
|
||||
# Optional app settings.
|
||||
JWT_SECRET="replace-me"
|
||||
@@ -7,3 +8,13 @@ UPLOAD_TOKEN="replace-me"
|
||||
|
||||
# Main points mall bridge API, for example http://localhost/open.
|
||||
POINTS_MALL_OPEN_API_BASE="http://localhost/open"
|
||||
|
||||
# Supplier open API crypto keys used by /supplierOpenApi/pointOrder/queryOrderInfo.
|
||||
SUPPLIER_OPEN_SM4_KEY_HEX="00112233445566778899aabbccddeeff"
|
||||
SUPPLIER_OPEN_SM4_IV_HEX="0102030405060708090a0b0c0d0e0f10"
|
||||
|
||||
# Redis token cache. Redis DB 9 is used for platform bridge tokens.
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT="6379"
|
||||
REDIS_PASSWORD=""
|
||||
REDIS_DB="9"
|
||||
|
||||
Generated
-30
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"version": "5",
|
||||
"remote": {
|
||||
"https://edge.netlify.com/": "fd941d61d88673d5f28aab283fb86fcc50f08a3bc80ee5470498fcfa88c65cfb",
|
||||
"https://edge.netlify.com/bootstrap/config.ts": "6a2ce0e544e15e8f8883a5c18da5948e37fd0f2619f68cb31f3af53c51817025",
|
||||
"https://edge.netlify.com/bootstrap/context.ts": "e97240232121e2f369f6546ce961490f34d961ea1ea54be3ff09633e3f08373f",
|
||||
"https://edge.netlify.com/bootstrap/cookie.ts": "8b0baae708989ca183c6f3b4ab3d029e6abcbc2e43f93edeb0ff447b3bbc3a05",
|
||||
"https://edge.netlify.com/bootstrap/edge_function.ts": "b8253e86aa83c67341f5cfedeba5049d77fbf84dcab7eceff7566b7728ae9b39",
|
||||
"https://edge.netlify.com/bootstrap/globals/types.ts": "eaa6148ded3121d8dee62dd91c86e7fe76601df0f3ca8d7962243a30f4c8935f"
|
||||
},
|
||||
"workspace": {
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@neondatabase/serverless@^1.0.1",
|
||||
"npm:@netlify/functions@^4.2.5",
|
||||
"npm:@prisma/adapter-neon@^6.16.0",
|
||||
"npm:@prisma/client@^6.16.0",
|
||||
"npm:@types/crypto-js@^4.2.2",
|
||||
"npm:@types/node@^24.10.1",
|
||||
"npm:@types/ws@^8.18.1",
|
||||
"npm:crypto-js@^4.2.0",
|
||||
"npm:dotenv@^17.2.3",
|
||||
"npm:netlify-cli@^23.13.3",
|
||||
"npm:prisma@^6.19.3",
|
||||
"npm:typescript@^5.9.3",
|
||||
"npm:ws@^8.18.3"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[build]
|
||||
command = "npm run build"
|
||||
functions = "netlify/functions"
|
||||
|
||||
[functions]
|
||||
node_bundler = "esbuild"
|
||||
external_node_modules = ["@prisma/client", "@prisma/adapter-neon", "@neondatabase/serverless", "ws"]
|
||||
|
||||
[[headers]]
|
||||
for = "/.netlify/functions/*"
|
||||
[headers.values]
|
||||
Access-Control-Allow-Origin = "*"
|
||||
Access-Control-Allow-Headers = "Content-Type, Authorization"
|
||||
Access-Control-Allow-Methods = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
@@ -1,106 +0,0 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
|
||||
type OrderListRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
type OrderStatusView = {
|
||||
text: string
|
||||
statusClass: string
|
||||
actionText: string
|
||||
buttonClass: string
|
||||
}
|
||||
|
||||
const statusMap: Record<number, OrderStatusView> = {
|
||||
0: { text: '待付款', statusClass: 'pending', actionText: '去支付', buttonClass: 'primary-action' },
|
||||
1: { text: '待发货', statusClass: 'pending', actionText: '查看订单', buttonClass: 'primary-action' },
|
||||
2: { text: '待发货', statusClass: 'pending', actionText: '查看订单', buttonClass: 'primary-action' },
|
||||
3: { text: '待收货', statusClass: 'pending', actionText: '确认收货', buttonClass: 'dark-action' },
|
||||
4: { text: '已完成', statusClass: 'done', actionText: '再次购买', buttonClass: 'dark-action' },
|
||||
5: { text: '已完成', statusClass: 'done', actionText: '再次购买', buttonClass: 'dark-action' }
|
||||
}
|
||||
|
||||
const toYuanText = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => {
|
||||
if (value && typeof value === 'object' && 'toFixed' in value) {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
const firstImage = (picInfo: unknown) => {
|
||||
if (Array.isArray(picInfo)) {
|
||||
return String(picInfo[0] || '')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderListRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
|
||||
if (!supplierId || !userId) {
|
||||
return fail('供应商或用户信息不能为空')
|
||||
}
|
||||
|
||||
const subOrders = await prisma.supplierOrderSub.findMany({
|
||||
where: {
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
},
|
||||
include: {
|
||||
mainOrder: true,
|
||||
products: {
|
||||
where: { deleted: 0 },
|
||||
orderBy: { createAt: 'asc' }
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ createTime: 'desc' },
|
||||
{ createAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
const orders = subOrders.map(order => {
|
||||
const status = statusMap[order.orderState] || statusMap[1]
|
||||
const products = order.products.map(product => ({
|
||||
id: product.id,
|
||||
sku: product.sku,
|
||||
name: product.name,
|
||||
number: product.number,
|
||||
price: toYuanText(product.price),
|
||||
image: product.productImgUrl || firstImage(product.picInfo)
|
||||
}))
|
||||
|
||||
return {
|
||||
id: order.orderId,
|
||||
orderId: order.orderId,
|
||||
orderNumberId: order.orderNumberId || '',
|
||||
mainOrderId: order.mainOrderId,
|
||||
mainOrderNumberId: order.mainOrder?.orderNumberId || '',
|
||||
supplierId: order.supplierId,
|
||||
status: status.text,
|
||||
statusClass: status.statusClass,
|
||||
actionText: status.actionText,
|
||||
buttonClass: status.buttonClass,
|
||||
orderState: order.orderState,
|
||||
amount: toYuanText(order.totalPrice),
|
||||
count: products.reduce((total, item) => total + Number(item.number || 0), 0),
|
||||
products
|
||||
}
|
||||
})
|
||||
|
||||
return ok({ orders })
|
||||
})
|
||||
@@ -1,87 +0,0 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { createBridgeSignature } from '../utils/crypto.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
|
||||
type SupplierTokenRequest = {
|
||||
userId?: string
|
||||
supplierIdentification?: string
|
||||
}
|
||||
|
||||
type BridgeTokenResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
accessToken?: string
|
||||
refreshToken?: string
|
||||
expire?: number
|
||||
refreshExpire?: number
|
||||
time?: string
|
||||
}
|
||||
|
||||
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<SupplierTokenRequest>(request)
|
||||
const userId = String(body.userId || '').trim()
|
||||
const supplierIdentification = String(body.supplierIdentification || '').trim().toLowerCase()
|
||||
|
||||
if (!userId || !supplierIdentification) {
|
||||
return fail('用户信息和供应商唯一标识不能为空')
|
||||
}
|
||||
|
||||
const supplier = await prisma.supplierInfo.findUnique({
|
||||
where: { supplierIdentification }
|
||||
})
|
||||
|
||||
if (!supplier || supplier.deleted === 1) {
|
||||
return fail('供应商信息未配置或已停用', 404, 'SUPPLIER_NOT_CONFIGURED')
|
||||
}
|
||||
|
||||
const timestamp = String(Date.now())
|
||||
const signature = createBridgeSignature(supplier.clientId, supplier.clientSecret, timestamp)
|
||||
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
||||
|
||||
if (!apiBase) {
|
||||
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiBase}/connect/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
supplierId: supplier.supplierId,
|
||||
clientId: supplier.clientId,
|
||||
clientSecret: supplier.clientSecret,
|
||||
sendSha256: signature,
|
||||
code: '',
|
||||
timestamp
|
||||
})
|
||||
})
|
||||
|
||||
const tokenPayload = (await response.json().catch(() => ({}))) as BridgeTokenResponse
|
||||
|
||||
if (!response.ok || tokenPayload.resultCode !== 0) {
|
||||
return fail(tokenPayload.message || '获取平台授权令牌失败', 502, 'BRIDGE_TOKEN_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
supplierIdentification,
|
||||
supplierId: supplier.supplierId,
|
||||
supplierName: supplier.supplierName,
|
||||
userId,
|
||||
token: tokenPayload.accessToken,
|
||||
accessToken: tokenPayload.accessToken,
|
||||
refreshToken: tokenPayload.refreshToken,
|
||||
expiresIn: tokenPayload.expire,
|
||||
refreshExpiresIn: tokenPayload.refreshExpire,
|
||||
issuedAt: tokenPayload.time
|
||||
})
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
export type ApiSuccess<T> = {
|
||||
success: true
|
||||
data: T
|
||||
}
|
||||
|
||||
export type ApiFailure = {
|
||||
success: false
|
||||
error: {
|
||||
code: string
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = ApiSuccess<T> | ApiFailure
|
||||
Generated
-22218
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "netlify dev",
|
||||
"dev": "tsx src/server.ts",
|
||||
"start": "tsx src/server.ts",
|
||||
"build": "prisma generate",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prisma:generate": "prisma generate",
|
||||
@@ -14,19 +15,25 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@netlify/functions": "^4.2.5",
|
||||
"@prisma/adapter-neon": "^6.16.0",
|
||||
"@prisma/client": "^6.16.0",
|
||||
"@prisma/adapter-neon": "6.19.3",
|
||||
"@prisma/client": "6.19.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"ioredis": "^5.4.2",
|
||||
"@koa/router": "^13.1.1",
|
||||
"koa": "^2.15.3",
|
||||
"koa-bodyparser": "^4.4.1",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/koa": "^2.15.0",
|
||||
"@types/koa-bodyparser": "^4.3.12",
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"netlify-cli": "^23.13.3",
|
||||
"prisma": "^6.19.3",
|
||||
"prisma": "6.19.3",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ model User {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
carts CartItem[]
|
||||
orders Order[]
|
||||
}
|
||||
|
||||
model SupplierProduct {
|
||||
@@ -44,7 +43,6 @@ model SupplierProduct {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
carts CartItem[]
|
||||
orderItems OrderItem[]
|
||||
|
||||
@@map("supplier_product")
|
||||
}
|
||||
@@ -56,6 +54,8 @@ model SupplierInfo {
|
||||
supplierName String
|
||||
clientId String
|
||||
clientSecret String
|
||||
supplierApiUrl String?
|
||||
sroNo String?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
creator String?
|
||||
@@ -81,35 +81,6 @@ model CartItem {
|
||||
@@unique([userId, productId, skuText])
|
||||
}
|
||||
|
||||
model Order {
|
||||
id Int @id @default(autoincrement())
|
||||
orderNo String @unique
|
||||
userId Int
|
||||
status OrderStatus @default(PENDING_PAYMENT)
|
||||
totalAmount Decimal @db.Decimal(10, 2)
|
||||
payType PayType?
|
||||
paidAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
items OrderItem[]
|
||||
}
|
||||
|
||||
model OrderItem {
|
||||
id Int @id @default(autoincrement())
|
||||
orderId Int
|
||||
productId Int
|
||||
name String
|
||||
imageUrl String?
|
||||
skuText String?
|
||||
price Decimal @db.Decimal(10, 2)
|
||||
quantity Int
|
||||
|
||||
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
product SupplierProduct @relation(fields: [productId], references: [id])
|
||||
}
|
||||
|
||||
model SupplierOrderMain {
|
||||
id Int @id @default(autoincrement())
|
||||
orderId String @unique
|
||||
@@ -130,6 +101,8 @@ model SupplierOrderMain {
|
||||
logisticsOrderId String?
|
||||
crrgBsnNm String?
|
||||
orderState Int @default(0)
|
||||
isSplit Int @default(0)
|
||||
splitTime DateTime?
|
||||
totalPrice Decimal @default(0) @db.Decimal(10, 2)
|
||||
wPayOrAPay Decimal @default(0) @db.Decimal(10, 2)
|
||||
pointsPrice Decimal @default(0) @db.Decimal(10, 2)
|
||||
@@ -202,16 +175,121 @@ model SupplierOrderProduct {
|
||||
@@map("supplier_order_product")
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PENDING_PAYMENT
|
||||
PAID
|
||||
SHIPPED
|
||||
COMPLETED
|
||||
CANCELLED
|
||||
model SupplierPointsLedger {
|
||||
id Int @id @default(autoincrement())
|
||||
ledgerNo String @unique
|
||||
supplierId String
|
||||
userId String
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
bizType Int
|
||||
direction Int
|
||||
points Decimal @db.Decimal(10, 2)
|
||||
beforePoints Decimal? @db.Decimal(10, 2)
|
||||
afterPoints Decimal? @db.Decimal(10, 2)
|
||||
remark String? @db.Text
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, userId])
|
||||
@@index([mainOrderId])
|
||||
@@index([subOrderId])
|
||||
@@map("supplier_points_ledger")
|
||||
}
|
||||
|
||||
enum PayType {
|
||||
POINTS
|
||||
WECHAT
|
||||
ALIPAY
|
||||
model SupplierOrderPushLog {
|
||||
id Int @id @default(autoincrement())
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
orderId String? @db.VarChar(1000)
|
||||
orderType Int?
|
||||
apiPath String
|
||||
requestBody Json?
|
||||
responseBody Json?
|
||||
httpStatus Int?
|
||||
resultCode Int?
|
||||
pushStatus Int @default(0)
|
||||
retryCount Int @default(0)
|
||||
errorMessage String? @db.Text
|
||||
lastPushTime DateTime?
|
||||
nextRetryTime DateTime?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([orderType, pushStatus])
|
||||
@@index([nextRetryTime])
|
||||
@@map("supplier_order_push_log")
|
||||
}
|
||||
|
||||
model SupplierOrderOperateLog {
|
||||
id Int @id @default(autoincrement())
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
operateType Int
|
||||
beforeState Int?
|
||||
afterState Int?
|
||||
beforeSplit Int?
|
||||
afterSplit Int?
|
||||
remark String? @db.Text
|
||||
operator String?
|
||||
createTime DateTime @default(now())
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([subOrderId])
|
||||
@@index([operateType])
|
||||
@@map("supplier_order_operate_log")
|
||||
}
|
||||
|
||||
model SupplierRefundExchange {
|
||||
id Int @id @default(autoincrement())
|
||||
serviceOrderId String @unique
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
serviceType Int
|
||||
serviceState Int @default(0)
|
||||
amount Decimal @default(0) @db.Decimal(10, 2)
|
||||
reason String? @db.Text
|
||||
requestBody Json?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([subOrderId])
|
||||
@@index([serviceType, serviceState])
|
||||
@@map("supplier_refund_exchange")
|
||||
}
|
||||
|
||||
model SupplierInvoiceInfo {
|
||||
id Int @id @default(autoincrement())
|
||||
invoiceBizId String @unique
|
||||
supplierId String
|
||||
userId String?
|
||||
mainOrderId String?
|
||||
subOrderId String?
|
||||
settlementId String?
|
||||
invoiceCode String?
|
||||
invoiceIds String? @db.VarChar(1000)
|
||||
orderIds String? @db.VarChar(1000)
|
||||
invoiceStatus Int @default(0)
|
||||
fileId String?
|
||||
filePath String?
|
||||
requestBody Json?
|
||||
createTime DateTime @default(now())
|
||||
updateTime DateTime @updatedAt
|
||||
deleted Int @default(0)
|
||||
|
||||
@@index([supplierId, mainOrderId])
|
||||
@@index([settlementId])
|
||||
@@index([invoiceCode])
|
||||
@@map("supplier_invoice_info")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
CREATE TABLE IF NOT EXISTS `supplier_points_ledger` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '积分流水主键ID',
|
||||
`ledgerNo` VARCHAR(191) NOT NULL COMMENT '流水号,业务唯一',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NOT NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
||||
`bizType` INT NOT NULL COMMENT '业务类型:1支付消费,2取消返还,3退货退款,4人工调整',
|
||||
`direction` INT NOT NULL COMMENT '流水方向:1增加,2扣减',
|
||||
`points` DECIMAL(10, 2) NOT NULL COMMENT '积分变动金额',
|
||||
`beforePoints` DECIMAL(10, 2) NULL COMMENT '变动前积分余额',
|
||||
`afterPoints` DECIMAL(10, 2) NULL COMMENT '变动后积分余额',
|
||||
`remark` TEXT NULL COMMENT '流水备注',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_points_ledger_ledgerNo_key` (`ledgerNo`),
|
||||
KEY `supplier_points_ledger_supplierId_userId_idx` (`supplierId`, `userId`),
|
||||
KEY `supplier_points_ledger_mainOrderId_idx` (`mainOrderId`),
|
||||
KEY `supplier_points_ledger_subOrderId_idx` (`subOrderId`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商用户积分消费流水表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `supplier_order_push_log` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '订单推送日志主键ID',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '主单ID',
|
||||
`orderId` VARCHAR(1000) NULL COMMENT '推送订单ID,多个子单用英文逗号分隔',
|
||||
`orderType` INT NULL COMMENT '推送事件类型:1取消主单,2拆单子单,3取消子单,7取消已支付未拆单整单',
|
||||
`apiPath` VARCHAR(191) NOT NULL COMMENT '推送接口路径',
|
||||
`requestBody` JSON NULL COMMENT '推送请求体',
|
||||
`responseBody` JSON NULL COMMENT '推送响应体',
|
||||
`httpStatus` INT NULL COMMENT 'HTTP状态码',
|
||||
`resultCode` INT NULL COMMENT '远程业务返回码',
|
||||
`pushStatus` INT NOT NULL DEFAULT 0 COMMENT '推送状态:0待推送,1推送成功,2推送失败',
|
||||
`retryCount` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
|
||||
`errorMessage` TEXT NULL COMMENT '失败原因',
|
||||
`lastPushTime` DATETIME(3) NULL COMMENT '最后推送时间',
|
||||
`nextRetryTime` DATETIME(3) NULL COMMENT '下次重试时间',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `supplier_order_push_log_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_order_push_log_orderType_pushStatus_idx` (`orderType`, `pushStatus`),
|
||||
KEY `supplier_order_push_log_nextRetryTime_idx` (`nextRetryTime`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商订单远程推送日志表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `supplier_order_operate_log` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '订单操作日志主键ID',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '子单ID',
|
||||
`operateType` INT NOT NULL COMMENT '操作类型:1创建待支付,2支付成功,3拆单,4取消主单,5取消子单,6取消已支付未拆单整单,7已妥投,8已完成,9退款,10换货',
|
||||
`beforeState` INT NULL COMMENT '操作前订单状态',
|
||||
`afterState` INT NULL COMMENT '操作后订单状态',
|
||||
`beforeSplit` INT NULL COMMENT '操作前拆单状态:0未拆单,1已拆单',
|
||||
`afterSplit` INT NULL COMMENT '操作后拆单状态:0未拆单,1已拆单',
|
||||
`remark` TEXT NULL COMMENT '操作备注',
|
||||
`operator` VARCHAR(191) NULL COMMENT '操作人或系统标识',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `supplier_order_operate_log_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_order_operate_log_subOrderId_idx` (`subOrderId`),
|
||||
KEY `supplier_order_operate_log_operateType_idx` (`operateType`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商订单操作日志表';
|
||||
@@ -0,0 +1,56 @@
|
||||
SET @fk_order_item_order = (
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'orderitem'
|
||||
AND REFERENCED_TABLE_NAME = 'order'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SET @sql = IF(
|
||||
@fk_order_item_order IS NULL,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `orderitem` DROP FOREIGN KEY `', @fk_order_item_order, '`')
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @fk_order_item_product = (
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'orderitem'
|
||||
AND REFERENCED_TABLE_NAME = 'supplier_product'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SET @sql = IF(
|
||||
@fk_order_item_product IS NULL,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `orderitem` DROP FOREIGN KEY `', @fk_order_item_product, '`')
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @fk_order_user = (
|
||||
SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'order'
|
||||
AND REFERENCED_TABLE_NAME = 'User'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SET @sql = IF(
|
||||
@fk_order_user IS NULL,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `order` DROP FOREIGN KEY `', @fk_order_user, '`')
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
DROP TABLE IF EXISTS `orderitem`;
|
||||
DROP TABLE IF EXISTS `order`;
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE `supplier_order_main`
|
||||
MODIFY COLUMN `orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1已支付,2进行中,3已取消,4已妥投,5已完成,6换货,7退货,8废除',
|
||||
ADD COLUMN `isSplit` TINYINT NOT NULL DEFAULT 0 COMMENT '主单是否拆单:0未拆单,1已拆单' AFTER `orderState`,
|
||||
ADD COLUMN `splitTime` DATETIME(3) NULL COMMENT '主单拆单时间' AFTER `isSplit`;
|
||||
|
||||
ALTER TABLE `supplier_order_sub`
|
||||
MODIFY COLUMN `orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态:0待付款,1已支付,2进行中,3已取消,4已妥投,5已完成,6换货,7退货,8废除';
|
||||
@@ -0,0 +1,46 @@
|
||||
CREATE TABLE IF NOT EXISTS `supplier_refund_exchange` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '售后记录主键ID',
|
||||
`serviceOrderId` VARCHAR(191) NOT NULL COMMENT '退货/换货业务单号',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
||||
`serviceType` INT NOT NULL COMMENT '售后类型:4退货,5换货',
|
||||
`serviceState` INT NOT NULL DEFAULT 0 COMMENT '售后状态:0已创建,1处理中,2已完成,3已取消',
|
||||
`amount` DECIMAL(10, 2) NOT NULL DEFAULT 0 COMMENT '售后金额',
|
||||
`reason` TEXT NULL COMMENT '售后原因',
|
||||
`requestBody` JSON NULL COMMENT '原始业务请求体',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_refund_exchange_serviceOrderId_key` (`serviceOrderId`),
|
||||
KEY `supplier_refund_exchange_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_refund_exchange_subOrderId_idx` (`subOrderId`),
|
||||
KEY `supplier_refund_exchange_serviceType_serviceState_idx` (`serviceType`, `serviceState`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商退货换货售后记录表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `supplier_invoice_info` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT COMMENT '发票信息主键ID',
|
||||
`invoiceBizId` VARCHAR(191) NOT NULL COMMENT '发票业务唯一ID,优先使用发票号或事件订单号',
|
||||
`supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID',
|
||||
`userId` VARCHAR(191) NULL COMMENT '用户ID',
|
||||
`mainOrderId` VARCHAR(191) NULL COMMENT '关联主单ID',
|
||||
`subOrderId` VARCHAR(191) NULL COMMENT '关联子单ID',
|
||||
`settlementId` VARCHAR(191) NULL COMMENT '结算单号',
|
||||
`invoiceCode` VARCHAR(191) NULL COMMENT '发票代码或纳税识别号',
|
||||
`invoiceIds` VARCHAR(1000) NULL COMMENT '发票号码列表,多个用英文逗号分隔',
|
||||
`orderIds` VARCHAR(1000) NULL COMMENT '关联订单号列表,多个用英文逗号分隔',
|
||||
`invoiceStatus` INT NOT NULL DEFAULT 0 COMMENT '发票状态:0已记录,1已申请,2已开票,3失败',
|
||||
`fileId` VARCHAR(191) NULL COMMENT '发票文件ID',
|
||||
`filePath` VARCHAR(191) NULL COMMENT '发票文件路径',
|
||||
`requestBody` JSON NULL COMMENT '原始业务请求体',
|
||||
`createTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
`updateTime` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
`deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `supplier_invoice_info_invoiceBizId_key` (`invoiceBizId`),
|
||||
KEY `supplier_invoice_info_supplierId_mainOrderId_idx` (`supplierId`, `mainOrderId`),
|
||||
KEY `supplier_invoice_info_settlementId_idx` (`settlementId`),
|
||||
KEY `supplier_invoice_info_invoiceCode_idx` (`invoiceCode`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商发票信息表';
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `supplier_info`
|
||||
ADD COLUMN `supplierApiUrl` VARCHAR(1000) NULL COMMENT '供应商开放接口地址' AFTER `clientSecret`,
|
||||
ADD COLUMN `sroNo` VARCHAR(100) NULL COMMENT 'BAB7服务商编号' AFTER `supplierApiUrl`;
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
@@ -9,3 +9,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
|
||||
return ok({ message: '加入购物车接口待实现' })
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
@@ -9,3 +9,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
|
||||
return ok({ message: '登录接口待实现' })
|
||||
})
|
||||
|
||||
+27
-59
@@ -1,13 +1,14 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderCancelMainRequest = {
|
||||
supplierId?: string
|
||||
token?: string
|
||||
userId?: string
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
@@ -18,26 +19,18 @@ type BridgeOrderResponse = {
|
||||
message?: string
|
||||
}
|
||||
|
||||
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderCancelMainRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const token = String(body.token || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const mainOrderId = String(body.mainOrderId || body.orderId || '').trim()
|
||||
const orderId = String(body.orderId || mainOrderId).trim()
|
||||
|
||||
if (!supplierId || !token || !userId || !mainOrderId) {
|
||||
return fail('供应商、用户、授权或订单信息不能为空')
|
||||
}
|
||||
|
||||
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
||||
if (!apiBase) {
|
||||
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
|
||||
if (!supplierId || !userId || !mainOrderId) {
|
||||
return fail('供应商、用户或订单信息不能为空')
|
||||
}
|
||||
|
||||
const order = await prisma.supplierOrderMain.findFirst({
|
||||
@@ -53,47 +46,15 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
return fail('主单不存在或已取消', 404, 'MAIN_ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
const bridgeResponse = await fetch(`${apiBase}/pointOrder/pushOrderInfoMsg`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
supplierId,
|
||||
orderType: 1,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId,
|
||||
time: Date.now(),
|
||||
token
|
||||
})
|
||||
})
|
||||
|
||||
const bridgePayload = (await bridgeResponse.json().catch(() => ({}))) as BridgeOrderResponse
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '取消主单推送失败', 502, 'BRIDGE_CANCEL_MAIN_FAILED')
|
||||
if (order.orderState !== ORDER_STATE.PENDING_PAY) {
|
||||
return fail('已支付主单不能通过未支付取消接口取消', 400, 'MAIN_ORDER_ALREADY_PAID')
|
||||
}
|
||||
|
||||
const subOrders = await prisma.supplierOrderSub.findMany({
|
||||
where: {
|
||||
mainOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
},
|
||||
select: {
|
||||
orderId: true
|
||||
}
|
||||
})
|
||||
const subOrderIds = subOrders.map(item => item.orderId)
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId: mainOrderId },
|
||||
data: {
|
||||
deleted: 1,
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
@@ -106,27 +67,34 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
deleted: 0
|
||||
},
|
||||
data: {
|
||||
deleted: 1,
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderProduct.updateMany({
|
||||
where: {
|
||||
subOrderId: { in: subOrderIds },
|
||||
deleted: 0
|
||||
},
|
||||
data: {
|
||||
deleted: 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { response: bridgeResponse, payload: bridgePayload } = await bridgeFetchWithAuth<BridgeOrderResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pushOrderInfoMsg',
|
||||
{
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_MAIN,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId,
|
||||
time: Date.now()
|
||||
}
|
||||
)
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '取消主单推送失败', 502, 'BRIDGE_CANCEL_MAIN_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
supplierId,
|
||||
orderType: 1,
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_MAIN,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_SPLIT_STATE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderCancelPaidMainRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
}
|
||||
|
||||
type BridgeOrderResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderCancelPaidMainRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const mainOrderId = String(body.mainOrderId || body.orderId || '').trim()
|
||||
const orderId = String(body.orderId || mainOrderId).trim()
|
||||
|
||||
if (!supplierId || !userId || !mainOrderId) {
|
||||
return fail('供应商、用户或订单信息不能为空')
|
||||
}
|
||||
|
||||
const order = await prisma.supplierOrderMain.findFirst({
|
||||
where: {
|
||||
orderId: mainOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
}
|
||||
})
|
||||
|
||||
if (!order) {
|
||||
return fail('主单不存在或已取消', 404, 'MAIN_ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
if (order.orderState === ORDER_STATE.PENDING_PAY) {
|
||||
return fail('未支付主单请使用未支付取消接口', 400, 'MAIN_ORDER_NOT_PAID')
|
||||
}
|
||||
|
||||
if (order.orderState !== ORDER_STATE.FINISH_PAY || order.isSplit === ORDER_SPLIT_STATE.SPLIT) {
|
||||
return fail('该主单已拆单,不能按已支付未拆单整单取消', 400, 'MAIN_ORDER_ALREADY_SPLIT')
|
||||
}
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId: mainOrderId },
|
||||
data: {
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: {
|
||||
mainOrderId,
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
},
|
||||
data: {
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { response: bridgeResponse, payload: bridgePayload } = await bridgeFetchWithAuth<BridgeOrderResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pushOrderInfoMsg',
|
||||
{
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_PAID_NOT_SPLIT_MAIN,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId,
|
||||
time: Date.now()
|
||||
}
|
||||
)
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '取消已支付未拆单整单推送失败', 502, 'BRIDGE_CANCEL_PAID_MAIN_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
supplierId,
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_PAID_NOT_SPLIT_MAIN,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId
|
||||
})
|
||||
})
|
||||
|
||||
+23
-48
@@ -1,13 +1,14 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderCancelSubRequest = {
|
||||
supplierId?: string
|
||||
token?: string
|
||||
userId?: string
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
@@ -18,25 +19,17 @@ type BridgeOrderResponse = {
|
||||
message?: string
|
||||
}
|
||||
|
||||
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderCancelSubRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const token = String(body.token || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const orderId = String(body.orderId || '').trim()
|
||||
|
||||
if (!supplierId || !token || !userId || !orderId) {
|
||||
return fail('供应商、用户、授权或子单信息不能为空')
|
||||
}
|
||||
|
||||
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
||||
if (!apiBase) {
|
||||
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
|
||||
if (!supplierId || !userId || !orderId) {
|
||||
return fail('供应商、用户或子单信息不能为空')
|
||||
}
|
||||
|
||||
const subOrder = await prisma.supplierOrderSub.findFirst({
|
||||
@@ -54,54 +47,36 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
|
||||
const mainOrderId = String(body.mainOrderId || subOrder.mainOrderId).trim()
|
||||
|
||||
const bridgeResponse = await fetch(`${apiBase}/pointOrder/pushOrderInfoMsg`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
supplierId,
|
||||
orderType: 3,
|
||||
await prisma.supplierOrderSub.update({
|
||||
where: { orderId },
|
||||
data: {
|
||||
orderState: ORDER_STATE.CANCELLED,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
const { response: bridgeResponse, payload: bridgePayload } = await bridgeFetchWithAuth<BridgeOrderResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pushOrderInfoMsg',
|
||||
{
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_SUB,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId,
|
||||
time: Date.now(),
|
||||
token
|
||||
})
|
||||
})
|
||||
|
||||
const bridgePayload = (await bridgeResponse.json().catch(() => ({}))) as BridgeOrderResponse
|
||||
time: Date.now()
|
||||
}
|
||||
)
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '取消子单推送失败', 502, 'BRIDGE_CANCEL_SUB_FAILED')
|
||||
}
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
await tx.supplierOrderSub.update({
|
||||
where: { orderId },
|
||||
data: {
|
||||
deleted: 1,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderProduct.updateMany({
|
||||
where: {
|
||||
subOrderId: orderId,
|
||||
deleted: 0
|
||||
},
|
||||
data: {
|
||||
deleted: 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return ok({
|
||||
supplierId,
|
||||
orderType: 3,
|
||||
orderType: ORDER_EVENT_TYPE.CANCEL_SUB,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId
|
||||
})
|
||||
})
|
||||
|
||||
+99
-88
@@ -1,13 +1,15 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_SPLIT_STATE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type CheckoutGoods = {
|
||||
id?: string | number
|
||||
sku?: string
|
||||
sku?: string | number | Record<string, unknown>
|
||||
count?: number
|
||||
number?: number
|
||||
price?: string | number
|
||||
@@ -20,7 +22,6 @@ type CheckoutGoods = {
|
||||
|
||||
type OrderCreateRequest = {
|
||||
supplierId?: string
|
||||
token?: string
|
||||
userId?: string
|
||||
orderNo?: string
|
||||
orderNumberId?: string
|
||||
@@ -33,7 +34,16 @@ type BridgeOrderResponse = {
|
||||
message?: string
|
||||
}
|
||||
|
||||
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
||||
type OrderProductSnapshot = {
|
||||
sku: string
|
||||
number: number
|
||||
price: number
|
||||
name: string
|
||||
taxRate: number
|
||||
picInfo: string[]
|
||||
fileId: string | null
|
||||
filePath: string | null
|
||||
}
|
||||
|
||||
const nowText = () => {
|
||||
const now = new Date()
|
||||
@@ -49,13 +59,19 @@ const createOrderNumberId = () => {
|
||||
return `TR${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}${pad(now.getMilliseconds(), 3)}`
|
||||
}
|
||||
|
||||
const toYuan = (centAmount?: number) => Number(((Number(centAmount || 0)) / 100).toFixed(2))
|
||||
const toYuan = (centAmount?: number) => Number((Number(centAmount || 0) / 100).toFixed(2))
|
||||
const toMoney = (value: number) => Number(value.toFixed(2))
|
||||
const createSubOrderId = (orderId: string, index: number) => `${orderId}_${String(index + 1).padStart(3, '0')}`
|
||||
const createSubOrderNumberId = (orderNumberId: string, index: number) => `${orderNumberId}_${String(index + 1).padStart(3, '0')}`
|
||||
|
||||
const toOrderProducts = (goods: CheckoutGoods[]) => goods.map((item, index) => {
|
||||
const image = item.image || item.imageUrl || item.picInfo?.[0] || ''
|
||||
const sku = typeof item.sku === 'object' && item.sku
|
||||
? [item.id, item.sku.color, item.sku.size].filter(Boolean).join('-')
|
||||
: item.sku
|
||||
|
||||
return {
|
||||
sku: String(item.sku || item.id || `SKU_${index + 1}`),
|
||||
sku: String(sku || item.id || `SKU_${index + 1}`),
|
||||
number: Number(item.count || item.number || 1),
|
||||
price: Number(item.price || 0),
|
||||
name: String(item.name || ''),
|
||||
@@ -66,36 +82,47 @@ const toOrderProducts = (goods: CheckoutGoods[]) => goods.map((item, index) => {
|
||||
}
|
||||
})
|
||||
|
||||
const productSubtotal = (item: OrderProductSnapshot) => toMoney(Number(item.price || 0) * Number(item.number || 0))
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderCreateRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const token = String(body.token || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const orderId = String(body.orderNo || '').trim()
|
||||
const amount = toYuan(body.amount)
|
||||
const goods = Array.isArray(body.goods) ? body.goods : []
|
||||
const orderProInfos = toOrderProducts(goods)
|
||||
|
||||
if (!supplierId || !token || !userId || !orderId) {
|
||||
return fail('供应商、用户、授权或订单信息不能为空')
|
||||
if (!supplierId || !userId || !orderId) {
|
||||
return fail('供应商、用户或订单信息不能为空')
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
return fail('订单金额必须大于0')
|
||||
}
|
||||
|
||||
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
||||
if (!apiBase) {
|
||||
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
|
||||
if (!orderProInfos.length) {
|
||||
return fail('订单商品不能为空')
|
||||
}
|
||||
|
||||
const orderNumberId = String(body.orderNumberId || createOrderNumberId())
|
||||
const subOrderId = `${orderId}_001`
|
||||
const subOrderNumberId = `${orderNumberId}_001`
|
||||
const orderTime = nowText()
|
||||
const orderProInfos = toOrderProducts(goods)
|
||||
const localOrderState = ORDER_STATE.PENDING_PAY
|
||||
const now = new Date()
|
||||
const subOrders = orderProInfos.map((item, index) => {
|
||||
const subTotal = productSubtotal(item)
|
||||
|
||||
return {
|
||||
orderId: createSubOrderId(orderId, index),
|
||||
orderNumberId: createSubOrderNumberId(orderNumberId, index),
|
||||
product: item,
|
||||
totalPrice: subTotal,
|
||||
pointsPrice: subTotal
|
||||
}
|
||||
})
|
||||
const orderInfo = {
|
||||
orderId,
|
||||
orderNumberId,
|
||||
@@ -113,7 +140,7 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
wPayOrAPayRefund: 0,
|
||||
logisticsOrderId: null,
|
||||
crrgBsnNm: null,
|
||||
orderState: 1,
|
||||
orderState: ORDER_STATE.PENDING_PAY,
|
||||
totalPrice: amount,
|
||||
wPayOrAPay: 0,
|
||||
pointsPrice: amount,
|
||||
@@ -122,6 +149,21 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
}
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
const existingSubOrders = await tx.supplierOrderSub.findMany({
|
||||
where: { mainOrderId: orderId },
|
||||
select: { orderId: true }
|
||||
})
|
||||
const existingSubOrderIds = existingSubOrders.map(item => item.orderId)
|
||||
|
||||
if (existingSubOrderIds.length) {
|
||||
await tx.supplierOrderProduct.deleteMany({
|
||||
where: { subOrderId: { in: existingSubOrderIds } }
|
||||
})
|
||||
await tx.supplierOrderSub.deleteMany({
|
||||
where: { mainOrderId: orderId }
|
||||
})
|
||||
}
|
||||
|
||||
await tx.supplierOrderMain.upsert({
|
||||
where: { orderId },
|
||||
update: {
|
||||
@@ -129,13 +171,15 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
supplierId,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
updateTime: new Date(),
|
||||
updateTime: now,
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
freightCost: orderInfo.freightCost,
|
||||
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
|
||||
refund: orderInfo.refund,
|
||||
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
|
||||
orderState: orderInfo.orderState,
|
||||
orderState: localOrderState,
|
||||
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
|
||||
splitTime: null,
|
||||
totalPrice: orderInfo.totalPrice,
|
||||
wPayOrAPay: orderInfo.wPayOrAPay,
|
||||
pointsPrice: orderInfo.pointsPrice,
|
||||
@@ -148,14 +192,15 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
supplierId,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
createTime: new Date(),
|
||||
updateTime: new Date(),
|
||||
createTime: now,
|
||||
updateTime: now,
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
freightCost: orderInfo.freightCost,
|
||||
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
|
||||
refund: orderInfo.refund,
|
||||
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
|
||||
orderState: orderInfo.orderState,
|
||||
orderState: localOrderState,
|
||||
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
|
||||
totalPrice: orderInfo.totalPrice,
|
||||
wPayOrAPay: orderInfo.wPayOrAPay,
|
||||
pointsPrice: orderInfo.pointsPrice,
|
||||
@@ -163,88 +208,53 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderSub.upsert({
|
||||
where: { orderId: subOrderId },
|
||||
update: {
|
||||
orderNumberId: subOrderNumberId,
|
||||
await tx.supplierOrderSub.createMany({
|
||||
data: subOrders.map(subOrder => ({
|
||||
orderId: subOrder.orderId,
|
||||
orderNumberId: subOrder.orderNumberId,
|
||||
mainOrderId: orderId,
|
||||
supplierId,
|
||||
token,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
updateTime: new Date(),
|
||||
createTime: now,
|
||||
updateTime: now,
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
freightCost: orderInfo.freightCost,
|
||||
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
|
||||
refund: orderInfo.refund,
|
||||
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
|
||||
orderState: orderInfo.orderState,
|
||||
totalPrice: orderInfo.totalPrice,
|
||||
wPayOrAPay: orderInfo.wPayOrAPay,
|
||||
pointsPrice: orderInfo.pointsPrice,
|
||||
invoiceCode: orderInfo.invoiceCode,
|
||||
deleted: 0
|
||||
},
|
||||
create: {
|
||||
orderId: subOrderId,
|
||||
orderNumberId: subOrderNumberId,
|
||||
mainOrderId: orderId,
|
||||
supplierId,
|
||||
token,
|
||||
userId,
|
||||
aplName: orderInfo.aplName,
|
||||
createTime: new Date(),
|
||||
updateTime: new Date(),
|
||||
deliveryStatus: orderInfo.deliveryStatus,
|
||||
freightCost: orderInfo.freightCost,
|
||||
wPayOrAPayFreightCost: orderInfo.wPayOrAPayFreightCost,
|
||||
refund: orderInfo.refund,
|
||||
wPayOrAPayRefund: orderInfo.wPayOrAPayRefund,
|
||||
orderState: orderInfo.orderState,
|
||||
totalPrice: orderInfo.totalPrice,
|
||||
wPayOrAPay: orderInfo.wPayOrAPay,
|
||||
pointsPrice: orderInfo.pointsPrice,
|
||||
orderState: localOrderState,
|
||||
totalPrice: subOrder.totalPrice,
|
||||
wPayOrAPay: 0,
|
||||
pointsPrice: subOrder.pointsPrice,
|
||||
invoiceCode: orderInfo.invoiceCode
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
await tx.supplierOrderProduct.deleteMany({
|
||||
where: { subOrderId }
|
||||
await tx.supplierOrderProduct.createMany({
|
||||
data: subOrders.map(subOrder => ({
|
||||
subOrderId: subOrder.orderId,
|
||||
sku: subOrder.product.sku,
|
||||
number: subOrder.product.number,
|
||||
price: subOrder.product.price,
|
||||
name: subOrder.product.name,
|
||||
taxRate: subOrder.product.taxRate,
|
||||
productImgUrl: subOrder.product.picInfo[0] || null,
|
||||
fileId: subOrder.product.fileId,
|
||||
filePath: subOrder.product.filePath,
|
||||
picInfo: subOrder.product.picInfo
|
||||
}))
|
||||
})
|
||||
|
||||
if (orderProInfos.length) {
|
||||
await tx.supplierOrderProduct.createMany({
|
||||
data: orderProInfos.map(item => ({
|
||||
subOrderId,
|
||||
sku: item.sku,
|
||||
number: item.number,
|
||||
price: item.price,
|
||||
name: item.name,
|
||||
taxRate: item.taxRate,
|
||||
productImgUrl: item.picInfo[0] || null,
|
||||
fileId: item.fileId,
|
||||
filePath: item.filePath,
|
||||
picInfo: item.picInfo
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const bridgeResponse = await fetch(`${apiBase}/pointOrder/pendingOrderInfo`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
supplierId,
|
||||
const { response: bridgeResponse, payload: bridgePayload } = await bridgeFetchWithAuth<BridgeOrderResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pendingOrderInfo',
|
||||
{
|
||||
userId,
|
||||
orderInfo,
|
||||
token
|
||||
})
|
||||
})
|
||||
|
||||
const bridgePayload = (await bridgeResponse.json().catch(() => ({}))) as BridgeOrderResponse
|
||||
orderInfo
|
||||
}
|
||||
)
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '提交待支付订单失败', 502, 'BRIDGE_PENDING_ORDER_FAILED')
|
||||
@@ -253,7 +263,8 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
return ok({
|
||||
orderId,
|
||||
orderNumberId,
|
||||
subOrderId,
|
||||
subOrderIds: subOrders.map(item => item.orderId),
|
||||
orderInfo
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_STATE } from '../utils/order-state.js'
|
||||
|
||||
type OrderListRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
type OrderStatusView = {
|
||||
text: string
|
||||
statusClass: string
|
||||
actionText: string
|
||||
buttonClass: string
|
||||
}
|
||||
|
||||
const statusMap: Record<number, OrderStatusView> = {
|
||||
[ORDER_STATE.PENDING_PAY]: { text: '待付款', statusClass: 'pending', actionText: '去支付', buttonClass: 'primary-action' },
|
||||
[ORDER_STATE.FINISH_PAY]: { text: '待发货', statusClass: 'pending', actionText: '查看订单', buttonClass: 'primary-action' },
|
||||
[ORDER_STATE.IN_PROGRESS]: { text: '待收货', statusClass: 'pending', actionText: '查看订单', buttonClass: 'primary-action' },
|
||||
[ORDER_STATE.CANCELLED]: { text: '已取消', statusClass: 'muted', actionText: '', buttonClass: 'dark-action' },
|
||||
[ORDER_STATE.COMPLETED]: { text: '已完成', statusClass: 'done', actionText: '再次购买', buttonClass: 'dark-action' },
|
||||
[ORDER_STATE.EXCHANGE]: { text: '换货', statusClass: 'muted', actionText: '查看订单', buttonClass: 'dark-action' },
|
||||
[ORDER_STATE.REFUNDED]: { text: '退货', statusClass: 'muted', actionText: '查看订单', buttonClass: 'dark-action' }
|
||||
}
|
||||
|
||||
const toYuanText = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => {
|
||||
if (value && typeof value === 'object' && 'toFixed' in value) {
|
||||
return value.toFixed(2)
|
||||
}
|
||||
|
||||
return Number(value || 0).toFixed(2)
|
||||
}
|
||||
|
||||
const firstImage = (picInfo: unknown) => {
|
||||
if (Array.isArray(picInfo)) {
|
||||
return String(picInfo[0] || '')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderListRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
|
||||
if (!supplierId || !userId) {
|
||||
return fail('供应商或用户信息不能为空')
|
||||
}
|
||||
|
||||
const mainOrders = await prisma.supplierOrderMain.findMany({
|
||||
where: {
|
||||
supplierId,
|
||||
userId,
|
||||
deleted: 0
|
||||
},
|
||||
include: {
|
||||
subOrders: {
|
||||
where: { deleted: 0 },
|
||||
include: {
|
||||
products: {
|
||||
where: { deleted: 0 },
|
||||
orderBy: { createAt: 'asc' }
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ createTime: 'desc' },
|
||||
{ createAt: 'desc' }
|
||||
]
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ createTime: 'desc' },
|
||||
{ createAt: 'desc' }
|
||||
]
|
||||
})
|
||||
|
||||
const orders = mainOrders.map(mainOrder => {
|
||||
const mainStatus = statusMap[mainOrder.orderState] || statusMap[ORDER_STATE.FINISH_PAY]
|
||||
const subOrders = mainOrder.subOrders.map(subOrder => {
|
||||
const status = statusMap[subOrder.orderState] || mainStatus
|
||||
const products = subOrder.products.map(product => ({
|
||||
id: product.id,
|
||||
sku: product.sku,
|
||||
name: product.name,
|
||||
number: product.number,
|
||||
price: toYuanText(product.price),
|
||||
image: product.productImgUrl || firstImage(product.picInfo)
|
||||
}))
|
||||
|
||||
return {
|
||||
id: subOrder.orderId,
|
||||
orderId: subOrder.orderId,
|
||||
orderNumberId: subOrder.orderNumberId || '',
|
||||
mainOrderId: subOrder.mainOrderId,
|
||||
supplierId: subOrder.supplierId,
|
||||
status: status.text,
|
||||
statusClass: status.statusClass,
|
||||
actionText: status.actionText,
|
||||
buttonClass: status.buttonClass,
|
||||
orderState: subOrder.orderState,
|
||||
amount: toYuanText(subOrder.totalPrice),
|
||||
count: products.reduce((total, item) => total + Number(item.number || 0), 0),
|
||||
products
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: mainOrder.orderId,
|
||||
mainOrderId: mainOrder.orderId,
|
||||
mainOrderNumberId: mainOrder.orderNumberId || '',
|
||||
supplierId: mainOrder.supplierId,
|
||||
status: mainStatus.text,
|
||||
statusClass: mainStatus.statusClass,
|
||||
orderState: mainOrder.orderState,
|
||||
isSplit: mainOrder.isSplit,
|
||||
splitTime: mainOrder.splitTime,
|
||||
canCancel: mainOrder.orderState === ORDER_STATE.PENDING_PAY,
|
||||
subOrders
|
||||
}
|
||||
})
|
||||
|
||||
return ok({ orders })
|
||||
})
|
||||
|
||||
+68
-53
@@ -1,13 +1,14 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { ORDER_EVENT_TYPE, ORDER_SPLIT_STATE, ORDER_STATE } from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderPayRequest = {
|
||||
supplierId?: string
|
||||
token?: string
|
||||
orderId?: string
|
||||
amount?: number
|
||||
totalPrice?: number
|
||||
@@ -20,7 +21,6 @@ type BridgePayResponse = {
|
||||
message?: string
|
||||
}
|
||||
|
||||
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
||||
const centsToYuan = (value?: number) => Number((Number(value || 0) / 100).toFixed(2))
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
@@ -29,25 +29,19 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
|
||||
const body = await readJson<OrderPayRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const token = String(body.token || '').trim()
|
||||
const orderId = String(body.orderId || '').trim()
|
||||
const totalPrice = Number(body.totalPrice ?? centsToYuan(body.amount))
|
||||
const wPayOrAPay = Number(body.wPayOrAPay ?? 0)
|
||||
const pointsPrice = Number(body.pointsPrice ?? totalPrice)
|
||||
|
||||
if (!supplierId || !token || !orderId) {
|
||||
return fail('供应商、授权或订单信息不能为空')
|
||||
if (!supplierId || !orderId) {
|
||||
return fail('供应商或订单信息不能为空')
|
||||
}
|
||||
|
||||
if (totalPrice <= 0) {
|
||||
return fail('支付金额必须大于0')
|
||||
}
|
||||
|
||||
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
||||
if (!apiBase) {
|
||||
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
|
||||
}
|
||||
|
||||
const order = await prisma.supplierOrderMain.findUnique({
|
||||
where: { orderId }
|
||||
})
|
||||
@@ -56,33 +50,12 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
return fail('待支付订单不存在', 404, 'ORDER_NOT_FOUND')
|
||||
}
|
||||
|
||||
const bridgeResponse = await fetch(`${apiBase}/pointOrder/confirmPurchase`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
supplierId,
|
||||
orderId,
|
||||
totalPrice,
|
||||
wPayOrAPay,
|
||||
pointsPrice,
|
||||
token
|
||||
})
|
||||
})
|
||||
|
||||
const bridgePayload = (await bridgeResponse.json().catch(() => ({}))) as BridgePayResponse
|
||||
|
||||
if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) {
|
||||
return fail(bridgePayload.message || '确认购买失败', 502, 'BRIDGE_CONFIRM_PURCHASE_FAILED')
|
||||
}
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId },
|
||||
data: {
|
||||
orderState: 1,
|
||||
orderState: ORDER_STATE.FINISH_PAY,
|
||||
isSplit: ORDER_SPLIT_STATE.NOT_SPLIT,
|
||||
totalPrice,
|
||||
wPayOrAPay,
|
||||
pointsPrice,
|
||||
@@ -93,15 +66,44 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: { mainOrderId: orderId, deleted: 0 },
|
||||
data: {
|
||||
orderState: 1,
|
||||
totalPrice,
|
||||
wPayOrAPay,
|
||||
pointsPrice,
|
||||
orderState: ORDER_STATE.FINISH_PAY,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { response: confirmResponse, payload: confirmPayload } = await bridgeFetchWithAuth<BridgePayResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/confirmPurchase',
|
||||
{
|
||||
orderId,
|
||||
totalPrice,
|
||||
wPayOrAPay,
|
||||
pointsPrice
|
||||
}
|
||||
)
|
||||
|
||||
if (!confirmResponse.ok || confirmPayload.resultCode !== 0) {
|
||||
return fail(confirmPayload.message || '确认购买推送失败', 502, 'BRIDGE_CONFIRM_PURCHASE_FAILED')
|
||||
}
|
||||
|
||||
const { response: purchaseResultResponse, payload: purchaseResultPayload } = await bridgeFetchWithAuth<BridgePayResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/purchaseResult',
|
||||
{
|
||||
orderId,
|
||||
totalPrice,
|
||||
wPayOrAPay,
|
||||
pointsPrice,
|
||||
purchaseResult: true,
|
||||
message: '支付成功'
|
||||
}
|
||||
)
|
||||
|
||||
if (!purchaseResultResponse.ok || purchaseResultPayload.resultCode !== 0) {
|
||||
return fail(purchaseResultPayload.message || '支付结果通知推送失败', 502, 'BRIDGE_PURCHASE_RESULT_FAILED')
|
||||
}
|
||||
|
||||
const subOrders = await prisma.supplierOrderSub.findMany({
|
||||
where: {
|
||||
mainOrderId: orderId,
|
||||
@@ -114,24 +116,36 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
const subOrderIds = subOrders.map(item => item.orderId).filter(Boolean).join(',')
|
||||
|
||||
if (subOrderIds) {
|
||||
const splitResponse = await fetch(`${apiBase}/pointOrder/pushOrderInfoMsg`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
supplierId,
|
||||
orderType: 2,
|
||||
orderId: subOrderIds,
|
||||
mainOrderId: orderId,
|
||||
userId: order.userId,
|
||||
time: Date.now(),
|
||||
token
|
||||
await prisma.$transaction(async tx => {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId },
|
||||
data: {
|
||||
isSplit: ORDER_SPLIT_STATE.SPLIT,
|
||||
splitTime: new Date(),
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: { mainOrderId: orderId, deleted: 0 },
|
||||
data: {
|
||||
orderState: ORDER_STATE.IN_PROGRESS,
|
||||
updateTime: new Date()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const splitPayload = (await splitResponse.json().catch(() => ({}))) as BridgePayResponse
|
||||
const { response: splitResponse, payload: splitPayload } = await bridgeFetchWithAuth<BridgePayResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/pushOrderInfoMsg',
|
||||
{
|
||||
orderType: ORDER_EVENT_TYPE.SPLIT_SUB_ORDER,
|
||||
orderId: subOrderIds,
|
||||
mainOrderId: orderId,
|
||||
userId: order.userId,
|
||||
time: Date.now()
|
||||
}
|
||||
)
|
||||
|
||||
if (!splitResponse.ok || splitPayload.resultCode !== 0) {
|
||||
return fail(splitPayload.message || '推送拆单信息失败', 502, 'BRIDGE_SPLIT_ORDER_FAILED')
|
||||
@@ -146,3 +160,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
pointsPrice
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import {
|
||||
ORDER_EVENT_TYPE,
|
||||
ORDER_OPERATE_TYPE,
|
||||
ORDER_PUSH_STATUS,
|
||||
ORDER_STATE
|
||||
} from '../utils/order-state.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type OrderPushEventRequest = {
|
||||
supplierId?: string
|
||||
userId?: string
|
||||
orderType?: number
|
||||
orderId?: string
|
||||
mainOrderId?: string
|
||||
subOrderId?: string
|
||||
time?: number
|
||||
amount?: number
|
||||
reason?: string
|
||||
settlementId?: string
|
||||
invoiceBizId?: string
|
||||
invoiceCode?: string
|
||||
invoiceIds?: string
|
||||
orderIds?: string
|
||||
fileId?: string
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
type BridgeOrderResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
const PUSH_API_PATH = '/pointOrder/pushOrderInfoMsg'
|
||||
const supportedOrderTypes = new Set<number>([
|
||||
ORDER_EVENT_TYPE.RETURN_ORDER,
|
||||
ORDER_EVENT_TYPE.EXCHANGE_ORDER,
|
||||
ORDER_EVENT_TYPE.CONFIRM_RECEIVE,
|
||||
ORDER_EVENT_TYPE.INVOICE_INFO
|
||||
])
|
||||
|
||||
const splitIds = (value?: string) => String(value || '')
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const eventState = (orderType: number) => {
|
||||
if (orderType === ORDER_EVENT_TYPE.RETURN_ORDER) return ORDER_STATE.REFUNDED
|
||||
if (orderType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) return ORDER_STATE.EXCHANGE
|
||||
if (orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE) return ORDER_STATE.COMPLETED
|
||||
return null
|
||||
}
|
||||
|
||||
const operateType = (orderType: number) => {
|
||||
if (orderType === ORDER_EVENT_TYPE.RETURN_ORDER) return ORDER_OPERATE_TYPE.REFUND
|
||||
if (orderType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) return ORDER_OPERATE_TYPE.EXCHANGE
|
||||
if (orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE) return ORDER_OPERATE_TYPE.COMPLETE
|
||||
if (orderType === ORDER_EVENT_TYPE.INVOICE_INFO) return ORDER_OPERATE_TYPE.INVOICE_INFO
|
||||
return orderType
|
||||
}
|
||||
|
||||
const buildBridgeBody = (body: OrderPushEventRequest) => ({
|
||||
supplierId: String(body.supplierId || '').trim(),
|
||||
orderType: Number(body.orderType),
|
||||
orderId: String(body.orderId || '').trim(),
|
||||
mainOrderId: String(body.mainOrderId || '').trim(),
|
||||
userId: String(body.userId || '').trim(),
|
||||
time: Number(body.time || Date.now())
|
||||
})
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<OrderPushEventRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
const orderType = Number(body.orderType)
|
||||
const orderId = String(body.orderId || '').trim()
|
||||
const mainOrderId = String(body.mainOrderId || '').trim()
|
||||
|
||||
if (!supplierId || !userId || !orderType || !orderId) {
|
||||
return fail('供应商、用户或订单事件信息不能为空')
|
||||
}
|
||||
|
||||
if (!supportedOrderTypes.has(orderType)) {
|
||||
return fail('暂不支持该订单事件类型', 400, 'ORDER_EVENT_NOT_SUPPORTED')
|
||||
}
|
||||
|
||||
const bridgeBody = buildBridgeBody(body)
|
||||
const now = new Date()
|
||||
let pushLogId = 0
|
||||
let affectedSubOrderIds: string[] = []
|
||||
|
||||
await prisma.$transaction(async tx => {
|
||||
const mainOrder = mainOrderId
|
||||
? await tx.supplierOrderMain.findFirst({
|
||||
where: { orderId: mainOrderId, supplierId, deleted: 0 }
|
||||
})
|
||||
: null
|
||||
|
||||
const explicitSubIds = splitIds(body.subOrderId || '')
|
||||
const orderIds = splitIds(orderId)
|
||||
const subOrdersByOrderId = await tx.supplierOrderSub.findMany({
|
||||
where: {
|
||||
supplierId,
|
||||
deleted: 0,
|
||||
OR: [
|
||||
{ orderId: { in: orderIds } },
|
||||
...(explicitSubIds.length ? [{ orderId: { in: explicitSubIds } }] : []),
|
||||
...(mainOrderId ? [{ mainOrderId }] : [])
|
||||
]
|
||||
},
|
||||
orderBy: { createAt: 'asc' }
|
||||
})
|
||||
|
||||
const directlyMatched = subOrdersByOrderId.filter(item => orderIds.includes(item.orderId) || explicitSubIds.includes(item.orderId))
|
||||
const mainOrderSubs = mainOrderId ? subOrdersByOrderId.filter(item => item.mainOrderId === mainOrderId) : []
|
||||
const targetSubOrders = directlyMatched.length ? directlyMatched : mainOrderSubs.length === 1 ? mainOrderSubs : []
|
||||
affectedSubOrderIds = targetSubOrders.map(item => item.orderId)
|
||||
|
||||
const nextState = eventState(orderType)
|
||||
if (nextState !== null && targetSubOrders.length) {
|
||||
await tx.supplierOrderSub.updateMany({
|
||||
where: {
|
||||
orderId: { in: affectedSubOrderIds },
|
||||
supplierId,
|
||||
deleted: 0
|
||||
},
|
||||
data: {
|
||||
orderState: nextState,
|
||||
orderFinishTime: orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE ? now : undefined,
|
||||
completeTime: orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE ? now : undefined,
|
||||
updateTime: now
|
||||
}
|
||||
})
|
||||
|
||||
await tx.supplierOrderOperateLog.createMany({
|
||||
data: targetSubOrders.map(subOrder => ({
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: subOrder.mainOrderId,
|
||||
subOrderId: subOrder.orderId,
|
||||
operateType: operateType(orderType),
|
||||
beforeState: subOrder.orderState,
|
||||
afterState: nextState,
|
||||
remark: body.reason || null,
|
||||
operator: 'system'
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
if (orderType === ORDER_EVENT_TYPE.RETURN_ORDER || orderType === ORDER_EVENT_TYPE.EXCHANGE_ORDER) {
|
||||
await tx.supplierRefundExchange.upsert({
|
||||
where: { serviceOrderId: orderId },
|
||||
update: {
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || targetSubOrders[0]?.mainOrderId || null,
|
||||
subOrderId: targetSubOrders[0]?.orderId || null,
|
||||
serviceType: orderType,
|
||||
serviceState: 1,
|
||||
amount: Number(body.amount || targetSubOrders[0]?.totalPrice || 0),
|
||||
reason: body.reason || null,
|
||||
requestBody: body,
|
||||
deleted: 0
|
||||
},
|
||||
create: {
|
||||
serviceOrderId: orderId,
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || targetSubOrders[0]?.mainOrderId || null,
|
||||
subOrderId: targetSubOrders[0]?.orderId || null,
|
||||
serviceType: orderType,
|
||||
serviceState: 1,
|
||||
amount: Number(body.amount || targetSubOrders[0]?.totalPrice || 0),
|
||||
reason: body.reason || null,
|
||||
requestBody: body
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (orderType === ORDER_EVENT_TYPE.INVOICE_INFO) {
|
||||
const invoiceBizId = String(body.invoiceBizId || body.invoiceCode || orderId).trim()
|
||||
await tx.supplierInvoiceInfo.upsert({
|
||||
where: { invoiceBizId },
|
||||
update: {
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || null,
|
||||
subOrderId: targetSubOrders[0]?.orderId || null,
|
||||
settlementId: body.settlementId || null,
|
||||
invoiceCode: body.invoiceCode || null,
|
||||
invoiceIds: body.invoiceIds || orderId,
|
||||
orderIds: body.orderIds || mainOrderId || targetSubOrders[0]?.orderId || null,
|
||||
invoiceStatus: 2,
|
||||
fileId: body.fileId || null,
|
||||
filePath: body.filePath || null,
|
||||
requestBody: body,
|
||||
deleted: 0
|
||||
},
|
||||
create: {
|
||||
invoiceBizId,
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || null,
|
||||
subOrderId: targetSubOrders[0]?.orderId || null,
|
||||
settlementId: body.settlementId || null,
|
||||
invoiceCode: body.invoiceCode || null,
|
||||
invoiceIds: body.invoiceIds || orderId,
|
||||
orderIds: body.orderIds || mainOrderId || targetSubOrders[0]?.orderId || null,
|
||||
invoiceStatus: 2,
|
||||
fileId: body.fileId || null,
|
||||
filePath: body.filePath || null,
|
||||
requestBody: body
|
||||
}
|
||||
})
|
||||
|
||||
if (body.invoiceCode && mainOrder) {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId: mainOrder.orderId },
|
||||
data: { invoiceCode: body.invoiceCode, updateTime: now }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (orderType === ORDER_EVENT_TYPE.CONFIRM_RECEIVE && mainOrderId) {
|
||||
const unfinishedCount = await tx.supplierOrderSub.count({
|
||||
where: {
|
||||
mainOrderId,
|
||||
deleted: 0,
|
||||
orderState: { notIn: [ORDER_STATE.COMPLETED, ORDER_STATE.CANCELLED, ORDER_STATE.REFUNDED] }
|
||||
}
|
||||
})
|
||||
if (unfinishedCount === 0) {
|
||||
await tx.supplierOrderMain.update({
|
||||
where: { orderId: mainOrderId },
|
||||
data: {
|
||||
orderState: ORDER_STATE.COMPLETED,
|
||||
completeTime: now,
|
||||
updateTime: now
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const pushLog = await tx.supplierOrderPushLog.create({
|
||||
data: {
|
||||
supplierId,
|
||||
userId,
|
||||
mainOrderId: mainOrderId || targetSubOrders[0]?.mainOrderId || null,
|
||||
orderId,
|
||||
orderType,
|
||||
apiPath: PUSH_API_PATH,
|
||||
requestBody: bridgeBody,
|
||||
pushStatus: ORDER_PUSH_STATUS.PENDING
|
||||
}
|
||||
})
|
||||
pushLogId = pushLog.id
|
||||
})
|
||||
|
||||
let bridgeResponse: Response | null = null
|
||||
let bridgePayload: BridgeOrderResponse = {}
|
||||
try {
|
||||
const result = await bridgeFetchWithAuth<BridgeOrderResponse>(supplierId, PUSH_API_PATH, bridgeBody)
|
||||
bridgeResponse = result.response
|
||||
bridgePayload = result.payload
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '订单事件推送失败'
|
||||
await prisma.supplierOrderPushLog.update({
|
||||
where: { id: pushLogId },
|
||||
data: {
|
||||
pushStatus: ORDER_PUSH_STATUS.FAILED,
|
||||
errorMessage: message,
|
||||
lastPushTime: new Date()
|
||||
}
|
||||
})
|
||||
return ok({ localSaved: true, pushSuccess: false, message, affectedSubOrderIds })
|
||||
}
|
||||
|
||||
const pushSuccess = bridgeResponse.ok && bridgePayload.resultCode === 0
|
||||
|
||||
await prisma.supplierOrderPushLog.update({
|
||||
where: { id: pushLogId },
|
||||
data: {
|
||||
responseBody: bridgePayload,
|
||||
httpStatus: bridgeResponse.status,
|
||||
resultCode: bridgePayload.resultCode,
|
||||
pushStatus: pushSuccess ? ORDER_PUSH_STATUS.SUCCESS : ORDER_PUSH_STATUS.FAILED,
|
||||
errorMessage: pushSuccess ? null : bridgePayload.message || '订单事件推送失败',
|
||||
lastPushTime: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
return ok({
|
||||
localSaved: true,
|
||||
pushSuccess,
|
||||
orderType,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
affectedSubOrderIds,
|
||||
message: pushSuccess ? '订单事件已保存并推送成功' : bridgePayload.message || '订单事件已保存,推送失败'
|
||||
})
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
@@ -61,3 +61,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
|
||||
return ok(toProductDetail(product))
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
@@ -67,3 +67,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
products: products.map(toProductItem)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import {
|
||||
findSupplierByIdentification,
|
||||
getBridgeApiBase,
|
||||
getBridgeToken
|
||||
} from '../utils/bridge-auth.js'
|
||||
import { sha256 } from '../utils/crypto.js'
|
||||
import { createSupplierOpenSignature } from '../utils/supplier-open-crypto.js'
|
||||
|
||||
type SupplierDebugTokensRequest = {
|
||||
supplierIdentification?: string
|
||||
forceRefreshZhghToken?: boolean
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<SupplierDebugTokensRequest>(request)
|
||||
const supplierIdentification = String(body.supplierIdentification || '').trim().toLowerCase()
|
||||
|
||||
if (!supplierIdentification) {
|
||||
return fail('供应商唯一标识不能为空')
|
||||
}
|
||||
|
||||
const supplier = await findSupplierByIdentification(supplierIdentification).catch(error => error)
|
||||
|
||||
if (supplier instanceof Error) {
|
||||
return fail(supplier.message || '供应商信息未配置或已停用', 404, 'SUPPLIER_NOT_CONFIGURED')
|
||||
}
|
||||
|
||||
const timestamp = Date.now()
|
||||
const orderType = 2
|
||||
const orderId = 'SUB_ORDER_ID_1,SUB_ORDER_ID_2'
|
||||
const mainOrderId = 'MAIN_ORDER_ID'
|
||||
const userId = 'USER_ID'
|
||||
const supplierOpenToken = createSupplierOpenSignature(supplier.supplierId, supplier.clientId, timestamp)
|
||||
const zhghBridgeToken = await getBridgeToken(supplier.supplierId, Boolean(body.forceRefreshZhghToken))
|
||||
|
||||
return ok({
|
||||
supplierOpen: {
|
||||
name: 'supplierOpenToken',
|
||||
usage: '调用供应商开放接口 /supplierOpenApi/pointOrder/queryOrderInfo',
|
||||
supplierId: supplier.supplierId,
|
||||
clientId: supplier.clientId,
|
||||
timestamp,
|
||||
token: supplierOpenToken,
|
||||
orderType,
|
||||
orderId,
|
||||
queryOrderInfoBody: {
|
||||
supplierId: supplier.supplierId,
|
||||
token: supplierOpenToken,
|
||||
orderType,
|
||||
orderId,
|
||||
timestamp
|
||||
}
|
||||
},
|
||||
zhghBridge: {
|
||||
name: 'zhghBridgeToken',
|
||||
usage: '调用 zhgh /open/pointOrder/* 桥接接口',
|
||||
apiBase: getBridgeApiBase(),
|
||||
supplierId: supplier.supplierId,
|
||||
clientId: supplier.clientId,
|
||||
timestamp,
|
||||
sendSha256: sha256(`${supplier.clientId}${supplier.clientSecret}${timestamp}`),
|
||||
token: zhghBridgeToken,
|
||||
splitOrderBody: {
|
||||
supplierId: supplier.supplierId,
|
||||
token: zhghBridgeToken,
|
||||
orderType,
|
||||
orderId,
|
||||
mainOrderId,
|
||||
userId,
|
||||
time: timestamp
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
@@ -35,3 +35,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
supplierName: supplier.supplierName
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { prisma } from '../utils/prisma.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { sm4CbcDecrypt, verifySupplierOpenSignature } from '../utils/supplier-open-crypto.js'
|
||||
|
||||
type SupplierOrderQueryRequest = {
|
||||
supplierId?: string
|
||||
token?: string
|
||||
orderType?: number
|
||||
orderId?: string
|
||||
timestamp?: number
|
||||
AplParmObjct?: string
|
||||
}
|
||||
|
||||
const success = (orderInfo: unknown[]) => Response.json({
|
||||
resultCode: 0,
|
||||
message: '成功',
|
||||
orderInfo
|
||||
})
|
||||
|
||||
const fail = (resultCode: number, message: string) => Response.json({
|
||||
resultCode,
|
||||
message,
|
||||
orderInfo: []
|
||||
})
|
||||
|
||||
const parseBody = (body: SupplierOrderQueryRequest) => {
|
||||
if (!body.AplParmObjct) return body
|
||||
|
||||
const keyHex = process.env.SUPPLIER_OPEN_SM4_KEY_HEX || process.env.SM4CBC_KEY_HEX || ''
|
||||
const ivHex = process.env.SUPPLIER_OPEN_SM4_IV_HEX || process.env.SM4CBC_IV_HEX || ''
|
||||
if (!keyHex || !ivHex) {
|
||||
throw new Error('供应商开放接口SM4配置缺失')
|
||||
}
|
||||
|
||||
return JSON.parse(sm4CbcDecrypt(body.AplParmObjct, keyHex, ivHex)) as SupplierOrderQueryRequest
|
||||
}
|
||||
|
||||
const toText = (date?: Date | null) => {
|
||||
if (!date) return null
|
||||
const pad = (value: number) => String(value).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const toMoney = (value: { toFixed: (digits: number) => string } | number | string | null | undefined) => Number(value || 0).toFixed(2)
|
||||
|
||||
const productSku = (product: { sku: string; subOrderId: string; name: string }) => {
|
||||
if (product.sku && product.sku !== '[object Object]') {
|
||||
return product.sku
|
||||
}
|
||||
|
||||
return `${product.subOrderId}-${product.name || 'SKU'}`
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
let body: SupplierOrderQueryRequest
|
||||
try {
|
||||
body = parseBody(await readJson<SupplierOrderQueryRequest>(request))
|
||||
} catch (error) {
|
||||
return fail(400, error instanceof Error ? error.message : '请求参数解密失败')
|
||||
}
|
||||
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const token = String(body.token || '').trim()
|
||||
const orderIds = String(body.orderId || '')
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)
|
||||
const timestamp = Number(body.timestamp || 0)
|
||||
|
||||
if (!supplierId || !token || !timestamp || !orderIds.length) {
|
||||
return fail(400, '供应商、订单或签名信息不能为空')
|
||||
}
|
||||
|
||||
const supplier = await prisma.supplierInfo.findFirst({
|
||||
where: { supplierId, deleted: 0 }
|
||||
})
|
||||
if (!supplier) {
|
||||
return fail(404, '供应商信息不存在')
|
||||
}
|
||||
|
||||
if (!verifySupplierOpenSignature(supplier.supplierId, supplier.clientId, timestamp, token)) {
|
||||
return fail(401, '签名无效或已过期')
|
||||
}
|
||||
|
||||
const orders = await prisma.supplierOrderSub.findMany({
|
||||
where: {
|
||||
supplierId,
|
||||
orderId: { in: orderIds },
|
||||
deleted: 0
|
||||
},
|
||||
include: {
|
||||
products: {
|
||||
where: { deleted: 0 },
|
||||
orderBy: { createAt: 'asc' }
|
||||
}
|
||||
},
|
||||
orderBy: { createAt: 'asc' }
|
||||
})
|
||||
|
||||
const orderInfo = orders.map(order => ({
|
||||
orderId: order.orderId,
|
||||
orderNumberId: order.orderNumberId,
|
||||
supplierId: order.supplierId,
|
||||
token: order.token,
|
||||
aplName: order.aplName,
|
||||
createTime: toText(order.createTime),
|
||||
updateTime: toText(order.updateTime),
|
||||
deliveryTime: toText(order.deliveryTime),
|
||||
orderFinishTime: toText(order.orderFinishTime),
|
||||
completeTime: toText(order.completeTime),
|
||||
deliveryStatus: order.deliveryStatus,
|
||||
freightCost: toMoney(order.freightCost),
|
||||
wPayOrAPayFreightCost: toMoney(order.wPayOrAPayFreightCost),
|
||||
refund: toMoney(order.refund),
|
||||
wPayOrAPayRefund: toMoney(order.wPayOrAPayRefund),
|
||||
logisticsOrderId: order.logisticsOrderId,
|
||||
crrgBsnNm: order.crrgBsnNm,
|
||||
orderState: order.orderState,
|
||||
totalPrice: toMoney(order.totalPrice),
|
||||
wPayOrAPay: toMoney(order.wPayOrAPay),
|
||||
pointsPrice: toMoney(order.pointsPrice),
|
||||
invoiceCode: order.invoiceCode,
|
||||
orderProInfos: order.products.map(product => ({
|
||||
sku: productSku(product),
|
||||
number: product.number,
|
||||
price: toMoney(product.price),
|
||||
name: product.name,
|
||||
taxRate: Number(product.taxRate || 0),
|
||||
picInfo: Array.isArray(product.picInfo) ? product.picInfo : product.productImgUrl ? [product.productImgUrl] : [],
|
||||
fileId: product.fileId,
|
||||
filePath: product.filePath
|
||||
}))
|
||||
}))
|
||||
|
||||
return success(orderInfo)
|
||||
})
|
||||
|
||||
+10
-27
@@ -1,12 +1,12 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { bridgeFetchWithAuth } from '../utils/bridge-auth.js'
|
||||
|
||||
type SupplierPointsRequest = {
|
||||
supplierId?: string
|
||||
token?: string
|
||||
userId?: string
|
||||
}
|
||||
|
||||
@@ -16,41 +16,23 @@ type BridgePointsResponse = {
|
||||
userPoints?: number | string
|
||||
}
|
||||
|
||||
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<SupplierPointsRequest>(request)
|
||||
const supplierId = String(body.supplierId || '').trim()
|
||||
const token = String(body.token || '').trim()
|
||||
const userId = String(body.userId || '').trim()
|
||||
|
||||
if (!supplierId || !token || !userId) {
|
||||
return fail('供应商、用户或授权信息不能为空')
|
||||
if (!supplierId || !userId) {
|
||||
return fail('供应商或用户信息不能为空')
|
||||
}
|
||||
|
||||
const apiBase = normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
||||
|
||||
if (!apiBase) {
|
||||
return fail('平台积分商城接口地址未配置', 500, 'OPEN_API_BASE_MISSING')
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiBase}/pointOrder/queryPoints`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
supplierId,
|
||||
token,
|
||||
userId
|
||||
})
|
||||
})
|
||||
|
||||
const pointsPayload = (await response.json().catch(() => ({}))) as BridgePointsResponse
|
||||
const { response, payload: pointsPayload } = await bridgeFetchWithAuth<BridgePointsResponse>(
|
||||
supplierId,
|
||||
'/pointOrder/queryPoints',
|
||||
{ userId }
|
||||
)
|
||||
|
||||
if (!response.ok || pointsPayload.resultCode !== 0) {
|
||||
return fail(pointsPayload.message || '查询用户积分失败', 502, 'BRIDGE_POINTS_FAILED')
|
||||
@@ -60,3 +42,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
userPoints: Number(pointsPayload.userPoints || 0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { fail, ok } from '../utils/http.js'
|
||||
import { readJson } from '../utils/request.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
import { findSupplierByIdentification, warmBridgeToken } from '../utils/bridge-auth.js'
|
||||
|
||||
type SupplierTokenRequest = {
|
||||
userId?: string
|
||||
supplierIdentification?: string
|
||||
}
|
||||
|
||||
export default withErrorBoundary(async (request: Request, _context: Context) => {
|
||||
const methodResponse = guardMethod(request, ['POST'])
|
||||
if (methodResponse) return methodResponse
|
||||
|
||||
const body = await readJson<SupplierTokenRequest>(request)
|
||||
const userId = String(body.userId || '').trim()
|
||||
const supplierIdentification = String(body.supplierIdentification || '').trim().toLowerCase()
|
||||
|
||||
if (!userId || !supplierIdentification) {
|
||||
return fail('用户信息和供应商唯一标识不能为空')
|
||||
}
|
||||
|
||||
const supplier = await findSupplierByIdentification(supplierIdentification).catch(error => error)
|
||||
|
||||
if (supplier instanceof Error) {
|
||||
return fail(supplier.message || '供应商信息未配置或已停用', 404, 'SUPPLIER_NOT_CONFIGURED')
|
||||
}
|
||||
|
||||
try {
|
||||
await warmBridgeToken(supplier)
|
||||
} catch (error) {
|
||||
return fail(error instanceof Error ? error.message : '获取平台授权令牌失败', 502, 'BRIDGE_TOKEN_FAILED')
|
||||
}
|
||||
|
||||
return ok({
|
||||
supplierIdentification,
|
||||
supplierId: supplier.supplierId,
|
||||
supplierName: supplier.supplierName,
|
||||
userId
|
||||
})
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
@@ -9,3 +9,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
|
||||
return ok({ message: '上传接口待实现' })
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import type { Context } from '../utils/handler.js'
|
||||
import { guardMethod } from '../utils/method.js'
|
||||
import { ok } from '../utils/http.js'
|
||||
import { withErrorBoundary } from '../utils/handler.js'
|
||||
@@ -9,3 +9,4 @@ export default withErrorBoundary(async (request: Request, _context: Context) =>
|
||||
|
||||
return ok({ message: '用户信息接口待实现' })
|
||||
})
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'dotenv/config'
|
||||
import Koa from 'koa'
|
||||
import Router from '@koa/router'
|
||||
import bodyParser from 'koa-bodyparser'
|
||||
import type { FunctionHandler } from './utils/handler.js'
|
||||
import cartAdd from './functions/cart-add.js'
|
||||
import login from './functions/login.js'
|
||||
import orderCancelMain from './functions/order-cancel-main.js'
|
||||
import orderCancelPaidMain from './functions/order-cancel-paid-main.js'
|
||||
import orderCancelSub from './functions/order-cancel-sub.js'
|
||||
import orderCreate from './functions/order-create.js'
|
||||
import orderList from './functions/order-list.js'
|
||||
import orderPay from './functions/order-pay.js'
|
||||
import orderPushEvent from './functions/order-push-event.js'
|
||||
import productDetail from './functions/product-detail.js'
|
||||
import productList from './functions/product-list.js'
|
||||
import supplierDebugTokens from './functions/supplier-debug-tokens.js'
|
||||
import supplierInfo from './functions/supplier-info.js'
|
||||
import supplierOrderInfo from './functions/supplier-order-info.js'
|
||||
import supplierPoints from './functions/supplier-points.js'
|
||||
import supplierToken from './functions/supplier-token.js'
|
||||
import upload from './functions/upload.js'
|
||||
import userInfo from './functions/user-info.js'
|
||||
|
||||
const app = new Koa()
|
||||
const router = new Router()
|
||||
const port = Number(process.env.PORT || 8888)
|
||||
|
||||
const handlers: Record<string, FunctionHandler> = {
|
||||
'cart-add': cartAdd,
|
||||
login,
|
||||
'order-cancel-main': orderCancelMain,
|
||||
'order-cancel-paid-main': orderCancelPaidMain,
|
||||
'order-cancel-sub': orderCancelSub,
|
||||
'order-create': orderCreate,
|
||||
'order-list': orderList,
|
||||
'order-pay': orderPay,
|
||||
'order-push-event': orderPushEvent,
|
||||
'product-detail': productDetail,
|
||||
'product-list': productList,
|
||||
'supplier-debug-tokens': supplierDebugTokens,
|
||||
'supplier-info': supplierInfo,
|
||||
'supplier-order-info': supplierOrderInfo,
|
||||
'supplier-points': supplierPoints,
|
||||
'supplier-token': supplierToken,
|
||||
upload,
|
||||
'user-info': userInfo
|
||||
}
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
|
||||
}
|
||||
|
||||
const requestBody = (ctx: Koa.Context) => {
|
||||
if (ctx.method === 'GET' || ctx.method === 'HEAD') {
|
||||
return undefined
|
||||
}
|
||||
return JSON.stringify(ctx.request.body || {})
|
||||
}
|
||||
|
||||
const toRequest = (ctx: Koa.Context) =>
|
||||
new Request(`${ctx.protocol}://${ctx.host}${ctx.originalUrl}`, {
|
||||
method: ctx.method,
|
||||
headers: ctx.headers as HeadersInit,
|
||||
body: requestBody(ctx)
|
||||
})
|
||||
|
||||
const writeResponse = async (ctx: Koa.Context, response: Response) => {
|
||||
ctx.status = response.status
|
||||
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
||||
response.headers.forEach((value, key) => ctx.set(key, value))
|
||||
ctx.body = response.body ? await response.text() : undefined
|
||||
}
|
||||
|
||||
const dispatch = (name: string) => async (ctx: Koa.Context) => {
|
||||
const handler = handlers[name]
|
||||
if (!handler) {
|
||||
ctx.status = 404
|
||||
ctx.body = { success: false, error: { code: 'NOT_FOUND', message: '接口不存在' } }
|
||||
return
|
||||
}
|
||||
const response = await handler(toRequest(ctx), {})
|
||||
await writeResponse(ctx, response)
|
||||
}
|
||||
|
||||
app.use(bodyParser())
|
||||
app.use(async (ctx, next) => {
|
||||
Object.entries(corsHeaders).forEach(([key, value]) => ctx.set(key, value))
|
||||
if (ctx.method === 'OPTIONS') {
|
||||
ctx.status = 204
|
||||
return
|
||||
}
|
||||
await next()
|
||||
})
|
||||
|
||||
router.all('/api/:name', async ctx => dispatch(ctx.params.name)(ctx))
|
||||
router.post('/supplierOpenApi/pointOrder/queryOrderInfo', dispatch('supplier-order-info'))
|
||||
router.post('/supplierOpenApi/debug/tokens', dispatch('supplier-debug-tokens'))
|
||||
|
||||
app.use(router.routes())
|
||||
app.use(router.allowedMethods())
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`supplied-service Koa server running at http://localhost:${port}`)
|
||||
})
|
||||
@@ -0,0 +1,203 @@
|
||||
import type { SupplierInfo } from '@prisma/client'
|
||||
import { createBridgeSignature } from './crypto.js'
|
||||
import { prisma } from './prisma.js'
|
||||
import { getRedis } from './redis.js'
|
||||
|
||||
type BridgeTokenResponse = {
|
||||
resultCode?: number
|
||||
message?: string
|
||||
accessToken?: string
|
||||
refreshToken?: string
|
||||
expire?: number
|
||||
refreshExpire?: number
|
||||
time?: string
|
||||
}
|
||||
|
||||
type CachedBridgeToken = {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiresAt: number
|
||||
refreshExpiresAt: number
|
||||
}
|
||||
|
||||
export type BridgeResponse<T> = {
|
||||
response: Response
|
||||
payload: T
|
||||
token: string
|
||||
}
|
||||
|
||||
const normalizeBase = (base: string) => base.replace(/\/$/, '')
|
||||
const tokenKey = (supplierId: string) => `points-mall:bridge-token:${supplierId}`
|
||||
const TOKEN_EXPIRE_BUFFER_MS = 60_000
|
||||
|
||||
export const getBridgeApiBase = () => normalizeBase(process.env.POINTS_MALL_OPEN_API_BASE || '')
|
||||
|
||||
export const findSupplierForBridge = async (supplierId: string) => {
|
||||
const supplier = await prisma.supplierInfo.findFirst({
|
||||
where: {
|
||||
supplierId,
|
||||
deleted: 0
|
||||
}
|
||||
})
|
||||
|
||||
if (!supplier) {
|
||||
throw new Error('供应商信息未配置或已停用')
|
||||
}
|
||||
return supplier
|
||||
}
|
||||
|
||||
export const findSupplierByIdentification = async (supplierIdentification: string) => {
|
||||
const supplier = await prisma.supplierInfo.findUnique({
|
||||
where: {
|
||||
supplierIdentification: supplierIdentification.toLowerCase()
|
||||
}
|
||||
})
|
||||
|
||||
if (!supplier || supplier.deleted === 1) {
|
||||
throw new Error('供应商信息未配置或已停用')
|
||||
}
|
||||
return supplier
|
||||
}
|
||||
|
||||
const readCachedToken = async (supplierId: string) => {
|
||||
const raw = await getRedis().get(tokenKey(supplierId))
|
||||
if (!raw) return null
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as CachedBridgeToken
|
||||
} catch (_error) {
|
||||
await getRedis().del(tokenKey(supplierId))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const writeCachedToken = async (supplierId: string, token: CachedBridgeToken) => {
|
||||
const ttlSeconds = Math.max(60, Math.floor((token.refreshExpiresAt - Date.now()) / 1000))
|
||||
await getRedis().set(tokenKey(supplierId), JSON.stringify(token), 'EX', ttlSeconds)
|
||||
}
|
||||
|
||||
const isTokenUsable = (token: CachedBridgeToken | null) =>
|
||||
Boolean(token?.accessToken && token.expiresAt - TOKEN_EXPIRE_BUFFER_MS > Date.now())
|
||||
|
||||
const isRefreshUsable = (token: CachedBridgeToken | null) =>
|
||||
Boolean(token?.refreshToken && token.refreshExpiresAt - TOKEN_EXPIRE_BUFFER_MS > Date.now())
|
||||
|
||||
const toCachedToken = (payload: BridgeTokenResponse): CachedBridgeToken => {
|
||||
const now = Date.now()
|
||||
const expire = Number(payload.expire || 0)
|
||||
const refreshExpire = Number(payload.refreshExpire || 0)
|
||||
|
||||
if (!payload.accessToken || !payload.refreshToken || expire <= 0 || refreshExpire <= 0) {
|
||||
throw new Error(payload.message || '平台授权令牌返回异常')
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: payload.accessToken,
|
||||
refreshToken: payload.refreshToken,
|
||||
expiresAt: now + expire * 1000,
|
||||
refreshExpiresAt: now + refreshExpire * 1000
|
||||
}
|
||||
}
|
||||
|
||||
const requestToken = async (supplier: SupplierInfo, refreshToken?: string) => {
|
||||
const apiBase = getBridgeApiBase()
|
||||
if (!apiBase) {
|
||||
throw new Error('平台积分商城接口地址未配置')
|
||||
}
|
||||
|
||||
const timestamp = String(Date.now())
|
||||
const signature = createBridgeSignature(supplier.clientId, supplier.clientSecret, timestamp)
|
||||
const path = refreshToken ? '/connect/refreshToken' : '/connect/token'
|
||||
const response = await fetch(`${apiBase}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
supplierId: supplier.supplierId,
|
||||
clientId: supplier.clientId,
|
||||
clientSecret: supplier.clientSecret,
|
||||
sendSha256: signature,
|
||||
refreshToken,
|
||||
code: '',
|
||||
timestamp
|
||||
})
|
||||
})
|
||||
const payload = (await response.json().catch(() => ({}))) as BridgeTokenResponse
|
||||
|
||||
if (!response.ok || payload.resultCode !== 0) {
|
||||
throw new Error(payload.message || '获取平台授权令牌失败')
|
||||
}
|
||||
|
||||
const cached = toCachedToken(payload)
|
||||
await writeCachedToken(supplier.supplierId, cached)
|
||||
return cached
|
||||
}
|
||||
|
||||
export const getBridgeToken = async (supplierId: string, forceRefresh = false) => {
|
||||
const supplier = await findSupplierForBridge(supplierId)
|
||||
const cached = await readCachedToken(supplier.supplierId)
|
||||
|
||||
if (!forceRefresh && isTokenUsable(cached)) {
|
||||
return cached!.accessToken
|
||||
}
|
||||
|
||||
if (isRefreshUsable(cached)) {
|
||||
try {
|
||||
const refreshed = await requestToken(supplier, cached!.refreshToken)
|
||||
return refreshed.accessToken
|
||||
} catch (_error) {
|
||||
await getRedis().del(tokenKey(supplier.supplierId))
|
||||
}
|
||||
}
|
||||
|
||||
const created = await requestToken(supplier)
|
||||
return created.accessToken
|
||||
}
|
||||
|
||||
export const warmBridgeToken = async (supplier: SupplierInfo) => {
|
||||
await getBridgeToken(supplier.supplierId)
|
||||
}
|
||||
|
||||
const isInvalidAccessTokenPayload = (payload: unknown) => {
|
||||
if (!payload || typeof payload !== 'object') return false
|
||||
const message = String((payload as { message?: unknown }).message || '')
|
||||
return message.includes('accessToken') || message.includes('令牌') || message.includes('token')
|
||||
}
|
||||
|
||||
export const bridgeFetchWithAuth = async <T>(
|
||||
supplierId: string,
|
||||
path: string,
|
||||
body: Record<string, unknown>
|
||||
): Promise<BridgeResponse<T>> => {
|
||||
const apiBase = getBridgeApiBase()
|
||||
if (!apiBase) {
|
||||
throw new Error('平台积分商城接口地址未配置')
|
||||
}
|
||||
|
||||
const call = async (forceRefresh = false) => {
|
||||
const token = await getBridgeToken(supplierId, forceRefresh)
|
||||
const response = await fetch(`${apiBase}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
supplierId,
|
||||
token
|
||||
})
|
||||
})
|
||||
const payload = (await response.json().catch(() => ({}))) as T
|
||||
return { response, payload, token }
|
||||
}
|
||||
|
||||
const first = await call(false)
|
||||
const resultCode = Number((first.payload as { resultCode?: unknown }).resultCode)
|
||||
if (first.response.status === 401 || resultCode === 401 || isInvalidAccessTokenPayload(first.payload)) {
|
||||
return call(true)
|
||||
}
|
||||
|
||||
return first
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Context } from '@netlify/functions'
|
||||
import { fail } from './http.js'
|
||||
import { fail } from './http.js'
|
||||
|
||||
export type Context = Record<string, unknown>
|
||||
|
||||
export type FunctionHandler = (request: Request, context: Context) => Promise<Response> | Response
|
||||
|
||||
@@ -13,3 +14,4 @@ export const withErrorBoundary = (handler: FunctionHandler): FunctionHandler =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
export const ORDER_STATE = {
|
||||
PENDING_PAY: 0,
|
||||
FINISH_PAY: 1,
|
||||
IN_PROGRESS: 2,
|
||||
CANCELLED: 3,
|
||||
RECEIVED: 4,
|
||||
COMPLETED: 5,
|
||||
EXCHANGE: 6,
|
||||
REFUNDED: 7
|
||||
} as const
|
||||
|
||||
export const ORDER_SPLIT_STATE = {
|
||||
NOT_SPLIT: 0,
|
||||
SPLIT: 1
|
||||
} as const
|
||||
|
||||
export const ORDER_EVENT_TYPE = {
|
||||
CANCEL_MAIN: 1,
|
||||
SPLIT_SUB_ORDER: 2,
|
||||
CANCEL_SUB: 3,
|
||||
RETURN_ORDER: 4,
|
||||
EXCHANGE_ORDER: 5,
|
||||
CONFIRM_RECEIVE: 6,
|
||||
CANCEL_PAID_NOT_SPLIT_MAIN: 7,
|
||||
INVOICE_INFO: 11
|
||||
} as const
|
||||
|
||||
export const POINTS_LEDGER_BIZ_TYPE = {
|
||||
PAY_CONSUME: 1,
|
||||
CANCEL_REFUND: 2,
|
||||
REFUND: 3,
|
||||
MANUAL_ADJUST: 4
|
||||
} as const
|
||||
|
||||
export const POINTS_LEDGER_DIRECTION = {
|
||||
INCREASE: 1,
|
||||
DECREASE: 2
|
||||
} as const
|
||||
|
||||
export const ORDER_PUSH_STATUS = {
|
||||
PENDING: 0,
|
||||
SUCCESS: 1,
|
||||
FAILED: 2
|
||||
} as const
|
||||
|
||||
export const ORDER_OPERATE_TYPE = {
|
||||
CREATE_PENDING: 1,
|
||||
PAY_SUCCESS: 2,
|
||||
SPLIT_SUB_ORDER: 3,
|
||||
CANCEL_MAIN: 4,
|
||||
CANCEL_SUB: 5,
|
||||
CANCEL_PAID_NOT_SPLIT_MAIN: 6,
|
||||
RECEIVE: 7,
|
||||
COMPLETE: 8,
|
||||
REFUND: 9,
|
||||
EXCHANGE: 10,
|
||||
INVOICE_INFO: 12
|
||||
} as const
|
||||
@@ -0,0 +1,21 @@
|
||||
import Redis from 'ioredis'
|
||||
|
||||
let redisClient: Redis | null = null
|
||||
|
||||
export const getRedis = () => {
|
||||
if (redisClient) return redisClient
|
||||
|
||||
const redisUrl = process.env.REDIS_URL || ''
|
||||
const db = Number(process.env.REDIS_DB || 9)
|
||||
|
||||
redisClient = redisUrl
|
||||
? new Redis(redisUrl, { db })
|
||||
: new Redis({
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: Number(process.env.REDIS_PORT || 6379),
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
db
|
||||
})
|
||||
|
||||
return redisClient
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createCipheriv, createDecipheriv, createHash, timingSafeEqual } from 'node:crypto'
|
||||
|
||||
const hexToBuffer = (value: string) => Buffer.from(value, 'hex')
|
||||
|
||||
export const sm3 = (value: string) => createHash('sm3').update(value, 'utf8').digest('hex')
|
||||
|
||||
export const createSupplierOpenSignature = (supplierId: string, clientId: string, timestamp: number | string) => {
|
||||
return sm3(`${supplierId}${clientId}${timestamp}`)
|
||||
}
|
||||
|
||||
export const verifySupplierOpenSignature = (
|
||||
supplierId: string,
|
||||
clientId: string,
|
||||
timestamp: number | string,
|
||||
token: string,
|
||||
timeWindowMs = 300_000
|
||||
) => {
|
||||
const timestampValue = Number(timestamp || 0)
|
||||
if (!timestampValue || Math.abs(Date.now() - timestampValue) > timeWindowMs) {
|
||||
return false
|
||||
}
|
||||
|
||||
const expected = createSupplierOpenSignature(supplierId, clientId, timestampValue)
|
||||
const expectedBuffer = Buffer.from(expected)
|
||||
const tokenBuffer = Buffer.from(String(token || ''))
|
||||
return expectedBuffer.length === tokenBuffer.length && timingSafeEqual(expectedBuffer, tokenBuffer)
|
||||
}
|
||||
|
||||
export const sm4CbcDecrypt = (encryptedData: string, keyHex: string, ivHex: string) => {
|
||||
const decipher = createDecipheriv('sm4-cbc', hexToBuffer(keyHex), hexToBuffer(ivHex))
|
||||
return Buffer.concat([
|
||||
decipher.update(Buffer.from(encryptedData, 'base64')),
|
||||
decipher.final()
|
||||
]).toString('utf8')
|
||||
}
|
||||
|
||||
export const sm4CbcEncrypt = (data: string, keyHex: string, ivHex: string) => {
|
||||
const cipher = createCipheriv('sm4-cbc', hexToBuffer(keyHex), hexToBuffer(ivHex))
|
||||
return Buffer.concat([
|
||||
cipher.update(Buffer.from(data, 'utf8')),
|
||||
cipher.final()
|
||||
]).toString('base64')
|
||||
}
|
||||
@@ -11,5 +11,5 @@
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["netlify/**/*.ts", "prisma.config.ts"]
|
||||
"include": ["src/**/*.ts", "prisma.config.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user