feat(store): 新增线下付款确认收款功能并改为关闭订单

- 新增接口 confirmOfflinePayment 支持门店确认线下付款收款
- 修改订单操作类型,增加“确认收款”按钮供线下付款未付款订单使用
- 确认收款弹窗新增备注输入框和支付凭证上传(必填,单张)
- 调整提交逻辑,确认收款操作调用新接口,并校验凭证上传
- 订单列表页“删除订单”改为“关闭订单”,调用更新接口逻辑关闭订单
- 修改订单详情页金额明细渲染逻辑,修复0时错误显示问题
This commit is contained in:
2026-07-17 00:38:57 +08:00
parent d8e0563b9d
commit 22fed3b163
5 changed files with 132 additions and 43 deletions

View 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 && ...` 短路求值为 0React 渲染文本"0"
- **修复**:改为 `{Number(order.reducePrice || 0) > 0 && (...)}`,始终返回 boolean
### 验证
- `npx taro build --type weapp` 构建成功

View File

@@ -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));
}

View File

@@ -183,6 +183,8 @@ export interface ShopOrder {
buyerRemarks?: string;
// 商户备注(门店修改金额原因等)
merchantRemarks?: string;
// 线下付款支付凭证图片URL
paymentVoucher?: string;
// 排序号
sortNumber?: number;
// 是否删除, 0否, 1是

View File

@@ -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>

View File

@@ -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}>