fix(ticket): 修复订单取消时的票券回滚逻辑和加载状态控制
- 添加 orderCancelLoadingById 状态管理订单取消加载状态 - 实现 getTicketUsedQty 函数统一处理票券已使用数量字段 - 完善 rollbackUserTicketAfterOrderCancel 方法支持已使用数量回滚 - 添加防重复提交机制避免订单取消多次触发 - 更新订单修改和取消按钮禁用状态防止并发操作 - 优化票券可用数量和已使用数量的计算逻辑
This commit is contained in:
@@ -37,6 +37,7 @@ const UserTicketList = () => {
|
|||||||
const [orderHasMore, setOrderHasMore] = useState(true);
|
const [orderHasMore, setOrderHasMore] = useState(true);
|
||||||
const [orderPage, setOrderPage] = useState(1);
|
const [orderPage, setOrderPage] = useState(1);
|
||||||
const [orderTotal, setOrderTotal] = useState(0);
|
const [orderTotal, setOrderTotal] = useState(0);
|
||||||
|
const [orderCancelLoadingById, setOrderCancelLoadingById] = useState<Record<number, boolean>>({});
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'ticket' | 'order'>(() => {
|
const [activeTab, setActiveTab] = useState<'ticket' | 'order'>(() => {
|
||||||
const tab = Taro.getCurrentInstance().router?.params?.tab
|
const tab = Taro.getCurrentInstance().router?.params?.tab
|
||||||
@@ -320,40 +321,85 @@ const UserTicketList = () => {
|
|||||||
return Number.isFinite(computed) ? computed : 0;
|
return Number.isFinite(computed) ? computed : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTicketUsedQty = (t?: Partial<GltUserTicket> | null) => {
|
||||||
|
if (!t) return 0;
|
||||||
|
const anyT: any = t;
|
||||||
|
const raw = anyT.usedQty ?? anyT.usedNum ?? anyT.usedCount;
|
||||||
|
const n = Number(raw);
|
||||||
|
return Number.isFinite(n) ? n : 0;
|
||||||
|
};
|
||||||
|
|
||||||
const rollbackUserTicketAfterOrderCancel = async (order: GltTicketOrder, before?: GltUserTicket | null) => {
|
const rollbackUserTicketAfterOrderCancel = async (order: GltTicketOrder, before?: GltUserTicket | null) => {
|
||||||
|
const orderId = Number(order?.id);
|
||||||
const ticketId = Number(order?.userTicketId);
|
const ticketId = Number(order?.userTicketId);
|
||||||
const qty = Math.max(0, Math.floor(Number((order as any)?.totalNum ?? 0)));
|
const qty = Math.max(0, Math.floor(Number((order as any)?.totalNum ?? 0)));
|
||||||
|
if (!Number.isFinite(orderId) || orderId <= 0) return;
|
||||||
if (!Number.isFinite(ticketId) || ticketId <= 0) return;
|
if (!Number.isFinite(ticketId) || ticketId <= 0) return;
|
||||||
if (!Number.isFinite(qty) || qty <= 0) return;
|
if (!Number.isFinite(qty) || qty <= 0) return;
|
||||||
|
|
||||||
|
const rollbackKey = `glt_ticket_order_rollback:${orderId}`;
|
||||||
|
if (Taro.getStorageSync(rollbackKey)) return;
|
||||||
|
|
||||||
const after = await getGltUserTicket(ticketId);
|
const after = await getGltUserTicket(ticketId);
|
||||||
if (!after?.id) return;
|
if (!after?.id) return;
|
||||||
|
|
||||||
const beforeAvail = before ? getTicketAvailableQty(before) : undefined;
|
const beforeAvail = before ? getTicketAvailableQty(before) : undefined;
|
||||||
const afterAvail = getTicketAvailableQty(after);
|
const afterAvail = getTicketAvailableQty(after);
|
||||||
|
const beforeUsed = before ? getTicketUsedQty(before) : undefined;
|
||||||
|
const afterUsed = getTicketUsedQty(after);
|
||||||
|
|
||||||
let need = qty;
|
let needAvail = qty;
|
||||||
if (typeof beforeAvail === 'number') {
|
if (typeof beforeAvail === 'number') {
|
||||||
const delta = afterAvail - beforeAvail;
|
const delta = afterAvail - beforeAvail;
|
||||||
if (delta >= qty) return; // backend already rolled back
|
if (delta >= qty) {
|
||||||
if (delta > 0) need = Math.max(0, qty - delta);
|
Taro.setStorageSync(rollbackKey, Date.now());
|
||||||
|
return; // backend already rolled back
|
||||||
|
}
|
||||||
|
if (delta > 0) needAvail = Math.max(0, qty - delta);
|
||||||
|
}
|
||||||
|
let needUsed = qty;
|
||||||
|
if (typeof beforeUsed === 'number') {
|
||||||
|
const delta = beforeUsed - afterUsed;
|
||||||
|
if (delta >= qty) {
|
||||||
|
needUsed = 0; // backend already rolled back used qty
|
||||||
|
} else if (delta > 0) {
|
||||||
|
needUsed = Math.max(0, qty - delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needAvail <= 0 && needUsed <= 0) {
|
||||||
|
Taro.setStorageSync(rollbackKey, Date.now());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (need <= 0) return;
|
|
||||||
|
|
||||||
const currentAvailRaw = Number((after as any)?.availableQty);
|
const currentAvailRaw = Number((after as any)?.availableQty);
|
||||||
const baseAvail = Number.isFinite(currentAvailRaw) ? currentAvailRaw : afterAvail;
|
const baseAvail = Number.isFinite(currentAvailRaw) ? currentAvailRaw : afterAvail;
|
||||||
const nextAvail = (Number.isFinite(baseAvail) ? baseAvail : 0) + need;
|
const safeBaseAvail = Number.isFinite(baseAvail) ? baseAvail : 0;
|
||||||
|
|
||||||
|
const totalRaw = Number((after as any)?.totalQty ?? 0);
|
||||||
|
const total = Number.isFinite(totalRaw) ? totalRaw : undefined;
|
||||||
const frozenRaw = Number((after as any)?.frozenQty ?? 0);
|
const frozenRaw = Number((after as any)?.frozenQty ?? 0);
|
||||||
const frozen = Number.isFinite(frozenRaw) ? frozenRaw : 0;
|
const frozen = Number.isFinite(frozenRaw) ? frozenRaw : 0;
|
||||||
const reduceFrozen = Math.min(frozen, need);
|
|
||||||
const nextFrozen = reduceFrozen > 0 ? Math.max(0, frozen - reduceFrozen) : undefined;
|
const currentUsedRaw = Number((after as any)?.usedQty);
|
||||||
|
const baseUsed = Number.isFinite(currentUsedRaw) ? currentUsedRaw : afterUsed;
|
||||||
|
const safeBaseUsed = Number.isFinite(baseUsed) ? baseUsed : 0;
|
||||||
|
let nextUsed = safeBaseUsed - needUsed;
|
||||||
|
if (nextUsed < 0) nextUsed = 0;
|
||||||
|
|
||||||
|
const maxAvail = typeof total === 'number' ? Math.max(0, total - frozen - nextUsed) : undefined;
|
||||||
|
|
||||||
|
let nextAvail = safeBaseAvail + needAvail;
|
||||||
|
if (typeof maxAvail === 'number' && Number.isFinite(maxAvail) && nextAvail > maxAvail) nextAvail = maxAvail;
|
||||||
|
if (nextAvail < 0) nextAvail = 0;
|
||||||
|
|
||||||
await updateGltUserTicket({
|
await updateGltUserTicket({
|
||||||
...after,
|
...after,
|
||||||
availableQty: nextAvail,
|
availableQty: nextAvail,
|
||||||
...(nextFrozen !== undefined ? { frozenQty: nextFrozen } : {})
|
usedQty: nextUsed
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Taro.setStorageSync(rollbackKey, Date.now());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Allow users to modify/cancel before delivery starts (e.g. 待派单 / 待配送).
|
// Allow users to modify/cancel before delivery starts (e.g. 待派单 / 待配送).
|
||||||
@@ -391,6 +437,7 @@ const UserTicketList = () => {
|
|||||||
Taro.showToast({ title: '仅配送未开始的订单可取消', icon: 'none' });
|
Taro.showToast({ title: '仅配送未开始的订单可取消', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (orderCancelLoadingById[order.id]) return;
|
||||||
|
|
||||||
const modal = await Taro.showModal({
|
const modal = await Taro.showModal({
|
||||||
title: '取消订单',
|
title: '取消订单',
|
||||||
@@ -400,6 +447,7 @@ const UserTicketList = () => {
|
|||||||
if (!modal.confirm) return;
|
if (!modal.confirm) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setOrderCancelLoadingById((prev) => ({ ...prev, [order.id as number]: true }));
|
||||||
Taro.showLoading({ title: '取消中...' });
|
Taro.showLoading({ title: '取消中...' });
|
||||||
let beforeTicket: GltUserTicket | null = null;
|
let beforeTicket: GltUserTicket | null = null;
|
||||||
if (order.userTicketId) {
|
if (order.userTicketId) {
|
||||||
@@ -427,6 +475,7 @@ const UserTicketList = () => {
|
|||||||
Taro.showToast({ title: '取消失败,请重试', icon: 'none' });
|
Taro.showToast({ title: '取消失败,请重试', icon: 'none' });
|
||||||
} finally {
|
} finally {
|
||||||
Taro.hideLoading();
|
Taro.hideLoading();
|
||||||
|
setOrderCancelLoadingById((prev) => ({ ...prev, [order.id as number]: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -703,7 +752,10 @@ const UserTicketList = () => {
|
|||||||
<View className="mt-3 flex justify-end gap-2">
|
<View className="mt-3 flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
disabled={!isTicketOrderPendingDelivery(item)}
|
disabled={
|
||||||
|
!isTicketOrderPendingDelivery(item) ||
|
||||||
|
!!orderCancelLoadingById[item.id as number]
|
||||||
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void handleOrderModify(item);
|
void handleOrderModify(item);
|
||||||
@@ -714,7 +766,10 @@ const UserTicketList = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
disabled={!isTicketOrderPendingDelivery(item)}
|
disabled={
|
||||||
|
!isTicketOrderPendingDelivery(item) ||
|
||||||
|
!!orderCancelLoadingById[item.id as number]
|
||||||
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void handleOrderCancel(item);
|
void handleOrderCancel(item);
|
||||||
|
|||||||
Reference in New Issue
Block a user