{error &&
{error}
}
diff --git a/apps/h5-partner/src/styles.css b/apps/h5-partner/src/styles.css
index c24aff5..09ad919 100644
--- a/apps/h5-partner/src/styles.css
+++ b/apps/h5-partner/src/styles.css
@@ -3067,6 +3067,68 @@ body {
border-top: 1px dashed rgba(166, 29, 36, 0.15);
}
+.partner-home--flush-top .partner-home-body,
+.partner-home--flush-top.partner-store-page,
+.partner-home--flush-top.partner-center-page {
+ padding-top: 0;
+}
+
+.partner-home--flush-top .partner-sticky-filter {
+ padding-top: 12px;
+}
+
+.partner-leaderboard-row--self {
+ background: rgba(166, 29, 36, 0.08);
+ border: 1px solid rgba(166, 29, 36, 0.18);
+ border-radius: 10px;
+}
+
+.partner-leaderboard-self-tag {
+ display: inline-block;
+ margin-left: 6px;
+ padding: 1px 6px;
+ font-size: 10px;
+ font-weight: 700;
+ color: var(--color-heritage-red);
+ background: rgba(166, 29, 36, 0.12);
+ border-radius: 4px;
+ vertical-align: middle;
+}
+
+.partner-order-card-wrap {
+ margin: 0 var(--space-page) 12px;
+}
+
+.partner-order-ship-btn {
+ display: block;
+ width: 100%;
+ margin-top: -4px;
+ padding: 10px 16px;
+ border: none;
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
+ background: var(--color-heritage-red);
+ color: #fff;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+}
+
+.partner-order-ship-btn:disabled {
+ opacity: 0.6;
+}
+
+.partner-order-footer-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ width: 100%;
+}
+
+.partner-order-ship-btn--footer {
+ width: 100%;
+ justify-content: center;
+}
+
/* 微信 H5 系统标题已展示:隐藏页内重复标题,保留返回键与操作区 */
.app-page-title {
display: none !important;
diff --git a/packages/shared-types/src/partner.ts b/packages/shared-types/src/partner.ts
index 847e132..28f339d 100644
--- a/packages/shared-types/src/partner.ts
+++ b/packages/shared-types/src/partner.ts
@@ -10,6 +10,9 @@ export interface PartnerMe {
staffRole?: PartnerStaffRole;
permissions?: string[];
primaryAccountId?: string;
+ /** 子账号联系主账号用 */
+ primaryPhone?: string;
+ primaryName?: string;
}
export interface UpdatePartnerMeRequest {
diff --git a/server/dukang-api/src/common/decorators/partner-permission.decorator.ts b/server/dukang-api/src/common/decorators/partner-permission.decorator.ts
new file mode 100644
index 0000000..98c1dd0
--- /dev/null
+++ b/server/dukang-api/src/common/decorators/partner-permission.decorator.ts
@@ -0,0 +1,7 @@
+import { SetMetadata } from '@nestjs/common';
+import type { PartnerPermissionKey } from '@dukang/shared-types';
+
+export const PARTNER_PERMISSIONS_KEY = 'partner_permissions';
+
+export const RequirePartnerPermissions = (...permissions: PartnerPermissionKey[]) =>
+ SetMetadata(PARTNER_PERMISSIONS_KEY, permissions);
diff --git a/server/dukang-api/src/common/guards/partner-permission.guard.ts b/server/dukang-api/src/common/guards/partner-permission.guard.ts
new file mode 100644
index 0000000..12ac3c9
--- /dev/null
+++ b/server/dukang-api/src/common/guards/partner-permission.guard.ts
@@ -0,0 +1,45 @@
+import {
+ CanActivate,
+ ExecutionContext,
+ ForbiddenException,
+ Injectable,
+} from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import type { PartnerPermissionKey } from '@dukang/shared-types';
+import { PARTNER_PERMISSIONS_KEY } from '../decorators/partner-permission.decorator';
+import { PrismaService } from '../prisma/prisma.module';
+import { AuthUser, JwtAuthGuard } from './jwt-auth.guard';
+
+@Injectable()
+export class PartnerPermissionGuard implements CanActivate {
+ constructor(
+ private readonly jwtAuthGuard: JwtAuthGuard,
+ private readonly prisma: PrismaService,
+ private readonly reflector: Reflector,
+ ) {}
+
+ async canActivate(context: ExecutionContext): Promise
{
+ await this.jwtAuthGuard.canActivate(context);
+ const req = context.switchToHttp().getRequest();
+ const user = req.user as AuthUser;
+ if (user.actorType !== 'PARTNER') {
+ throw new ForbiddenException('仅合伙人可操作');
+ }
+ const account = await this.prisma.partnerAccount.findUniqueOrThrow({
+ where: { id: user.actorId },
+ });
+ if (account.isPrimary === 1) return true;
+
+ const required = this.reflector.getAllAndOverride(
+ PARTNER_PERMISSIONS_KEY,
+ [context.getHandler(), context.getClass()],
+ );
+ if (!required?.length) return true;
+
+ const perms = Array.isArray(account.permissions)
+ ? (account.permissions as string[])
+ : [];
+ if (required.some((p) => perms.includes(p))) return true;
+ throw new ForbiddenException('当前子账号无此操作权限');
+ }
+}
diff --git a/server/dukang-api/src/modules/iam/iam.module.ts b/server/dukang-api/src/modules/iam/iam.module.ts
index 8702871..45ecd70 100644
--- a/server/dukang-api/src/modules/iam/iam.module.ts
+++ b/server/dukang-api/src/modules/iam/iam.module.ts
@@ -21,6 +21,7 @@ import { PhoneVerifiedGuard } from '../../common/guards/phone-verified.guard';
import { OptionalJwtAuthGuard } from '../../common/guards/optional-jwt-auth.guard';
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
+import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
import { StoreMembershipService } from '../../common/guards/store-membership.service';
@@ -54,6 +55,7 @@ import { StoreMembershipService } from '../../common/guards/store-membership.ser
OptionalJwtAuthGuard,
HqAuthGuard,
PartnerPrimaryGuard,
+ PartnerPermissionGuard,
ShopStoreGuard,
],
exports: [
@@ -68,6 +70,7 @@ import { StoreMembershipService } from '../../common/guards/store-membership.ser
OptionalJwtAuthGuard,
HqAuthGuard,
PartnerPrimaryGuard,
+ PartnerPermissionGuard,
ShopStoreGuard,
],
})
diff --git a/server/dukang-api/src/modules/settlement/settlement.controller.ts b/server/dukang-api/src/modules/settlement/settlement.controller.ts
index f346dcb..1cdf07c 100644
--- a/server/dukang-api/src/modules/settlement/settlement.controller.ts
+++ b/server/dukang-api/src/modules/settlement/settlement.controller.ts
@@ -263,6 +263,8 @@ export class PartnerMeController {
staffRole: account.staffRole ?? undefined,
permissions: Array.isArray(account.permissions) ? account.permissions : undefined,
primaryAccountId: primary.id.toString(),
+ primaryPhone: primary.phone,
+ primaryName: primary.name,
companyName: primary.companyName ?? undefined,
hasWechat: !!account.wxOpenId,
};
diff --git a/server/dukang-api/src/modules/store/store.controller.ts b/server/dukang-api/src/modules/store/store.controller.ts
index 5e14a48..d9d44db 100644
--- a/server/dukang-api/src/modules/store/store.controller.ts
+++ b/server/dukang-api/src/modules/store/store.controller.ts
@@ -3,6 +3,8 @@ import { StoreService } from './store.service';
import { RedeemService } from '../redeem/redeem.service';
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
+import { RequirePartnerPermissions } from '../../common/decorators/partner-permission.decorator';
+import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
@@ -76,7 +78,8 @@ export class PartnerStoreController {
}
@Controller('partner/dashboard')
-@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
+@UseGuards(JwtAuthGuard, PartnerPermissionGuard)
+@RequirePartnerPermissions('store:create', 'store:manage', 'order:view', 'warehouse:manage')
export class PartnerDashboardController {
constructor(private readonly storeService: StoreService) {}
diff --git a/server/dukang-api/src/modules/store/store.service.ts b/server/dukang-api/src/modules/store/store.service.ts
index a50a30f..36a2c13 100644
--- a/server/dukang-api/src/modules/store/store.service.ts
+++ b/server/dukang-api/src/modules/store/store.service.ts
@@ -468,33 +468,56 @@ export class StoreService {
async partnerDashboard(partnerAccountId: bigint) {
const { account, primaryId } = await this.resolvePartnerScope(partnerAccountId);
- this.assertPrimaryAccount(account);
- const partnerStoreIds = await this.prisma.store.findMany({
- where: { partnerAccountId: primaryId },
- select: { id: true },
+ const primaryAccount = await this.prisma.partnerAccount.findUniqueOrThrow({
+ where: { id: primaryId },
});
- const storeIds = partnerStoreIds.map((s) => s.id);
- const [storeCount, orderCount, recentStores, pendingAuditCount, recentNotices] = await Promise.all([
- this.prisma.store.count({ where: { partnerAccountId: primaryId } }),
- this.prisma.order.count({
- where: await this.partnerCityService.buildPartnerOrderWhere(primaryId),
- }),
+ const orderCount = await this.prisma.order.count({
+ where: await this.partnerCityService.buildPartnerOrderWhere(primaryId),
+ });
+
+ let storeWhere: { partnerAccountId: bigint; id?: { in: bigint[] } } = {
+ partnerAccountId: primaryId,
+ };
+ if (this.isSubAccount(account)) {
+ const storeIds = await this.getStoreIdsCreatedByAccount(partnerAccountId);
+ if (storeIds.length === 0) {
+ return {
+ storeCount: 0,
+ orderCount,
+ companyName: primaryAccount.companyName ?? '',
+ recentStores: [],
+ pendingAuditCount: 0,
+ notifications: [],
+ };
+ }
+ storeWhere = { partnerAccountId: primaryId, id: { in: storeIds } };
+ }
+
+ const storeIdsForNotices = (
+ await this.prisma.store.findMany({
+ where: storeWhere,
+ select: { id: true },
+ })
+ ).map((s) => s.id);
+
+ const [storeCount, recentStores, pendingAuditCount, recentNotices] = await Promise.all([
+ this.prisma.store.count({ where: storeWhere }),
this.prisma.store.findMany({
- where: { partnerAccountId: primaryId },
+ where: storeWhere,
select: { id: true, name: true, status: true, auditStatus: true, createdAt: true },
orderBy: { createdAt: 'desc' },
take: 10,
}),
this.prisma.store.count({
- where: { partnerAccountId: primaryId, auditStatus: 'PENDING' },
+ where: { ...storeWhere, auditStatus: 'PENDING' },
}),
- storeIds.length === 0
+ storeIdsForNotices.length === 0
? Promise.resolve([])
: this.prisma.commonEvent.findMany({
where: {
eventType: 'STORE_AUDIT',
refType: 'STORE',
- refId: { in: storeIds },
+ refId: { in: storeIdsForNotices },
status: { in: ['APPROVED', 'REJECTED'] },
actorType: 'HQ',
},
@@ -515,7 +538,7 @@ export class StoreService {
return {
storeCount,
orderCount,
- companyName: account.companyName ?? '',
+ companyName: primaryAccount.companyName ?? '',
recentStores: serializeBigInt(recentStores),
pendingAuditCount,
notifications: serializeBigInt(
diff --git a/server/dukang-api/src/modules/trade/trade.controller.ts b/server/dukang-api/src/modules/trade/trade.controller.ts
index 4dcc1f0..2713874 100644
--- a/server/dukang-api/src/modules/trade/trade.controller.ts
+++ b/server/dukang-api/src/modules/trade/trade.controller.ts
@@ -3,6 +3,8 @@ import type { Request } from 'express';
import { TradeService } from './trade.service';
import { JwtAuthGuard, AuthUser } from '../../common/guards/jwt-auth.guard';
import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
+import { RequirePartnerPermissions } from '../../common/decorators/partner-permission.decorator';
+import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import {
PartnerProxyOrderCreateDto,
@@ -70,7 +72,8 @@ export class TradeController {
}
@Controller('partner/orders')
-@UseGuards(JwtAuthGuard, PartnerPrimaryGuard)
+@UseGuards(JwtAuthGuard, PartnerPermissionGuard)
+@RequirePartnerPermissions('order:view', 'warehouse:manage')
export class PartnerOrderController {
constructor(private readonly tradeService: TradeService) {}
From f6d97b4ee82cc9fa088c150d2294e4be52e85991 Mon Sep 17 00:00:00 2001
From: jacy <18049821889@163.com>
Date: Thu, 16 Jul 2026 17:51:20 +0800
Subject: [PATCH 2/2] =?UTF-8?q?feat(trade):=20=E4=BB=93=E9=85=8D=E5=8F=AF?=
=?UTF-8?q?=E8=B0=83=E9=85=8D=E5=B1=A5=E7=BA=A6=E4=B8=8E=E4=B8=89=E5=88=86?=
=?UTF-8?q?=E6=94=AF=E6=8E=A8=E5=8D=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
同城有仓按仓库绑定承运商自动推单或自管填单,无仓/跨城走总部快递;新增仓配注册表、FulfillmentService 及三端运单追踪。
Co-authored-by: Cursor
---
apps/admin-web/src/App.tsx | 2 +
apps/admin-web/src/layouts/AdminLayout.tsx | 1 +
.../src/pages/CityWarehousesPage.tsx | 98 ++++++
.../src/pages/FulfillmentProvidersPage.tsx | 196 +++++++++++
apps/admin-web/src/pages/OrdersPage.tsx | 68 +++-
apps/h5-partner/src/pages/OrderListPage.tsx | 69 +++-
apps/h5-partner/src/styles.css | 44 +++
apps/h5-user/src/pages/OrderDetailPage.tsx | 69 +++-
packages/shared-types/src/city-warehouse.ts | 22 +-
packages/shared-types/src/enums.ts | 30 ++
.../shared-types/src/fulfillment-provider.ts | 51 +++
packages/shared-types/src/index.ts | 1 +
server/dukang-api/prisma/clear-cities.ts | 1 +
server/dukang-api/prisma/schema.prisma | 107 ++++--
server/dukang-api/prisma/seed-v31.ts | 25 ++
.../city-scope/city-warehouse.service.ts | 87 ++++-
.../city-scope/partner-city.service.ts | 29 ++
.../fulfillment-provider.service.ts | 119 +++++++
.../modules/fulfillment/fulfillment.module.ts | 12 +
.../fulfillment/fulfillment.service.ts | 306 ++++++++++++++++++
.../ops/admin-city-warehouses.controller.ts | 31 +-
.../admin-fulfillment-providers.controller.ts | 61 ++++
.../modules/ops/admin-orders.controller.ts | 13 +-
.../src/modules/ops/admin-orders.service.ts | 10 +-
.../src/modules/ops/dto/admin-mutate.dto.ts | 116 +++++++
.../dukang-api/src/modules/ops/ops.module.ts | 5 +-
.../src/modules/trade/trade.controller.ts | 21 ++
.../src/modules/trade/trade.module.ts | 2 +
.../src/modules/trade/trade.service.ts | 64 +++-
杜康好客-v3-PRD.md | 12 +-
30 files changed, 1615 insertions(+), 57 deletions(-)
create mode 100644 apps/admin-web/src/pages/FulfillmentProvidersPage.tsx
create mode 100644 packages/shared-types/src/fulfillment-provider.ts
create mode 100644 server/dukang-api/src/modules/fulfillment/fulfillment-provider.service.ts
create mode 100644 server/dukang-api/src/modules/fulfillment/fulfillment.module.ts
create mode 100644 server/dukang-api/src/modules/fulfillment/fulfillment.service.ts
create mode 100644 server/dukang-api/src/modules/ops/admin-fulfillment-providers.controller.ts
diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx
index 86d1122..7b80897 100644
--- a/apps/admin-web/src/App.tsx
+++ b/apps/admin-web/src/App.tsx
@@ -18,6 +18,7 @@ import HqAccountsPage from './pages/HqAccountsPage';
import CitiesPage from './pages/CitiesPage';
import CityPartnersPage from './pages/CityPartnersPage';
import CityWarehousesPage from './pages/CityWarehousesPage';
+import FulfillmentProvidersPage from './pages/FulfillmentProvidersPage';
import StoreMediaPage from './pages/StoreMediaPage';
import PromoCodesPage from './pages/PromoCodesPage';
import PromoCodeDetailLayout from './pages/promo/PromoCodeDetailLayout';
@@ -74,6 +75,7 @@ export default function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx
index fc5b93b..ea65028 100644
--- a/apps/admin-web/src/layouts/AdminLayout.tsx
+++ b/apps/admin-web/src/layouts/AdminLayout.tsx
@@ -56,6 +56,7 @@ const MENU_ITEMS: MenuProps['items'] = [
{ key: '/cities', label: '城市' },
{ key: '/city-partners', label: '城市合伙人' },
{ key: '/city-warehouses', label: '仓库' },
+ { key: '/fulfillment-providers', label: '仓配管理' },
],
},
{
diff --git a/apps/admin-web/src/pages/CityWarehousesPage.tsx b/apps/admin-web/src/pages/CityWarehousesPage.tsx
index c063654..095f2a7 100644
--- a/apps/admin-web/src/pages/CityWarehousesPage.tsx
+++ b/apps/admin-web/src/pages/CityWarehousesPage.tsx
@@ -6,6 +6,7 @@ import {
Descriptions,
Form,
Input,
+ InputNumber,
Modal,
Popconfirm,
Select,
@@ -17,10 +18,13 @@ import {
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
+ WAREHOUSE_FULFILLMENT_MODE_LABELS,
WAREHOUSE_MANAGER_LABELS,
WAREHOUSE_STATUS_LABELS,
+ WarehouseFulfillmentMode,
WarehouseManagerType,
WarehouseStatus,
+ type FulfillmentProviderDto,
} from '@dukang/shared-types';
import { request, type Paginated } from '../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
@@ -39,6 +43,13 @@ type Row = {
partnerAccountId: string | null;
partnerCompanyName?: string | null;
status: string;
+ fulfillmentMode?: string;
+ fulfillmentProviderId?: string | null;
+ fulfillmentProviderName?: string | null;
+ manualCarrierLabel?: string | null;
+ manualQueryUrlTemplate?: string | null;
+ lng?: number | null;
+ lat?: number | null;
createdAt: string;
};
@@ -48,6 +59,54 @@ type PartnerOption = { id: string; companyName: string };
const MANAGER_OPTIONS = Object.entries(WAREHOUSE_MANAGER_LABELS).map(([value, label]) => ({ value, label }));
const STATUS_OPTIONS = Object.entries(WAREHOUSE_STATUS_LABELS).map(([value, label]) => ({ value, label }));
+const FULFILLMENT_MODE_OPTIONS = Object.entries(WAREHOUSE_FULFILLMENT_MODE_LABELS).map(([value, label]) => ({
+ value,
+ label,
+}));
+
+function FulfillmentFields({
+ mode,
+ providerOptions,
+}: {
+ mode: WarehouseFulfillmentMode;
+ providerOptions: FulfillmentProviderDto[];
+}) {
+ return (
+ <>
+ {mode === WarehouseFulfillmentMode.API_AUTO && (
+
+
+ )}
+ {mode === WarehouseFulfillmentMode.MANUAL && (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
export default function CityWarehousesPage() {
const [filterForm] = Form.useForm();
const [createForm] = Form.useForm();
@@ -73,6 +132,13 @@ export default function CityWarehousesPage() {
const [createManagerType, setCreateManagerType] = useState(WarehouseManagerType.HQ);
const [editManagerType, setEditManagerType] = useState(WarehouseManagerType.HQ);
const [createCityId, setCreateCityId] = useState();
+ const [createFulfillmentMode, setCreateFulfillmentMode] = useState(
+ WarehouseFulfillmentMode.MANUAL,
+ );
+ const [editFulfillmentMode, setEditFulfillmentMode] = useState(
+ WarehouseFulfillmentMode.MANUAL,
+ );
+ const [providerOptions, setProviderOptions] = useState([]);
const loadCities = useCallback(async () => {
const res = await request>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`);
@@ -88,6 +154,9 @@ export default function CityWarehousesPage() {
useEffect(() => {
void loadCities();
+ void request('/admin/fulfillment-providers/active-api')
+ .then(setProviderOptions)
+ .catch(() => {});
}, [loadCities]);
async function openEdit(row: Row) {
@@ -102,7 +171,14 @@ export default function CityWarehousesPage() {
managerType: row.managerType,
partnerAccountId: row.partnerAccountId,
status: row.status,
+ fulfillmentMode: row.fulfillmentMode ?? WarehouseFulfillmentMode.MANUAL,
+ fulfillmentProviderId: row.fulfillmentProviderId ?? undefined,
+ manualCarrierLabel: row.manualCarrierLabel ?? undefined,
+ manualQueryUrlTemplate: row.manualQueryUrlTemplate ?? undefined,
+ lng: row.lng ?? undefined,
+ lat: row.lat ?? undefined,
});
+ setEditFulfillmentMode((row.fulfillmentMode as WarehouseFulfillmentMode) ?? WarehouseFulfillmentMode.MANUAL);
setEditOpen(true);
}
@@ -149,6 +225,14 @@ export default function CityWarehousesPage() {
'—'
),
},
+ {
+ title: '履约',
+ width: 120,
+ render: (_, row) =>
+ row.fulfillmentMode === 'API_AUTO'
+ ? row.fulfillmentProviderName || 'API'
+ : WAREHOUSE_FULFILLMENT_MODE_LABELS[WarehouseFulfillmentMode.MANUAL],
+ },
{
title: '状态',
dataIndex: 'status',
@@ -341,6 +425,13 @@ export default function CityWarehousesPage() {
+
+
+
@@ -399,6 +490,13 @@ export default function CityWarehousesPage() {
+
+
+
diff --git a/apps/admin-web/src/pages/FulfillmentProvidersPage.tsx b/apps/admin-web/src/pages/FulfillmentProvidersPage.tsx
new file mode 100644
index 0000000..6533f96
--- /dev/null
+++ b/apps/admin-web/src/pages/FulfillmentProvidersPage.tsx
@@ -0,0 +1,196 @@
+import { useCallback, useEffect, useState } from 'react';
+import {
+ Button,
+ Form,
+ Input,
+ Modal,
+ Select,
+ Space,
+ Table,
+ Tag,
+ Typography,
+ message,
+} from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import {
+ FULFILLMENT_PROVIDER_STATUS_LABELS,
+ FULFILLMENT_PROVIDER_TYPE_LABELS,
+ FulfillmentProviderStatus,
+ FulfillmentProviderType,
+ type FulfillmentProviderDto,
+} from '@dukang/shared-types';
+import { request } from '../lib/api';
+import { fmtTime } from '../lib/constants';
+
+const TYPE_OPTIONS = Object.entries(FULFILLMENT_PROVIDER_TYPE_LABELS).map(([value, label]) => ({
+ value,
+ label,
+}));
+const STATUS_OPTIONS = Object.entries(FULFILLMENT_PROVIDER_STATUS_LABELS).map(([value, label]) => ({
+ value,
+ label,
+}));
+
+export default function FulfillmentProvidersPage() {
+ const [rows, setRows] = useState
([]);
+ const [loading, setLoading] = useState(false);
+ const [open, setOpen] = useState(false);
+ const [editRow, setEditRow] = useState(null);
+ const [form] = Form.useForm();
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const res = await request('/admin/fulfillment-providers');
+ setRows(res);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ function openCreate() {
+ setEditRow(null);
+ form.resetFields();
+ form.setFieldsValue({
+ type: FulfillmentProviderType.API,
+ status: FulfillmentProviderStatus.ACTIVE,
+ capabilitiesJson: JSON.stringify(
+ { createShipment: true, getTrack: true, callback: true },
+ null,
+ 2,
+ ),
+ });
+ setOpen(true);
+ }
+
+ function openEdit(row: FulfillmentProviderDto) {
+ setEditRow(row);
+ form.setFieldsValue({
+ code: row.code,
+ name: row.name,
+ type: row.type,
+ status: row.status,
+ capabilitiesJson: row.capabilities ? JSON.stringify(row.capabilities, null, 2) : '',
+ configJson: '',
+ });
+ setOpen(true);
+ }
+
+ async function submit() {
+ const v = await form.validateFields();
+ if (editRow) {
+ await request(`/admin/fulfillment-providers/${editRow.id}`, {
+ method: 'PUT',
+ body: JSON.stringify({
+ name: v.name,
+ type: v.type,
+ status: v.status,
+ configJson: v.configJson || undefined,
+ capabilitiesJson: v.capabilitiesJson || undefined,
+ }),
+ });
+ message.success('已更新');
+ } else {
+ await request('/admin/fulfillment-providers', {
+ method: 'POST',
+ body: JSON.stringify(v),
+ });
+ message.success('已创建');
+ }
+ setOpen(false);
+ void load();
+ }
+
+ const columns: ColumnsType = [
+ { title: '编码', dataIndex: 'code', width: 100 },
+ { title: '名称', dataIndex: 'name' },
+ {
+ title: '类型',
+ dataIndex: 'type',
+ render: (v) => FULFILLMENT_PROVIDER_TYPE_LABELS[v as FulfillmentProviderType] || v,
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ render: (v) => (
+
+ {FULFILLMENT_PROVIDER_STATUS_LABELS[v as FulfillmentProviderStatus] || v}
+
+ ),
+ },
+ {
+ title: '能力',
+ render: (_, row) => {
+ const caps = row.capabilities;
+ if (!caps) return '—';
+ return Object.entries(caps)
+ .filter(([, on]) => on)
+ .map(([k]) => k)
+ .join('、') || '—';
+ },
+ },
+ { title: '更新时间', dataIndex: 'updatedAt', width: 170, render: fmtTime },
+ {
+ title: '操作',
+ width: 80,
+ render: (_, row) => (
+
+ ),
+ },
+ ];
+
+ return (
+
+ );
+}
diff --git a/apps/admin-web/src/pages/OrdersPage.tsx b/apps/admin-web/src/pages/OrdersPage.tsx
index b518c7f..405b0c6 100644
--- a/apps/admin-web/src/pages/OrdersPage.tsx
+++ b/apps/admin-web/src/pages/OrdersPage.tsx
@@ -51,6 +51,8 @@ type OrderDetail = AdminOrderRow & {
productAmount?: number;
freightAmount?: number;
benefitAmount?: number;
+ deliveryType?: string;
+ fulfillmentWarehouseId?: string | null;
paidAt?: string | null;
payExpireAt?: string | null;
items?: Array>;
@@ -63,6 +65,7 @@ type OrderDetail = AdminOrderRow & {
export default function OrdersPage() {
const [form] = Form.useForm();
const [shipForm] = Form.useForm();
+ const [logisticsForm] = Form.useForm();
const [data, setData] = useState | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
@@ -74,6 +77,7 @@ export default function OrdersPage() {
const [batchDeleting, setBatchDeleting] = useState(false);
const [shipDefaults, setShipDefaults] = useState(null);
const [shipping, setShipping] = useState(false);
+ const [logisticsShipping, setLogisticsShipping] = useState(false);
const selectedOrders = useMemo(
() => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)),
@@ -143,6 +147,25 @@ export default function OrdersPage() {
}
}
+ async function submitLogisticsShip() {
+ if (!detail) return;
+ const values = await logisticsForm.validateFields();
+ setLogisticsShipping(true);
+ try {
+ const res = await request(`/admin/orders/${detail.id}/logistics-ship`, {
+ method: 'POST',
+ body: JSON.stringify(values),
+ });
+ message.success('快递单已录入');
+ setDetail(res);
+ void load();
+ } catch (e) {
+ message.error(e instanceof Error ? e.message : '填单失败');
+ } finally {
+ setLogisticsShipping(false);
+ }
+ }
+
async function confirmBatchDelete() {
if (!selectedRowKeys.length) return;
setBatchDeleting(true);
@@ -324,16 +347,55 @@ export default function OrdersPage() {
{detail.delivery && (
- {DELIVERY_PROVIDER_LABELS[detail.delivery.provider] || detail.delivery.provider}
+ {detail.delivery.logisticsCompany ||
+ DELIVERY_PROVIDER_LABELS[detail.delivery.provider] ||
+ detail.delivery.provider}
{detail.delivery.trackingNo || '—'}
{detail.delivery.providerOrderNo || '—'}
+ {detail.delivery.manualQueryUrl && (
+
+
+ 打开物流查询
+
+
+ )}
)}
{['PENDING_SHIP', 'OUT_WAREHOUSE'].includes(detail.status) && !detail.delivery?.trackingNo && (
- 发货
+ {(detail.deliveryType === 'CROSS_CITY' || !detail.fulfillmentWarehouseId) && (
+ <>
+ 总部快递填单
+
+ 适用于跨城订单或同城无仓订单
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {detail.fulfillmentWarehouseId && (
+ <>
+
+ 仓配小飞侠重试
+
+
+ 仓配订单通常支付后自动推单;失败时可手动重试
+
)}
diff --git a/apps/h5-partner/src/pages/OrderListPage.tsx b/apps/h5-partner/src/pages/OrderListPage.tsx
index e5b3ac9..c9e683a 100644
--- a/apps/h5-partner/src/pages/OrderListPage.tsx
+++ b/apps/h5-partner/src/pages/OrderListPage.tsx
@@ -41,6 +41,8 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
const [dateFilter, setDateFilter] = useState('today');
const [statusFilter, setStatusFilter] = useState('ALL');
const [shippingId, setShippingId] = useState(null);
+ const [shipForm, setShipForm] = useState({ logisticsCompany: '', trackingNo: '', manualQueryUrl: '' });
+ const [shipModalId, setShipModalId] = useState(null);
useEffect(() => {
document.title = '订单管理';
@@ -64,14 +66,27 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
async function handleShip(orderId: string, e: React.MouseEvent) {
e.preventDefault();
e.stopPropagation();
- setShippingId(orderId);
+ setShipModalId(orderId);
+ setShipForm({ logisticsCompany: '', trackingNo: '', manualQueryUrl: '' });
+ }
+
+ async function submitManualShip() {
+ if (!shipModalId) return;
+ if (!shipForm.logisticsCompany.trim() || !shipForm.trackingNo.trim()) {
+ window.alert('请填写快递公司和运单号');
+ return;
+ }
+ setShippingId(shipModalId);
try {
- await request('PARTNER_H5', `/partner/orders/${orderId}/mock-advance-delivery`, {
+ await request('PARTNER_H5', `/partner/orders/${shipModalId}/manual-ship`, {
method: 'POST',
- body: JSON.stringify({ targetStatus: 'OUT_WAREHOUSE' }),
+ body: JSON.stringify(shipForm),
});
+ setShipModalId(null);
const next = await request<{ list: Array> }>('PARTNER_H5', '/partner/orders');
setData(next);
+ } catch (err) {
+ window.alert(err instanceof Error ? err.message : '发货失败');
} finally {
setShippingId(null);
}
@@ -137,14 +152,14 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
{String(o.receiverAddress || '收货地址')}
- {canShip(String(o.status)) && (
+ {canShip(String(o.status)) && !(o.delivery as { trackingNo?: string } | undefined)?.trackingNo && (