diff --git a/supplied-h5/src/api/supplied-service.js b/supplied-h5/src/api/supplied-service.js index c8ced21..e2e6105 100644 --- a/supplied-h5/src/api/supplied-service.js +++ b/supplied-h5/src/api/supplied-service.js @@ -2,6 +2,25 @@ const API_BASE = process.env.VUE_APP_SUPPLIED_API_BASE || 'http://localhost:8888 const normalizeBase = base => base.replace(/\/$/, '') +export const getFunction = async (name, params = {}) => { + const url = new URL(`${normalizeBase(API_BASE)}/${name}`) + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + url.searchParams.set(key, value) + } + }) + + const response = await fetch(url.toString()) + const payload = await response.json().catch(() => ({})) + + if (!response.ok || payload.success === false) { + const message = payload.error?.message || payload.msg || '请求失败' + throw new Error(message) + } + + return payload.data +} + export const postFunction = async (name, body = {}) => { const response = await fetch(`${normalizeBase(API_BASE)}/${name}`, { method: 'POST', diff --git a/supplied-h5/src/assets/joyful-selection/profile/avatar.png b/supplied-h5/src/assets/joyful-selection/profile/avatar.png new file mode 100644 index 0000000..4306cb6 Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/avatar.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/bag.png b/supplied-h5/src/assets/joyful-selection/profile/bag.png new file mode 100644 index 0000000..3ba6f7a Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/bag.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/clipboard.png b/supplied-h5/src/assets/joyful-selection/profile/clipboard.png new file mode 100644 index 0000000..ba56c7d Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/clipboard.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/coins.png b/supplied-h5/src/assets/joyful-selection/profile/coins.png new file mode 100644 index 0000000..304e5c2 Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/coins.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/empty-box.png b/supplied-h5/src/assets/joyful-selection/profile/empty-box.png new file mode 100644 index 0000000..2a5eefd Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/empty-box.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/gift.png b/supplied-h5/src/assets/joyful-selection/profile/gift.png new file mode 100644 index 0000000..6eda663 Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/gift.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/headset.png b/supplied-h5/src/assets/joyful-selection/profile/headset.png new file mode 100644 index 0000000..7530ecf Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/headset.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/heart.png b/supplied-h5/src/assets/joyful-selection/profile/heart.png new file mode 100644 index 0000000..e50fbc5 Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/heart.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/help.png b/supplied-h5/src/assets/joyful-selection/profile/help.png new file mode 100644 index 0000000..5432ee8 Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/help.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/invoice.png b/supplied-h5/src/assets/joyful-selection/profile/invoice.png new file mode 100644 index 0000000..9061f14 Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/invoice.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/location.png b/supplied-h5/src/assets/joyful-selection/profile/location.png new file mode 100644 index 0000000..b9cd74f Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/location.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/profile-bg-soft.png b/supplied-h5/src/assets/joyful-selection/profile/profile-bg-soft.png new file mode 100644 index 0000000..1e28c7b Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/profile-bg-soft.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/truck.png b/supplied-h5/src/assets/joyful-selection/profile/truck.png new file mode 100644 index 0000000..7295f99 Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/truck.png differ diff --git a/supplied-h5/src/assets/joyful-selection/profile/wallet.png b/supplied-h5/src/assets/joyful-selection/profile/wallet.png new file mode 100644 index 0000000..0996357 Binary files /dev/null and b/supplied-h5/src/assets/joyful-selection/profile/wallet.png differ diff --git a/supplied-h5/src/store/user.js b/supplied-h5/src/store/user.js index a8c804c..750477f 100644 --- a/supplied-h5/src/store/user.js +++ b/supplied-h5/src/store/user.js @@ -3,6 +3,7 @@ import { postFunction } from '@/api/supplied-service' const USER_KEY = 'joyful-selection-user' const DEFAULT_SUPPLIER_IDENTIFICATION = (process.env.VUE_APP_SUPPLIER_IDENTIFICATION || '').toLowerCase() +let bridgeTokenPromise = null const readUser = () => { try { @@ -22,10 +23,10 @@ export const useUserStore = defineStore('joyfulSelectionUser', { supplierIdentification: '', supplierId: '', supplierName: '', - clientId: '', token: '', refreshToken: '', tokenExpiresAt: 0, + pointsBalance: 0, loaded: false, loadingToken: false }), @@ -42,10 +43,10 @@ export const useUserStore = defineStore('joyfulSelectionUser', { this.supplierIdentification = supplierIdentification this.supplierId = supplierChanged ? '' : data.supplierId || '' this.supplierName = supplierChanged ? '' : data.supplierName || '' - this.clientId = supplierChanged ? '' : data.clientId || '' 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 }, persistUser() { @@ -54,10 +55,10 @@ export const useUserStore = defineStore('joyfulSelectionUser', { supplierIdentification: this.supplierIdentification, supplierId: this.supplierId, supplierName: this.supplierName, - clientId: this.clientId, token: this.token, refreshToken: this.refreshToken, - tokenExpiresAt: this.tokenExpiresAt + tokenExpiresAt: this.tokenExpiresAt, + pointsBalance: this.pointsBalance }) this.loaded = true }, @@ -73,6 +74,7 @@ export const useUserStore = defineStore('joyfulSelectionUser', { this.token = '' this.refreshToken = '' this.tokenExpiresAt = 0 + this.pointsBalance = 0 } if (!this.supplierIdentification) { this.supplierIdentification = DEFAULT_SUPPLIER_IDENTIFICATION @@ -100,19 +102,21 @@ export const useUserStore = defineStore('joyfulSelectionUser', { this.supplierId = data?.supplierId || '' this.supplierName = data?.supplierName || '' - this.clientId = data?.clientId || '' this.persistUser() }, async fetchBridgeToken() { if (!this.loaded) { this.loadUser() } - if (!this.userId || !this.supplierIdentification || this.loadingToken || this.hasToken) { + if (!this.userId || !this.supplierIdentification || this.hasToken) { return } + if (this.loadingToken && bridgeTokenPromise) { + return bridgeTokenPromise + } this.loadingToken = true - try { + bridgeTokenPromise = (async () => { if (!this.supplierId) { await this.fetchSupplierInfo() } @@ -129,9 +133,34 @@ export const useUserStore = defineStore('joyfulSelectionUser', { const expiresIn = Number(data?.expiresIn || 0) this.tokenExpiresAt = this.token && expiresIn > 0 ? Date.now() + expiresIn * 1000 : 0 this.persistUser() + })() + + try { + await bridgeTokenPromise } finally { this.loadingToken = false + bridgeTokenPromise = null } + }, + async fetchUserPoints() { + if (!this.loaded) { + this.loadUser() + } + await this.fetchBridgeToken() + + if (!this.userId || !this.supplierId || !this.token) { + return this.pointsBalance + } + + const data = await postFunction('supplier-points', { + userId: this.userId, + supplierId: this.supplierId, + token: this.token + }) + + this.pointsBalance = Number(data?.userPoints || 0) + this.persistUser() + return this.pointsBalance } } }) diff --git a/supplied-h5/src/view/joyful-selection/index.vue b/supplied-h5/src/view/joyful-selection/index.vue index 164c771..b49ee82 100644 --- a/supplied-h5/src/view/joyful-selection/index.vue +++ b/supplied-h5/src/view/joyful-selection/index.vue @@ -24,7 +24,7 @@
限时秒杀更多 ›
-
+

{{ item.name }}

¥{{ item.price }} @@ -36,7 +36,7 @@
爆款推荐更多 ›
-
+

{{ item.name }}

¥{{ item.price }} @@ -44,12 +44,11 @@
- @@ -121,18 +173,20 @@ export default { position: relative; z-index: 1; display: flex; + min-height: 92px; + align-items: center; + gap: 12px; margin: -40px 10px 16px; - padding: 20px 0 18px; + padding: 14px 16px; border-radius: 8px; background: #fff; box-shadow: 0 4px 16px rgba(61, 68, 92, .06); } -.asset-card div { +.asset-info { display: grid; flex: 1; - gap: 6px; - text-align: center; + gap: 5px; } .asset-card small { @@ -142,7 +196,23 @@ export default { .asset-card b { color: #ff5b20; - font-size: 16px; + font-size: 24px; + font-weight: 700; + line-height: 1; +} + +.points-link { + display: inline-flex; + height: 44px; + min-width: 118px; + align-items: center; + justify-content: center; + gap: 5px; + border: 0; + border-radius: 999px; + background: #fff0ec; + color: #ff5b20; + font-size: 13px; font-weight: 700; } diff --git a/supplied-h5/src/view/joyful-selection/profile/orders/index.vue b/supplied-h5/src/view/joyful-selection/profile/orders/index.vue index 0b7b59a..0f294e0 100644 --- a/supplied-h5/src/view/joyful-selection/profile/orders/index.vue +++ b/supplied-h5/src/view/joyful-selection/profile/orders/index.vue @@ -1,86 +1,109 @@ @@ -123,6 +146,12 @@ export default { padding: 8px 8px 14px; } +.order-loading { + display: flex; + justify-content: center; + padding: 42px 0; +} + .order-card { overflow: hidden; margin-bottom: 10px; @@ -132,16 +161,21 @@ export default { .order-card header { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; padding: 12px 12px 8px; } -.order-card header span { +.order-nos { min-width: 0; +} + +.order-nos span { + display: block; overflow: hidden; color: #777; font-size: 11px; + line-height: 18px; text-overflow: ellipsis; white-space: nowrap; } @@ -151,6 +185,7 @@ export default { margin-left: 10px; font-size: 12px; font-weight: 700; + line-height: 18px; } .order-card header .pending { diff --git a/supplied-h5/src/view/joyful-selection/profile/points/index.vue b/supplied-h5/src/view/joyful-selection/profile/points/index.vue index d2148fc..e4fe9d9 100644 --- a/supplied-h5/src/view/joyful-selection/profile/points/index.vue +++ b/supplied-h5/src/view/joyful-selection/profile/points/index.vue @@ -1,3 +1,138 @@ - - - + + + + + diff --git a/supplied-service/netlify/functions/order-cancel-main.ts b/supplied-service/netlify/functions/order-cancel-main.ts new file mode 100644 index 0000000..28684ab --- /dev/null +++ b/supplied-service/netlify/functions/order-cancel-main.ts @@ -0,0 +1,132 @@ +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 OrderCancelMainRequest = { + supplierId?: string + token?: string + userId?: string + orderId?: string + mainOrderId?: string +} + +type BridgeOrderResponse = { + resultCode?: number + 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(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') + } + + const order = await prisma.supplierOrderMain.findFirst({ + where: { + orderId: mainOrderId, + supplierId, + userId, + deleted: 0 + } + }) + + if (!order) { + 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') + } + + 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, + updateTime: new Date() + } + }) + + await tx.supplierOrderSub.updateMany({ + where: { + mainOrderId, + supplierId, + userId, + deleted: 0 + }, + data: { + deleted: 1, + updateTime: new Date() + } + }) + + await tx.supplierOrderProduct.updateMany({ + where: { + subOrderId: { in: subOrderIds }, + deleted: 0 + }, + data: { + deleted: 1 + } + }) + }) + + return ok({ + supplierId, + orderType: 1, + orderId, + mainOrderId, + userId + }) +}) diff --git a/supplied-service/netlify/functions/order-cancel-sub.ts b/supplied-service/netlify/functions/order-cancel-sub.ts new file mode 100644 index 0000000..12d4c31 --- /dev/null +++ b/supplied-service/netlify/functions/order-cancel-sub.ts @@ -0,0 +1,107 @@ +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 OrderCancelSubRequest = { + supplierId?: string + token?: string + userId?: string + orderId?: string + mainOrderId?: string +} + +type BridgeOrderResponse = { + resultCode?: number + 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(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') + } + + const subOrder = await prisma.supplierOrderSub.findFirst({ + where: { + orderId, + supplierId, + userId, + deleted: 0 + } + }) + + if (!subOrder) { + return fail('子单不存在或已取消', 404, 'SUB_ORDER_NOT_FOUND') + } + + const mainOrderId = String(body.mainOrderId || subOrder.mainOrderId).trim() + + const bridgeResponse = await fetch(`${apiBase}/pointOrder/pushOrderInfoMsg`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + token + }, + body: JSON.stringify({ + supplierId, + orderType: 3, + 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_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, + orderId, + mainOrderId, + userId + }) +}) diff --git a/supplied-service/netlify/functions/order-create.ts b/supplied-service/netlify/functions/order-create.ts index 01770d9..68730a6 100644 --- a/supplied-service/netlify/functions/order-create.ts +++ b/supplied-service/netlify/functions/order-create.ts @@ -1,11 +1,259 @@ import type { Context } from '@netlify/functions' import { guardMethod } from '../utils/method.js' -import { ok } from '../utils/http.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 CheckoutGoods = { + id?: string | number + sku?: string + count?: number + number?: number + price?: string | number + name?: string + image?: string + imageUrl?: string + picInfo?: string[] + taxRate?: number +} + +type OrderCreateRequest = { + supplierId?: string + token?: string + userId?: string + orderNo?: string + orderNumberId?: string + amount?: number + goods?: CheckoutGoods[] +} + +type BridgeOrderResponse = { + resultCode?: number + message?: string +} + +const normalizeBase = (base: string) => base.replace(/\/$/, '') + +const nowText = () => { + const now = new Date() + const pad = (value: number) => String(value).padStart(2, '0') + + return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` +} + +const createOrderNumberId = () => { + const now = new Date() + const pad = (value: number, size = 2) => String(value).padStart(size, '0') + + return `TR${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}${pad(now.getMilliseconds(), 3)}` +} + +const toYuan = (centAmount?: number) => Number(((Number(centAmount || 0)) / 100).toFixed(2)) + +const toOrderProducts = (goods: CheckoutGoods[]) => goods.map((item, index) => { + const image = item.image || item.imageUrl || item.picInfo?.[0] || '' + + return { + sku: String(item.sku || item.id || `SKU_${index + 1}`), + number: Number(item.count || item.number || 1), + price: Number(item.price || 0), + name: String(item.name || ''), + taxRate: Number(item.taxRate || 0.13), + picInfo: item.picInfo?.length ? item.picInfo : image ? [image] : [], + fileId: image || null, + filePath: image || null + } +}) + export default withErrorBoundary(async (request: Request, _context: Context) => { const methodResponse = guardMethod(request, ['POST']) if (methodResponse) return methodResponse - return ok({ message: '创建订单接口待实现' }) + const body = await readJson(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 : [] + + if (!supplierId || !token || !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') + } + + const orderNumberId = String(body.orderNumberId || createOrderNumberId()) + const subOrderId = `${orderId}_001` + const subOrderNumberId = `${orderNumberId}_001` + const orderTime = nowText() + const orderProInfos = toOrderProducts(goods) + const orderInfo = { + orderId, + orderNumberId, + supplierId, + aplName: '积分商城', + createTime: orderTime, + updateTime: orderTime, + deliveryTime: null, + orderFinishTime: null, + completeTime: null, + deliveryStatus: '0', + freightCost: 0, + wPayOrAPayFreightCost: 0, + refund: 0, + wPayOrAPayRefund: 0, + logisticsOrderId: null, + crrgBsnNm: null, + orderState: 1, + totalPrice: amount, + wPayOrAPay: 0, + pointsPrice: amount, + invoiceCode: null, + orderProInfos + } + + await prisma.$transaction(async tx => { + await tx.supplierOrderMain.upsert({ + where: { orderId }, + update: { + orderNumberId, + supplierId, + userId, + aplName: orderInfo.aplName, + 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, + invoiceCode: orderInfo.invoiceCode, + deleted: 0 + }, + create: { + orderId, + orderNumberId, + supplierId, + 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, + invoiceCode: orderInfo.invoiceCode + } + }) + + await tx.supplierOrderSub.upsert({ + where: { orderId: subOrderId }, + update: { + orderNumberId: subOrderNumberId, + mainOrderId: orderId, + supplierId, + token, + userId, + aplName: orderInfo.aplName, + 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, + 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, + invoiceCode: orderInfo.invoiceCode + } + }) + + await tx.supplierOrderProduct.deleteMany({ + where: { subOrderId } + }) + + 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, + userId, + orderInfo, + token + }) + }) + + const bridgePayload = (await bridgeResponse.json().catch(() => ({}))) as BridgeOrderResponse + + if (!bridgeResponse.ok || bridgePayload.resultCode !== 0) { + return fail(bridgePayload.message || '提交待支付订单失败', 502, 'BRIDGE_PENDING_ORDER_FAILED') + } + + return ok({ + orderId, + orderNumberId, + subOrderId, + orderInfo + }) }) diff --git a/supplied-service/netlify/functions/order-list.ts b/supplied-service/netlify/functions/order-list.ts new file mode 100644 index 0000000..ceb8737 --- /dev/null +++ b/supplied-service/netlify/functions/order-list.ts @@ -0,0 +1,106 @@ +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 = { + 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(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 }) +}) diff --git a/supplied-service/netlify/functions/order-pay.ts b/supplied-service/netlify/functions/order-pay.ts index 24eedde..402a90f 100644 --- a/supplied-service/netlify/functions/order-pay.ts +++ b/supplied-service/netlify/functions/order-pay.ts @@ -1,11 +1,148 @@ import type { Context } from '@netlify/functions' import { guardMethod } from '../utils/method.js' -import { ok } from '../utils/http.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 OrderPayRequest = { + supplierId?: string + token?: string + orderId?: string + amount?: number + totalPrice?: number + wPayOrAPay?: number + pointsPrice?: number +} + +type BridgePayResponse = { + resultCode?: number + 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) => { const methodResponse = guardMethod(request, ['POST']) if (methodResponse) return methodResponse - return ok({ message: '订单支付接口待实现' }) + const body = await readJson(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 (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 } + }) + + if (!order || order.deleted === 1) { + 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, + totalPrice, + wPayOrAPay, + pointsPrice, + updateTime: new Date() + } + }) + + await tx.supplierOrderSub.updateMany({ + where: { mainOrderId: orderId, deleted: 0 }, + data: { + orderState: 1, + totalPrice, + wPayOrAPay, + pointsPrice, + updateTime: new Date() + } + }) + }) + + const subOrders = await prisma.supplierOrderSub.findMany({ + where: { + mainOrderId: orderId, + deleted: 0 + }, + select: { + orderId: true + } + }) + const subOrderIds = subOrders.map(item => item.orderId).filter(Boolean).join(',') + + if (subOrderIds) { + 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 + }) + }) + + const splitPayload = (await splitResponse.json().catch(() => ({}))) as BridgePayResponse + + if (!splitResponse.ok || splitPayload.resultCode !== 0) { + return fail(splitPayload.message || '推送拆单信息失败', 502, 'BRIDGE_SPLIT_ORDER_FAILED') + } + } + + return ok({ + orderId, + subOrderIds, + totalPrice, + wPayOrAPay, + pointsPrice + }) }) diff --git a/supplied-service/netlify/functions/product-detail.ts b/supplied-service/netlify/functions/product-detail.ts index f936c23..c70711b 100644 --- a/supplied-service/netlify/functions/product-detail.ts +++ b/supplied-service/netlify/functions/product-detail.ts @@ -1,11 +1,63 @@ import type { Context } from '@netlify/functions' import { guardMethod } from '../utils/method.js' -import { ok } from '../utils/http.js' +import { fail, ok } from '../utils/http.js' +import { prisma } from '../utils/prisma.js' import { withErrorBoundary } from '../utils/handler.js' +const toProductDetail = (product: NonNullable>>) => { + const picInfo = Array.isArray(product.picInfo) ? product.picInfo : [] + const image = product.imageUrl || String(picInfo[0] || '') + const price = product.price.toFixed(2) + + return { + id: product.id, + sku: product.sku, + number: product.number, + price, + name: product.name, + taxRate: Number(product.taxRate), + picInfo, + desc: product.description || '', + detail: product.detail || product.description || '', + originPrice: product.originPrice ? product.originPrice.toFixed(2) : price, + image, + imageUrl: image, + categoryId: product.categoryId || '', + category: product.category || '', + tag: product.tag || '', + sales: product.sales || '', + stock: product.stock + } +} + export default withErrorBoundary(async (request: Request, _context: Context) => { const methodResponse = guardMethod(request, ['GET']) if (methodResponse) return methodResponse - return ok({ message: '商品详情接口待实现' }) + const url = new URL(request.url) + const id = (url.searchParams.get('id') || '').trim() + const sku = (url.searchParams.get('sku') || '').trim() + const numericId = Number(id) + const querySku = sku || (id && !Number.isInteger(numericId) ? id : '') + + if (!id && !sku) { + return fail('商品参数不能为空') + } + + const product = await prisma.supplierProduct.findFirst({ + where: { + enabled: true, + deleted: 0, + OR: [ + ...(Number.isInteger(numericId) && numericId > 0 ? [{ id: numericId }] : []), + ...(querySku ? [{ sku: querySku }] : []) + ] + } + }) + + if (!product) { + return fail('商品不存在或已下架', 404, 'PRODUCT_NOT_FOUND') + } + + return ok(toProductDetail(product)) }) diff --git a/supplied-service/netlify/functions/product-list.ts b/supplied-service/netlify/functions/product-list.ts index 0bdc2af..feec94e 100644 --- a/supplied-service/netlify/functions/product-list.ts +++ b/supplied-service/netlify/functions/product-list.ts @@ -1,11 +1,69 @@ import type { Context } from '@netlify/functions' import { guardMethod } from '../utils/method.js' import { ok } from '../utils/http.js' +import { prisma } from '../utils/prisma.js' import { withErrorBoundary } from '../utils/handler.js' +const categories = [ + { id: 'recommend', name: '推荐' }, + { id: 'homeLiving', name: '居家生活' }, + { id: 'digitalAppliance', name: '数码家电' }, + { id: 'beautyCare', name: '美妆个护' }, + { id: 'foodDrink', name: '食品饮料' }, + { id: 'motherBaby', name: '母婴玩具' }, + { id: 'fashionBag', name: '服饰鞋包' }, + { id: 'sportsOutdoor', name: '运动户外' } +] + +const toProductItem = (product: Awaited>[number]) => { + const picInfo = Array.isArray(product.picInfo) ? product.picInfo : [] + const image = product.imageUrl || String(picInfo[0] || '') + const price = product.price.toFixed(2) + + return { + id: product.id, + sku: product.sku, + number: product.number, + price, + name: product.name, + taxRate: Number(product.taxRate), + picInfo, + desc: product.description || '', + detail: product.detail || product.description || '', + originPrice: product.originPrice ? product.originPrice.toFixed(2) : price, + image, + imageUrl: image, + categoryId: product.categoryId || '', + category: product.category || '', + tag: product.tag || '', + sales: product.sales || '', + stock: product.stock + } +} + export default withErrorBoundary(async (request: Request, _context: Context) => { const methodResponse = guardMethod(request, ['GET']) if (methodResponse) return methodResponse - return ok({ message: '商品列表接口待实现' }) + const url = new URL(request.url) + const keyword = (url.searchParams.get('keyword') || '').trim() + const categoryId = (url.searchParams.get('categoryId') || '').trim() + + const products = await prisma.supplierProduct.findMany({ + where: { + enabled: true, + deleted: 0, + ...(keyword ? { name: { contains: keyword } } : {}), + ...(categoryId && categoryId !== 'recommend' ? { categoryId } : {}) + }, + orderBy: [ + { sortOrder: 'asc' }, + { createdAt: 'desc' } + ] + }) + + return ok({ + categories, + products: products.map(toProductItem) + }) }) diff --git a/supplied-service/netlify/functions/supplier-info.ts b/supplied-service/netlify/functions/supplier-info.ts index b5b9c14..3470b8f 100644 --- a/supplied-service/netlify/functions/supplier-info.ts +++ b/supplied-service/netlify/functions/supplier-info.ts @@ -32,7 +32,6 @@ export default withErrorBoundary(async (request: Request, _context: Context) => id: supplier.id, supplierIdentification: supplier.supplierIdentification, supplierId: supplier.supplierId, - supplierName: supplier.supplierName, - clientId: supplier.clientId + supplierName: supplier.supplierName }) }) diff --git a/supplied-service/netlify/functions/supplier-points.ts b/supplied-service/netlify/functions/supplier-points.ts new file mode 100644 index 0000000..a1398ee --- /dev/null +++ b/supplied-service/netlify/functions/supplier-points.ts @@ -0,0 +1,62 @@ +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 { withErrorBoundary } from '../utils/handler.js' + +type SupplierPointsRequest = { + supplierId?: string + token?: string + userId?: string +} + +type BridgePointsResponse = { + resultCode?: number + message?: string + 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(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('供应商、用户或授权信息不能为空') + } + + 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 + + if (!response.ok || pointsPayload.resultCode !== 0) { + return fail(pointsPayload.message || '查询用户积分失败', 502, 'BRIDGE_POINTS_FAILED') + } + + return ok({ + userPoints: Number(pointsPayload.userPoints || 0) + }) +}) diff --git a/supplied-service/netlify/functions/supplier-token.ts b/supplied-service/netlify/functions/supplier-token.ts index 0f15467..d9dc9d6 100644 --- a/supplied-service/netlify/functions/supplier-token.ts +++ b/supplied-service/netlify/functions/supplier-token.ts @@ -59,7 +59,8 @@ export default withErrorBoundary(async (request: Request, _context: Context) => body: JSON.stringify({ supplierId: supplier.supplierId, clientId: supplier.clientId, - clientSecret: signature, + clientSecret: supplier.clientSecret, + sendSha256: signature, code: '', timestamp }) diff --git a/supplied-service/netlify/utils/crypto.ts b/supplied-service/netlify/utils/crypto.ts index c29662e..7e3309f 100644 --- a/supplied-service/netlify/utils/crypto.ts +++ b/supplied-service/netlify/utils/crypto.ts @@ -5,5 +5,5 @@ export const sha256 = (value: string) => { } export const createBridgeSignature = (clientId: string, clientSecret: string, timestamp: string) => { - return sha256(`${clientId}${clientSecret}${timestamp}${clientSecret}`) + return sha256(`${clientId}${clientSecret}${timestamp}`) } diff --git a/supplied-service/package.json b/supplied-service/package.json index ae01753..75778cc 100644 --- a/supplied-service/package.json +++ b/supplied-service/package.json @@ -9,7 +9,8 @@ "typecheck": "tsc --noEmit", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", - "prisma:studio": "prisma studio" + "prisma:studio": "prisma studio", + "seed:products": "node prisma/seed-products.mjs" }, "dependencies": { "@neondatabase/serverless": "^1.0.1", diff --git a/supplied-service/prisma/schema.prisma b/supplied-service/prisma/schema.prisma index 712486f..edab3c7 100644 --- a/supplied-service/prisma/schema.prisma +++ b/supplied-service/prisma/schema.prisma @@ -8,7 +8,7 @@ datasource db { } model User { - id String @id @default(cuid()) + id Int @id @default(autoincrement()) mobile String @unique nickname String? avatarUrl String? @@ -20,22 +20,33 @@ model User { orders Order[] } -model Product { - id String @id @default(cuid()) +model SupplierProduct { + id Int @id @default(autoincrement()) + sku String @unique + number Int @default(0) + price Decimal @db.Decimal(10, 2) name String + taxRate Decimal @default(0.13) @db.Decimal(5, 4) + picInfo Json? description String? detail String? - price Decimal @db.Decimal(10, 2) + originPrice Decimal? @db.Decimal(10, 2) imageUrl String? + categoryId String? category String? tag String? + sales String? stock Int @default(0) + sortOrder Int @default(0) enabled Boolean @default(true) + deleted Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt carts CartItem[] orderItems OrderItem[] + + @@map("supplier_product") } model SupplierInfo { @@ -56,24 +67,24 @@ model SupplierInfo { } model CartItem { - id String @id @default(cuid()) - userId String - productId String + id Int @id @default(autoincrement()) + userId Int + productId Int skuText String? quantity Int @default(1) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) - product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + product SupplierProduct @relation(fields: [productId], references: [id], onDelete: Cascade) @@unique([userId, productId, skuText]) } model Order { - id String @id @default(cuid()) + id Int @id @default(autoincrement()) orderNo String @unique - userId String + userId Int status OrderStatus @default(PENDING_PAYMENT) totalAmount Decimal @db.Decimal(10, 2) payType PayType? @@ -86,17 +97,109 @@ model Order { } model OrderItem { - id String @id @default(cuid()) - orderId String - productId String + 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 Product @relation(fields: [productId], references: [id]) + 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 + orderNumberId String? + supplierId String + userId String? + aplName String? + createTime DateTime? + updateTime DateTime? + deliveryTime DateTime? + orderFinishTime DateTime? + completeTime DateTime? + deliveryStatus String? + freightCost Decimal @default(0) @db.Decimal(10, 2) + wPayOrAPayFreightCost Decimal @default(0) @db.Decimal(10, 2) + refund Decimal @default(0) @db.Decimal(10, 2) + wPayOrAPayRefund Decimal @default(0) @db.Decimal(10, 2) + logisticsOrderId String? + crrgBsnNm String? + orderState Int @default(0) + totalPrice Decimal @default(0) @db.Decimal(10, 2) + wPayOrAPay Decimal @default(0) @db.Decimal(10, 2) + pointsPrice Decimal @default(0) @db.Decimal(10, 2) + invoiceCode String? + createAt DateTime @default(now()) + updateAt DateTime @updatedAt + deleted Int @default(0) + + subOrders SupplierOrderSub[] + + @@map("supplier_order_main") +} + +model SupplierOrderSub { + id Int @id @default(autoincrement()) + orderId String @unique + orderNumberId String? + mainOrderId String + supplierId String + token String? + userId String? + aplName String? + createTime DateTime? + updateTime DateTime? + deliveryTime DateTime? + orderFinishTime DateTime? + completeTime DateTime? + deliveryStatus String? + freightCost Decimal @default(0) @db.Decimal(10, 2) + wPayOrAPayFreightCost Decimal @default(0) @db.Decimal(10, 2) + refund Decimal @default(0) @db.Decimal(10, 2) + wPayOrAPayRefund Decimal @default(0) @db.Decimal(10, 2) + logisticsOrderId String? + crrgBsnNm String? + orderState Int @default(0) + totalPrice Decimal @default(0) @db.Decimal(10, 2) + wPayOrAPay Decimal @default(0) @db.Decimal(10, 2) + pointsPrice Decimal @default(0) @db.Decimal(10, 2) + invoiceCode String? + createAt DateTime @default(now()) + updateAt DateTime @updatedAt + deleted Int @default(0) + + mainOrder SupplierOrderMain @relation(fields: [mainOrderId], references: [orderId], onDelete: Cascade) + products SupplierOrderProduct[] + + @@index([mainOrderId]) + @@map("supplier_order_sub") +} + +model SupplierOrderProduct { + id Int @id @default(autoincrement()) + subOrderId String + sku String + number Int + price Decimal @db.Decimal(10, 2) + name String + taxRate Decimal @default(0.13) @db.Decimal(5, 4) + productImgUrl String? + fileId String? + filePath String? + picInfo Json? + createAt DateTime @default(now()) + updateAt DateTime @updatedAt + deleted Int @default(0) + + subOrder SupplierOrderSub @relation(fields: [subOrderId], references: [orderId], onDelete: Cascade) + + @@index([subOrderId]) + @@map("supplier_order_product") } enum OrderStatus { diff --git a/supplied-service/prisma/seed-products.mjs b/supplied-service/prisma/seed-products.mjs new file mode 100644 index 0000000..8bf724f --- /dev/null +++ b/supplied-service/prisma/seed-products.mjs @@ -0,0 +1,96 @@ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +const imagePool = [ + 'https://ts3.tc.mm.bing.net/th/id/OIP-C.im_WvX9bWGyvagEZp4Pz0AHaGX?w=142&h=108&c=7&qlt=90&bgcl=a01e15&r=0&o=6&dpr=1.3&pid=13.1', + 'https://ts3.tc.mm.bing.net/th/id/OIP-C.MkmCbLoY2S4zqzMeHvCDMQHaHD?w=128&h=108&c=7&qlt=90&bgcl=f61537&r=0&o=6&dpr=1.3&pid=13.1', + 'https://ts2.tc.mm.bing.net/th/id/OIP-C.CKSBSB33LwT9qJD_Yhb1mwHaKk?w=132&h=108&c=7&qlt=90&bgcl=2e287d&r=0&o=6&dpr=1.3&pid=13.1', + 'https://tse1-mm.cn.bing.net/th/id/OIP-C.StrDRqennoZNbzSPZapKZwHaEJ?w=286&h=180&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3', + 'https://tse3-mm.cn.bing.net/th/id/OIP-C.ynSetSr3z884UC6sEp4yiwHaEJ?w=331&h=186&c=7&r=0&o=7&cb=thfc1falcon&dpr=1.3&pid=1.7&rm=3' +] + +const categories = { + homeLiving: '居家生活', + digitalAppliance: '数码家电', + beautyCare: '美妆个护', + foodDrink: '食品饮料', + motherBaby: '母婴玩具', + fashionBag: '服饰鞋包', + sportsOutdoor: '运动户外', + recommend: '精选推荐' +} + +const rawProducts = [ + ['SKU001', '蓝牙耳机', '轻盈佩戴,稳定连接,通勤运动都好用', 149.99, 199.99, 'digitalAppliance', '积分兑换', '1.2万件', 2], + ['SKU002', '手机壳', '防滑耐磨,贴合机身,日常保护更安心', 10.01, 19.9, 'digitalAppliance', '赠品', '5632件', 1], + ['SKU003', '智能音箱 Pro', '语音唤醒,沉浸音效,适合卧室和书房', 99.0, 139.0, 'digitalAppliance', '热门推荐', '2.1万件', 1], + ['SKU004', '奶油白迷你烤箱', '小巧不占地,早餐烘焙好帮手', 129.0, 169.0, 'homeLiving', '新品上架', '1.2万件', 1], + ['SKU005', '北欧陶瓷餐具套装', '简约釉色,日常用餐更有仪式感', 69.9, 99.9, 'homeLiving', '品质精选', '6453件', 1], + ['SKU006', '纯棉舒适四件套', '柔软亲肤,安心睡眠,四季可用', 179.0, 229.0, 'homeLiving', '积分兑换', '8321件', 1], + ['SKU007', '植萃保湿护肤礼盒', '补水保湿,清爽不黏腻', 99.9, 139.9, 'beautyCare', '礼盒精选', '4820件', 1], + ['SKU008', '香氛洗护旅行套装', '温和清洁,清新留香,出行便携', 59.9, 89.9, 'beautyCare', '新品上架', '7932件', 1], + ['SKU009', '便携补光化妆镜', '高清镜面,柔光补妆,随身携带', 49.9, 79.9, 'beautyCare', '热门推荐', '3651件', 1], + ['SKU010', '橙意零食礼盒', '精选零食组合,办公室分享刚刚好', 59.9, 89.9, 'foodDrink', '礼盒精选', '6324件', 1], + ['SKU011', '咖啡随行杯组合', '锁温防漏,通勤随行更方便', 49.9, 69.9, 'foodDrink', '积分兑换', '3520件', 1], + ['SKU012', '下午茶点心礼盒', '多种风味,一盒满足甜咸口味', 69.9, 99.9, 'foodDrink', '品质精选', '4891件', 1], + ['SKU013', '安心儿童水杯', '轻巧易握,防摔耐用,出门好携带', 39.9, 59.9, 'motherBaby', '新品上架', '4250件', 1], + ['SKU014', '益智积木套装', '色彩柔和,锻炼动手能力', 89.0, 129.0, 'motherBaby', '热门推荐', '2186件', 1], + ['SKU015', '柔光儿童夜灯', '温和光线,陪伴睡前时光', 69.9, 99.9, 'motherBaby', '品质精选', '3548件', 1], + ['SKU016', '大容量电脑背包', '分区收纳,通勤出行更轻松', 149.0, 199.0, 'fashionBag', '积分兑换', '1.6万件', 1], + ['SKU017', '轻巧通勤斜挎包', '简洁百搭,日常出门更实用', 79.9, 109.9, 'fashionBag', '新品上架', '5321件', 1], + ['SKU018', '柔软撞色丝巾礼盒', '细腻亲肤,点亮日常穿搭', 69.9, 99.9, 'fashionBag', '礼盒精选', '2680件', 1], + ['SKU019', '运动保温水杯', '长效锁温,户外运动随行', 49.9, 79.9, 'sportsOutdoor', '热门推荐', '7342件', 1], + ['SKU020', '筋膜放松按摩仪 Mini', '轻巧便携,缓解久坐酸胀', 99.0, 149.0, 'sportsOutdoor', '积分兑换', '3921件', 1], + ['SKU021', '户外露营灯', '多档照明,露营夜晚更安心', 79.0, 119.0, 'sportsOutdoor', '新品上架', '2568件', 1], + ['SKU022', '悦享精选礼包', '多场景精选好物,福利兑换优选', 59.9, 89.9, 'recommend', '精选推荐', '8820件', 1], + ['SKU023', '品质生活组合装', '居家、通勤、日用好物组合', 119.0, 159.0, 'recommend', '热门推荐', '6902件', 1], + ['SKU024', '积分兑换惊喜盒', '随机精选商品,兑换更有新鲜感', 39.9, 69.9, 'recommend', '限时秒杀', '5310件', 1] +] + +const products = rawProducts.map(([sku, name, description, price, originPrice, categoryId, tag, sales, number], index) => { + const imageUrl = imagePool[index % imagePool.length] + const nextImageUrl = imagePool[(index + 1) % imagePool.length] + + return { + sku, + number, + price, + name, + taxRate: 0.13, + picInfo: [imageUrl, nextImageUrl], + description, + detail: `${description}。精选品质商品,适合积分商城兑换和日常福利采购场景。`, + originPrice, + imageUrl, + categoryId, + category: categories[categoryId], + tag, + sales, + stock: 180 + index * 7, + sortOrder: index + 1, + enabled: true, + deleted: 0 + } +}) + +const main = async () => { + for (const product of products) { + await prisma.supplierProduct.upsert({ + where: { sku: product.sku }, + update: product, + create: product + }) + } +} + +main() + .then(async () => { + await prisma.$disconnect() + console.log(`已导入 ${products.length} 条商品数据`) + }) + .catch(async error => { + console.error(error) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/supplied-service/prisma/sql/product-order-schema.sql b/supplied-service/prisma/sql/product-order-schema.sql new file mode 100644 index 0000000..93d89bc --- /dev/null +++ b/supplied-service/prisma/sql/product-order-schema.sql @@ -0,0 +1,115 @@ +CREATE TABLE IF NOT EXISTS `supplier_product` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '商品主键ID', + `sku` VARCHAR(191) NOT NULL COMMENT '商品SKU编码', + `number` INT NOT NULL DEFAULT 0 COMMENT '默认购买数量或导入数量', + `price` DECIMAL(10, 2) NOT NULL COMMENT '商品销售价格', + `name` VARCHAR(191) NOT NULL COMMENT '商品名称', + `taxRate` DECIMAL(5, 4) NOT NULL DEFAULT 0.1300 COMMENT '商品税率', + `picInfo` JSON NULL COMMENT '商品图片列表,JSON数组', + `description` VARCHAR(191) NULL COMMENT '商品简短描述', + `detail` VARCHAR(191) NULL COMMENT '商品详情描述', + `originPrice` DECIMAL(10, 2) NULL COMMENT '商品原价', + `imageUrl` VARCHAR(191) NULL COMMENT '商品主图地址', + `categoryId` VARCHAR(191) NULL COMMENT '商品分类ID', + `category` VARCHAR(191) NULL COMMENT '商品分类名称', + `tag` VARCHAR(191) NULL COMMENT '商品标签', + `sales` VARCHAR(191) NULL COMMENT '商品销量展示文案', + `stock` INT NOT NULL DEFAULT 0 COMMENT '商品库存', + `sortOrder` INT NOT NULL DEFAULT 0 COMMENT '排序值,越小越靠前', + `enabled` BOOLEAN NOT NULL DEFAULT true COMMENT '是否启用', + `deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除', + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + `updatedAt` DATETIME(3) NOT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `supplier_product_sku_key` (`sku`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商商品信息表'; + +CREATE TABLE IF NOT EXISTS `supplier_order_main` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '主单主键ID', + `orderId` VARCHAR(191) NOT NULL COMMENT '主订单ID', + `orderNumberId` VARCHAR(191) NULL COMMENT '主订单编号', + `supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID', + `userId` VARCHAR(191) NULL COMMENT '平台用户ID', + `aplName` VARCHAR(191) NULL COMMENT '应用名称', + `createTime` DATETIME(3) NULL COMMENT '订单创建时间', + `updateTime` DATETIME(3) NULL COMMENT '订单更新时间', + `deliveryTime` DATETIME(3) NULL COMMENT '发货时间', + `orderFinishTime` DATETIME(3) NULL COMMENT '订单完成时间', + `completeTime` DATETIME(3) NULL COMMENT '交易完成时间', + `deliveryStatus` VARCHAR(191) NULL COMMENT '配送状态', + `freightCost` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '运费积分金额', + `wPayOrAPayFreightCost` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付运费金额', + `refund` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分退款金额', + `wPayOrAPayRefund` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝退款金额', + `logisticsOrderId` VARCHAR(191) NULL COMMENT '物流单号', + `crrgBsnNm` VARCHAR(191) NULL COMMENT '承运商名称', + `orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态', + `totalPrice` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额', + `wPayOrAPay` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付金额', + `pointsPrice` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分支付金额', + `invoiceCode` VARCHAR(191) NULL COMMENT '发票编码', + `createAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间', + `updateAt` DATETIME(3) NOT NULL COMMENT '记录更新时间', + `deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `supplier_order_main_orderId_key` (`orderId`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商主订单表'; + +CREATE TABLE IF NOT EXISTS `supplier_order_sub` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '子单主键ID', + `orderId` VARCHAR(191) NOT NULL COMMENT '子订单ID', + `orderNumberId` VARCHAR(191) NULL COMMENT '子订单编号', + `mainOrderId` VARCHAR(191) NOT NULL COMMENT '所属主订单ID', + `supplierId` VARCHAR(191) NOT NULL COMMENT '供应商ID', + `token` VARCHAR(191) NULL COMMENT '平台桥接访问令牌快照', + `userId` VARCHAR(191) NULL COMMENT '平台用户ID', + `aplName` VARCHAR(191) NULL COMMENT '应用名称', + `createTime` DATETIME(3) NULL COMMENT '订单创建时间', + `updateTime` DATETIME(3) NULL COMMENT '订单更新时间', + `deliveryTime` DATETIME(3) NULL COMMENT '发货时间', + `orderFinishTime` DATETIME(3) NULL COMMENT '订单完成时间', + `completeTime` DATETIME(3) NULL COMMENT '交易完成时间', + `deliveryStatus` VARCHAR(191) NULL COMMENT '配送状态', + `freightCost` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '运费积分金额', + `wPayOrAPayFreightCost` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付运费金额', + `refund` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分退款金额', + `wPayOrAPayRefund` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝退款金额', + `logisticsOrderId` VARCHAR(191) NULL COMMENT '物流单号', + `crrgBsnNm` VARCHAR(191) NULL COMMENT '承运商名称', + `orderState` INT NOT NULL DEFAULT 0 COMMENT '订单状态', + `totalPrice` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '子订单总金额', + `wPayOrAPay` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '微信或支付宝支付金额', + `pointsPrice` DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '积分支付金额', + `invoiceCode` VARCHAR(191) NULL COMMENT '发票编码', + `createAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间', + `updateAt` DATETIME(3) NOT NULL COMMENT '记录更新时间', + `deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除', + PRIMARY KEY (`id`), + UNIQUE KEY `supplier_order_sub_orderId_key` (`orderId`), + KEY `supplier_order_sub_mainOrderId_idx` (`mainOrderId`), + CONSTRAINT `supplier_order_sub_mainOrderId_fkey` + FOREIGN KEY (`mainOrderId`) REFERENCES `supplier_order_main` (`orderId`) + ON DELETE CASCADE ON UPDATE CASCADE +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商子订单表'; + +CREATE TABLE IF NOT EXISTS `supplier_order_product` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '子单商品主键ID', + `subOrderId` VARCHAR(191) NOT NULL COMMENT '所属子订单ID', + `sku` VARCHAR(191) NOT NULL COMMENT '商品SKU编码', + `number` INT NOT NULL COMMENT '商品购买数量', + `price` DECIMAL(10, 2) NOT NULL COMMENT '商品成交单价', + `name` VARCHAR(191) NOT NULL COMMENT '商品名称快照', + `taxRate` DECIMAL(5, 4) NOT NULL DEFAULT 0.1300 COMMENT '商品税率快照', + `productImgUrl` VARCHAR(191) NULL COMMENT '商品图片地址', + `fileId` VARCHAR(191) NULL COMMENT '平台文件ID或图片ID', + `filePath` VARCHAR(191) NULL COMMENT '文件存储路径', + `picInfo` JSON NULL COMMENT '商品图片列表快照,JSON数组', + `createAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间', + `updateAt` DATETIME(3) NOT NULL COMMENT '记录更新时间', + `deleted` INT NOT NULL DEFAULT 0 COMMENT '删除状态:0未删除,1已删除', + PRIMARY KEY (`id`), + KEY `supplier_order_product_subOrderId_idx` (`subOrderId`), + CONSTRAINT `supplier_order_product_subOrderId_fkey` + FOREIGN KEY (`subOrderId`) REFERENCES `supplier_order_sub` (`orderId`) + ON DELETE CASCADE ON UPDATE CASCADE +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT='供应商子订单商品表';