feat(ops): partner proxy order list and HQ order delete
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Add partner-facing proxy order list/detail; unlock SUPER_ADMIN order delete with cascade cleanup and type filter. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
@@ -175,6 +176,7 @@ export default function OrdersPage() {
|
|||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||||
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false);
|
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false);
|
||||||
const [batchDeleting, setBatchDeleting] = useState(false);
|
const [batchDeleting, setBatchDeleting] = useState(false);
|
||||||
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
const [shipDefaults, setShipDefaults] = useState<ShipDefaults | null>(null);
|
const [shipDefaults, setShipDefaults] = useState<ShipDefaults | null>(null);
|
||||||
const [shipping, setShipping] = useState(false);
|
const [shipping, setShipping] = useState(false);
|
||||||
const [logisticsShipping, setLogisticsShipping] = useState(false);
|
const [logisticsShipping, setLogisticsShipping] = useState(false);
|
||||||
@@ -217,6 +219,7 @@ export default function OrdersPage() {
|
|||||||
const qs = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
|
const qs = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
|
||||||
if (values.orderNo) qs.set('orderNo', values.orderNo);
|
if (values.orderNo) qs.set('orderNo', values.orderNo);
|
||||||
if (values.status) qs.set('status', values.status);
|
if (values.status) qs.set('status', values.status);
|
||||||
|
if (values.orderType) qs.set('orderType', values.orderType);
|
||||||
if (values.cityId) qs.set('cityId', values.cityId);
|
if (values.cityId) qs.set('cityId', values.cityId);
|
||||||
if (values.receiverPhone) qs.set('receiverPhone', values.receiverPhone);
|
if (values.receiverPhone) qs.set('receiverPhone', values.receiverPhone);
|
||||||
if (values.fulfillmentHold) qs.set('fulfillmentHold', 'true');
|
if (values.fulfillmentHold) qs.set('fulfillmentHold', 'true');
|
||||||
@@ -389,6 +392,43 @@ export default function OrdersPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function askDeleteOrder(id: string, orderNo?: string) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除订单?',
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<p>将永久删除订单{orderNo ? ` ${orderNo}` : ''} 及其配送、权益券、核销、发票等关联数据。</p>
|
||||||
|
<p style={{ color: 'var(--ant-color-error)', marginBottom: 0 }}>此操作不可恢复,请确认不是真实线上订单。</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
okText: '确认删除',
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => deleteOrder(id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOrder(id: string) {
|
||||||
|
setDeletingId(id);
|
||||||
|
try {
|
||||||
|
const res = await request<{ deleted: number; message: string }>(`/admin/orders/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
message.success(res.message || '订单已删除');
|
||||||
|
if (detail?.id === id) {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
setDetail(null);
|
||||||
|
}
|
||||||
|
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
|
||||||
|
void load();
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e instanceof Error ? e.message : '删除失败');
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
setDeletingId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<AdminOrderRow> = [
|
const columns: ColumnsType<AdminOrderRow> = [
|
||||||
{ title: '订单号', dataIndex: 'orderNo', width: 180 },
|
{ title: '订单号', dataIndex: 'orderNo', width: 180 },
|
||||||
{
|
{
|
||||||
@@ -455,7 +495,7 @@ export default function OrdersPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 140,
|
width: 180,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space size={0}>
|
<Space size={0}>
|
||||||
@@ -467,6 +507,17 @@ export default function OrdersPage() {
|
|||||||
配送
|
配送
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{canDeleteOrders && (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
loading={deletingId === row.id}
|
||||||
|
onClick={() => askDeleteOrder(row.id, row.orderNo)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -493,6 +544,21 @@ export default function OrdersPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
|
{!canDeleteOrders && profile ? (
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
message="当前账号无「删除订单」权限"
|
||||||
|
description={
|
||||||
|
<span>
|
||||||
|
非超管账号需在 <Link to="/hq-permissions">权限分配</Link> 中勾选危险操作「删除订单」。
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
|
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
|
||||||
<Form.Item name="orderNo" label="订单号">
|
<Form.Item name="orderNo" label="订单号">
|
||||||
<Input placeholder="DK..." allowClear />
|
<Input placeholder="DK..." allowClear />
|
||||||
@@ -500,6 +566,17 @@ export default function OrdersPage() {
|
|||||||
<Form.Item name="status" label="状态">
|
<Form.Item name="status" label="状态">
|
||||||
<Select allowClear style={{ width: 120 }} options={Object.entries(ORDER_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
<Select allowClear style={{ width: 120 }} options={Object.entries(ORDER_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="orderType" label="类型">
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
style={{ width: 120 }}
|
||||||
|
placeholder="全部"
|
||||||
|
options={[
|
||||||
|
{ value: 'NORMAL', label: '普通订单' },
|
||||||
|
{ value: 'PROXY', label: '代下单' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item name="cityId" label="城市">
|
<Form.Item name="cityId" label="城市">
|
||||||
<Select
|
<Select
|
||||||
allowClear
|
allowClear
|
||||||
@@ -563,6 +640,15 @@ export default function OrdersPage() {
|
|||||||
配送发货
|
配送发货
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{canDeleteOrders && (
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
loading={deletingId === detail.id}
|
||||||
|
onClick={() => askDeleteOrder(detail.id, detail.orderNo)}
|
||||||
|
>
|
||||||
|
删除订单
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Typography.Text type="secondary">调试改状态</Typography.Text>
|
<Typography.Text type="secondary">调试改状态</Typography.Text>
|
||||||
<Select
|
<Select
|
||||||
value={detail.status}
|
value={detail.status}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import SettlementPage from './pages/SettlementPage';
|
|||||||
import WeeklyReportPage from './pages/WeeklyReportPage';
|
import WeeklyReportPage from './pages/WeeklyReportPage';
|
||||||
import ProxyOrderPage from './pages/ProxyOrderPage';
|
import ProxyOrderPage from './pages/ProxyOrderPage';
|
||||||
import ProxyOrderProductsPage from './pages/ProxyOrderProductsPage';
|
import ProxyOrderProductsPage from './pages/ProxyOrderProductsPage';
|
||||||
|
import ProxyOrderListPage from './pages/ProxyOrderListPage';
|
||||||
|
import ProxyOrderDetailPage from './pages/ProxyOrderDetailPage';
|
||||||
import StaffListPage from './pages/StaffListPage';
|
import StaffListPage from './pages/StaffListPage';
|
||||||
import StaffCreatePage from './pages/StaffCreatePage';
|
import StaffCreatePage from './pages/StaffCreatePage';
|
||||||
import LeaderboardPage from './pages/LeaderboardPage';
|
import LeaderboardPage from './pages/LeaderboardPage';
|
||||||
@@ -33,6 +35,8 @@ function PrimaryRoutes() {
|
|||||||
<Route path="/center/settlement" element={<SettlementPage />} />
|
<Route path="/center/settlement" element={<SettlementPage />} />
|
||||||
<Route path="/center/staff" element={<StaffListPage />} />
|
<Route path="/center/staff" element={<StaffListPage />} />
|
||||||
<Route path="/center/staff/new" element={<StaffCreatePage />} />
|
<Route path="/center/staff/new" element={<StaffCreatePage />} />
|
||||||
|
<Route path="/center/proxy-orders" element={<ProxyOrderListPage />} />
|
||||||
|
<Route path="/center/proxy-orders/:id" element={<ProxyOrderDetailPage />} />
|
||||||
<Route path="/proxy-order" element={<ProxyOrderPage />} />
|
<Route path="/proxy-order" element={<ProxyOrderPage />} />
|
||||||
<Route path="/proxy-order/products" element={<ProxyOrderProductsPage />} />
|
<Route path="/proxy-order/products" element={<ProxyOrderProductsPage />} />
|
||||||
<Route path="/reports/weekly" element={<WeeklyReportPage />} />
|
<Route path="/reports/weekly" element={<WeeklyReportPage />} />
|
||||||
|
|||||||
@@ -219,6 +219,15 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
|||||||
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link to="/center/proxy-orders" className="partner-menu-item">
|
||||||
|
<div className="partner-menu-item-left">
|
||||||
|
<div className="partner-menu-icon">
|
||||||
|
<span className="material-symbols-outlined">receipt_long</span>
|
||||||
|
</div>
|
||||||
|
<span className="body-md" style={{ fontSize: 16 }}>代下单列表</span>
|
||||||
|
</div>
|
||||||
|
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||||
|
</Link>
|
||||||
<Link to="/stores?audit=pending" className="partner-menu-item">
|
<Link to="/stores?audit=pending" className="partner-menu-item">
|
||||||
<div className="partner-menu-item-left">
|
<div className="partner-menu-item-left">
|
||||||
<div className="partner-menu-icon">
|
<div className="partner-menu-icon">
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
|
import type { PartnerProxyOrderListItem } from '@dukang/shared-types';
|
||||||
|
import { request } from '../lib/api';
|
||||||
|
|
||||||
|
function fmtMoney(n: number) {
|
||||||
|
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(value: string | undefined) {
|
||||||
|
if (!value) return '—';
|
||||||
|
const d = new Date(value);
|
||||||
|
if (Number.isNaN(d.getTime())) return value;
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deliveryLabel(order: PartnerProxyOrderListItem) {
|
||||||
|
const t = String(order.deliveryType || '').toUpperCase();
|
||||||
|
if (t === 'ON_SITE_PICKUP') return '现场提货';
|
||||||
|
if (t === 'CROSS_CITY') return '跨城配送';
|
||||||
|
if (t === 'LOCAL') return '同城配送';
|
||||||
|
return '线下代下单';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProxyOrderDetailPage() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [order, setOrder] = useState<PartnerProxyOrderListItem | null>(null);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = '代下单详情';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) return;
|
||||||
|
void request<PartnerProxyOrderListItem>('PARTNER_H5', `/partner/proxy-orders/${id}`)
|
||||||
|
.then(setOrder)
|
||||||
|
.catch((e) => setError(e instanceof Error ? e.message : '加载失败'));
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const img = order?.imageResource?.url || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-no-tab partner-orders-page">
|
||||||
|
<PageHeader title="代下单详情" onBack={() => navigate('/center/proxy-orders')} />
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="partner-form-error" role="alert" style={{ margin: '16px 20px' }}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!order && !error && <p className="label-md text-muted" style={{ margin: 20 }}>加载中…</p>}
|
||||||
|
|
||||||
|
{order && (
|
||||||
|
<div style={{ padding: '0 16px 32px' }}>
|
||||||
|
<div className="partner-order-card" style={{ pointerEvents: 'none' }}>
|
||||||
|
<div className="partner-order-card-top">
|
||||||
|
<span className="label-md text-muted">NO. {order.orderNo}</span>
|
||||||
|
<span className="label-md" style={{ color: 'var(--color-success-green)', fontWeight: 600 }}>
|
||||||
|
已完成
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="partner-order-product">
|
||||||
|
<div
|
||||||
|
className="partner-order-product-img"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: img ? `center/cover no-repeat url(${img})` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!img && <span className="material-symbols-outlined text-muted">liquor</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<h3 className="headline-md">{order.productName || '杜康好酒'}</h3>
|
||||||
|
{order.productSpec && (
|
||||||
|
<p className="label-md text-muted" style={{ marginTop: 4 }}>
|
||||||
|
{order.productSpec}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="body-md" style={{ marginTop: 8 }}>
|
||||||
|
×{order.quantity} · ¥{fmtMoney(Number(order.payAmount || 0))}
|
||||||
|
</p>
|
||||||
|
<p className="label-md text-muted" style={{ marginTop: 4 }}>
|
||||||
|
权益 ¥{fmtMoney(Number(order.benefitAmount || 0))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="partner-form-card" style={{ marginTop: 12 }}>
|
||||||
|
<h3 className="headline-md" style={{ marginBottom: 12 }}>客户信息</h3>
|
||||||
|
<p className="body-md">姓名:{order.receiverName || '—'}</p>
|
||||||
|
<p className="body-md" style={{ marginTop: 8 }}>手机:{order.receiverPhone || '—'}</p>
|
||||||
|
<p className="body-md" style={{ marginTop: 8 }}>
|
||||||
|
地址:{order.receiverAddress || (String(order.deliveryType).toUpperCase() === 'ON_SITE_PICKUP' ? '现场提货' : '—')}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="partner-form-card" style={{ marginTop: 12 }}>
|
||||||
|
<h3 className="headline-md" style={{ marginBottom: 12 }}>订单信息</h3>
|
||||||
|
<p className="body-md">配送方式:{deliveryLabel(order)}</p>
|
||||||
|
<p className="body-md" style={{ marginTop: 8 }}>下单时间:{fmtTime(order.createdAt)}</p>
|
||||||
|
<p className="body-md" style={{ marginTop: 8 }}>支付状态:已付款(线下)</p>
|
||||||
|
<p className="body-md" style={{ marginTop: 8 }}>订单状态:已完成并发放权益</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { useCallback, useEffect, useState, type FormEvent } from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
|
import type { PartnerProxyOrderListItem, PartnerProxyOrderListResponse } from '@dukang/shared-types';
|
||||||
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
|
|
||||||
|
function fmtMoney(n: number) {
|
||||||
|
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(value: string | undefined) {
|
||||||
|
if (!value) return '—';
|
||||||
|
const d = new Date(value);
|
||||||
|
if (Number.isNaN(d.getTime())) return value;
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deliveryLabel(item: PartnerProxyOrderListItem) {
|
||||||
|
const t = String(item.deliveryType || '').toUpperCase();
|
||||||
|
if (t === 'ON_SITE_PICKUP') return '现场提货';
|
||||||
|
if (t === 'CROSS_CITY') return '跨城配送';
|
||||||
|
if (t === 'LOCAL') return '同城配送';
|
||||||
|
return '线下代下单';
|
||||||
|
}
|
||||||
|
|
||||||
|
function coverUrl(item: PartnerProxyOrderListItem) {
|
||||||
|
return item.imageResource?.url || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProxyOrderListPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [list, setList] = useState<PartnerProxyOrderListItem[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [keyword, setKeyword] = useState('');
|
||||||
|
const [q, setQ] = useState('');
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const loadList = useCallback(() => {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
navigate('/login');
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
const params = new URLSearchParams({ page: '1', pageSize: '50' });
|
||||||
|
if (q.trim()) params.set('keyword', q.trim());
|
||||||
|
return request<PartnerProxyOrderListResponse>('PARTNER_H5', `/partner/proxy-orders?${params}`)
|
||||||
|
.then((res) => {
|
||||||
|
setList(Array.isArray(res.list) ? res.list : []);
|
||||||
|
setTotal(Number(res.total) || 0);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setList([]);
|
||||||
|
setTotal(0);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [navigate, q]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = '代下单列表';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadList();
|
||||||
|
}, [loadList]);
|
||||||
|
|
||||||
|
function submitSearch(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setQ(keyword.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PullToRefresh onRefresh={loadList} className="page-no-tab partner-orders-page">
|
||||||
|
<PageHeader title="代下单列表" onBack={() => navigate('/center')} />
|
||||||
|
|
||||||
|
<div style={{ padding: '0 20px 12px' }}>
|
||||||
|
<form className="partner-search" onSubmit={submitSearch}>
|
||||||
|
<span className="material-symbols-outlined">search</span>
|
||||||
|
<input
|
||||||
|
placeholder="订单号 / 手机号 / 姓名 / 商品"
|
||||||
|
value={keyword}
|
||||||
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||||
|
{loading ? '加载中…' : `共 ${total} 笔代下单`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: '0 16px 24px' }}>
|
||||||
|
{!loading && list.length === 0 && <div className="empty">暂无代下单记录</div>}
|
||||||
|
|
||||||
|
{list.map((o) => {
|
||||||
|
const orderId = String(o.id);
|
||||||
|
const img = coverUrl(o);
|
||||||
|
return (
|
||||||
|
<Link key={orderId} to={`/center/proxy-orders/${orderId}`} className="partner-order-card" style={{ marginBottom: 12 }}>
|
||||||
|
<div className="partner-order-card-top">
|
||||||
|
<span className="label-md text-muted">NO. {o.orderNo}</span>
|
||||||
|
<span className="label-md" style={{ color: 'var(--color-success-green)', fontWeight: 600 }}>
|
||||||
|
已完成
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="partner-order-product">
|
||||||
|
<div
|
||||||
|
className="partner-order-product-img"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: img ? `center/cover no-repeat url(${img})` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!img && <span className="material-symbols-outlined text-muted">liquor</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<h3 className="headline-md line-2-clamp">{o.productName || '杜康好酒'}</h3>
|
||||||
|
<p className="text-variant body-md" style={{ marginTop: 4 }}>
|
||||||
|
×{o.quantity} · ¥{fmtMoney(Number(o.payAmount || 0))}
|
||||||
|
</p>
|
||||||
|
<p className="label-md text-muted" style={{ marginTop: 4 }}>
|
||||||
|
{deliveryLabel(o)} · {fmtTime(o.createdAt)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="partner-order-address">
|
||||||
|
<span className="material-symbols-outlined" style={{ fontSize: 18 }}>person</span>
|
||||||
|
<p className="line-2-clamp">
|
||||||
|
{o.receiverName || '客户'}
|
||||||
|
{o.receiverPhone ? ` · ${o.receiverPhone}` : ''}
|
||||||
|
{o.receiverAddress ? ` · ${o.receiverAddress}` : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PullToRefresh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -188,7 +188,7 @@ export default function ProxyOrderPage() {
|
|||||||
});
|
});
|
||||||
toastSuccess(`代下单成功:${order.orderNo}`);
|
toastSuccess(`代下单成功:${order.orderNo}`);
|
||||||
clearProxyOrderDraft();
|
clearProxyOrderDraft();
|
||||||
navigate(`/orders/${order.id}`);
|
navigate(`/center/proxy-orders/${order.id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setMsg(e instanceof Error ? e.message : '下单失败');
|
setMsg(e instanceof Error ? e.message : '下单失败');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const HQ_PERMISSION_CATALOG = [
|
|||||||
|
|
||||||
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
||||||
|
|
||||||
/** 危险操作:角色默认与超管自动权限均不含,需在权限分配中显式勾选 */
|
/** 危险操作:非超管角色默认不含,需在权限分配中显式勾选;超管默认拥有 */
|
||||||
export const HQ_DANGEROUS_PERMISSION_KEYS = [
|
export const HQ_DANGEROUS_PERMISSION_KEYS = [
|
||||||
'users_delete',
|
'users_delete',
|
||||||
'orders_delete',
|
'orders_delete',
|
||||||
|
|||||||
@@ -110,6 +110,23 @@ export type PartnerProxyOrderCreateRequest = {
|
|||||||
promoCodeId?: string;
|
promoCodeId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 合伙人代下单列表项(与 OrderDto 兼容,附带收货信息) */
|
||||||
|
export type PartnerProxyOrderListItem = OrderDto & {
|
||||||
|
receiverName?: string | null;
|
||||||
|
receiverPhone?: string | null;
|
||||||
|
receiverAddress?: string | null;
|
||||||
|
deliveryType?: string | null;
|
||||||
|
productSpec?: string | null;
|
||||||
|
imageResource?: { url?: string } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PartnerProxyOrderListResponse = {
|
||||||
|
list: PartnerProxyOrderListItem[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
};
|
||||||
|
|
||||||
/** 总部代下单(线下完成发权益,无需短信验证) */
|
/** 总部代下单(线下完成发权益,无需短信验证) */
|
||||||
export type HqProxyOrderCreateRequest = {
|
export type HqProxyOrderCreateRequest = {
|
||||||
phone: string;
|
phone: string;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import {
|
import {
|
||||||
|
HQ_DANGEROUS_PERMISSION_KEYS,
|
||||||
HQ_ROLE_DEFAULT_PERMISSIONS,
|
HQ_ROLE_DEFAULT_PERMISSIONS,
|
||||||
expandHqPermissionKeys,
|
expandHqPermissionKeys,
|
||||||
hasAnySystemSettingsPermission,
|
hasAnySystemSettingsPermission,
|
||||||
@@ -43,8 +44,12 @@ export class HqPermissionsResolver {
|
|||||||
const userKeys = userRows.map((r) => r.permissionKey);
|
const userKeys = userRows.map((r) => r.permissionKey);
|
||||||
|
|
||||||
if (account.adminRole === 'SUPER_ADMIN') {
|
if (account.adminRole === 'SUPER_ADMIN') {
|
||||||
// 超管默认拥有业务/管理权限,但不含危险操作;危险权限需按用户显式勾选
|
// 超管含危险操作(删用户/订单/城市);其他角色仍需在权限分配中显式勾选
|
||||||
return expandHqPermissionKeys([...hqBasePermissionKeys(), ...userKeys]);
|
return expandHqPermissionKeys([
|
||||||
|
...hqBasePermissionKeys(),
|
||||||
|
...HQ_DANGEROUS_PERMISSION_KEYS,
|
||||||
|
...userKeys,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleRows = await this.prisma.hqRolePermission.findMany({
|
const roleRows = await this.prisma.hqRolePermission.findMany({
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const HqOperationAction = {
|
|||||||
ORDER_SHIP: 'ORDER_SHIP',
|
ORDER_SHIP: 'ORDER_SHIP',
|
||||||
ORDER_STATUS_DEBUG: 'ORDER_STATUS_DEBUG',
|
ORDER_STATUS_DEBUG: 'ORDER_STATUS_DEBUG',
|
||||||
ORDER_BATCH_DELETE: 'ORDER_BATCH_DELETE',
|
ORDER_BATCH_DELETE: 'ORDER_BATCH_DELETE',
|
||||||
|
ORDER_DELETE: 'ORDER_DELETE',
|
||||||
ORDER_PROXY_CREATE: 'ORDER_PROXY_CREATE',
|
ORDER_PROXY_CREATE: 'ORDER_PROXY_CREATE',
|
||||||
STORE_CREATE: 'STORE_CREATE',
|
STORE_CREATE: 'STORE_CREATE',
|
||||||
STORE_UPDATE: 'STORE_UPDATE',
|
STORE_UPDATE: 'STORE_UPDATE',
|
||||||
@@ -123,6 +124,7 @@ export const HQ_OPERATION_ACTION_LABELS: Record<string, string> = {
|
|||||||
[HqOperationAction.ORDER_SHIP]: '订单发货',
|
[HqOperationAction.ORDER_SHIP]: '订单发货',
|
||||||
[HqOperationAction.ORDER_STATUS_DEBUG]: '订单状态调试',
|
[HqOperationAction.ORDER_STATUS_DEBUG]: '订单状态调试',
|
||||||
[HqOperationAction.ORDER_BATCH_DELETE]: '批量删除订单',
|
[HqOperationAction.ORDER_BATCH_DELETE]: '批量删除订单',
|
||||||
|
[HqOperationAction.ORDER_DELETE]: '删除订单',
|
||||||
[HqOperationAction.ORDER_PROXY_CREATE]: '总部代下单',
|
[HqOperationAction.ORDER_PROXY_CREATE]: '总部代下单',
|
||||||
[HqOperationAction.STORE_CREATE]: '新增门店',
|
[HqOperationAction.STORE_CREATE]: '新增门店',
|
||||||
[HqOperationAction.STORE_UPDATE]: '编辑门店',
|
[HqOperationAction.STORE_UPDATE]: '编辑门店',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
|
HQ_DANGEROUS_PERMISSION_KEYS,
|
||||||
HQ_PERMISSION_CATALOG,
|
HQ_PERMISSION_CATALOG,
|
||||||
HQ_ROLE_DEFAULT_PERMISSIONS,
|
HQ_ROLE_DEFAULT_PERMISSIONS,
|
||||||
LEGACY_SYSTEM_SETTINGS_KEY,
|
LEGACY_SYSTEM_SETTINGS_KEY,
|
||||||
@@ -82,7 +83,10 @@ export class AdminHqPermissionsService {
|
|||||||
const userPermissionKeys = expandHqPermissionKeys(userPerms.map((p) => p.permissionKey));
|
const userPermissionKeys = expandHqPermissionKeys(userPerms.map((p) => p.permissionKey));
|
||||||
|
|
||||||
if (account.adminRole === 'SUPER_ADMIN') {
|
if (account.adminRole === 'SUPER_ADMIN') {
|
||||||
const rolePermissionKeys = hqBasePermissionKeys();
|
const rolePermissionKeys = [
|
||||||
|
...hqBasePermissionKeys(),
|
||||||
|
...HQ_DANGEROUS_PERMISSION_KEYS,
|
||||||
|
] as HqPermissionKey[];
|
||||||
const effectivePermissionKeys = [
|
const effectivePermissionKeys = [
|
||||||
...new Set([...rolePermissionKeys, ...userPermissionKeys]),
|
...new Set([...rolePermissionKeys, ...userPermissionKeys]),
|
||||||
] as HqPermissionKey[];
|
] as HqPermissionKey[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||||
import {
|
import {
|
||||||
HqPermissionGuard,
|
HqPermissionGuard,
|
||||||
@@ -43,6 +43,18 @@ export class AdminOrdersController {
|
|||||||
return this.ordersService.detail(BigInt(id));
|
return this.ordersService.detail(BigInt(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@UseGuards(HqPermissionGuard)
|
||||||
|
@RequireHqPermissions('orders_delete')
|
||||||
|
@HqOperation({
|
||||||
|
action: HqOperationAction.ORDER_DELETE,
|
||||||
|
refType: 'ORDER',
|
||||||
|
refIdParam: 'id',
|
||||||
|
})
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.ordersService.deleteOrder(BigInt(id));
|
||||||
|
}
|
||||||
|
|
||||||
/** HQ 发货:调用小飞侠创建运单并更新配送信息 */
|
/** HQ 发货:调用小飞侠创建运单并更新配送信息 */
|
||||||
@Post(':id/ship')
|
@Post(':id/ship')
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export class AdminOrdersService {
|
|||||||
|
|
||||||
if (query.orderNo) where.orderNo = { contains: query.orderNo };
|
if (query.orderNo) where.orderNo = { contains: query.orderNo };
|
||||||
if (query.status) where.status = query.status as Prisma.EnumOrderStatusFilter['equals'];
|
if (query.status) where.status = query.status as Prisma.EnumOrderStatusFilter['equals'];
|
||||||
|
if (query.orderType) where.orderType = query.orderType as Prisma.EnumOrderTypeFilter['equals'];
|
||||||
if (query.userId) where.userId = BigInt(query.userId);
|
if (query.userId) where.userId = BigInt(query.userId);
|
||||||
if (query.cityId) where.cityId = BigInt(query.cityId);
|
if (query.cityId) where.cityId = BigInt(query.cityId);
|
||||||
if (query.receiverPhone) where.receiverPhone = { contains: query.receiverPhone };
|
if (query.receiverPhone) where.receiverPhone = { contains: query.receiverPhone };
|
||||||
@@ -335,6 +336,25 @@ export class AdminOrdersService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteOrder(id: bigint) {
|
||||||
|
const order = await this.prisma.order.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { id: true, orderNo: true },
|
||||||
|
});
|
||||||
|
if (!order) throw new NotFoundException('订单不存在');
|
||||||
|
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
await this.deleteOrdersInTx(tx, [id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
deleted: 1,
|
||||||
|
orderNo: order.orderNo,
|
||||||
|
message: `订单 ${order.orderNo} 及关联业务数据已删除`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async deleteOrdersInTx(tx: Prisma.TransactionClient, orderIds: bigint[]) {
|
private async deleteOrdersInTx(tx: Prisma.TransactionClient, orderIds: bigint[]) {
|
||||||
if (!orderIds.length) return;
|
if (!orderIds.length) return;
|
||||||
|
|
||||||
@@ -368,6 +388,10 @@ export class AdminOrdersService {
|
|||||||
await tx.benefitCoupon.deleteMany({ where: { id: { in: couponIds } } });
|
await tx.benefitCoupon.deleteMany({ where: { id: { in: couponIds } } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await tx.userInvoice.deleteMany({ where: { orderId: { in: orderIds } } });
|
||||||
|
await tx.wineryBillItem.deleteMany({ where: { orderId: { in: orderIds } } });
|
||||||
|
await tx.logisticsBillItem.deleteMany({ where: { orderId: { in: orderIds } } });
|
||||||
|
|
||||||
await tx.order.updateMany({
|
await tx.order.updateMany({
|
||||||
where: { originOrderId: { in: orderIds } },
|
where: { originOrderId: { in: orderIds } },
|
||||||
data: { originOrderId: null },
|
data: { originOrderId: null },
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ export class AdminOrdersQueryDto extends PaginationQueryDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['NORMAL', 'PROXY'])
|
||||||
|
orderType?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
|||||||
@@ -208,6 +208,26 @@ export class PartnerProxyOrderController {
|
|||||||
return this.tradeService.getPartnerProxyOrderOptions(user.actorId);
|
return this.tradeService.getPartnerProxyOrderOptions(user.actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
list(
|
||||||
|
@CurrentUser() user: AuthUser,
|
||||||
|
@Query('page') page = '1',
|
||||||
|
@Query('pageSize') pageSize = '20',
|
||||||
|
@Query('keyword') keyword?: string,
|
||||||
|
) {
|
||||||
|
return this.tradeService.listPartnerProxyOrders(
|
||||||
|
user.actorId,
|
||||||
|
Number(page),
|
||||||
|
Number(pageSize),
|
||||||
|
keyword,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
detail(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||||
|
return this.tradeService.getPartnerProxyOrder(user.actorId, BigInt(id));
|
||||||
|
}
|
||||||
|
|
||||||
@Post('preview')
|
@Post('preview')
|
||||||
preview(@CurrentUser() user: AuthUser, @Body() dto: PartnerProxyOrderPreviewDto) {
|
preview(@CurrentUser() user: AuthUser, @Body() dto: PartnerProxyOrderPreviewDto) {
|
||||||
return this.tradeService.previewPartnerProxyOrderForPartner(user.actorId, dto);
|
return this.tradeService.previewPartnerProxyOrderForPartner(user.actorId, dto);
|
||||||
|
|||||||
@@ -1359,7 +1359,76 @@ export class TradeService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.getPartnerOrder(partnerAccountId, order.id);
|
return serializeBigInt(mapOrderCompat(order));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 合伙人代下单列表:按 proxyPartnerAccountId 归属,与管仓订单无关 */
|
||||||
|
async listPartnerProxyOrders(
|
||||||
|
partnerAccountId: bigint,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 20,
|
||||||
|
keyword?: string,
|
||||||
|
) {
|
||||||
|
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||||
|
const kw = keyword?.trim();
|
||||||
|
const where = {
|
||||||
|
orderType: 'PROXY' as const,
|
||||||
|
proxyPartnerAccountId: primary.id,
|
||||||
|
...(kw
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{ orderNo: { contains: kw } },
|
||||||
|
{ receiverPhone: { contains: kw } },
|
||||||
|
{ receiverName: { contains: kw } },
|
||||||
|
{ productName: { contains: kw } },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
const take = Math.min(Math.max(pageSize, 1), 50);
|
||||||
|
const skip = (Math.max(page, 1) - 1) * take;
|
||||||
|
const [list, total] = await Promise.all([
|
||||||
|
this.prisma.order.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
delivery: true,
|
||||||
|
imageResource: true,
|
||||||
|
user: { select: { phone: true, nickname: true } },
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
}),
|
||||||
|
this.prisma.order.count({ where }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
list: serializeBigInt(list.map(mapOrderCompat)),
|
||||||
|
total,
|
||||||
|
page: Math.max(page, 1),
|
||||||
|
pageSize: take,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPartnerProxyOrder(partnerAccountId: bigint, orderId: bigint) {
|
||||||
|
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||||
|
const order = await this.prisma.order.findFirst({
|
||||||
|
where: {
|
||||||
|
id: orderId,
|
||||||
|
orderType: 'PROXY',
|
||||||
|
proxyPartnerAccountId: primary.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
delivery: true,
|
||||||
|
user: true,
|
||||||
|
imageResource: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!order) throw new NotFoundException('代下单不存在');
|
||||||
|
const statusLogs = await this.prisma.commonEvent.findMany({
|
||||||
|
where: orderStatusLogWhere(orderId),
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
return serializeBigInt(mapOrderCompat({ ...order, statusLogs: mapStatusLogCompat(statusLogs) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** HQ 代下单:商品/推广码选项(运营侧可看白名单测试酒) */
|
/** HQ 代下单:商品/推广码选项(运营侧可看白名单测试酒) */
|
||||||
|
|||||||
Reference in New Issue
Block a user