feat: 去掉商品取货码,管理员可直接发货完成
已付款订单不再生成或核验取货码,管理员确认后即可完成订单。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,8 +8,8 @@
|
|||||||
- 各尺码、颜色按独立商品管理,在标题中标明规格;不引入购物车、物流、优惠券或复杂 SKU 系统。
|
- 各尺码、颜色按独立商品管理,在标题中标明规格;不引入购物车、物流、优惠券或复杂 SKU 系统。
|
||||||
- 创建商品默认库存为 0;保存后通过库存调整补货。调整数量为有符号整数,必须填写原因。
|
- 创建商品默认库存为 0;保存后通过库存调整补货。调整数量为有符号整数,必须填写原因。
|
||||||
- 顾客填写取货人、手机号及数量,确认购买后微信付款。价格变化时先阻止下单,要求确认最新价格。
|
- 顾客填写取货人、手机号及数量,确认购买后微信付款。价格变化时先阻止下单,要求确认最新价格。
|
||||||
- 付款成功后显示提货码。管理员核对商品数量和取货人,输入顾客的 12 位提货码并确认交付。
|
- 付款成功后订单进入待发货。管理员核对商品数量和取货人后,直接确认发货并完成订单。
|
||||||
- 管理端支持商品搜索、订单状态筛选、商品名 / 订单号 / 手机号搜索、分页、累计成交额、销量、待取货数、低库存提示。
|
- 管理端支持商品搜索、订单状态筛选、商品名 / 订单号 / 手机号搜索、分页、累计成交额、销量、待发货数、低库存提示。
|
||||||
- 售后通过详情页「联系场馆」沟通;本版不含线上退款流程。不能把已付款订单当作未付款订单取消。若后续引入商户平台退款,需配套退款对账及库存回补逻辑,不能仅修改订单状态。
|
- 售后通过详情页「联系场馆」沟通;本版不含线上退款流程。不能把已付款订单当作未付款订单取消。若后续引入商户平台退款,需配套退款对账及库存回补逻辑,不能仅修改订单状态。
|
||||||
|
|
||||||
## 现有架构与接入方式
|
## 现有架构与接入方式
|
||||||
@@ -33,9 +33,9 @@
|
|||||||
4. 客户端支付成功不直接更新订单。服务端查询微信,核对商户、应用、订单号、金额、币种、openid 和微信交易号,才能从 PENDING 更新为 PAID。
|
4. 客户端支付成功不直接更新订单。服务端查询微信,核对商户、应用、订单号、金额、币种、openid 和微信交易号,才能从 PENDING 更新为 PAID。
|
||||||
5. 商品回调独立使用 `/api/shop/wx-notify`,只接受 AES-GCM 解密后的资源,再查单确认,不依赖旧会员卡回调的验签占位实现。成功返回 HTTP 204,绕过统一 API 包装。
|
5. 商品回调独立使用 `/api/shop/wx-notify`,只接受 AES-GCM 解密后的资源,再查单确认,不依赖旧会员卡回调的验签占位实现。成功返回 HTTP 204,绕过统一 API 包装。
|
||||||
6. 定时任务每 30 秒按最近对账时间分批扫描待付款订单,支持漏回调恢复;用户也可刷新支付状态。失败会保留库存并重试。
|
6. 定时任务每 30 秒按最近对账时间分批扫描待付款订单,支持漏回调恢复;用户也可刷新支付状态。失败会保留库存并重试。
|
||||||
7. 取消订单先查单、关单,关单成功后才原子释放库存;若先查到支付成功则转为待取货。关单失败绝不提前释放。
|
7. 取消订单先查单、关单,关单成功后才原子释放库存;若先查到支付成功则转为待发货。关单失败绝不提前释放。
|
||||||
8. 微信尚无订单时,可能存在正在途中的预支付请求;过期后再等待 60 秒才释放,避免“取消释放后又支付”。因此用户从未成功调起支付时,取消可能提示等待,最晚由超时任务回收(微信服务异常期间会继续保留并重试)。
|
8. 微信尚无订单时,可能存在正在途中的预支付请求;过期后再等待 60 秒才释放,避免“取消释放后又支付”。因此用户从未成功调起支付时,取消可能提示等待,最晚由超时任务回收(微信服务异常期间会继续保留并重试)。
|
||||||
9. 状态更新带条件,重复回调、重复取消或并发扫描不会重复计算销量、重复返还库存。核销仅允许 PAID + 正确提货码,并记录操作人/时间。
|
9. 状态更新带条件,重复回调、重复取消或并发扫描不会重复计算销量、重复返还库存。发货完成仅允许已付款订单,并记录操作人/时间。
|
||||||
10. 管理员保存商品和库存均校验版本;发生竞争时提示刷新。库存调整记录最近 50 条人工调整原因及操作人;系统预占/释放由订单本身追溯。
|
10. 管理员保存商品和库存均校验版本;发生竞争时提示刷新。库存调整记录最近 50 条人工调整原因及操作人;系统预占/释放由订单本身追溯。
|
||||||
|
|
||||||
状态:`PENDING → PAID → COMPLETED`;`PENDING → CANCELLED`。商品可下架但不删除,保证历史订单有效。
|
状态:`PENDING → PAID → COMPLETED`;`PENDING → CANCELLED`。商品可下架但不删除,保证历史订单有效。
|
||||||
@@ -60,12 +60,12 @@
|
|||||||
4. 沿用微信支付配置和 COS 配置,无新增必填环境变量。确保 `API_BASE_URL` 指向服务域名、外网能访问 `/api/shop/wx-notify`,调度进程正常运行。
|
4. 沿用微信支付配置和 COS 配置,无新增必填环境变量。确保 `API_BASE_URL` 指向服务域名、外网能访问 `/api/shop/wx-notify`,调度进程正常运行。
|
||||||
5. 构建发布小程序 `pnpm build:app`;新页面仍调用既有线上 API 域名,必须先上线后端。
|
5. 构建发布小程序 `pnpm build:app`;新页面仍调用既有线上 API 域名,必须先上线后端。
|
||||||
6. 管理员创建真实商品、上传封面、补货、上架。没有硬编码测试商品写入数据库。
|
6. 管理员创建真实商品、上传封面、补货、上架。没有硬编码测试商品写入数据库。
|
||||||
7. 在微信真机执行一笔小额购买:取消支付后续付、查看服务端支付确认、提货码核销、超时关单及库存回补。退款不在本版本内。
|
7. 在微信真机执行一笔小额购买:取消支付后续付、查看服务端支付确认、管理员直接发货完成、超时关单及库存回补。退款不在本版本内。
|
||||||
|
|
||||||
## 验证与限制
|
## 验证与限制
|
||||||
|
|
||||||
- 共享包、服务端构建;uni-app 类型检查;微信小程序构建;Prisma schema validate;Git diff 空白检查。
|
- 共享包、服务端构建;uni-app 类型检查;微信小程序构建;Prisma schema validate;Git diff 空白检查。
|
||||||
- Jest 覆盖价格变更、越权归属、负数/小数/非法输入、请求幂等、最后库存竞争、支付金额及付款人核对、重复回调、关单失败、过期释放、版本冲突、核销状态和微信 GET 签名规范。
|
- Jest 覆盖价格变更、越权归属、负数/小数/非法输入、请求幂等、最后库存竞争、支付金额及付款人核对、重复回调、关单失败、过期释放、版本冲突、发货完成状态和微信 GET 签名规范。
|
||||||
- 全量 Jest 覆盖率运行:26 个套件、418 项测试通过(新增 60 项测试);并发测试基于条件更新行为模拟,不能替代真实 MySQL 多连接并发验证。
|
- 全量 Jest 覆盖率运行:26 个套件、418 项测试通过(新增 60 项测试);并发测试基于条件更新行为模拟,不能替代真实 MySQL 多连接并发验证。
|
||||||
- 用本地独立测试数据在 390px 手机尺寸 H5 预览检查列表、详情、管理汇总、编辑页面。测试图片为标注 PREVIEW 的占位插画,未上传 COS、未接触线上商品。
|
- 用本地独立测试数据在 390px 手机尺寸 H5 预览检查列表、详情、管理汇总、编辑页面。测试图片为标注 PREVIEW 的占位插画,未上传 COS、未接触线上商品。
|
||||||
- 已完成生产数据库迁移、服务端部署和线上接口冒烟验证。尚未执行真实微信扣款、COS 实际上传或微信真机验收;小程序新版仍需发布。
|
- 已完成生产数据库迁移、服务端部署和线上接口冒烟验证。尚未执行真实微信扣款、COS 实际上传或微信真机验收;小程序新版仍需发布。
|
||||||
|
|||||||
@@ -45,21 +45,9 @@
|
|||||||
<view class="divider" />
|
<view class="divider" />
|
||||||
<text class="muted">{{ order.contactName }} · {{ order.contactPhone }}</text>
|
<text class="muted">{{ order.contactName }} · {{ order.contactPhone }}</text>
|
||||||
<text class="order-no" @tap="copy(order.orderNo)">订单号 {{ order.orderNo }} ⧉</text>
|
<text class="order-no" @tap="copy(order.orderNo)">订单号 {{ order.orderNo }} ⧉</text>
|
||||||
<template v-if="order.status === 'PAID'">
|
<text v-if="order.status === 'PAID' && !admin" class="notice">
|
||||||
<view v-if="!admin" class="notice">
|
已付款,到馆即可领取。工作人员确认交付后,订单即完成。
|
||||||
到馆出示提货码
|
</text>
|
||||||
<text class="code" @tap="copy(order.pickupCode)">{{ order.pickupCode }}</text>
|
|
||||||
请在领取商品时向工作人员出示。
|
|
||||||
</view>
|
|
||||||
<view v-else class="field">
|
|
||||||
<input
|
|
||||||
v-model="codes[order.id]"
|
|
||||||
class="input"
|
|
||||||
maxlength="12"
|
|
||||||
placeholder="输入顾客的 12 位提货码"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
<text v-if="order.status === 'PENDING'" class="muted">
|
<text v-if="order.status === 'PENDING'" class="muted">
|
||||||
付款截止 {{ date(order.expiresAt) }},超时后自动取消。
|
付款截止 {{ date(order.expiresAt) }},超时后自动取消。
|
||||||
</text>
|
</text>
|
||||||
@@ -77,7 +65,7 @@
|
|||||||
:disabled="!!busy"
|
:disabled="!!busy"
|
||||||
@tap="complete(order)"
|
@tap="complete(order)"
|
||||||
>
|
>
|
||||||
确认已交付商品
|
确认发货并完成
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -105,8 +93,7 @@ const items = ref<ShopOrder[]>([]),
|
|||||||
total = ref(0),
|
total = ref(0),
|
||||||
loading = ref(false),
|
loading = ref(false),
|
||||||
error = ref(''),
|
error = ref(''),
|
||||||
busy = ref(''),
|
busy = ref('')
|
||||||
codes = ref<Record<string, string>>({})
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ value: '', label: '全部' },
|
{ value: '', label: '全部' },
|
||||||
...Object.entries(shopStatus).map(([value, label]) => ({ value, label })),
|
...Object.entries(shopStatus).map(([value, label]) => ({ value, label })),
|
||||||
@@ -199,22 +186,17 @@ async function cancel(order: ShopOrder) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function complete(order: ShopOrder) {
|
async function complete(order: ShopOrder) {
|
||||||
const code = (codes.value[order.id] || '').trim().toUpperCase()
|
|
||||||
if (!/^[A-F0-9]{12}$/.test(code)) {
|
|
||||||
shopError(new Error('请输入完整提货码'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const answer = await uni.showModal({
|
const answer = await uni.showModal({
|
||||||
title: '确认商品已交付',
|
title: '确认发货并完成',
|
||||||
content: `${order.title} × ${order.quantity},取货人 ${order.contactName}。核销后订单将完成。`,
|
content: `${order.title} × ${order.quantity},取货人 ${order.contactName}。确认后订单将完成。`,
|
||||||
})
|
})
|
||||||
if (!answer.confirm || busy.value) return
|
if (!answer.confirm || busy.value) return
|
||||||
busy.value = order.id
|
busy.value = order.id
|
||||||
try {
|
try {
|
||||||
await post(`/admin/shop/orders/${order.id}/complete`, { pickupCode: code })
|
await post(`/admin/shop/orders/${order.id}/complete`)
|
||||||
await load(true)
|
await load(true)
|
||||||
emit('changed')
|
emit('changed')
|
||||||
uni.showToast({ title: '提货完成' })
|
uni.showToast({ title: '订单已完成' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
shopError(e)
|
shopError(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<view class="row">
|
<view class="row">
|
||||||
<view>
|
<view>
|
||||||
<text class="label">上架展示</text>
|
<text class="label">上架展示</text>
|
||||||
<text class="muted">下架不影响已有订单的提货</text>
|
<text class="muted">下架不影响已付款订单</text>
|
||||||
</view>
|
</view>
|
||||||
<switch :checked="form.isActive" color="#647c6b" @change="toggleActive" />
|
<switch :checked="form.isActive" color="#647c6b" @change="toggleActive" />
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<text class="muted">{{ summary.orders }} 笔已付款 · {{ summary.quantity }} 件商品</text>
|
<text class="muted">{{ summary.orders }} 笔已付款 · {{ summary.quantity }} 件商品</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="stats">
|
<view class="stats">
|
||||||
<text>{{ summary.pendingPickup }} 待取货</text>
|
<text>{{ summary.pendingPickup }} 待发货</text>
|
||||||
<text>{{ summary.activeProducts }} 已上架</text>
|
<text>{{ summary.activeProducts }} 已上架</text>
|
||||||
<text>低库存 {{ summary.lowStock }} 款</text>
|
<text>低库存 {{ summary.lowStock }} 款</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<text class="tab" :class="{ active: tab === 'products' }" @tap="tab = 'products'">
|
<text class="tab" :class="{ active: tab === 'products' }" @tap="tab = 'products'">
|
||||||
商品与库存
|
商品与库存
|
||||||
</text>
|
</text>
|
||||||
<text class="tab" :class="{ active: tab === 'orders' }" @tap="showOrders">订单与提货</text>
|
<text class="tab" :class="{ active: tab === 'orders' }" @tap="showOrders">销售订单</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-show="tab === 'products'">
|
<view v-show="tab === 'products'">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<text class="muted">可售 {{ product.stock }} 件 · 已售 {{ product.sold }} 件</text>
|
<text class="muted">可售 {{ product.stock }} 件 · 已售 {{ product.sold }} 件</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="notice">
|
<view class="notice">
|
||||||
到馆自提 · 付款后凭订单提货码领取
|
到馆自提 · 付款后由工作人员确认交付
|
||||||
<text></text>
|
<text></text>
|
||||||
{{ studio.studioInfo?.address || '到馆领取前可联系场馆确认营业时间' }}
|
{{ studio.studioInfo?.address || '到馆领取前可联系场馆确认营业时间' }}
|
||||||
<text v-if="studio.studioInfo?.phone" @tap="call">· 联系场馆 ›</text>
|
<text v-if="studio.studioInfo?.phone" @tap="call">· 联系场馆 ›</text>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<view class="content">
|
<view class="content">
|
||||||
<text class="eyebrow">YOUR STUDIO FINDS</text>
|
<text class="eyebrow">YOUR STUDIO FINDS</text>
|
||||||
<text class="headline">把喜欢带回家</text>
|
<text class="headline">把喜欢带回家</text>
|
||||||
<text class="muted">付款后到馆领取,提货码在订单内查看。</text>
|
<text class="muted">付款后到馆领取,工作人员确认交付后订单完成。</text>
|
||||||
<ShopOrders v-if="user.loggedIn" ref="orders" />
|
<ShopOrders v-if="user.loggedIn" ref="orders" />
|
||||||
<view v-else class="state">
|
<view v-else class="state">
|
||||||
<text>登录后查看你的商品订单</text>
|
<text>登录后查看你的商品订单</text>
|
||||||
|
|||||||
@@ -156,14 +156,6 @@ button::after {
|
|||||||
color: #a09487;
|
color: #a09487;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
.code {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 38rpx;
|
|
||||||
letter-spacing: 4rpx;
|
|
||||||
color: #547060;
|
|
||||||
margin: 18rpx 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.load-more {
|
.load-more {
|
||||||
margin: 24rpx auto;
|
margin: 24rpx auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { post } from './request'
|
|||||||
export const shopMoney = (fen: number) => (fen / 100).toFixed(2)
|
export const shopMoney = (fen: number) => (fen / 100).toFixed(2)
|
||||||
export const shopStatus: Record<ShopOrderStatus, string> = {
|
export const shopStatus: Record<ShopOrderStatus, string> = {
|
||||||
PENDING: '待付款',
|
PENDING: '待付款',
|
||||||
PAID: '待到馆取货',
|
PAID: '待发货',
|
||||||
COMPLETED: '已取货',
|
COMPLETED: '已完成',
|
||||||
CANCELLED: '已取消',
|
CANCELLED: '已取消',
|
||||||
}
|
}
|
||||||
export const shopError = (error: unknown) =>
|
export const shopError = (error: unknown) =>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Paid product orders are fulfilled directly by an administrator.
|
||||||
|
-- The pickup code is no longer generated, shown, or required.
|
||||||
|
ALTER TABLE `product_orders` DROP COLUMN `pickup_code`;
|
||||||
@@ -663,7 +663,6 @@ model ProductOrder {
|
|||||||
contactName String @map("contact_name") @db.VarChar(40)
|
contactName String @map("contact_name") @db.VarChar(40)
|
||||||
contactPhone String @map("contact_phone") @db.VarChar(20)
|
contactPhone String @map("contact_phone") @db.VarChar(20)
|
||||||
status ProductOrderStatus @default(PENDING)
|
status ProductOrderStatus @default(PENDING)
|
||||||
pickupCode String @map("pickup_code") @db.VarChar(12)
|
|
||||||
wxTransactionId String? @unique @map("wx_transaction_id")
|
wxTransactionId String? @unique @map("wx_transaction_id")
|
||||||
expiresAt DateTime @map("expires_at")
|
expiresAt DateTime @map("expires_at")
|
||||||
paidAt DateTime? @map("paid_at")
|
paidAt DateTime? @map("paid_at")
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const order = () => ({
|
|||||||
contactName: '顾客',
|
contactName: '顾客',
|
||||||
contactPhone: '13800138000',
|
contactPhone: '13800138000',
|
||||||
status: 'PENDING',
|
status: 'PENDING',
|
||||||
pickupCode: 'ABCDEF123456',
|
|
||||||
expiresAt: new Date(Date.now() + 900000),
|
expiresAt: new Date(Date.now() + 900000),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -78,7 +77,6 @@ describe('ShopService inventory and payment', () => {
|
|||||||
create: jest.fn(async ({ data }) => ({ ...current, ...data })),
|
create: jest.fn(async ({ data }) => ({ ...current, ...data })),
|
||||||
updateMany: jest.fn(async ({ where, data }) => {
|
updateMany: jest.fn(async ({ where, data }) => {
|
||||||
if (where.status && where.status !== current.status) return { count: 0 }
|
if (where.status && where.status !== current.status) return { count: 0 }
|
||||||
if (where.pickupCode && where.pickupCode !== current.pickupCode) return { count: 0 }
|
|
||||||
Object.assign(current, data)
|
Object.assign(current, data)
|
||||||
return { count: 1 }
|
return { count: 1 }
|
||||||
}),
|
}),
|
||||||
@@ -246,14 +244,13 @@ describe('ShopService inventory and payment', () => {
|
|||||||
data: { productId: 'p1', delta: 2, reason: '到货', operatorId: 'admin' },
|
data: { productId: 'p1', delta: 2, reason: '到货', operatorId: 'admin' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('requires correct pickup code and paid state', async () => {
|
it('completes a paid order directly and only once', async () => {
|
||||||
await expect(service.complete('o1', current.pickupCode, 'admin')).rejects.toThrow('不可核销')
|
await expect(service.complete('o1', 'admin')).rejects.toThrow('仅已付款')
|
||||||
current.status = 'PAID'
|
current.status = 'PAID'
|
||||||
await expect(service.complete('o1', 'WRONG', 'admin')).rejects.toThrow('不可核销')
|
await service.complete('o1', 'admin')
|
||||||
await service.complete('o1', current.pickupCode, 'admin')
|
|
||||||
expect(current.status).toBe('COMPLETED')
|
expect(current.status).toBe('COMPLETED')
|
||||||
expect(current.completedBy).toBe('admin')
|
expect(current.completedBy).toBe('admin')
|
||||||
await expect(service.complete('o1', current.pickupCode, 'admin')).rejects.toThrow('不可核销')
|
await expect(service.complete('o1', 'admin')).rejects.toThrow('仅已付款')
|
||||||
})
|
})
|
||||||
it('scopes user lists and hides inactive products publicly', async () => {
|
it('scopes user lists and hides inactive products publicly', async () => {
|
||||||
await service.orders(new ShopQueryDto(), 'u1')
|
await service.orders(new ShopQueryDto(), 'u1')
|
||||||
|
|||||||
@@ -62,6 +62,3 @@ export class ShopQueryDto {
|
|||||||
@IsOptional() @IsString() @MaxLength(80) search?: string
|
@IsOptional() @IsString() @MaxLength(80) search?: string
|
||||||
@IsOptional() @IsString() @MaxLength(50) productId?: string
|
@IsOptional() @IsString() @MaxLength(50) productId?: string
|
||||||
}
|
}
|
||||||
export class PickupDto {
|
|
||||||
@IsString() @Matches(/^[A-F0-9]{12}$/) pickupCode!: string
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { RolesGuard } from '../auth/roles.guard'
|
|||||||
import { Roles } from '../auth/roles.decorator'
|
import { Roles } from '../auth/roles.decorator'
|
||||||
import { CurrentUser } from '../common/decorators/current-user.decorator'
|
import { CurrentUser } from '../common/decorators/current-user.decorator'
|
||||||
import { ShopService } from './shop.service'
|
import { ShopService } from './shop.service'
|
||||||
import { BuyProductDto, PickupDto, ProductDto, ShopQueryDto, StockDto } from './dto/shop.dto'
|
import { BuyProductDto, ProductDto, ShopQueryDto, StockDto } from './dto/shop.dto'
|
||||||
|
|
||||||
@Controller('shop')
|
@Controller('shop')
|
||||||
export class ShopController {
|
export class ShopController {
|
||||||
@@ -101,9 +101,8 @@ export class ShopAdminController {
|
|||||||
}
|
}
|
||||||
@Post('orders/:id/complete') complete(
|
@Post('orders/:id/complete') complete(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() dto: PickupDto,
|
|
||||||
@CurrentUser('sub') userId: string,
|
@CurrentUser('sub') userId: string,
|
||||||
) {
|
) {
|
||||||
return this.shop.complete(id, dto.pickupCode, userId)
|
return this.shop.complete(id, userId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '@nestjs/common'
|
} from '@nestjs/common'
|
||||||
import { Cron } from '@nestjs/schedule'
|
import { Cron } from '@nestjs/schedule'
|
||||||
import { Prisma, ProductOrder } from '@prisma/client'
|
import { Prisma, ProductOrder } from '@prisma/client'
|
||||||
import { randomBytes, randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import { PrismaService } from '../prisma/prisma.service'
|
import { PrismaService } from '../prisma/prisma.service'
|
||||||
import { WechatPayService } from '../payment/wechat-pay.service'
|
import { WechatPayService } from '../payment/wechat-pay.service'
|
||||||
import { BuyProductDto, ProductDto, ShopQueryDto, StockDto } from './dto/shop.dto'
|
import { BuyProductDto, ProductDto, ShopQueryDto, StockDto } from './dto/shop.dto'
|
||||||
@@ -139,7 +139,6 @@ export class ShopService {
|
|||||||
coverUrl: product.coverUrl,
|
coverUrl: product.coverUrl,
|
||||||
unitPrice: product.price,
|
unitPrice: product.price,
|
||||||
amount: product.price * dto.quantity,
|
amount: product.price * dto.quantity,
|
||||||
pickupCode: randomBytes(6).toString('hex').toUpperCase(),
|
|
||||||
expiresAt: new Date(Date.now() + 15 * 60 * 1000),
|
expiresAt: new Date(Date.now() + 15 * 60 * 1000),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -273,12 +272,12 @@ export class ShopService {
|
|||||||
return { items, total, page: query.page, limit: query.limit }
|
return { items, total, page: query.page, limit: query.limit }
|
||||||
}
|
}
|
||||||
|
|
||||||
async complete(id: string, pickupCode: string, operatorId: string) {
|
async complete(id: string, operatorId: string) {
|
||||||
const result = await this.prisma.productOrder.updateMany({
|
const result = await this.prisma.productOrder.updateMany({
|
||||||
where: { id, pickupCode, status: 'PAID' },
|
where: { id, status: 'PAID' },
|
||||||
data: { status: 'COMPLETED', completedAt: new Date(), completedBy: operatorId },
|
data: { status: 'COMPLETED', completedAt: new Date(), completedBy: operatorId },
|
||||||
})
|
})
|
||||||
if (!result.count) throw new BadRequestException('提货码不正确,或订单不可核销')
|
if (!result.count) throw new BadRequestException('仅已付款订单可以发货完成')
|
||||||
return this.prisma.productOrder.findUniqueOrThrow({ where: { id } })
|
return this.prisma.productOrder.findUniqueOrThrow({ where: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export interface ShopOrder {
|
|||||||
contactName: string
|
contactName: string
|
||||||
contactPhone: string
|
contactPhone: string
|
||||||
status: ShopOrderStatus
|
status: ShopOrderStatus
|
||||||
pickupCode: string
|
|
||||||
expiresAt: string
|
expiresAt: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
paidAt: string | null
|
paidAt: string | null
|
||||||
|
|||||||
Reference in New Issue
Block a user