fix(ticket): 修复订单取消时的票券回滚逻辑和加载状态控制

- 添加 orderCancelLoadingById 状态管理订单取消加载状态
- 实现 getTicketUsedQty 函数统一处理票券已使用数量字段
- 完善 rollbackUserTicketAfterOrderCancel 方法支持已使用数量回滚
- 添加防重复提交机制避免订单取消多次触发
- 更新订单修改和取消按钮禁用状态防止并发操作
- 优化票券可用数量和已使用数量的计算逻辑
This commit is contained in:
2026-03-10 17:18:18 +08:00
parent cc58bd791d
commit e7caee08c1

View File

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