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)); 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; buyerRemarks?: string;
// 商户备注(门店修改金额原因等) // 商户备注(门店修改金额原因等)
merchantRemarks?: string; merchantRemarks?: string;
// 线下付款支付凭证图片URL
paymentVoucher?: string;
// 排序号 // 排序号
sortNumber?: number; sortNumber?: number;
// 是否删除, 0否, 1是 // 是否删除, 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-500'></Text>
<Text className='text-sm text-gray-700'>¥{order.totalPrice || '0.00'}</Text> <Text className='text-sm text-gray-700'>¥{order.totalPrice || '0.00'}</Text>
</View> </View>
{order.reducePrice && Number(order.reducePrice) > 0 && ( {Number(order.reducePrice || 0) > 0 && (
<View className='flex justify-between'> <View className='flex justify-between'>
<Text className='text-sm text-gray-500'></Text> <Text className='text-sm text-gray-500'></Text>
<Text className='text-sm text-green-600'>-¥{order.reducePrice}</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 React, {useState, useEffect, useCallback, useRef} from 'react'
import {View, Text, Image, ScrollView, Input, Textarea} from '@tarojs/components' import {View, Text, Image, ScrollView, Input, Textarea} from '@tarojs/components'
import Taro, {useDidShow} from '@tarojs/taro' 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 type {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model'
import {TenantId} from '@/config/app' import {TenantId} from '@/config/app'
import {useNewOrderDetector} from '@/hooks/useNewOrderDetector' 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> = { const OP_LABEL: Record<OpType, string> = {
pay: '收款', confirmPay: '收款',
complete: '已完成', complete: '已完成',
editPrice: '金额', editPrice: '金额',
} }
const OP_DESC: Record<OpType, string> = { const OP_DESC: Record<OpType, string> = {
pay: '变更支付状态为已付款', confirmPay: '确认已收到线下付款,订单将进入待发货状态',
complete: '同时变更支付状态为已付款、收货状态为已收货、订单状态为已完成', complete: '同时变更支付状态为已付款、收货状态为已收货、订单状态为已完成',
editPrice: '修改订单实付金额', editPrice: '修改订单实付金额',
} }
@@ -125,8 +125,11 @@ function getOrderActions(order: ShopOrder): { label: string; type: OpType }[] {
const actions: { label: string; type: OpType }[] = [] const actions: { label: string; type: OpType }[] = []
// 修改金额(门店权限) // 修改金额(门店权限)
actions.push({label: '修改金额', type: 'editPrice'}) 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) { if (order.orderStatus !== 1 && order.orderStatus !== 2) {
actions.push({label: '确认完成', type: 'complete'}) actions.push({label: '确认完成', type: 'complete'})
} }
@@ -148,6 +151,7 @@ export default function StoreOrdersPage() {
const [currentOrder, setCurrentOrder] = useState<ShopOrder | null>(null) const [currentOrder, setCurrentOrder] = useState<ShopOrder | null>(null)
const [opType, setOpType] = useState<OpType>('pay') const [opType, setOpType] = useState<OpType>('pay')
const [proofImages, setProofImages] = useState<string[]>([]) const [proofImages, setProofImages] = useState<string[]>([])
const [payRemarks, setPayRemarks] = useState('')
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
// 修改金额弹窗状态 // 修改金额弹窗状态
@@ -266,6 +270,7 @@ export default function StoreOrdersPage() {
setCurrentOrder(order) setCurrentOrder(order)
setOpType(type) setOpType(type)
setProofImages([]) setProofImages([])
setPayRemarks('')
setShowModal(true) setShowModal(true)
} }
@@ -274,6 +279,7 @@ export default function StoreOrdersPage() {
setShowModal(false) setShowModal(false)
setCurrentOrder(null) setCurrentOrder(null)
setProofImages([]) setProofImages([])
setPayRemarks('')
} }
/** 打开修改金额弹窗 */ /** 打开修改金额弹窗 */
@@ -329,7 +335,7 @@ export default function StoreOrdersPage() {
/** 选择并上传凭证图片 */ /** 选择并上传凭证图片 */
const chooseProofImage = async () => { const chooseProofImage = async () => {
const maxCount = 3 const maxCount = opType === 'confirmPay' ? 1 : 3
const remaining = maxCount - proofImages.length const remaining = maxCount - proofImages.length
if (remaining <= 0) return if (remaining <= 0) return
@@ -375,15 +381,25 @@ export default function StoreOrdersPage() {
return return
} }
// 确认收款必须上传支付凭证
if (opType === 'confirmPay' && proofImages.length === 0) {
Taro.showToast({title: '请上传支付凭证', icon: 'none'})
return
}
setSubmitting(true) setSubmitting(true)
try { try {
if (opType === 'confirmPay') {
// 确认线下收款:调用专用接口
await confirmOfflinePayment(
currentOrder.orderId!,
payRemarks.trim() || undefined,
proofImages[0]
)
} else {
const updateData: any = {orderId: currentOrder.orderId} const updateData: any = {orderId: currentOrder.orderId}
if (opType === 'pay') { if (opType === 'complete') {
// 确认收款:变更支付状态为已付款
updateData.payStatus = true
updateData.payTime = formatDateTime(new Date())
} else if (opType === 'complete') {
// 确认完成:用户已收到货 // 确认完成:用户已收到货
updateData.payStatus = true updateData.payStatus = true
updateData.payTime = formatDateTime(new Date()) updateData.payTime = formatDateTime(new Date())
@@ -393,13 +409,9 @@ export default function StoreOrdersPage() {
updateData.sendEndImg = JSON.stringify(proofImages) // 配送员送达拍照JSON数组 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'}) Taro.showToast({title: '操作成功', icon: 'success'})
closeModal() closeModal()
loadOrders(activeTab, 1) // 刷新列表 loadOrders(activeTab, 1) // 刷新列表
@@ -410,23 +422,23 @@ export default function StoreOrdersPage() {
} }
} }
/** 删除订单 */ /** 关闭订单 */
const handleDelete = (order: ShopOrder) => { const handleCloseOrder = (order: ShopOrder) => {
Taro.showModal({ Taro.showModal({
title: '提示', title: '提示',
content: `确定要删除订单 ${order.orderNo} 吗?删除后无法恢复。`, content: `确定要关闭订单 ${order.orderNo} 吗?关闭后无法恢复。`,
confirmColor: '#ee0a24', confirmColor: '#ee0a24',
success: async (res) => { success: async (res) => {
if (!res.confirm) return if (!res.confirm) return
try { try {
Taro.showLoading({title: '删除中...'}) Taro.showLoading({title: '关闭中...'})
await removeShopOrder(order.orderId) await updateShopOrder({orderId: order.orderId, orderStatus: 2} as ShopOrder)
Taro.hideLoading() Taro.hideLoading()
Taro.showToast({title: '删除成功', icon: 'success'}) Taro.showToast({title: '关闭成功', icon: 'success'})
loadOrders(activeTab, 1) loadOrders(activeTab, 1)
} catch (e: any) { } catch (e: any) {
Taro.hideLoading() 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 renderOrderCard = (order: ShopOrder) => {
const actions = getOrderActions(order) const actions = getOrderActions(order)
const canDelete = isOrderActionable(order) // 仅未完成、未关闭的订单可删除 const canDelete = isOrderActionable(order) // 仅未完成、未关闭的订单可关闭
const orderGoods = (order as any).orderGoods || [] const orderGoods = (order as any).orderGoods || []
return ( return (
@@ -522,13 +534,13 @@ export default function StoreOrdersPage() {
{/* 操作按钮 */} {/* 操作按钮 */}
<View className='flex justify-between items-center border-t border-gray-50 pt-3'> <View className='flex justify-between items-center border-t border-gray-50 pt-3'>
{/* 删除按钮:仅未完成/未关闭的订单显示 */} {/* 关闭按钮:仅未完成/未关闭的订单显示 */}
{canDelete ? ( {canDelete ? (
<View <View
className='px-3 py-1.5' 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>
) : ( ) : (
<View/> <View/>
@@ -541,6 +553,8 @@ export default function StoreOrdersPage() {
className={`px-4 py-2 rounded-lg ${ className={`px-4 py-2 rounded-lg ${
act.type === 'complete' act.type === 'complete'
? 'bg-green-500' ? 'bg-green-500'
: act.type === 'confirmPay'
? 'bg-blue-500'
: act.type === 'editPrice' : act.type === 'editPrice'
? 'bg-orange-500' ? 'bg-orange-500'
: 'border border-blue-500' : 'border border-blue-500'
@@ -548,7 +562,7 @@ export default function StoreOrdersPage() {
onClick={() => act.type === 'editPrice' ? openEditPriceModal(order) : openModal(order, act.type)} onClick={() => act.type === 'editPrice' ? openEditPriceModal(order) : openModal(order, act.type)}
> >
<Text className={`text-sm ${ <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> }`}>{act.label}</Text>
</View> </View>
))} ))}
@@ -652,9 +666,13 @@ export default function StoreOrdersPage() {
{/* 凭证上传 */} {/* 凭证上传 */}
<Text className='text-sm text-gray-600 mb-3 block'> <Text className='text-sm text-gray-600 mb-3 block'>
{opType === 'complete' ? '必填配送货物到达后拍照上传最多3张' : '选填最多3张'} {opType === 'confirmPay'
? '上传支付凭证(必填,如微信转账截图)'
: opType === 'complete'
? '上传凭证照片必填配送货物到达后拍照上传最多3张'
: '上传凭证照片选填最多3张'}
</Text> </Text>
<View className='flex flex-wrap gap-3 mb-6'> <View className='flex flex-wrap gap-3 mb-4'>
{proofImages.map((url, idx) => ( {proofImages.map((url, idx) => (
<View key={idx} className='relative'> <View key={idx} className='relative'>
<Image className='w-20 h-20 rounded-lg bg-gray-50' src={getCompressedImageUrl(url)} mode='aspectFill'/> <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>
</View> </View>
))} ))}
{proofImages.length < 3 && ( {proofImages.length < (opType === 'confirmPay' ? 1 : 3) && (
<View <View
className='w-20 h-20 rounded-lg bg-gray-50 border-2 border-dashed border-gray-200 flex items-center justify-center' className='w-20 h-20 rounded-lg bg-gray-50 border-2 border-dashed border-gray-200 flex items-center justify-center'
onClick={chooseProofImage} onClick={chooseProofImage}
@@ -676,6 +694,24 @@ export default function StoreOrdersPage() {
)} )}
</View> </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 gap-3'>
<View className='flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick={closeModal}> <View className='flex-1 py-3 rounded-xl bg-gray-100 text-center' onClick={closeModal}>