Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b5f42acdf | |||
| 71149044ca | |||
| 1ecc6a11df | |||
| cfd42dfc32 | |||
| a423fb71e8 | |||
| 4111e5c0a6 | |||
| 3a9f27eb38 | |||
| e2f5aa9dd8 | |||
| 6a5c0cb090 | |||
| 448b9983d3 | |||
| 00bb6a2227 | |||
| 1f06530894 | |||
| 93390ccf75 | |||
| a2976cdc8f | |||
| 8f2ebbfef4 | |||
| ee1a3f4b6c | |||
| f2fbf95c53 | |||
| 98d52ff173 | |||
| 43de361e61 | |||
| b1511ecb92 | |||
| 0b548a2764 | |||
| 30f8d649cb | |||
| 40e8a12596 | |||
| d7241d54b1 | |||
| 3d819dc10b | |||
| 5e0691ab35 | |||
| 8984465893 | |||
| b90a4e9f8f | |||
| 3f208d27a9 | |||
| 6973e3d7f5 | |||
| fa69ced448 | |||
| a144a35c3d | |||
| 18c5abea79 | |||
| 012626f6bd | |||
| 5d4ed566de | |||
| 3004a65b35 | |||
| 5fdddae41c | |||
| 7d2e4ef57e | |||
| 22c0b03a47 | |||
| 73fd27bdf6 | |||
| f2d03e2595 | |||
| cd9696bc1c | |||
| 0c28c204b2 | |||
| b5d4f8b27c | |||
| 9a80f0f8d6 | |||
| e7f49a9639 | |||
| f941dcf072 | |||
| e8b585e623 | |||
| 5702266276 | |||
| 3382d36a6c | |||
| 9b8e3f1347 | |||
| a8a37fbc4a | |||
| 1440a59fc8 | |||
| e34416a9b3 | |||
| 11659484c6 | |||
| 6c44e9c56f | |||
| 0fb7ab7abb | |||
| 905e145af3 |
@@ -74,56 +74,6 @@ body.admin-col-resizing * {
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
/* 订单页筛选:第一行常驻,第二行起可收起 */
|
||||
.admin-orders-filter-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-orders-filter-row--tools {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.admin-orders-filter-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.admin-orders-filter-item--sm {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.admin-orders-filter-item--md {
|
||||
width: 168px;
|
||||
}
|
||||
|
||||
.admin-orders-filter-item--search {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.admin-orders-filter-item--status {
|
||||
width: 220px;
|
||||
min-width: 180px;
|
||||
flex: 1 1 180px;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.admin-orders-filter-item--date {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.admin-orders-filter-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.admin-orders-date-picker {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
/* 列表页顶栏:标题左、主操作右、列设置最右 */
|
||||
.admin-list-header {
|
||||
display: flex;
|
||||
|
||||
@@ -15,13 +15,7 @@ type Props = {
|
||||
|
||||
export default function RedeemRecordDetailDescriptions({ detail, maskUserPhone = false }: Props) {
|
||||
const user = detail.user as
|
||||
| {
|
||||
id?: string;
|
||||
userNo?: string;
|
||||
nickname?: string | null;
|
||||
phone?: string | null;
|
||||
hqRemark?: string | null;
|
||||
}
|
||||
| { userNo?: string; nickname?: string | null; phone?: string | null }
|
||||
| undefined;
|
||||
const store = detail.store as
|
||||
| {
|
||||
@@ -47,13 +41,7 @@ export default function RedeemRecordDetailDescriptions({ detail, maskUserPhone =
|
||||
<Descriptions.Item label="结算额">¥{Number(detail.settleAmount ?? 0).toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt ?? ''))}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户编号">{user?.userNo ?? '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户昵称">
|
||||
{(() => {
|
||||
const name = user?.nickname?.trim() || '—';
|
||||
const remark = user?.hqRemark?.trim();
|
||||
return remark ? `${name}(${remark})` : name;
|
||||
})()}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="用户昵称">{user?.nickname?.trim() || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="用户手机">
|
||||
{maskUserPhone ? maskPhone(user?.phone) : user?.phone || '—'}
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Descriptions,
|
||||
@@ -111,16 +111,9 @@ type CouponDetail = Row & {
|
||||
|
||||
export default function BenefitCouponsPage() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialCouponNo = searchParams.get('couponNo')?.trim() || '';
|
||||
const [form] = Form.useForm();
|
||||
const [grantForm] = Form.useForm<AdminBenefitGrantRequest>();
|
||||
const [filters, setFilters] = useState<Record<string, string>>(() => {
|
||||
const init: Record<string, string> = {};
|
||||
if (initialCouponNo) init.couponNo = initialCouponNo;
|
||||
return init;
|
||||
});
|
||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||
const [grantOpen, setGrantOpen] = useState(false);
|
||||
const [granting, setGranting] = useState(false);
|
||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||||
@@ -139,35 +132,6 @@ export default function BenefitCouponsPage() {
|
||||
const [redeemDetail, setRedeemDetail] = useState<Record<string, unknown> | null>(null);
|
||||
const [redeemDrawerOpen, setRedeemDrawerOpen] = useState(false);
|
||||
const [redeemDetailLoading, setRedeemDetailLoading] = useState(false);
|
||||
const deepLinkOpenedRef = useRef(false);
|
||||
|
||||
async function openCouponDetail(id: string) {
|
||||
setDetail(await request<CouponDetail>(`/admin/benefit/coupons/${id}`));
|
||||
setDrawerOpen(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (initialCouponNo) form.setFieldsValue({ couponNo: initialCouponNo });
|
||||
}, [form, initialCouponNo]);
|
||||
|
||||
useEffect(() => {
|
||||
const openCouponId = (location.state as { openCouponId?: string } | null)?.openCouponId;
|
||||
if (openCouponId && !deepLinkOpenedRef.current) {
|
||||
deepLinkOpenedRef.current = true;
|
||||
void openCouponDetail(openCouponId).catch((e) => {
|
||||
message.error(e instanceof Error ? e.message : '加载权益券失败');
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!initialCouponNo || deepLinkOpenedRef.current || loading) return;
|
||||
const first = data?.items?.[0];
|
||||
if (first && String(first.couponNo) === initialCouponNo) {
|
||||
deepLinkOpenedRef.current = true;
|
||||
void openCouponDetail(first.id).catch((e) => {
|
||||
message.error(e instanceof Error ? e.message : '加载权益券失败');
|
||||
});
|
||||
}
|
||||
}, [data, initialCouponNo, loading, location.state]);
|
||||
|
||||
async function openRedeemDetail(redeemId: string) {
|
||||
setRedeemDetailLoading(true);
|
||||
@@ -190,7 +154,12 @@ export default function BenefitCouponsPage() {
|
||||
width: 200,
|
||||
ellipsis: false,
|
||||
render: (v, row) => (
|
||||
<AdminPrimaryLink onClick={() => void openCouponDetail(row.id)}>
|
||||
<AdminPrimaryLink
|
||||
onClick={async () => {
|
||||
setDetail(await request(`/admin/benefit/coupons/${row.id}`));
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
</AdminPrimaryLink>
|
||||
),
|
||||
@@ -242,7 +211,10 @@ export default function BenefitCouponsPage() {
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => void openCouponDetail(row.id)}
|
||||
onClick={async () => {
|
||||
setDetail(await request(`/admin/benefit/coupons/${row.id}`));
|
||||
setDrawerOpen(true);
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
|
||||
@@ -71,11 +71,11 @@ export default function DevPlanTasksPage() {
|
||||
const [batchForm] = Form.useForm<{ status?: DevPlanTaskStatusDto; versionId?: string }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!modalOpen && !batchEditOpen) return;
|
||||
if (!batchEditOpen) return;
|
||||
request<{ items: DevPlanVersionDto[] }>('/admin/dev-plan/versions?pageSize=100')
|
||||
.then((res) => setVersions(res.items ?? []))
|
||||
.catch(() => setVersions([]));
|
||||
}, [modalOpen, batchEditOpen]);
|
||||
}, [batchEditOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!modalOpen || editing) return;
|
||||
@@ -108,7 +108,6 @@ export default function DevPlanTasksPage() {
|
||||
content: row.content,
|
||||
type: row.type,
|
||||
status: row.status,
|
||||
versionIds: row.versionIds?.length ? row.versionIds : row.versions?.map((v) => v.id) ?? [],
|
||||
attachmentUrls: row.attachmentUrls?.length ? row.attachmentUrls : [''],
|
||||
});
|
||||
setModalOpen(true);
|
||||
@@ -122,11 +121,7 @@ export default function DevPlanTasksPage() {
|
||||
if (editing) {
|
||||
await request(`/admin/dev-plan/tasks/${editing.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
...values,
|
||||
attachmentUrls,
|
||||
versionIds: values.versionIds ?? [],
|
||||
}),
|
||||
body: JSON.stringify({ ...values, attachmentUrls }),
|
||||
});
|
||||
message.success('已更新');
|
||||
} else {
|
||||
@@ -290,21 +285,6 @@ export default function DevPlanTasksPage() {
|
||||
),
|
||||
},
|
||||
{ title: '来源工单', dataIndex: 'supportTicketNo', width: 140, render: (v) => v || '—' },
|
||||
{
|
||||
title: '关联版本',
|
||||
dataIndex: 'versions',
|
||||
width: 140,
|
||||
render: (_, row) =>
|
||||
row.versions?.length ? (
|
||||
<Space size={4} wrap>
|
||||
{row.versions.map((v) => (
|
||||
<Tag key={v.id}>{v.versionNo}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
},
|
||||
{ title: '创建人', dataIndex: 'creatorName', width: 90 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||
{
|
||||
@@ -425,21 +405,9 @@ export default function DevPlanTasksPage() {
|
||||
<Select options={TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
{editing ? (
|
||||
<>
|
||||
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
|
||||
<Select options={STATUS_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="versionIds" label="关联版本">
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="可选:关联版本"
|
||||
options={versions.map((v) => ({ value: v.id, label: v.versionNo }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
) : (
|
||||
<Form.Item name="supportTicketId" label="绑定技术支持工单">
|
||||
<Select
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
Col,
|
||||
Collapse,
|
||||
DatePicker,
|
||||
Descriptions,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
InputNumber,
|
||||
Modal,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
@@ -22,7 +24,6 @@ import {
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs, { type Dayjs } from 'dayjs';
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { ORDER_TYPE_LABELS } from '@dukang/shared-types';
|
||||
import { request, type AdminOrderItem, type AdminOrderRow, type HqProfile, type Paginated } from '../lib/api';
|
||||
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||
@@ -149,9 +150,8 @@ type OrderExportFilters = {
|
||||
orderType?: string;
|
||||
cityId?: string;
|
||||
receiverPhone?: string;
|
||||
userKeyword?: string;
|
||||
productKeyword?: string;
|
||||
fulfillmentHold?: boolean;
|
||||
excludeTest?: boolean;
|
||||
deliveryType?: string;
|
||||
promoCodeId?: string;
|
||||
dateRange?: [Dayjs, Dayjs];
|
||||
@@ -213,8 +213,6 @@ const DATE_PRESETS: Array<{ key: 'day' | 'week' | 'month' | 'quarter' | 'year';
|
||||
{ key: 'year', label: '当年' },
|
||||
];
|
||||
|
||||
const FILTERS_COLLAPSED_KEY = 'admin-orders-filters-collapsed';
|
||||
|
||||
function formatUserWithRemark(user?: {
|
||||
userNo?: string;
|
||||
hqRemark?: string | null;
|
||||
@@ -256,10 +254,9 @@ function buildExportPayload(
|
||||
if (filters.orderType) payload.orderType = filters.orderType;
|
||||
if (filters.cityId) payload.cityId = filters.cityId;
|
||||
if (filters.receiverPhone) payload.receiverPhone = filters.receiverPhone;
|
||||
if (filters.userKeyword) payload.userKeyword = filters.userKeyword;
|
||||
if (filters.productKeyword) payload.productKeyword = filters.productKeyword;
|
||||
if (filters.deliveryType) payload.deliveryType = filters.deliveryType;
|
||||
if (filters.fulfillmentHold) payload.fulfillmentHold = true;
|
||||
if (filters.excludeTest) payload.excludeTest = true;
|
||||
if (filters.promoCodeId) payload.promoCodeId = filters.promoCodeId;
|
||||
if (filters.dateRange?.[0]) payload.createdFrom = filters.dateRange[0].format('YYYY-MM-DD');
|
||||
if (filters.dateRange?.[1]) payload.createdTo = filters.dateRange[1].format('YYYY-MM-DD');
|
||||
@@ -338,13 +335,6 @@ export default function OrdersPage() {
|
||||
const [trackOpen, setTrackOpen] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [exportFormat, setExportFormat] = useState<OrderExportFormat>('xlsx');
|
||||
const [filtersCollapsed, setFiltersCollapsed] = useState(() => {
|
||||
try {
|
||||
return localStorage.getItem(FILTERS_COLLAPSED_KEY) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const canDeleteOrders = (profile?.permissionKeys ?? []).includes('orders_delete');
|
||||
const canProxyOrder = (profile?.permissionKeys ?? []).includes('orders');
|
||||
const canExportOrders = (profile?.permissionKeys ?? []).includes('orders');
|
||||
@@ -414,9 +404,8 @@ export default function OrdersPage() {
|
||||
if (values.orderType) qs.set('orderType', values.orderType);
|
||||
if (values.cityId) qs.set('cityId', values.cityId);
|
||||
if (values.receiverPhone) qs.set('receiverPhone', values.receiverPhone);
|
||||
if (values.userKeyword) qs.set('userKeyword', values.userKeyword);
|
||||
if (values.productKeyword) qs.set('productKeyword', values.productKeyword);
|
||||
if (values.fulfillmentHold) qs.set('fulfillmentHold', 'true');
|
||||
if (values.excludeTest) qs.set('excludeTest', 'true');
|
||||
if (values.deliveryType) qs.set('deliveryType', values.deliveryType);
|
||||
if (initialPromoCodeId) qs.set('promoCodeId', initialPromoCodeId);
|
||||
if (values.dateRange?.[0]) qs.set('createdFrom', values.dateRange[0].format('YYYY-MM-DD'));
|
||||
@@ -854,72 +843,14 @@ export default function OrdersPage() {
|
||||
) : null}
|
||||
|
||||
<Form form={form} layout="vertical" onFinish={() => { setPage(1); void load(); }}>
|
||||
<div className="admin-orders-filter-row">
|
||||
<Form.Item name="cityId" label="城市" className="admin-orders-filter-item admin-orders-filter-item--sm">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="全部"
|
||||
options={cities.map((c) => ({ value: c.id, label: c.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="receiverPhone" label="手机号" className="admin-orders-filter-item admin-orders-filter-item--md">
|
||||
<Input allowClear placeholder="手机号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="userKeyword" label="用户" className="admin-orders-filter-item admin-orders-filter-item--search">
|
||||
<Input allowClear placeholder="编号/昵称/备注/手机" />
|
||||
</Form.Item>
|
||||
<Form.Item name="productKeyword" label="商品" className="admin-orders-filter-item admin-orders-filter-item--search">
|
||||
<Input allowClear placeholder="名称/规格" />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" className="admin-orders-filter-item admin-orders-filter-item--status">
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="全部"
|
||||
options={Object.entries(ORDER_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="orderType" label="类型" className="admin-orders-filter-item admin-orders-filter-item--sm">
|
||||
<Select allowClear placeholder="全部" options={ORDER_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="deliveryType" label="配送" className="admin-orders-filter-item admin-orders-filter-item--sm">
|
||||
<Select allowClear placeholder="全部" options={DELIVERY_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item label=" " colon={false} className="admin-orders-filter-item admin-orders-filter-actions">
|
||||
<Space size={8} wrap>
|
||||
<Button type="primary" htmlType="submit">查询</Button>
|
||||
<Button
|
||||
type="link"
|
||||
icon={filtersCollapsed ? <DownOutlined /> : <UpOutlined />}
|
||||
onClick={() => {
|
||||
setFiltersCollapsed((prev) => {
|
||||
const next = !prev;
|
||||
try {
|
||||
localStorage.setItem(FILTERS_COLLAPSED_KEY, next ? '1' : '0');
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{filtersCollapsed ? '展开' : '收起'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{!filtersCollapsed ? (
|
||||
<>
|
||||
<div className="admin-orders-filter-row">
|
||||
<Form.Item label="下单日期" className="admin-orders-filter-item admin-orders-filter-item--date">
|
||||
<Space size={8} wrap>
|
||||
<Row gutter={[16, 8]}>
|
||||
<Col xs={24} md={14} lg={12}>
|
||||
<Form.Item label="下单日期" style={{ marginBottom: 12 }}>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<Form.Item name="dateRange" noStyle>
|
||||
<DatePicker.RangePicker allowClear className="admin-orders-date-picker" />
|
||||
<DatePicker.RangePicker allowClear style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Space size={8} wrap>
|
||||
{DATE_PRESETS.map((preset) => (
|
||||
<Button
|
||||
key={preset.key}
|
||||
@@ -930,10 +861,61 @@ export default function OrdersPage() {
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<Form.Item name="status" label="状态" style={{ marginBottom: 12 }}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="全部"
|
||||
options={Object.entries(ORDER_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={6} md={5} lg={3}>
|
||||
<Form.Item name="orderType" label="类型" style={{ marginBottom: 12 }}>
|
||||
<Select allowClear placeholder="全部" options={ORDER_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={6} md={5} lg={3}>
|
||||
<Form.Item name="deliveryType" label="配送" style={{ marginBottom: 12 }}>
|
||||
<Select allowClear placeholder="全部" options={DELIVERY_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={6} md={5} lg={3}>
|
||||
<Form.Item name="cityId" label="城市" style={{ marginBottom: 12 }}>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
placeholder="全部"
|
||||
options={cities.map((c) => ({ value: c.id, label: c.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={6} lg={6}>
|
||||
<Form.Item name="receiverPhone" label="收货手机" style={{ marginBottom: 12 }}>
|
||||
<Input allowClear placeholder="手机号" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={12} sm={8} md={8} lg={8}>
|
||||
<Form.Item label=" " colon={false} style={{ marginBottom: 12 }}>
|
||||
<Space size={16} wrap>
|
||||
<Form.Item name="fulfillmentHold" valuePropName="checked" noStyle>
|
||||
<Checkbox>大单拦截</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="excludeTest" valuePropName="checked" noStyle>
|
||||
<Checkbox>过滤测试</Checkbox>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div className="admin-orders-filter-row admin-orders-filter-row--tools">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', margin: '8px 0 16px', gap: 12, flexWrap: 'wrap' }}>
|
||||
{canExportOrders ? (
|
||||
<Space size={12} wrap>
|
||||
<Radio.Group
|
||||
@@ -958,9 +940,7 @@ export default function OrdersPage() {
|
||||
</Space>
|
||||
) : <span />}
|
||||
<Space size={12} wrap>
|
||||
<Form.Item name="fulfillmentHold" valuePropName="checked" noStyle>
|
||||
<Checkbox>大单拦截</Checkbox>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">查询</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
@@ -982,8 +962,6 @@ export default function OrdersPage() {
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Form>
|
||||
|
||||
<Table
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Button, Checkbox, Drawer, Form, Input, Select, Table, Tag, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { REDEEM_CHANNEL_LABELS, type RedeemChannel } from '@dukang/shared-types';
|
||||
@@ -20,28 +20,17 @@ type Row = {
|
||||
channel?: RedeemChannel;
|
||||
createdAt: string;
|
||||
isTest?: boolean;
|
||||
user?: {
|
||||
id?: string;
|
||||
userNo: string;
|
||||
phone: string | null;
|
||||
nickname?: string | null;
|
||||
hqRemark?: string | null;
|
||||
};
|
||||
user?: { userNo: string; phone: string | null; nickname?: string | null };
|
||||
store?: { name: string; cityName: string };
|
||||
coupon?: { id?: string; couponNo: string };
|
||||
coupon?: { couponNo: string };
|
||||
};
|
||||
|
||||
function formatNicknameWithRemark(user?: {
|
||||
nickname?: string | null;
|
||||
hqRemark?: string | null;
|
||||
} | null) {
|
||||
const name = user?.nickname?.trim() || '—';
|
||||
const remark = user?.hqRemark?.trim();
|
||||
return remark ? `${name}(${remark})` : name;
|
||||
function maskPhone(phone: string | null | undefined) {
|
||||
if (!phone || phone.length < 7) return phone ?? '—';
|
||||
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
|
||||
}
|
||||
|
||||
export default function RedeemRecordsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialRedeemNo = searchParams.get('redeemNo')?.trim() || '';
|
||||
const [form] = Form.useForm();
|
||||
@@ -119,60 +108,23 @@ export default function RedeemRecordsPage() {
|
||||
);
|
||||
},
|
||||
},
|
||||
{ title: '用户编号', dataIndex: ['user', 'userNo'], width: 110, render: (v: string | undefined, row) =>
|
||||
row.user?.id ? (
|
||||
<AdminPrimaryLink
|
||||
onClick={() => navigate('/users', { state: { openUserId: String(row.user!.id) } })}
|
||||
>
|
||||
{v || '—'}
|
||||
</AdminPrimaryLink>
|
||||
) : (
|
||||
v || '—'
|
||||
),
|
||||
},
|
||||
{ title: '用户编号', dataIndex: ['user', 'userNo'], width: 110 },
|
||||
{
|
||||
title: '用户昵称',
|
||||
dataIndex: ['user', 'nickname'],
|
||||
width: 160,
|
||||
render: (_: string | null | undefined, row) =>
|
||||
row.user?.id ? (
|
||||
<AdminPrimaryLink
|
||||
onClick={() => navigate('/users', { state: { openUserId: String(row.user!.id) } })}
|
||||
>
|
||||
{formatNicknameWithRemark(row.user)}
|
||||
</AdminPrimaryLink>
|
||||
) : (
|
||||
formatNicknameWithRemark(row.user)
|
||||
),
|
||||
width: 100,
|
||||
render: (v: string | null | undefined) => v || '—',
|
||||
},
|
||||
{
|
||||
title: '用户手机',
|
||||
dataIndex: ['user', 'phone'],
|
||||
width: 120,
|
||||
render: (v: string | null | undefined) => v || '—',
|
||||
render: (v: string | null | undefined) => maskPhone(v),
|
||||
},
|
||||
{ title: '门店', dataIndex: ['store', 'name'] },
|
||||
{ title: '核销额', dataIndex: 'amount', width: 90, render: (v) => `¥${v}` },
|
||||
{ title: '结算额', dataIndex: 'settleAmount', width: 90, render: (v) => `¥${v}` },
|
||||
{
|
||||
title: '券号',
|
||||
dataIndex: ['coupon', 'couponNo'],
|
||||
width: 160,
|
||||
render: (v: string | undefined, row) =>
|
||||
v ? (
|
||||
<AdminPrimaryLink
|
||||
onClick={() =>
|
||||
navigate(`/benefit/coupons?couponNo=${encodeURIComponent(v)}`, {
|
||||
state: row.coupon?.id ? { openCouponId: String(row.coupon.id) } : undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
{v}
|
||||
</AdminPrimaryLink>
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
},
|
||||
{ title: '券号', dataIndex: ['coupon', 'couponNo'], width: 160 },
|
||||
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||
{
|
||||
title: '操作',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
@@ -76,7 +75,6 @@ const KIND_COLORS: Record<StoreSettlementKind, string> = {
|
||||
};
|
||||
|
||||
export default function StoreBillsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialKind = searchParams.get('kind') === 'WITHDRAW' ? 'WITHDRAW' : '';
|
||||
const initialStoreId = searchParams.get('storeId') || '';
|
||||
@@ -296,11 +294,7 @@ export default function StoreBillsPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Tooltip title="T+1 为出账日(昨日 00:00–今日 00:00 窗口中的今天,不是该账单里最后一笔核销时间);手动提现为申请时间">
|
||||
出账日
|
||||
</Tooltip>
|
||||
),
|
||||
title: '日期',
|
||||
dataIndex: 'date',
|
||||
width: 160,
|
||||
render: (v, row) => (row.kind === 'T1_BILL' ? String(v || '').slice(0, 10) : fmtTime(v)),
|
||||
@@ -395,7 +389,6 @@ export default function StoreBillsPage() {
|
||||
<>
|
||||
含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口。
|
||||
「已打款」= T+1 出账终态,「已结算」= 手动提现终态。
|
||||
T+1「出账日」是任务跑的那天(昨日 00:00–今日 00:00 窗口中的今天),不是最后一笔核销时间。
|
||||
{overdueSummary && overdueSummary.overdueCount > 0 ? (
|
||||
<div>
|
||||
<Typography.Text type="danger">
|
||||
@@ -476,7 +469,7 @@ export default function StoreBillsPage() {
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="range" label="出账日">
|
||||
<Form.Item name="range" label="日期">
|
||||
<DatePicker.RangePicker />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
@@ -550,7 +543,7 @@ export default function StoreBillsPage() {
|
||||
<>
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label="账单号">{String(detail.billNo)}</Descriptions.Item>
|
||||
<Descriptions.Item label="出账日">
|
||||
<Descriptions.Item label="账单日">
|
||||
{String(detail.billDate || '').slice(0, 10)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="应付">
|
||||
@@ -599,22 +592,8 @@ export default function StoreBillsPage() {
|
||||
columns={[
|
||||
{
|
||||
title: '核销单号',
|
||||
render: (_, r) => {
|
||||
const redeemNo = String(
|
||||
(r.redeemRecord as { redeemNo?: string } | undefined)?.redeemNo || '',
|
||||
);
|
||||
return redeemNo ? (
|
||||
<AdminPrimaryLink
|
||||
onClick={() =>
|
||||
navigate(`/redeem-records?redeemNo=${encodeURIComponent(redeemNo)}`)
|
||||
}
|
||||
>
|
||||
{redeemNo}
|
||||
</AdminPrimaryLink>
|
||||
) : (
|
||||
'—'
|
||||
);
|
||||
},
|
||||
render: (_, r) =>
|
||||
String((r.redeemRecord as { redeemNo?: string })?.redeemNo || '—'),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
@@ -683,18 +662,7 @@ export default function StoreBillsPage() {
|
||||
const payout = r.storePayout as
|
||||
| { redeemRecord?: { redeemNo?: string }; payoutAmount?: number }
|
||||
| undefined;
|
||||
const redeemNo = payout?.redeemRecord?.redeemNo || '';
|
||||
return redeemNo ? (
|
||||
<AdminPrimaryLink
|
||||
onClick={() =>
|
||||
navigate(`/redeem-records?redeemNo=${encodeURIComponent(redeemNo)}`)
|
||||
}
|
||||
>
|
||||
{redeemNo}
|
||||
</AdminPrimaryLink>
|
||||
) : (
|
||||
'—'
|
||||
);
|
||||
return String(payout?.redeemRecord?.redeemNo || '—');
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,7 +47,6 @@ import { fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import OssUpload from '../components/OssUpload';
|
||||
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||
import { AdminListHeader } from '../components/AdminListHeader';
|
||||
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
|
||||
|
||||
|
||||
@@ -561,11 +560,19 @@ export default function SupportTicketsPage() {
|
||||
return (
|
||||
<div>
|
||||
{settingsModal}
|
||||
<AdminListHeader
|
||||
title="技术支持"
|
||||
settings={settingsButton}
|
||||
actions={
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||
技术支持
|
||||
</Typography.Title>
|
||||
{settingsButton}
|
||||
<Space>
|
||||
{isSuperAdmin ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -595,9 +602,8 @@ export default function SupportTicketsPage() {
|
||||
<Button type="primary" onClick={() => setCreateOpen(true)}>
|
||||
创建工单
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
layout="inline"
|
||||
|
||||
@@ -611,7 +611,7 @@ export default function UsersPage() {
|
||||
<Descriptions.Item label="用户编号">{detail.userNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="昵称">{detail.nickname || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="备注">{detail.hqRemark || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">{detail.phone || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">{maskPhone(detail.phone)}</Descriptions.Item>
|
||||
<Descriptions.Item label="验手机时间">
|
||||
{detail.phoneVerifiedAt ? new Date(detail.phoneVerifiedAt).toLocaleString('zh-CN') : '未验证'}
|
||||
</Descriptions.Item>
|
||||
@@ -645,7 +645,7 @@ export default function UsersPage() {
|
||||
<Descriptions.Item label="deviceKey">{detail.deviceKey || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="合并至">
|
||||
{detail.mergedInto
|
||||
? `${detail.mergedInto.userNo} (${detail.mergedInto.phone || '无手机'})`
|
||||
? `${detail.mergedInto.userNo} (${detail.mergedInto.phone ? maskPhone(detail.mergedInto.phone) : '无手机'})`
|
||||
: '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="被合并访客数">{detail.mergedFromCount ?? 0}</Descriptions.Item>
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
- 门店/合伙人/酒厂/物流账单列表、详情、导出展示收款账户(户名、账号、开户行)
|
||||
- 确认打款时可填写打款凭证号(`paymentRef`),已打款后在详情与导出中展示
|
||||
- 门店账单统一列表:T+1 终态「已打款」、手动提现终态「已结算」(均为 `PAID`,文案区分业务类型)
|
||||
- 门店 T+1「出账日」= 出账当天(核销窗口「昨日 00:00–今日 00:00」中的今天)
|
||||
- 核销详情展示核销门店主账户收款信息
|
||||
|
||||
### 3.4~3.8 Wave 能力
|
||||
|
||||
@@ -94,10 +94,6 @@ export interface DevPlanTaskDto {
|
||||
|
||||
attachmentUrls?: string[] | null;
|
||||
|
||||
versions?: Array<{ id: string; versionNo: string }>;
|
||||
|
||||
versionIds?: string[];
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -170,8 +166,6 @@ export interface UpdateDevPlanTaskInput {
|
||||
|
||||
attachmentUrls?: string[];
|
||||
|
||||
versionIds?: string[];
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ export interface AdminOrdersListQuery extends AdminListQuery {
|
||||
status?: string | string[];
|
||||
orderType?: string;
|
||||
userId?: string;
|
||||
userKeyword?: string;
|
||||
productKeyword?: string;
|
||||
cityId?: string;
|
||||
receiverPhone?: string;
|
||||
fulfillmentHold?: string | boolean;
|
||||
|
||||
@@ -146,7 +146,6 @@ export interface StoreBillDto {
|
||||
id: string;
|
||||
billNo: string;
|
||||
storeId: string;
|
||||
/** 出账日 YYYY-MM-DD(T+1 窗口「昨日 00:00–今日 00:00」中的今天) */
|
||||
billDate: string;
|
||||
redeemCount: number;
|
||||
redeemAmount: number;
|
||||
|
||||
@@ -91,12 +91,12 @@ async function main() {
|
||||
await prisma.storeBill.deleteMany({ where: { billNo: { startsWith: 'SBMOCK' } } });
|
||||
await prisma.partnerBill.deleteMany({ where: { billNo: { startsWith: 'PBMOCK' } } });
|
||||
|
||||
// ─── 1. 门店账单(今日出账未打款 + 昨日出账已打款)───
|
||||
// ─── 1. 门店账单(昨日未打款 + 前日已打款)───
|
||||
const storeBillUnpaid = await prisma.storeBill.create({
|
||||
data: {
|
||||
billNo: billNo('SB', `${yesterday.getTime()}U`),
|
||||
storeId: store.id,
|
||||
billDate: today,
|
||||
billDate: yesterday,
|
||||
redeemCount: 5,
|
||||
redeemAmount: 1500,
|
||||
settlementRate: 0.6,
|
||||
@@ -108,7 +108,7 @@ async function main() {
|
||||
data: {
|
||||
billNo: billNo('SB', `${dayBefore.getTime()}P`),
|
||||
storeId: store.id,
|
||||
billDate: yesterday,
|
||||
billDate: dayBefore,
|
||||
redeemCount: 3,
|
||||
redeemAmount: 800,
|
||||
settlementRate: 0.6,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AlertService } from '../common/alert/alert.service';
|
||||
|
||||
/**
|
||||
* 财务对账单定时任务(Asia/Shanghai)
|
||||
* - 每日 08:00:酒厂日账单(T+3:3 天前已完成订单)+ 门店日账单(昨日核销,出账日=今天)
|
||||
* - 每日 08:00:酒厂日账单(T+3:3 天前已完成订单)+ 门店日账单(昨日核销)
|
||||
* - 每月 1 日 08:00:合伙人上一自然月账单 + 物流承运商上一自然月对账
|
||||
* - 工作日 18:05:门店提现 T+0 审完预警(FIN-003)
|
||||
*/
|
||||
|
||||
@@ -156,16 +156,10 @@ export class DevPlanService {
|
||||
|
||||
},
|
||||
|
||||
extras?: {
|
||||
creatorName?: string | null;
|
||||
supportTicketNo?: string | null;
|
||||
versions?: Array<{ id: string; versionNo: string }>;
|
||||
},
|
||||
extras?: { creatorName?: string | null; supportTicketNo?: string | null },
|
||||
|
||||
): DevPlanTaskDto {
|
||||
|
||||
const versions = extras?.versions ?? [];
|
||||
|
||||
return {
|
||||
|
||||
id: String(row.id),
|
||||
@@ -194,10 +188,6 @@ export class DevPlanService {
|
||||
|
||||
attachmentUrls: parseAttachmentUrls(row.attachmentUrls),
|
||||
|
||||
versions,
|
||||
|
||||
versionIds: versions.map((v) => v.id),
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@@ -316,7 +306,7 @@ export class DevPlanService {
|
||||
|
||||
const ticketIds = rows.map((r) => r.supportTicketId).filter((id): id is bigint => id != null);
|
||||
|
||||
const [names, tickets, versionMap] = await Promise.all([
|
||||
const [names, tickets] = await Promise.all([
|
||||
|
||||
this.loadHqNames(creatorIds),
|
||||
|
||||
@@ -332,8 +322,6 @@ export class DevPlanService {
|
||||
|
||||
: Promise.resolve([]),
|
||||
|
||||
this.loadTaskVersionMap(rows.map((r) => r.id)),
|
||||
|
||||
]);
|
||||
|
||||
const ticketMap = new Map<string, string>(
|
||||
@@ -352,8 +340,6 @@ export class DevPlanService {
|
||||
|
||||
supportTicketNo: r.supportTicketId != null ? ticketMap.get(String(r.supportTicketId)) ?? null : null,
|
||||
|
||||
versions: versionMap.get(String(r.id)) ?? [],
|
||||
|
||||
}),
|
||||
|
||||
);
|
||||
@@ -388,16 +374,12 @@ export class DevPlanService {
|
||||
|
||||
}
|
||||
|
||||
const versionMap = await this.loadTaskVersionMap([row.id]);
|
||||
|
||||
return this.mapTask(row, {
|
||||
|
||||
creatorName: names.get(String(row.creatorHqAccountId)) ?? null,
|
||||
|
||||
supportTicketNo,
|
||||
|
||||
versions: versionMap.get(String(row.id)) ?? [],
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -526,10 +508,6 @@ export class DevPlanService {
|
||||
|
||||
await this.prisma.devPlanTask.update({ where: { id }, data });
|
||||
|
||||
if (dto.versionIds !== undefined) {
|
||||
await this.replaceTaskVersions(id, dto.versionIds);
|
||||
}
|
||||
|
||||
return this.getTask(id);
|
||||
|
||||
}
|
||||
@@ -628,41 +606,6 @@ export class DevPlanService {
|
||||
|
||||
|
||||
|
||||
private async loadTaskVersionMap(
|
||||
taskIds: bigint[],
|
||||
): Promise<Map<string, Array<{ id: string; versionNo: string }>>> {
|
||||
const map = new Map<string, Array<{ id: string; versionNo: string }>>();
|
||||
if (!taskIds.length) return map;
|
||||
const links = await this.prisma.devPlanVersionTask.findMany({
|
||||
where: { taskId: { in: taskIds } },
|
||||
include: { version: { select: { id: true, versionNo: true } } },
|
||||
orderBy: { versionId: 'asc' },
|
||||
});
|
||||
for (const link of links) {
|
||||
const key = String(link.taskId);
|
||||
const list = map.get(key) ?? [];
|
||||
list.push({ id: String(link.version.id), versionNo: link.version.versionNo });
|
||||
map.set(key, list);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private async replaceTaskVersions(taskId: bigint, versionIds: string[]) {
|
||||
const unique = [...new Set(versionIds.map((id) => id.trim()).filter(Boolean))];
|
||||
const ids = unique.map(BigInt);
|
||||
if (ids.length) {
|
||||
const versions = await this.prisma.devPlanVersion.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (versions.length !== ids.length) throw new BadRequestException('部分版本不存在');
|
||||
}
|
||||
await this.prisma.devPlanVersionTask.deleteMany({ where: { taskId } });
|
||||
for (const versionId of ids) {
|
||||
await this.appendVersionTasks(versionId, [String(taskId)]);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadVersionTasks(versionId: bigint): Promise<{ tasks: DevPlanTaskDto[]; taskIds: string[] }> {
|
||||
|
||||
const links = await this.prisma.devPlanVersionTask.findMany({
|
||||
|
||||
@@ -62,11 +62,6 @@ export class UpdateDevPlanTaskDto {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
attachmentUrls?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
versionIds?: string[];
|
||||
}
|
||||
|
||||
export class CreateDevPlanVersionDto {
|
||||
|
||||
@@ -30,8 +30,6 @@ type OrderFilterInput = Pick<
|
||||
| 'status'
|
||||
| 'orderType'
|
||||
| 'userId'
|
||||
| 'userKeyword'
|
||||
| 'productKeyword'
|
||||
| 'cityId'
|
||||
| 'receiverPhone'
|
||||
| 'fulfillmentHold'
|
||||
@@ -179,28 +177,8 @@ export class AdminOrdersService {
|
||||
}
|
||||
if (query.orderType) where.orderType = query.orderType as Prisma.EnumOrderTypeFilter['equals'];
|
||||
if (query.userId) where.userId = BigInt(query.userId);
|
||||
const userKeyword = query.userKeyword?.trim();
|
||||
if (userKeyword) {
|
||||
const userOr: Prisma.UserWhereInput[] = [
|
||||
{ userNo: { contains: userKeyword } },
|
||||
{ nickname: { contains: userKeyword } },
|
||||
{ hqRemark: { contains: userKeyword } },
|
||||
{ phone: { contains: userKeyword } },
|
||||
];
|
||||
if (/^\d+$/.test(userKeyword)) {
|
||||
userOr.push({ id: BigInt(userKeyword) });
|
||||
}
|
||||
where.user = { OR: userOr };
|
||||
}
|
||||
if (query.cityId) where.cityId = BigInt(query.cityId);
|
||||
if (query.receiverPhone) where.receiverPhone = { contains: query.receiverPhone };
|
||||
const productKeyword = query.productKeyword?.trim();
|
||||
if (productKeyword) {
|
||||
where.OR = [
|
||||
{ productName: { contains: productKeyword } },
|
||||
{ productSpec: { contains: productKeyword } },
|
||||
];
|
||||
}
|
||||
if (query.deliveryType) {
|
||||
where.deliveryType = query.deliveryType as Prisma.EnumDeliveryTypeFilter['equals'];
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export class AdminRedeemService {
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true, hqRemark: true } },
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||
store: { select: { id: true, name: true, cityName: true } },
|
||||
coupon: { select: { id: true, couponNo: true, balance: true } },
|
||||
},
|
||||
@@ -77,7 +77,7 @@ export class AdminRedeemService {
|
||||
const record = await this.prisma.redeemRecord.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true, hqRemark: true } },
|
||||
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||
store: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -93,16 +93,6 @@ export class AdminOrdersQueryDto extends PaginationQueryDto {
|
||||
@IsString()
|
||||
userId?: string;
|
||||
|
||||
/** 用户编号 / 昵称 / 备注 / 手机 模糊匹配 */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userKeyword?: string;
|
||||
|
||||
/** 商品名称 / 规格 模糊匹配 */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productKeyword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cityId?: string;
|
||||
@@ -169,14 +159,6 @@ export class AdminOrdersExportDto {
|
||||
@IsString()
|
||||
cityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userKeyword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productKeyword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
receiverPhone?: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException, OnModuleInit } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import {
|
||||
DEFAULT_STORE_WITHDRAW_DAILY_LIMIT,
|
||||
@@ -55,16 +55,11 @@ function startOfDay(d: Date) {
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/** 核销窗口:昨日 00:00 ≤ t < 今日 00:00;出账日 = 今日 00:00(窗口右端) */
|
||||
function dayWindow(anchor = new Date()) {
|
||||
const end = startOfDay(anchor);
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 1);
|
||||
return { start, end, billDate: end };
|
||||
}
|
||||
|
||||
function shanghaiYmd(d: Date): string {
|
||||
return d.toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' });
|
||||
return { start, end, billDate: start };
|
||||
}
|
||||
|
||||
function wineryDayWindow(anchor = new Date(), lagDays = WINERY_SETTLEMENT_LAG_DAYS) {
|
||||
@@ -136,9 +131,7 @@ function isWithdrawOverdue(appliedAt: Date, now = new Date()): boolean {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SettlementService implements OnModuleInit {
|
||||
private readonly logger = new Logger(SettlementService.name);
|
||||
|
||||
export class SettlementService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
@@ -148,37 +141,6 @@ export class SettlementService implements OnModuleInit {
|
||||
private readonly wecomPush: WecomMessagePushService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
try {
|
||||
const shifted = await this.realignStoreBillIssueDates();
|
||||
if (shifted > 0) this.logger.log(`Store bill dates aligned to issue day: ${shifted}`);
|
||||
} catch (e) {
|
||||
this.logger.warn(`Store bill date align skipped: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧数据 billDate=核销自然日(窗口左端);现改为出账日(窗口右端=今天)。
|
||||
* 按 created_at 日历日对比,只把仍早一天的行 +1;从晚到早更新避免 (storeId,billDate) 冲突。
|
||||
*/
|
||||
private async realignStoreBillIssueDates(): Promise<number> {
|
||||
const locked = await this.prisma.$queryRaw<Array<{ acquired: number | bigint | null }>>`
|
||||
SELECT GET_LOCK('store_bill_issue_date_align', 5) AS acquired
|
||||
`;
|
||||
if (!Number(locked[0]?.acquired)) return 0;
|
||||
try {
|
||||
const shifted = await this.prisma.$executeRaw`
|
||||
UPDATE store_bill
|
||||
SET bill_date = DATE_ADD(bill_date, INTERVAL 1 DAY)
|
||||
WHERE DATE(bill_date) < DATE(created_at)
|
||||
ORDER BY bill_date DESC
|
||||
`;
|
||||
return Number(shifted);
|
||||
} finally {
|
||||
await this.prisma.$queryRaw`SELECT RELEASE_LOCK('store_bill_issue_date_align')`;
|
||||
}
|
||||
}
|
||||
|
||||
private notifyPartnerBillDigest(
|
||||
period: string,
|
||||
rows: Array<{
|
||||
@@ -1013,7 +975,7 @@ export class SettlementService implements OnModuleInit {
|
||||
|
||||
// ─── Store bills (daily header) ──────────────────────
|
||||
|
||||
/** 生成昨日核销窗口的门店对账单;billDate = 出账日(今日) */
|
||||
/** 生成指定自然日窗口内的门店对账单(默认昨日) */
|
||||
async generateStoreBillsForDay(anchor = new Date()) {
|
||||
const { start, end, billDate } = dayWindow(anchor);
|
||||
const payouts = await this.prisma.storePayout.findMany({
|
||||
@@ -1138,7 +1100,7 @@ export class SettlementService implements OnModuleInit {
|
||||
});
|
||||
}
|
||||
|
||||
const period = shanghaiYmd(billDate);
|
||||
const period = billDate.toISOString().slice(0, 10);
|
||||
if (createdBills.length) {
|
||||
const banks = await loadStorePrimaryBanksMap(
|
||||
this.prisma,
|
||||
@@ -1303,7 +1265,6 @@ export class SettlementService implements OnModuleInit {
|
||||
return serializeBigInt({
|
||||
items: slice.map((r) => ({
|
||||
...r,
|
||||
date: r.kind === 'T1_BILL' ? shanghaiYmd(r.date) : r.date,
|
||||
bankAccount:
|
||||
r.kind === 'T1_BILL'
|
||||
? bankMap.get(String(r.storeId)) ?? null
|
||||
@@ -1358,13 +1319,7 @@ export class SettlementService implements OnModuleInit {
|
||||
totalAmount: Number(aggregates._sum.payoutAmount ?? 0),
|
||||
};
|
||||
|
||||
return serializeBigInt({
|
||||
items: items.map((b) => ({ ...b, billDate: shanghaiYmd(b.billDate) })),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
summary,
|
||||
});
|
||||
return serializeBigInt({ items, total, page, pageSize, summary });
|
||||
}
|
||||
|
||||
async getAdminStoreBill(id: bigint) {
|
||||
@@ -1380,7 +1335,7 @@ export class SettlementService implements OnModuleInit {
|
||||
});
|
||||
if (!bill) throw new NotFoundException('门店对账单不存在');
|
||||
const storeAccount = await loadStorePrimaryBank(this.prisma, bill.storeId);
|
||||
return serializeBigInt({ ...bill, billDate: shanghaiYmd(bill.billDate), storeAccount });
|
||||
return serializeBigInt({ ...bill, storeAccount });
|
||||
}
|
||||
|
||||
async confirmStoreBill(id: bigint, dto: { paymentRef?: string } = {}) {
|
||||
@@ -1438,7 +1393,7 @@ export class SettlementService implements OnModuleInit {
|
||||
);
|
||||
const header = [
|
||||
'账单号',
|
||||
'出账日',
|
||||
'账单日',
|
||||
'门店',
|
||||
'城市',
|
||||
'核销笔数',
|
||||
@@ -1456,7 +1411,7 @@ export class SettlementService implements OnModuleInit {
|
||||
const bank = bankMap.get(String(b.storeId));
|
||||
return [
|
||||
csvEscape(b.billNo),
|
||||
shanghaiYmd(b.billDate),
|
||||
b.billDate.toISOString().slice(0, 10),
|
||||
csvEscape(b.store.name),
|
||||
csvEscape(b.store.cityName ?? ''),
|
||||
b.redeemCount,
|
||||
|
||||
Reference in New Issue
Block a user