Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b5f42acdf | |||
| 1ecc6a11df | |||
| a423fb71e8 | |||
| 3a9f27eb38 | |||
| 6a5c0cb090 | |||
| 00bb6a2227 | |||
| 93390ccf75 | |||
| 8f2ebbfef4 | |||
| f2fbf95c53 | |||
| 43de361e61 | |||
| 0b548a2764 | |||
| 40e8a12596 | |||
| 3d819dc10b | |||
| 8984465893 | |||
| 3f208d27a9 | |||
| fa69ced448 | |||
| 18c5abea79 | |||
| 5d4ed566de | |||
| 5fdddae41c | |||
| 22c0b03a47 | |||
| f2d03e2595 | |||
| 0c28c204b2 | |||
| e7f49a9639 | |||
| f941dcf072 | |||
| 3382d36a6c | |||
| 9b8e3f1347 | |||
| 1440a59fc8 | |||
| 11659484c6 | |||
| 0fb7ab7abb |
@@ -74,56 +74,6 @@ body.admin-col-resizing * {
|
|||||||
user-select: none !important;
|
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 {
|
.admin-list-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -15,13 +15,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function RedeemRecordDetailDescriptions({ detail, maskUserPhone = false }: Props) {
|
export default function RedeemRecordDetailDescriptions({ detail, maskUserPhone = false }: Props) {
|
||||||
const user = detail.user as
|
const user = detail.user as
|
||||||
| {
|
| { userNo?: string; nickname?: string | null; phone?: string | null }
|
||||||
id?: string;
|
|
||||||
userNo?: string;
|
|
||||||
nickname?: string | null;
|
|
||||||
phone?: string | null;
|
|
||||||
hqRemark?: string | null;
|
|
||||||
}
|
|
||||||
| undefined;
|
| undefined;
|
||||||
const store = detail.store as
|
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="结算额">¥{Number(detail.settleAmount ?? 0).toFixed(2)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt ?? ''))}</Descriptions.Item>
|
<Descriptions.Item label="时间">{fmtTime(String(detail.createdAt ?? ''))}</Descriptions.Item>
|
||||||
<Descriptions.Item label="用户编号">{user?.userNo ?? '—'}</Descriptions.Item>
|
<Descriptions.Item label="用户编号">{user?.userNo ?? '—'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="用户昵称">
|
<Descriptions.Item label="用户昵称">{user?.nickname?.trim() || '—'}</Descriptions.Item>
|
||||||
{(() => {
|
|
||||||
const name = user?.nickname?.trim() || '—';
|
|
||||||
const remark = user?.hqRemark?.trim();
|
|
||||||
return remark ? `${name}(${remark})` : name;
|
|
||||||
})()}
|
|
||||||
</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="用户手机">
|
<Descriptions.Item label="用户手机">
|
||||||
{maskUserPhone ? maskPhone(user?.phone) : user?.phone || '—'}
|
{maskUserPhone ? maskPhone(user?.phone) : user?.phone || '—'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
@@ -111,16 +111,9 @@ type CouponDetail = Row & {
|
|||||||
|
|
||||||
export default function BenefitCouponsPage() {
|
export default function BenefitCouponsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const initialCouponNo = searchParams.get('couponNo')?.trim() || '';
|
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [grantForm] = Form.useForm<AdminBenefitGrantRequest>();
|
const [grantForm] = Form.useForm<AdminBenefitGrantRequest>();
|
||||||
const [filters, setFilters] = useState<Record<string, string>>(() => {
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||||
const init: Record<string, string> = {};
|
|
||||||
if (initialCouponNo) init.couponNo = initialCouponNo;
|
|
||||||
return init;
|
|
||||||
});
|
|
||||||
const [grantOpen, setGrantOpen] = useState(false);
|
const [grantOpen, setGrantOpen] = useState(false);
|
||||||
const [granting, setGranting] = useState(false);
|
const [granting, setGranting] = useState(false);
|
||||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
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 [redeemDetail, setRedeemDetail] = useState<Record<string, unknown> | null>(null);
|
||||||
const [redeemDrawerOpen, setRedeemDrawerOpen] = useState(false);
|
const [redeemDrawerOpen, setRedeemDrawerOpen] = useState(false);
|
||||||
const [redeemDetailLoading, setRedeemDetailLoading] = 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) {
|
async function openRedeemDetail(redeemId: string) {
|
||||||
setRedeemDetailLoading(true);
|
setRedeemDetailLoading(true);
|
||||||
@@ -190,7 +154,12 @@ export default function BenefitCouponsPage() {
|
|||||||
width: 200,
|
width: 200,
|
||||||
ellipsis: false,
|
ellipsis: false,
|
||||||
render: (v, row) => (
|
render: (v, row) => (
|
||||||
<AdminPrimaryLink onClick={() => void openCouponDetail(row.id)}>
|
<AdminPrimaryLink
|
||||||
|
onClick={async () => {
|
||||||
|
setDetail(await request(`/admin/benefit/coupons/${row.id}`));
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
{v}
|
{v}
|
||||||
</AdminPrimaryLink>
|
</AdminPrimaryLink>
|
||||||
),
|
),
|
||||||
@@ -242,7 +211,10 @@ export default function BenefitCouponsPage() {
|
|||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => void openCouponDetail(row.id)}
|
onClick={async () => {
|
||||||
|
setDetail(await request(`/admin/benefit/coupons/${row.id}`));
|
||||||
|
setDrawerOpen(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
详情
|
详情
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ export default function DevPlanTasksPage() {
|
|||||||
const [batchForm] = Form.useForm<{ status?: DevPlanTaskStatusDto; versionId?: string }>();
|
const [batchForm] = Form.useForm<{ status?: DevPlanTaskStatusDto; versionId?: string }>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!modalOpen && !batchEditOpen) return;
|
if (!batchEditOpen) return;
|
||||||
request<{ items: DevPlanVersionDto[] }>('/admin/dev-plan/versions?pageSize=100')
|
request<{ items: DevPlanVersionDto[] }>('/admin/dev-plan/versions?pageSize=100')
|
||||||
.then((res) => setVersions(res.items ?? []))
|
.then((res) => setVersions(res.items ?? []))
|
||||||
.catch(() => setVersions([]));
|
.catch(() => setVersions([]));
|
||||||
}, [modalOpen, batchEditOpen]);
|
}, [batchEditOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!modalOpen || editing) return;
|
if (!modalOpen || editing) return;
|
||||||
@@ -108,7 +108,6 @@ export default function DevPlanTasksPage() {
|
|||||||
content: row.content,
|
content: row.content,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
status: row.status,
|
status: row.status,
|
||||||
versionIds: row.versionIds?.length ? row.versionIds : row.versions?.map((v) => v.id) ?? [],
|
|
||||||
attachmentUrls: row.attachmentUrls?.length ? row.attachmentUrls : [''],
|
attachmentUrls: row.attachmentUrls?.length ? row.attachmentUrls : [''],
|
||||||
});
|
});
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
@@ -122,11 +121,7 @@ export default function DevPlanTasksPage() {
|
|||||||
if (editing) {
|
if (editing) {
|
||||||
await request(`/admin/dev-plan/tasks/${editing.id}`, {
|
await request(`/admin/dev-plan/tasks/${editing.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ ...values, attachmentUrls }),
|
||||||
...values,
|
|
||||||
attachmentUrls,
|
|
||||||
versionIds: values.versionIds ?? [],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
message.success('已更新');
|
message.success('已更新');
|
||||||
} else {
|
} else {
|
||||||
@@ -290,21 +285,6 @@ export default function DevPlanTasksPage() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ title: '来源工单', dataIndex: 'supportTicketNo', width: 140, render: (v) => v || '—' },
|
{ 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: 'creatorName', width: 90 },
|
||||||
{ title: '创建时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
{ title: '创建时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||||
{
|
{
|
||||||
@@ -425,21 +405,9 @@ export default function DevPlanTasksPage() {
|
|||||||
<Select options={TYPE_OPTIONS} />
|
<Select options={TYPE_OPTIONS} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<>
|
|
||||||
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
|
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
|
||||||
<Select options={STATUS_OPTIONS} />
|
<Select options={STATUS_OPTIONS} />
|
||||||
</Form.Item>
|
</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="绑定技术支持工单">
|
<Form.Item name="supportTicketId" label="绑定技术支持工单">
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Col,
|
||||||
Collapse,
|
Collapse,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
InputNumber,
|
InputNumber,
|
||||||
Modal,
|
Modal,
|
||||||
Radio,
|
Radio,
|
||||||
|
Row,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Table,
|
Table,
|
||||||
@@ -22,7 +24,6 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import dayjs, { type Dayjs } from 'dayjs';
|
import dayjs, { type Dayjs } from 'dayjs';
|
||||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
|
||||||
import { ORDER_TYPE_LABELS } from '@dukang/shared-types';
|
import { ORDER_TYPE_LABELS } from '@dukang/shared-types';
|
||||||
import { request, type AdminOrderItem, type AdminOrderRow, type HqProfile, type Paginated } from '../lib/api';
|
import { request, type AdminOrderItem, type AdminOrderRow, type HqProfile, type Paginated } from '../lib/api';
|
||||||
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||||
@@ -149,9 +150,8 @@ type OrderExportFilters = {
|
|||||||
orderType?: string;
|
orderType?: string;
|
||||||
cityId?: string;
|
cityId?: string;
|
||||||
receiverPhone?: string;
|
receiverPhone?: string;
|
||||||
userKeyword?: string;
|
|
||||||
productKeyword?: string;
|
|
||||||
fulfillmentHold?: boolean;
|
fulfillmentHold?: boolean;
|
||||||
|
excludeTest?: boolean;
|
||||||
deliveryType?: string;
|
deliveryType?: string;
|
||||||
promoCodeId?: string;
|
promoCodeId?: string;
|
||||||
dateRange?: [Dayjs, Dayjs];
|
dateRange?: [Dayjs, Dayjs];
|
||||||
@@ -213,8 +213,6 @@ const DATE_PRESETS: Array<{ key: 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|||||||
{ key: 'year', label: '当年' },
|
{ key: 'year', label: '当年' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const FILTERS_COLLAPSED_KEY = 'admin-orders-filters-collapsed';
|
|
||||||
|
|
||||||
function formatUserWithRemark(user?: {
|
function formatUserWithRemark(user?: {
|
||||||
userNo?: string;
|
userNo?: string;
|
||||||
hqRemark?: string | null;
|
hqRemark?: string | null;
|
||||||
@@ -256,10 +254,9 @@ function buildExportPayload(
|
|||||||
if (filters.orderType) payload.orderType = filters.orderType;
|
if (filters.orderType) payload.orderType = filters.orderType;
|
||||||
if (filters.cityId) payload.cityId = filters.cityId;
|
if (filters.cityId) payload.cityId = filters.cityId;
|
||||||
if (filters.receiverPhone) payload.receiverPhone = filters.receiverPhone;
|
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.deliveryType) payload.deliveryType = filters.deliveryType;
|
||||||
if (filters.fulfillmentHold) payload.fulfillmentHold = true;
|
if (filters.fulfillmentHold) payload.fulfillmentHold = true;
|
||||||
|
if (filters.excludeTest) payload.excludeTest = true;
|
||||||
if (filters.promoCodeId) payload.promoCodeId = filters.promoCodeId;
|
if (filters.promoCodeId) payload.promoCodeId = filters.promoCodeId;
|
||||||
if (filters.dateRange?.[0]) payload.createdFrom = filters.dateRange[0].format('YYYY-MM-DD');
|
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');
|
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 [trackOpen, setTrackOpen] = useState(false);
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
const [exportFormat, setExportFormat] = useState<OrderExportFormat>('xlsx');
|
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 canDeleteOrders = (profile?.permissionKeys ?? []).includes('orders_delete');
|
||||||
const canProxyOrder = (profile?.permissionKeys ?? []).includes('orders');
|
const canProxyOrder = (profile?.permissionKeys ?? []).includes('orders');
|
||||||
const canExportOrders = (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.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.userKeyword) qs.set('userKeyword', values.userKeyword);
|
|
||||||
if (values.productKeyword) qs.set('productKeyword', values.productKeyword);
|
|
||||||
if (values.fulfillmentHold) qs.set('fulfillmentHold', 'true');
|
if (values.fulfillmentHold) qs.set('fulfillmentHold', 'true');
|
||||||
|
if (values.excludeTest) qs.set('excludeTest', 'true');
|
||||||
if (values.deliveryType) qs.set('deliveryType', values.deliveryType);
|
if (values.deliveryType) qs.set('deliveryType', values.deliveryType);
|
||||||
if (initialPromoCodeId) qs.set('promoCodeId', initialPromoCodeId);
|
if (initialPromoCodeId) qs.set('promoCodeId', initialPromoCodeId);
|
||||||
if (values.dateRange?.[0]) qs.set('createdFrom', values.dateRange[0].format('YYYY-MM-DD'));
|
if (values.dateRange?.[0]) qs.set('createdFrom', values.dateRange[0].format('YYYY-MM-DD'));
|
||||||
@@ -854,72 +843,14 @@ export default function OrdersPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Form form={form} layout="vertical" onFinish={() => { setPage(1); void load(); }}>
|
<Form form={form} layout="vertical" onFinish={() => { setPage(1); void load(); }}>
|
||||||
<div className="admin-orders-filter-row">
|
<Row gutter={[16, 8]}>
|
||||||
<Form.Item name="cityId" label="城市" className="admin-orders-filter-item admin-orders-filter-item--sm">
|
<Col xs={24} md={14} lg={12}>
|
||||||
<Select
|
<Form.Item label="下单日期" style={{ marginBottom: 12 }}>
|
||||||
allowClear
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||||
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>
|
|
||||||
<Form.Item name="dateRange" noStyle>
|
<Form.Item name="dateRange" noStyle>
|
||||||
<DatePicker.RangePicker allowClear className="admin-orders-date-picker" />
|
<DatePicker.RangePicker allowClear style={{ width: '100%' }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Space size={8} wrap>
|
||||||
{DATE_PRESETS.map((preset) => (
|
{DATE_PRESETS.map((preset) => (
|
||||||
<Button
|
<Button
|
||||||
key={preset.key}
|
key={preset.key}
|
||||||
@@ -930,10 +861,61 @@ export default function OrdersPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Space>
|
||||||
|
</Space>
|
||||||
</Form.Item>
|
</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 ? (
|
{canExportOrders ? (
|
||||||
<Space size={12} wrap>
|
<Space size={12} wrap>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
@@ -958,9 +940,7 @@ export default function OrdersPage() {
|
|||||||
</Space>
|
</Space>
|
||||||
) : <span />}
|
) : <span />}
|
||||||
<Space size={12} wrap>
|
<Space size={12} wrap>
|
||||||
<Form.Item name="fulfillmentHold" valuePropName="checked" noStyle>
|
<Button type="primary" htmlType="submit">查询</Button>
|
||||||
<Checkbox>大单拦截</Checkbox>
|
|
||||||
</Form.Item>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
@@ -982,8 +962,6 @@ export default function OrdersPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
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 { Button, Checkbox, Drawer, Form, Input, Select, Table, Tag, Typography } from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import { REDEEM_CHANNEL_LABELS, type RedeemChannel } from '@dukang/shared-types';
|
import { REDEEM_CHANNEL_LABELS, type RedeemChannel } from '@dukang/shared-types';
|
||||||
@@ -20,28 +20,17 @@ type Row = {
|
|||||||
channel?: RedeemChannel;
|
channel?: RedeemChannel;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
isTest?: boolean;
|
isTest?: boolean;
|
||||||
user?: {
|
user?: { userNo: string; phone: string | null; nickname?: string | null };
|
||||||
id?: string;
|
|
||||||
userNo: string;
|
|
||||||
phone: string | null;
|
|
||||||
nickname?: string | null;
|
|
||||||
hqRemark?: string | null;
|
|
||||||
};
|
|
||||||
store?: { name: string; cityName: string };
|
store?: { name: string; cityName: string };
|
||||||
coupon?: { id?: string; couponNo: string };
|
coupon?: { couponNo: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatNicknameWithRemark(user?: {
|
function maskPhone(phone: string | null | undefined) {
|
||||||
nickname?: string | null;
|
if (!phone || phone.length < 7) return phone ?? '—';
|
||||||
hqRemark?: string | null;
|
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
|
||||||
} | null) {
|
|
||||||
const name = user?.nickname?.trim() || '—';
|
|
||||||
const remark = user?.hqRemark?.trim();
|
|
||||||
return remark ? `${name}(${remark})` : name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RedeemRecordsPage() {
|
export default function RedeemRecordsPage() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const initialRedeemNo = searchParams.get('redeemNo')?.trim() || '';
|
const initialRedeemNo = searchParams.get('redeemNo')?.trim() || '';
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -119,60 +108,23 @@ export default function RedeemRecordsPage() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ title: '用户编号', dataIndex: ['user', 'userNo'], width: 110, render: (v: string | undefined, row) =>
|
{ title: '用户编号', dataIndex: ['user', 'userNo'], width: 110 },
|
||||||
row.user?.id ? (
|
|
||||||
<AdminPrimaryLink
|
|
||||||
onClick={() => navigate('/users', { state: { openUserId: String(row.user!.id) } })}
|
|
||||||
>
|
|
||||||
{v || '—'}
|
|
||||||
</AdminPrimaryLink>
|
|
||||||
) : (
|
|
||||||
v || '—'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '用户昵称',
|
title: '用户昵称',
|
||||||
dataIndex: ['user', 'nickname'],
|
dataIndex: ['user', 'nickname'],
|
||||||
width: 160,
|
width: 100,
|
||||||
render: (_: string | null | undefined, row) =>
|
render: (v: string | null | undefined) => v || '—',
|
||||||
row.user?.id ? (
|
|
||||||
<AdminPrimaryLink
|
|
||||||
onClick={() => navigate('/users', { state: { openUserId: String(row.user!.id) } })}
|
|
||||||
>
|
|
||||||
{formatNicknameWithRemark(row.user)}
|
|
||||||
</AdminPrimaryLink>
|
|
||||||
) : (
|
|
||||||
formatNicknameWithRemark(row.user)
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '用户手机',
|
title: '用户手机',
|
||||||
dataIndex: ['user', 'phone'],
|
dataIndex: ['user', 'phone'],
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (v: string | null | undefined) => v || '—',
|
render: (v: string | null | undefined) => maskPhone(v),
|
||||||
},
|
},
|
||||||
{ title: '门店', dataIndex: ['store', 'name'] },
|
{ title: '门店', dataIndex: ['store', 'name'] },
|
||||||
{ title: '核销额', dataIndex: 'amount', width: 90, render: (v) => `¥${v}` },
|
{ title: '核销额', dataIndex: 'amount', width: 90, render: (v) => `¥${v}` },
|
||||||
{ title: '结算额', dataIndex: 'settleAmount', width: 90, render: (v) => `¥${v}` },
|
{ title: '结算额', dataIndex: 'settleAmount', width: 90, render: (v) => `¥${v}` },
|
||||||
{
|
{ title: '券号', dataIndex: ['coupon', 'couponNo'], width: 160 },
|
||||||
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: 'createdAt', width: 160, render: fmtTime },
|
{ title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
Statistic,
|
Statistic,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
|
||||||
Typography,
|
Typography,
|
||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
@@ -76,7 +75,6 @@ const KIND_COLORS: Record<StoreSettlementKind, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function StoreBillsPage() {
|
export default function StoreBillsPage() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const initialKind = searchParams.get('kind') === 'WITHDRAW' ? 'WITHDRAW' : '';
|
const initialKind = searchParams.get('kind') === 'WITHDRAW' ? 'WITHDRAW' : '';
|
||||||
const initialStoreId = searchParams.get('storeId') || '';
|
const initialStoreId = searchParams.get('storeId') || '';
|
||||||
@@ -296,11 +294,7 @@ export default function StoreBillsPage() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: (
|
title: '日期',
|
||||||
<Tooltip title="T+1 为出账日(昨日 00:00–今日 00:00 窗口中的今天,不是该账单里最后一笔核销时间);手动提现为申请时间">
|
|
||||||
出账日
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
dataIndex: 'date',
|
dataIndex: 'date',
|
||||||
width: 160,
|
width: 160,
|
||||||
render: (v, row) => (row.kind === 'T1_BILL' ? String(v || '').slice(0, 10) : fmtTime(v)),
|
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 出账终态,「已结算」= 手动提现终态。
|
「已打款」= T+1 出账终态,「已结算」= 手动提现终态。
|
||||||
T+1「出账日」是任务跑的那天(昨日 00:00–今日 00:00 窗口中的今天),不是最后一笔核销时间。
|
|
||||||
{overdueSummary && overdueSummary.overdueCount > 0 ? (
|
{overdueSummary && overdueSummary.overdueCount > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text type="danger">
|
<Typography.Text type="danger">
|
||||||
@@ -476,7 +469,7 @@ export default function StoreBillsPage() {
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="range" label="出账日">
|
<Form.Item name="range" label="日期">
|
||||||
<DatePicker.RangePicker />
|
<DatePicker.RangePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
@@ -550,7 +543,7 @@ export default function StoreBillsPage() {
|
|||||||
<>
|
<>
|
||||||
<Descriptions column={1} size="small" bordered>
|
<Descriptions column={1} size="small" bordered>
|
||||||
<Descriptions.Item label="账单号">{String(detail.billNo)}</Descriptions.Item>
|
<Descriptions.Item label="账单号">{String(detail.billNo)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="出账日">
|
<Descriptions.Item label="账单日">
|
||||||
{String(detail.billDate || '').slice(0, 10)}
|
{String(detail.billDate || '').slice(0, 10)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="应付">
|
<Descriptions.Item label="应付">
|
||||||
@@ -599,22 +592,8 @@ export default function StoreBillsPage() {
|
|||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
title: '核销单号',
|
title: '核销单号',
|
||||||
render: (_, r) => {
|
render: (_, r) =>
|
||||||
const redeemNo = String(
|
String((r.redeemRecord as { redeemNo?: string })?.redeemNo || '—'),
|
||||||
(r.redeemRecord as { redeemNo?: string } | undefined)?.redeemNo || '',
|
|
||||||
);
|
|
||||||
return redeemNo ? (
|
|
||||||
<AdminPrimaryLink
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/redeem-records?redeemNo=${encodeURIComponent(redeemNo)}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{redeemNo}
|
|
||||||
</AdminPrimaryLink>
|
|
||||||
) : (
|
|
||||||
'—'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '金额',
|
title: '金额',
|
||||||
@@ -683,18 +662,7 @@ export default function StoreBillsPage() {
|
|||||||
const payout = r.storePayout as
|
const payout = r.storePayout as
|
||||||
| { redeemRecord?: { redeemNo?: string }; payoutAmount?: number }
|
| { redeemRecord?: { redeemNo?: string }; payoutAmount?: number }
|
||||||
| undefined;
|
| undefined;
|
||||||
const redeemNo = payout?.redeemRecord?.redeemNo || '';
|
return String(payout?.redeemRecord?.redeemNo || '—');
|
||||||
return redeemNo ? (
|
|
||||||
<AdminPrimaryLink
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/redeem-records?redeemNo=${encodeURIComponent(redeemNo)}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{redeemNo}
|
|
||||||
</AdminPrimaryLink>
|
|
||||||
) : (
|
|
||||||
'—'
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import { fmtTime } from '../lib/constants';
|
|||||||
import { useAdminList } from '../lib/useAdminList';
|
import { useAdminList } from '../lib/useAdminList';
|
||||||
import OssUpload from '../components/OssUpload';
|
import OssUpload from '../components/OssUpload';
|
||||||
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||||
import { AdminListHeader } from '../components/AdminListHeader';
|
|
||||||
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
|
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
|
||||||
|
|
||||||
|
|
||||||
@@ -561,11 +560,19 @@ export default function SupportTicketsPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{settingsModal}
|
{settingsModal}
|
||||||
<AdminListHeader
|
<div
|
||||||
title="技术支持"
|
style={{
|
||||||
settings={settingsButton}
|
display: 'flex',
|
||||||
actions={
|
justifyContent: 'space-between',
|
||||||
<>
|
alignItems: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
|
技术支持
|
||||||
|
</Typography.Title>
|
||||||
|
{settingsButton}
|
||||||
|
<Space>
|
||||||
{isSuperAdmin ? (
|
{isSuperAdmin ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@@ -595,9 +602,8 @@ export default function SupportTicketsPage() {
|
|||||||
<Button type="primary" onClick={() => setCreateOpen(true)}>
|
<Button type="primary" onClick={() => setCreateOpen(true)}>
|
||||||
创建工单
|
创建工单
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</Space>
|
||||||
}
|
</div>
|
||||||
/>
|
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
layout="inline"
|
layout="inline"
|
||||||
|
|||||||
@@ -611,7 +611,7 @@ export default function UsersPage() {
|
|||||||
<Descriptions.Item label="用户编号">{detail.userNo}</Descriptions.Item>
|
<Descriptions.Item label="用户编号">{detail.userNo}</Descriptions.Item>
|
||||||
<Descriptions.Item label="昵称">{detail.nickname || '—'}</Descriptions.Item>
|
<Descriptions.Item label="昵称">{detail.nickname || '—'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="备注">{detail.hqRemark || '—'}</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="验手机时间">
|
<Descriptions.Item label="验手机时间">
|
||||||
{detail.phoneVerifiedAt ? new Date(detail.phoneVerifiedAt).toLocaleString('zh-CN') : '未验证'}
|
{detail.phoneVerifiedAt ? new Date(detail.phoneVerifiedAt).toLocaleString('zh-CN') : '未验证'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@@ -645,7 +645,7 @@ export default function UsersPage() {
|
|||||||
<Descriptions.Item label="deviceKey">{detail.deviceKey || '—'}</Descriptions.Item>
|
<Descriptions.Item label="deviceKey">{detail.deviceKey || '—'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="合并至">
|
<Descriptions.Item label="合并至">
|
||||||
{detail.mergedInto
|
{detail.mergedInto
|
||||||
? `${detail.mergedInto.userNo} (${detail.mergedInto.phone || '无手机'})`
|
? `${detail.mergedInto.userNo} (${detail.mergedInto.phone ? maskPhone(detail.mergedInto.phone) : '无手机'})`
|
||||||
: '—'}
|
: '—'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="被合并访客数">{detail.mergedFromCount ?? 0}</Descriptions.Item>
|
<Descriptions.Item label="被合并访客数">{detail.mergedFromCount ?? 0}</Descriptions.Item>
|
||||||
|
|||||||
@@ -112,7 +112,6 @@
|
|||||||
- 门店/合伙人/酒厂/物流账单列表、详情、导出展示收款账户(户名、账号、开户行)
|
- 门店/合伙人/酒厂/物流账单列表、详情、导出展示收款账户(户名、账号、开户行)
|
||||||
- 确认打款时可填写打款凭证号(`paymentRef`),已打款后在详情与导出中展示
|
- 确认打款时可填写打款凭证号(`paymentRef`),已打款后在详情与导出中展示
|
||||||
- 门店账单统一列表:T+1 终态「已打款」、手动提现终态「已结算」(均为 `PAID`,文案区分业务类型)
|
- 门店账单统一列表:T+1 终态「已打款」、手动提现终态「已结算」(均为 `PAID`,文案区分业务类型)
|
||||||
- 门店 T+1「出账日」= 出账当天(核销窗口「昨日 00:00–今日 00:00」中的今天)
|
|
||||||
- 核销详情展示核销门店主账户收款信息
|
- 核销详情展示核销门店主账户收款信息
|
||||||
|
|
||||||
### 3.4~3.8 Wave 能力
|
### 3.4~3.8 Wave 能力
|
||||||
|
|||||||
@@ -94,10 +94,6 @@ export interface DevPlanTaskDto {
|
|||||||
|
|
||||||
attachmentUrls?: string[] | null;
|
attachmentUrls?: string[] | null;
|
||||||
|
|
||||||
versions?: Array<{ id: string; versionNo: string }>;
|
|
||||||
|
|
||||||
versionIds?: string[];
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -170,8 +166,6 @@ export interface UpdateDevPlanTaskInput {
|
|||||||
|
|
||||||
attachmentUrls?: string[];
|
attachmentUrls?: string[];
|
||||||
|
|
||||||
versionIds?: string[];
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export interface AdminOrdersListQuery extends AdminListQuery {
|
|||||||
status?: string | string[];
|
status?: string | string[];
|
||||||
orderType?: string;
|
orderType?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
userKeyword?: string;
|
|
||||||
productKeyword?: string;
|
|
||||||
cityId?: string;
|
cityId?: string;
|
||||||
receiverPhone?: string;
|
receiverPhone?: string;
|
||||||
fulfillmentHold?: string | boolean;
|
fulfillmentHold?: string | boolean;
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ export interface StoreBillDto {
|
|||||||
id: string;
|
id: string;
|
||||||
billNo: string;
|
billNo: string;
|
||||||
storeId: string;
|
storeId: string;
|
||||||
/** 出账日 YYYY-MM-DD(T+1 窗口「昨日 00:00–今日 00:00」中的今天) */
|
|
||||||
billDate: string;
|
billDate: string;
|
||||||
redeemCount: number;
|
redeemCount: number;
|
||||||
redeemAmount: number;
|
redeemAmount: number;
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ async function main() {
|
|||||||
await prisma.storeBill.deleteMany({ where: { billNo: { startsWith: 'SBMOCK' } } });
|
await prisma.storeBill.deleteMany({ where: { billNo: { startsWith: 'SBMOCK' } } });
|
||||||
await prisma.partnerBill.deleteMany({ where: { billNo: { startsWith: 'PBMOCK' } } });
|
await prisma.partnerBill.deleteMany({ where: { billNo: { startsWith: 'PBMOCK' } } });
|
||||||
|
|
||||||
// ─── 1. 门店账单(今日出账未打款 + 昨日出账已打款)───
|
// ─── 1. 门店账单(昨日未打款 + 前日已打款)───
|
||||||
const storeBillUnpaid = await prisma.storeBill.create({
|
const storeBillUnpaid = await prisma.storeBill.create({
|
||||||
data: {
|
data: {
|
||||||
billNo: billNo('SB', `${yesterday.getTime()}U`),
|
billNo: billNo('SB', `${yesterday.getTime()}U`),
|
||||||
storeId: store.id,
|
storeId: store.id,
|
||||||
billDate: today,
|
billDate: yesterday,
|
||||||
redeemCount: 5,
|
redeemCount: 5,
|
||||||
redeemAmount: 1500,
|
redeemAmount: 1500,
|
||||||
settlementRate: 0.6,
|
settlementRate: 0.6,
|
||||||
@@ -108,7 +108,7 @@ async function main() {
|
|||||||
data: {
|
data: {
|
||||||
billNo: billNo('SB', `${dayBefore.getTime()}P`),
|
billNo: billNo('SB', `${dayBefore.getTime()}P`),
|
||||||
storeId: store.id,
|
storeId: store.id,
|
||||||
billDate: yesterday,
|
billDate: dayBefore,
|
||||||
redeemCount: 3,
|
redeemCount: 3,
|
||||||
redeemAmount: 800,
|
redeemAmount: 800,
|
||||||
settlementRate: 0.6,
|
settlementRate: 0.6,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { AlertService } from '../common/alert/alert.service';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 财务对账单定时任务(Asia/Shanghai)
|
* 财务对账单定时任务(Asia/Shanghai)
|
||||||
* - 每日 08:00:酒厂日账单(T+3:3 天前已完成订单)+ 门店日账单(昨日核销,出账日=今天)
|
* - 每日 08:00:酒厂日账单(T+3:3 天前已完成订单)+ 门店日账单(昨日核销)
|
||||||
* - 每月 1 日 08:00:合伙人上一自然月账单 + 物流承运商上一自然月对账
|
* - 每月 1 日 08:00:合伙人上一自然月账单 + 物流承运商上一自然月对账
|
||||||
* - 工作日 18:05:门店提现 T+0 审完预警(FIN-003)
|
* - 工作日 18:05:门店提现 T+0 审完预警(FIN-003)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -156,16 +156,10 @@ export class DevPlanService {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
extras?: {
|
extras?: { creatorName?: string | null; supportTicketNo?: string | null },
|
||||||
creatorName?: string | null;
|
|
||||||
supportTicketNo?: string | null;
|
|
||||||
versions?: Array<{ id: string; versionNo: string }>;
|
|
||||||
},
|
|
||||||
|
|
||||||
): DevPlanTaskDto {
|
): DevPlanTaskDto {
|
||||||
|
|
||||||
const versions = extras?.versions ?? [];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
id: String(row.id),
|
id: String(row.id),
|
||||||
@@ -194,10 +188,6 @@ export class DevPlanService {
|
|||||||
|
|
||||||
attachmentUrls: parseAttachmentUrls(row.attachmentUrls),
|
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 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),
|
this.loadHqNames(creatorIds),
|
||||||
|
|
||||||
@@ -332,8 +322,6 @@ export class DevPlanService {
|
|||||||
|
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
|
|
||||||
this.loadTaskVersionMap(rows.map((r) => r.id)),
|
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ticketMap = new Map<string, string>(
|
const ticketMap = new Map<string, string>(
|
||||||
@@ -352,8 +340,6 @@ export class DevPlanService {
|
|||||||
|
|
||||||
supportTicketNo: r.supportTicketId != null ? ticketMap.get(String(r.supportTicketId)) ?? null : null,
|
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, {
|
return this.mapTask(row, {
|
||||||
|
|
||||||
creatorName: names.get(String(row.creatorHqAccountId)) ?? null,
|
creatorName: names.get(String(row.creatorHqAccountId)) ?? null,
|
||||||
|
|
||||||
supportTicketNo,
|
supportTicketNo,
|
||||||
|
|
||||||
versions: versionMap.get(String(row.id)) ?? [],
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -526,10 +508,6 @@ export class DevPlanService {
|
|||||||
|
|
||||||
await this.prisma.devPlanTask.update({ where: { id }, data });
|
await this.prisma.devPlanTask.update({ where: { id }, data });
|
||||||
|
|
||||||
if (dto.versionIds !== undefined) {
|
|
||||||
await this.replaceTaskVersions(id, dto.versionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getTask(id);
|
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[] }> {
|
private async loadVersionTasks(versionId: bigint): Promise<{ tasks: DevPlanTaskDto[]; taskIds: string[] }> {
|
||||||
|
|
||||||
const links = await this.prisma.devPlanVersionTask.findMany({
|
const links = await this.prisma.devPlanVersionTask.findMany({
|
||||||
|
|||||||
@@ -62,11 +62,6 @@ export class UpdateDevPlanTaskDto {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
attachmentUrls?: string[];
|
attachmentUrls?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
versionIds?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateDevPlanVersionDto {
|
export class CreateDevPlanVersionDto {
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ type OrderFilterInput = Pick<
|
|||||||
| 'status'
|
| 'status'
|
||||||
| 'orderType'
|
| 'orderType'
|
||||||
| 'userId'
|
| 'userId'
|
||||||
| 'userKeyword'
|
|
||||||
| 'productKeyword'
|
|
||||||
| 'cityId'
|
| 'cityId'
|
||||||
| 'receiverPhone'
|
| 'receiverPhone'
|
||||||
| 'fulfillmentHold'
|
| 'fulfillmentHold'
|
||||||
@@ -179,28 +177,8 @@ export class AdminOrdersService {
|
|||||||
}
|
}
|
||||||
if (query.orderType) where.orderType = query.orderType as Prisma.EnumOrderTypeFilter['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);
|
||||||
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.cityId) where.cityId = BigInt(query.cityId);
|
||||||
if (query.receiverPhone) where.receiverPhone = { contains: query.receiverPhone };
|
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) {
|
if (query.deliveryType) {
|
||||||
where.deliveryType = query.deliveryType as Prisma.EnumDeliveryTypeFilter['equals'];
|
where.deliveryType = query.deliveryType as Prisma.EnumDeliveryTypeFilter['equals'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export class AdminRedeemService {
|
|||||||
skip: (page - 1) * pageSize,
|
skip: (page - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
include: {
|
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 } },
|
store: { select: { id: true, name: true, cityName: true } },
|
||||||
coupon: { select: { id: true, couponNo: true, balance: true } },
|
coupon: { select: { id: true, couponNo: true, balance: true } },
|
||||||
},
|
},
|
||||||
@@ -77,7 +77,7 @@ export class AdminRedeemService {
|
|||||||
const record = await this.prisma.redeemRecord.findUnique({
|
const record = await this.prisma.redeemRecord.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
user: { select: { id: true, userNo: true, phone: true, nickname: true, hqRemark: true } },
|
user: { select: { id: true, userNo: true, phone: true, nickname: true } },
|
||||||
store: {
|
store: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -93,16 +93,6 @@ export class AdminOrdersQueryDto extends PaginationQueryDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
|
||||||
/** 用户编号 / 昵称 / 备注 / 手机 模糊匹配 */
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
userKeyword?: string;
|
|
||||||
|
|
||||||
/** 商品名称 / 规格 模糊匹配 */
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
productKeyword?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
cityId?: string;
|
cityId?: string;
|
||||||
@@ -169,14 +159,6 @@ export class AdminOrdersExportDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
cityId?: string;
|
cityId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
userKeyword?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
productKeyword?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
receiverPhone?: string;
|
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 { Prisma } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
DEFAULT_STORE_WITHDRAW_DAILY_LIMIT,
|
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);
|
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()) {
|
function dayWindow(anchor = new Date()) {
|
||||||
const end = startOfDay(anchor);
|
const end = startOfDay(anchor);
|
||||||
const start = new Date(end);
|
const start = new Date(end);
|
||||||
start.setDate(start.getDate() - 1);
|
start.setDate(start.getDate() - 1);
|
||||||
return { start, end, billDate: end };
|
return { start, end, billDate: start };
|
||||||
}
|
|
||||||
|
|
||||||
function shanghaiYmd(d: Date): string {
|
|
||||||
return d.toLocaleDateString('sv-SE', { timeZone: 'Asia/Shanghai' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function wineryDayWindow(anchor = new Date(), lagDays = WINERY_SETTLEMENT_LAG_DAYS) {
|
function wineryDayWindow(anchor = new Date(), lagDays = WINERY_SETTLEMENT_LAG_DAYS) {
|
||||||
@@ -136,9 +131,7 @@ function isWithdrawOverdue(appliedAt: Date, now = new Date()): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SettlementService implements OnModuleInit {
|
export class SettlementService {
|
||||||
private readonly logger = new Logger(SettlementService.name);
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly analyticsService: AnalyticsService,
|
||||||
@@ -148,37 +141,6 @@ export class SettlementService implements OnModuleInit {
|
|||||||
private readonly wecomPush: WecomMessagePushService,
|
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(
|
private notifyPartnerBillDigest(
|
||||||
period: string,
|
period: string,
|
||||||
rows: Array<{
|
rows: Array<{
|
||||||
@@ -1013,7 +975,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
|
|
||||||
// ─── Store bills (daily header) ──────────────────────
|
// ─── Store bills (daily header) ──────────────────────
|
||||||
|
|
||||||
/** 生成昨日核销窗口的门店对账单;billDate = 出账日(今日) */
|
/** 生成指定自然日窗口内的门店对账单(默认昨日) */
|
||||||
async generateStoreBillsForDay(anchor = new Date()) {
|
async generateStoreBillsForDay(anchor = new Date()) {
|
||||||
const { start, end, billDate } = dayWindow(anchor);
|
const { start, end, billDate } = dayWindow(anchor);
|
||||||
const payouts = await this.prisma.storePayout.findMany({
|
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) {
|
if (createdBills.length) {
|
||||||
const banks = await loadStorePrimaryBanksMap(
|
const banks = await loadStorePrimaryBanksMap(
|
||||||
this.prisma,
|
this.prisma,
|
||||||
@@ -1303,7 +1265,6 @@ export class SettlementService implements OnModuleInit {
|
|||||||
return serializeBigInt({
|
return serializeBigInt({
|
||||||
items: slice.map((r) => ({
|
items: slice.map((r) => ({
|
||||||
...r,
|
...r,
|
||||||
date: r.kind === 'T1_BILL' ? shanghaiYmd(r.date) : r.date,
|
|
||||||
bankAccount:
|
bankAccount:
|
||||||
r.kind === 'T1_BILL'
|
r.kind === 'T1_BILL'
|
||||||
? bankMap.get(String(r.storeId)) ?? null
|
? bankMap.get(String(r.storeId)) ?? null
|
||||||
@@ -1358,13 +1319,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
totalAmount: Number(aggregates._sum.payoutAmount ?? 0),
|
totalAmount: Number(aggregates._sum.payoutAmount ?? 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
return serializeBigInt({
|
return serializeBigInt({ items, total, page, pageSize, summary });
|
||||||
items: items.map((b) => ({ ...b, billDate: shanghaiYmd(b.billDate) })),
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
summary,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAdminStoreBill(id: bigint) {
|
async getAdminStoreBill(id: bigint) {
|
||||||
@@ -1380,7 +1335,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
if (!bill) throw new NotFoundException('门店对账单不存在');
|
if (!bill) throw new NotFoundException('门店对账单不存在');
|
||||||
const storeAccount = await loadStorePrimaryBank(this.prisma, bill.storeId);
|
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 } = {}) {
|
async confirmStoreBill(id: bigint, dto: { paymentRef?: string } = {}) {
|
||||||
@@ -1438,7 +1393,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
);
|
);
|
||||||
const header = [
|
const header = [
|
||||||
'账单号',
|
'账单号',
|
||||||
'出账日',
|
'账单日',
|
||||||
'门店',
|
'门店',
|
||||||
'城市',
|
'城市',
|
||||||
'核销笔数',
|
'核销笔数',
|
||||||
@@ -1456,7 +1411,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
const bank = bankMap.get(String(b.storeId));
|
const bank = bankMap.get(String(b.storeId));
|
||||||
return [
|
return [
|
||||||
csvEscape(b.billNo),
|
csvEscape(b.billNo),
|
||||||
shanghaiYmd(b.billDate),
|
b.billDate.toISOString().slice(0, 10),
|
||||||
csvEscape(b.store.name),
|
csvEscape(b.store.name),
|
||||||
csvEscape(b.store.cityName ?? ''),
|
csvEscape(b.store.cityName ?? ''),
|
||||||
b.redeemCount,
|
b.redeemCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user