feat(store): 新增线下付款确认收款功能并改为关闭订单
- 新增接口 confirmOfflinePayment 支持门店确认线下付款收款 - 修改订单操作类型,增加“确认收款”按钮供线下付款未付款订单使用 - 确认收款弹窗新增备注输入框和支付凭证上传(必填,单张) - 调整提交逻辑,确认收款操作调用新接口,并校验凭证上传 - 订单列表页“删除订单”改为“关闭订单”,调用更新接口逻辑关闭订单 - 修改订单详情页金额明细渲染逻辑,修复0时错误显示问题
This commit is contained in:
31
.workbuddy/memory/2026-07-17.md
Normal file
31
.workbuddy/memory/2026-07-17.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 2026-07-17 工作日志
|
||||
|
||||
## 门店订单管理页改造(src/pages/store/orders/index.tsx)
|
||||
|
||||
### 1. 删除订单改为关闭订单
|
||||
- 原"删除订单"按钮调用 `removeShopOrder`(物理删除),改为"关闭订单"调用 `updateShopOrder({ orderId, orderStatus: 2 })`(逻辑关闭)
|
||||
- 确认弹窗文案从"删除后无法恢复"改为"关闭后无法恢复"
|
||||
- 移除 `removeShopOrder` 导入,新增 `confirmOfflinePayment` 导入
|
||||
|
||||
### 2. 待付款订单加确认收款按钮
|
||||
- **背景**:线下付款(payType=9)且未付款的订单,门店需要"确认收款"功能,与后台管理(guilixu-admin)对齐
|
||||
- **后端接口**:`PUT /shop/shop-order/confirm-offline-payment/{id}?remarks=xxx&paymentVoucher=xxx`
|
||||
- 后端 `ShopOrderController.confirmOfflinePayment()` → `ShopOrderServiceImpl.confirmOfflinePayment()`
|
||||
- 校验:payType必须为9(线下付款)、orderStatus不能为2(已关闭)、不能重复确认
|
||||
- 确认后设置 payStatus=true、payTime=now(),可选保存 remarks(merchantRemarks) 和 paymentVoucher
|
||||
- **前端API**:在 `src/api/shop/shopOrder/index.ts` 新增 `confirmOfflinePayment(id, remarks?, paymentVoucher?)` 函数
|
||||
- 注意:Taro 的 `request.put` 不支持 `params` 参数,需手动拼接 query string
|
||||
- **ShopOrder Model**:新增 `paymentVoucher?: string` 字段
|
||||
- **页面改动**:
|
||||
- OpType 从 `'pay'|'complete'|'editPrice'` 改为 `'confirmPay'|'complete'|'editPrice'`
|
||||
- `getOrderActions` 新增条件:`!payStatus && payType===9 && orderStatus===0` → 显示"确认收款"按钮
|
||||
- 弹窗新增备注 Textarea(仅 confirmPay 显示),凭证图片限制1张(confirmPay),必填校验
|
||||
- `submitOperation` 新增 confirmPay 分支,调用 `confirmOfflinePayment` API
|
||||
- 新增 `payRemarks` state
|
||||
|
||||
### 3. 订单详情页金额明细0隐藏(src/pages/order/detail.tsx)
|
||||
- **Bug**:React 经典陷阱 `{order.reducePrice && Number(order.reducePrice) > 0 && (...)}`,当 reducePrice 为数字 0 时,`0 && ...` 短路求值为 0,React 渲染文本"0"
|
||||
- **修复**:改为 `{Number(order.reducePrice || 0) > 0 && (...)}`,始终返回 boolean
|
||||
|
||||
### 验证
|
||||
- `npx taro build --type weapp` 构建成功
|
||||
@@ -194,3 +194,23 @@ export async function refundShopOrder(data: ShopOrder) {
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认线下付款收款
|
||||
* 商家确认已收到线下付款(微信转账/银行汇款等),确认后订单进入待发货状态
|
||||
*/
|
||||
export async function confirmOfflinePayment(
|
||||
id: number,
|
||||
remarks?: string,
|
||||
paymentVoucher?: string
|
||||
) {
|
||||
const params: string[] = []
|
||||
if (remarks) params.push(`remarks=${encodeURIComponent(remarks)}`)
|
||||
if (paymentVoucher) params.push(`paymentVoucher=${encodeURIComponent(paymentVoucher)}`)
|
||||
const url = '/shop/shop-order/confirm-offline-payment/' + id + (params.length ? '?' + params.join('&') : '')
|
||||
const res = await request.put<ApiResult<unknown>>(url, null)
|
||||
if (res.code === 0) {
|
||||
return res.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.message));
|
||||
}
|
||||
|
||||
@@ -183,6 +183,8 @@ export interface ShopOrder {
|
||||
buyerRemarks?: string;
|
||||
// 商户备注(门店修改金额原因等)
|
||||
merchantRemarks?: string;
|
||||
// 线下付款支付凭证(图片URL)
|
||||
paymentVoucher?: string;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
// 是否删除, 0否, 1是
|
||||
|
||||
@@ -349,7 +349,7 @@ const OrderDetailPage: React.FC = () => {
|
||||
<Text className='text-sm text-gray-500'>商品总额</Text>
|
||||
<Text className='text-sm text-gray-700'>¥{order.totalPrice || '0.00'}</Text>
|
||||
</View>
|
||||
{order.reducePrice && Number(order.reducePrice) > 0 && (
|
||||
{Number(order.reducePrice || 0) > 0 && (
|
||||
<View className='flex justify-between'>
|
||||
<Text className='text-sm text-gray-500'>优惠</Text>
|
||||
<Text className='text-sm text-green-600'>-¥{order.reducePrice}</Text>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {useState, useEffect, useCallback, useRef} from 'react'
|
||||
import {View, Text, Image, ScrollView, Input, Textarea} from '@tarojs/components'
|
||||
import Taro, {useDidShow} from '@tarojs/taro'
|
||||
import {pageShopOrder, updateShopOrder, removeShopOrder} from '@/api/shop/shopOrder'
|
||||
import {pageShopOrder, updateShopOrder, confirmOfflinePayment} from '@/api/shop/shopOrder'
|
||||
import type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model'
|
||||
import {TenantId} from '@/config/app'
|
||||
import {useNewOrderDetector} from '@/hooks/useNewOrderDetector'
|
||||
@@ -26,16 +26,16 @@ const TABS: { key: TabKey; label: string; params: Partial<ShopOrderParam>[] }[]
|
||||
]
|
||||
|
||||
// ─── 操作类型 ─────────────────────────────────────────────────────
|
||||
type OpType = 'pay' | 'complete' | 'editPrice'
|
||||
type OpType = 'confirmPay' | 'complete' | 'editPrice'
|
||||
|
||||
const OP_LABEL: Record<OpType, string> = {
|
||||
pay: '已收款',
|
||||
confirmPay: '收款',
|
||||
complete: '已完成',
|
||||
editPrice: '金额',
|
||||
}
|
||||
|
||||
const OP_DESC: Record<OpType, string> = {
|
||||
pay: '变更支付状态为已付款',
|
||||
confirmPay: '确认已收到线下付款,订单将进入待发货状态',
|
||||
complete: '同时变更支付状态为已付款、收货状态为已收货、订单状态为已完成',
|
||||
editPrice: '修改订单实付金额',
|
||||
}
|
||||
@@ -125,8 +125,11 @@ function getOrderActions(order: ShopOrder): { label: string; type: OpType }[] {
|
||||
const actions: { label: string; type: OpType }[] = []
|
||||
// 修改金额(门店权限)
|
||||
actions.push({label: '修改金额', type: 'editPrice'})
|
||||
// 货到付款:未付款 / 已付款 都直接"确认完成",
|
||||
// 确认完成会同时设置 payStatus=true,无需单独的"确认收款"按钮
|
||||
// 线下付款·待确认收款:显示"确认收款"按钮
|
||||
if (!order.payStatus && order.payType === 9 && order.orderStatus === 0) {
|
||||
actions.push({label: '确认收款', type: 'confirmPay'})
|
||||
}
|
||||
// 确认完成
|
||||
if (order.orderStatus !== 1 && order.orderStatus !== 2) {
|
||||
actions.push({label: '确认完成', type: 'complete'})
|
||||
}
|
||||
@@ -148,6 +151,7 @@ export default function StoreOrdersPage() {
|
||||
const [currentOrder, setCurrentOrder] = useState<ShopOrder | null>(null)
|
||||
const [opType, setOpType] = useState<OpType>('pay')
|
||||
const [proofImages, setProofImages] = useState<string[]>([])
|
||||
const [payRemarks, setPayRemarks] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// 修改金额弹窗状态
|
||||
@@ -266,6 +270,7 @@ export default function StoreOrdersPage() {
|
||||
setCurrentOrder(order)
|
||||
setOpType(type)
|
||||
setProofImages([])
|
||||
setPayRemarks('')
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
@@ -274,6 +279,7 @@ export default function StoreOrdersPage() {
|
||||
setShowModal(false)
|
||||
setCurrentOrder(null)
|
||||
setProofImages([])
|
||||
setPayRemarks('')
|
||||
}
|
||||
|
||||
/** 打开修改金额弹窗 */
|
||||
@@ -329,7 +335,7 @@ export default function StoreOrdersPage() {
|
||||
|
||||
/** 选择并上传凭证图片 */
|
||||
const chooseProofImage = async () => {
|
||||
const maxCount = 3
|
||||
const maxCount = opType === 'confirmPay' ? 1 : 3
|
||||
const remaining = maxCount - proofImages.length
|
||||
if (remaining <= 0) return
|
||||
|
||||
@@ -375,31 +381,37 @@ export default function StoreOrdersPage() {
|
||||
return
|
||||
}
|
||||
|
||||
// 确认收款必须上传支付凭证
|
||||
if (opType === 'confirmPay' && proofImages.length === 0) {
|
||||
Taro.showToast({title: '请上传支付凭证', icon: 'none'})
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const updateData: any = {orderId: currentOrder.orderId}
|
||||
if (opType === 'confirmPay') {
|
||||
// 确认线下收款:调用专用接口
|
||||
await confirmOfflinePayment(
|
||||
currentOrder.orderId!,
|
||||
payRemarks.trim() || undefined,
|
||||
proofImages[0]
|
||||
)
|
||||
} else {
|
||||
const updateData: any = {orderId: currentOrder.orderId}
|
||||
|
||||
if (opType === 'pay') {
|
||||
// 确认收款:变更支付状态为已付款
|
||||
updateData.payStatus = true
|
||||
updateData.payTime = formatDateTime(new Date())
|
||||
} else if (opType === 'complete') {
|
||||
// 确认完成:用户已收到货
|
||||
updateData.payStatus = true
|
||||
updateData.payTime = formatDateTime(new Date())
|
||||
updateData.deliveryStatus = 20 // 发货状态 → 已收货
|
||||
updateData.deliveryTime = formatDateTime(new Date()) // 收货时间
|
||||
updateData.orderStatus = 1 // 订单状态 → 已完成
|
||||
updateData.sendEndImg = JSON.stringify(proofImages) // 配送员送达拍照(JSON数组)
|
||||
if (opType === 'complete') {
|
||||
// 确认完成:用户已收到货
|
||||
updateData.payStatus = true
|
||||
updateData.payTime = formatDateTime(new Date())
|
||||
updateData.deliveryStatus = 20 // 发货状态 → 已收货
|
||||
updateData.deliveryTime = formatDateTime(new Date()) // 收货时间
|
||||
updateData.orderStatus = 1 // 订单状态 → 已完成
|
||||
updateData.sendEndImg = JSON.stringify(proofImages) // 配送员送达拍照(JSON数组)
|
||||
}
|
||||
|
||||
await updateShopOrder(updateData)
|
||||
}
|
||||
|
||||
// 收款操作如有凭证也追加到备注
|
||||
if (opType === 'pay' && proofImages.length > 0) {
|
||||
const proofText = `【门店收款凭证】:${JSON.stringify(proofImages)}`
|
||||
updateData.comments = (currentOrder.comments || '') + `\n${proofText}`
|
||||
}
|
||||
|
||||
await updateShopOrder(updateData)
|
||||
Taro.showToast({title: '操作成功', icon: 'success'})
|
||||
closeModal()
|
||||
loadOrders(activeTab, 1) // 刷新列表
|
||||
@@ -410,23 +422,23 @@ export default function StoreOrdersPage() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除订单 */
|
||||
const handleDelete = (order: ShopOrder) => {
|
||||
/** 关闭订单 */
|
||||
const handleCloseOrder = (order: ShopOrder) => {
|
||||
Taro.showModal({
|
||||
title: '提示',
|
||||
content: `确定要删除订单 ${order.orderNo} 吗?删除后无法恢复。`,
|
||||
content: `确定要关闭订单 ${order.orderNo} 吗?关闭后无法恢复。`,
|
||||
confirmColor: '#ee0a24',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
Taro.showLoading({title: '删除中...'})
|
||||
await removeShopOrder(order.orderId)
|
||||
Taro.showLoading({title: '关闭中...'})
|
||||
await updateShopOrder({orderId: order.orderId, orderStatus: 2} as ShopOrder)
|
||||
Taro.hideLoading()
|
||||
Taro.showToast({title: '删除成功', icon: 'success'})
|
||||
Taro.showToast({title: '关闭成功', icon: 'success'})
|
||||
loadOrders(activeTab, 1)
|
||||
} catch (e: any) {
|
||||
Taro.hideLoading()
|
||||
Taro.showToast({title: e.message || '删除失败', icon: 'none'})
|
||||
Taro.showToast({title: e.message || '关闭失败', icon: 'none'})
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -435,7 +447,7 @@ export default function StoreOrdersPage() {
|
||||
/** 渲染单个订单卡片 */
|
||||
const renderOrderCard = (order: ShopOrder) => {
|
||||
const actions = getOrderActions(order)
|
||||
const canDelete = isOrderActionable(order) // 仅未完成、未关闭的订单可删除
|
||||
const canDelete = isOrderActionable(order) // 仅未完成、未关闭的订单可关闭
|
||||
const orderGoods = (order as any).orderGoods || []
|
||||
|
||||
return (
|
||||
@@ -522,13 +534,13 @@ export default function StoreOrdersPage() {
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className='flex justify-between items-center border-t border-gray-50 pt-3'>
|
||||
{/* 删除按钮:仅未完成/未关闭的订单显示 */}
|
||||
{/* 关闭按钮:仅未完成/未关闭的订单显示 */}
|
||||
{canDelete ? (
|
||||
<View
|
||||
className='px-3 py-1.5'
|
||||
onClick={() => handleDelete(order)}
|
||||
onClick={() => handleCloseOrder(order)}
|
||||
>
|
||||
<Text className='text-xs text-gray-400'>删除订单</Text>
|
||||
<Text className='text-xs text-gray-400'>关闭订单</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View/>
|
||||
@@ -541,6 +553,8 @@ export default function StoreOrdersPage() {
|
||||
className={`px-4 py-2 rounded-lg ${
|
||||
act.type === 'complete'
|
||||
? 'bg-green-500'
|
||||
: act.type === 'confirmPay'
|
||||
? 'bg-blue-500'
|
||||
: act.type === 'editPrice'
|
||||
? 'bg-orange-500'
|
||||
: 'border border-blue-500'
|
||||
@@ -548,7 +562,7 @@ export default function StoreOrdersPage() {
|
||||
onClick={() => act.type === 'editPrice' ? openEditPriceModal(order) : openModal(order, act.type)}
|
||||
>
|
||||
<Text className={`text-sm ${
|
||||
act.type === 'complete' || act.type === 'editPrice' ? 'text-white' : 'text-blue-500'
|
||||
act.type === 'complete' || act.type === 'editPrice' || act.type === 'confirmPay' ? 'text-white' : 'text-blue-500'
|
||||
}`}>{act.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
@@ -652,9 +666,13 @@ export default function StoreOrdersPage() {
|
||||
|
||||
{/* 凭证上传 */}
|
||||
<Text className='text-sm text-gray-600 mb-3 block'>
|
||||
上传凭证照片{opType === 'complete' ? '(必填,配送货物到达后拍照上传,最多3张)' : '(选填,最多3张)'}
|
||||
{opType === 'confirmPay'
|
||||
? '上传支付凭证(必填,如微信转账截图)'
|
||||
: opType === 'complete'
|
||||
? '上传凭证照片(必填,配送货物到达后拍照上传,最多3张)'
|
||||
: '上传凭证照片(选填,最多3张)'}
|
||||
</Text>
|
||||
<View className='flex flex-wrap gap-3 mb-6'>
|
||||
<View className='flex flex-wrap gap-3 mb-4'>
|
||||
{proofImages.map((url, idx) => (
|
||||
<View key={idx} className='relative'>
|
||||
<Image className='w-20 h-20 rounded-lg bg-gray-50' src={getCompressedImageUrl(url)} mode='aspectFill'/>
|
||||
@@ -666,7 +684,7 @@ export default function StoreOrdersPage() {
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
{proofImages.length < 3 && (
|
||||
{proofImages.length < (opType === 'confirmPay' ? 1 : 3) && (
|
||||
<View
|
||||
className='w-20 h-20 rounded-lg bg-gray-50 border-2 border-dashed border-gray-200 flex items-center justify-center'
|
||||
onClick={chooseProofImage}
|
||||
@@ -676,6 +694,24 @@ export default function StoreOrdersPage() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 备注(仅确认收款时显示) */}
|
||||
{opType === 'confirmPay' && (
|
||||
<View className='mb-6'>
|
||||
<Text className='text-sm text-gray-600 mb-2 block'>备注(选填)</Text>
|
||||
<Textarea
|
||||
className='w-full px-4 py-3 rounded-xl border border-gray-200 text-sm text-gray-800'
|
||||
style={{minHeight: '60px'}}
|
||||
value={payRemarks}
|
||||
onInput={(e) => setPayRemarks(e.detail.value)}
|
||||
placeholder='可填写备注(如:微信转账已收到)'
|
||||
maxlength={200}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 确认完成不需要备注时补充间距 */}
|
||||
{opType !== 'confirmPay' && <View className='mb-2'/>}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<View className='flex gap-3'>
|
||||
<View className='flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick={closeModal}>
|
||||
|
||||
Reference in New Issue
Block a user