城市合伙人去掉订单补发功能,酒的订单如果没有配置仓库应该是看不到的
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-07-17 08:04:53 +08:00
parent b36f2ea63a
commit 608e405468
11 changed files with 124 additions and 26 deletions
-2
View File
@@ -13,7 +13,6 @@ import CenterPage from './pages/CenterPage';
import PartnerMePage from './pages/PartnerMePage'; import PartnerMePage from './pages/PartnerMePage';
import BillsPage from './pages/BillsPage'; import BillsPage from './pages/BillsPage';
import SettlementPage from './pages/SettlementPage'; import SettlementPage from './pages/SettlementPage';
import ReshipPage from './pages/ReshipPage';
import WeeklyReportPage from './pages/WeeklyReportPage'; import WeeklyReportPage from './pages/WeeklyReportPage';
import ProxyOrderPage from './pages/ProxyOrderPage'; import ProxyOrderPage from './pages/ProxyOrderPage';
import StaffListPage from './pages/StaffListPage'; import StaffListPage from './pages/StaffListPage';
@@ -34,7 +33,6 @@ function PrimaryRoutes() {
<Route path="/center/staff" element={<StaffListPage />} /> <Route path="/center/staff" element={<StaffListPage />} />
<Route path="/center/staff/new" element={<StaffCreatePage />} /> <Route path="/center/staff/new" element={<StaffCreatePage />} />
<Route path="/proxy-order" element={<ProxyOrderPage />} /> <Route path="/proxy-order" element={<ProxyOrderPage />} />
<Route path="/reshipments" element={<ReshipPage />} />
<Route path="/reports/weekly" element={<WeeklyReportPage />} /> <Route path="/reports/weekly" element={<WeeklyReportPage />} />
<Route path="/leaderboard" element={<LeaderboardPage />} /> <Route path="/leaderboard" element={<LeaderboardPage />} />
<Route path="/stores/:id" element={<StoreDetailPage />} /> <Route path="/stores/:id" element={<StoreDetailPage />} />
@@ -55,6 +55,8 @@ function accountFromProfile(profile: PartnerSessionProfile): PartnerAccount {
hasWechat: profile.hasWechat, hasWechat: profile.hasWechat,
wxNickname: profile.wxNickname, wxNickname: profile.wxNickname,
wxAvatarUrl: profile.wxAvatarUrl, wxAvatarUrl: profile.wxAvatarUrl,
managedWarehouseId: profile.managedWarehouseId,
hasWarehouseAccess: profile.hasWarehouseAccess,
}; };
} }
+4
View File
@@ -36,6 +36,8 @@ export type PartnerSessionProfile = Pick<
| 'hasWechat' | 'hasWechat'
| 'wxNickname' | 'wxNickname'
| 'wxAvatarUrl' | 'wxAvatarUrl'
| 'managedWarehouseId'
| 'hasWarehouseAccess'
> & { > & {
staffRole?: PartnerStaffRole; staffRole?: PartnerStaffRole;
}; };
@@ -125,6 +127,8 @@ function profileFromMe(me: PartnerMe): PartnerSessionProfile {
hasWechat: me.hasWechat, hasWechat: me.hasWechat,
wxNickname: me.wxNickname, wxNickname: me.wxNickname,
wxAvatarUrl: me.wxAvatarUrl, wxAvatarUrl: me.wxAvatarUrl,
managedWarehouseId: me.managedWarehouseId,
hasWarehouseAccess: me.hasWarehouseAccess,
}; };
} }
+9
View File
@@ -35,6 +35,15 @@ export function getPartnerNavKind(account: PartnerMe | null | undefined): Partne
return 'store_staff'; return 'store_staff';
} }
/** 是否已配置管仓:主账号看 hasWarehouseAccess;子账号还需有仓库相关权限 */
export function hasWarehouseAccess(account: PartnerMe | null | undefined): boolean {
if (!account) return false;
if (account.hasWarehouseAccess === false) return false;
if (account.hasWarehouseAccess === true) return true;
// 旧缓存无字段时保守:主账号未知视为无,避免误展示全城订单
return false;
}
export function partnerHomePath(account: PartnerMe | null | undefined): string { export function partnerHomePath(account: PartnerMe | null | undefined): string {
return '/'; return '/';
} }
+19 -12
View File
@@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
import type { PartnerLeaderboardEntry } from '@dukang/shared-types'; import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
import { isLoggedIn, request } from '../lib/api'; import { isLoggedIn, request } from '../lib/api';
import { fetchPartnerLeaderboard } from '../lib/leaderboard'; import { fetchPartnerLeaderboard } from '../lib/leaderboard';
import { getPartnerNavKind } from '../lib/partnerAccess'; import { getPartnerNavKind, hasWarehouseAccess } from '../lib/partnerAccess';
import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { usePartnerSession } from '../contexts/PartnerSessionContext';
function fmtMoney(n: number) { function fmtMoney(n: number) {
@@ -135,6 +135,7 @@ export default function HomePage() {
const navKind = getPartnerNavKind(account); const navKind = getPartnerNavKind(account);
const isPrimary = navKind === 'primary'; const isPrimary = navKind === 'primary';
const isWarehouse = navKind === 'warehouse_staff'; const isWarehouse = navKind === 'warehouse_staff';
const warehouseOk = hasWarehouseAccess(account);
const [dash, setDash] = useState<Record<string, unknown> | null>(null); const [dash, setDash] = useState<Record<string, unknown> | null>(null);
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]); const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
@@ -147,10 +148,12 @@ export default function HomePage() {
useEffect(() => { useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; } if (!isLoggedIn()) { navigate('/login'); return; }
if (isWarehouse || isPrimary) { if ((isWarehouse || isPrimary) && warehouseOk) {
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders') request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders')
.then((data) => setOrders(Array.isArray(data.list) ? data.list : [])) .then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
.catch(() => setOrders([])); .catch(() => setOrders([]));
} else {
setOrders([]);
} }
if (!isWarehouse) { if (!isWarehouse) {
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {}); request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
@@ -165,7 +168,7 @@ export default function HomePage() {
} else { } else {
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {}); request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
} }
}, [navigate, isWarehouse, isPrimary]); }, [navigate, isWarehouse, isPrimary, warehouseOk]);
const storeCount = Number(dash?.storeCount || stores.length || 0); const storeCount = Number(dash?.storeCount || stores.length || 0);
const orderStats = useMemo(() => summarizeOrders(orders), [orders]); const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
@@ -190,7 +193,6 @@ export default function HomePage() {
const orderCount = Number(dash?.orderCount || orderStats.todayCount || 0); const orderCount = Number(dash?.orderCount || orderStats.todayCount || 0);
const revenue = orderCount * 128.45; const revenue = orderCount * 128.45;
const profit = revenue * 0.25; const profit = revenue * 0.25;
const pendingShipBadge = orderStats.pendingShip;
return ( return (
<div className="page partner-home partner-home--flush-top"> <div className="page partner-home partner-home--flush-top">
@@ -224,13 +226,6 @@ export default function HomePage() {
</div> </div>
<span className="partner-quick-action-label"></span> <span className="partner-quick-action-label"></span>
</Link> </Link>
<Link to="/reshipments" className="partner-quick-action">
<div className="partner-quick-action-icon partner-quick-action-icon--amber">
<span className="material-symbols-outlined">assignment_return</span>
{pendingShipBadge > 0 && <span className="partner-quick-badge-count">{pendingShipBadge}</span>}
</div>
<span className="partner-quick-action-label"></span>
</Link>
<Link to="/center/settlement" className="partner-quick-action"> <Link to="/center/settlement" className="partner-quick-action">
<div className="partner-quick-action-icon partner-quick-action-icon--green"> <div className="partner-quick-action-icon partner-quick-action-icon--green">
<span className="material-symbols-outlined">account_balance</span> <span className="material-symbols-outlined">account_balance</span>
@@ -252,7 +247,7 @@ export default function HomePage() {
<p className="headline-md" style={{ marginBottom: 8 }}></p> <p className="headline-md" style={{ marginBottom: 8 }}></p>
)} )}
{(isPrimary || isWarehouse) && ( {(isPrimary || isWarehouse) && warehouseOk && (
<OrderSummarySection <OrderSummarySection
title={isWarehouse ? '今日订单' : undefined} title={isWarehouse ? '今日订单' : undefined}
todayCount={orderStats.todayCount} todayCount={orderStats.todayCount}
@@ -262,6 +257,18 @@ export default function HomePage() {
/> />
)} )}
{(isPrimary || isWarehouse) && !warehouseOk && (
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
<div className="partner-warehouse-denied">
<span className="material-symbols-outlined text-primary">warehouse</span>
<p className="body-md" style={{ fontWeight: 600, marginTop: 8 }}></p>
<p className="label-md text-muted" style={{ marginTop: 4, lineHeight: 1.5 }}>
</p>
</div>
</section>
)}
{!isWarehouse && ( {!isWarehouse && (
<> <>
<section className="partner-bento-card" style={{ marginBottom: 16 }}> <section className="partner-bento-card" style={{ marginBottom: 16 }}>
+49 -5
View File
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader'; import PageHeader from '@dukang/shared-ui/PageHeader';
import { isLoggedIn, request } from '../lib/api'; import { isLoggedIn, request } from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { hasWarehouseAccess } from '../lib/partnerAccess';
type DateFilter = 'today' | '7d' | '30d'; type DateFilter = 'today' | '7d' | '30d';
type StatusFilter = 'ALL' | 'PENDING_SHIP' | 'SHIPPING' | 'COMPLETED' | 'ABNORMAL'; type StatusFilter = 'ALL' | 'PENDING_SHIP' | 'SHIPPING' | 'COMPLETED' | 'ABNORMAL';
@@ -34,15 +36,24 @@ type OrderListPageProps = {
tabRoot?: boolean; tabRoot?: boolean;
}; };
type OrdersResponse = {
list: Array<Record<string, unknown>>;
hasWarehouseAccess?: boolean;
message?: string;
};
export default function OrderListPage({ tabRoot = false }: OrderListPageProps) { export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [data, setData] = useState<{ list: Array<Record<string, unknown>> }>({ list: [] }); const { account } = usePartnerSession();
const warehouseOk = hasWarehouseAccess(account);
const [data, setData] = useState<OrdersResponse>({ list: [] });
const [tab, setTab] = useState<'orders' | 'coupons'>('orders'); const [tab, setTab] = useState<'orders' | 'coupons'>('orders');
const [dateFilter, setDateFilter] = useState<DateFilter>('today'); const [dateFilter, setDateFilter] = useState<DateFilter>('today');
const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL'); const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');
const [shippingId, setShippingId] = useState<string | null>(null); const [shippingId, setShippingId] = useState<string | null>(null);
const [shipForm, setShipForm] = useState({ logisticsCompany: '', trackingNo: '', manualQueryUrl: '' }); const [shipForm, setShipForm] = useState({ logisticsCompany: '', trackingNo: '', manualQueryUrl: '' });
const [shipModalId, setShipModalId] = useState<string | null>(null); const [shipModalId, setShipModalId] = useState<string | null>(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => { useEffect(() => {
document.title = '订单管理'; document.title = '订单管理';
@@ -50,8 +61,22 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
useEffect(() => { useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; } if (!isLoggedIn()) { navigate('/login'); return; }
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders').then(setData); if (!warehouseOk) {
}, [navigate]); setData({ list: [], hasWarehouseAccess: false, message: '未配置仓库管理权限' });
setLoaded(true);
return;
}
request<OrdersResponse>('PARTNER_H5', '/partner/orders')
.then((res) => {
setData({
list: Array.isArray(res.list) ? res.list : [],
hasWarehouseAccess: res.hasWarehouseAccess,
message: res.message,
});
})
.catch(() => setData({ list: [] }))
.finally(() => setLoaded(true));
}, [navigate, warehouseOk]);
const filtered = useMemo(() => data.list.filter((o) => { const filtered = useMemo(() => data.list.filter((o) => {
if (statusFilter === 'ALL') return true; if (statusFilter === 'ALL') return true;
@@ -83,8 +108,12 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
body: JSON.stringify(shipForm), body: JSON.stringify(shipForm),
}); });
setShipModalId(null); setShipModalId(null);
const next = await request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders'); const next = await request<OrdersResponse>('PARTNER_H5', '/partner/orders');
setData(next); setData({
list: Array.isArray(next.list) ? next.list : [],
hasWarehouseAccess: next.hasWarehouseAccess,
message: next.message,
});
} catch (err) { } catch (err) {
window.alert(err instanceof Error ? err.message : '发货失败'); window.alert(err instanceof Error ? err.message : '发货失败');
} finally { } finally {
@@ -97,6 +126,21 @@ export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
return s.includes('PENDING') || s === 'PAID' || s === 'PENDING_SHIP'; return s.includes('PENDING') || s === 'PAID' || s === 'PENDING_SHIP';
} }
if (loaded && !warehouseOk) {
return (
<div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
<div className="partner-warehouse-denied" style={{ margin: 20, padding: 32, textAlign: 'center' }}>
<span className="material-symbols-outlined text-primary" style={{ fontSize: 40 }}>warehouse</span>
<p className="headline-md" style={{ marginTop: 12 }}></p>
<p className="body-md text-muted" style={{ marginTop: 8, lineHeight: 1.6 }}>
</p>
</div>
</div>
);
}
return ( return (
<div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}> <div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />} {!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
+11 -2
View File
@@ -3278,8 +3278,17 @@ body {
border-top: 1px dashed rgba(166, 29, 36, 0.15); border-top: 1px dashed rgba(166, 29, 36, 0.15);
} }
.partner-home--flush-top .partner-home-body { .partner-warehouse-denied {
padding-top: 20px; display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 20px 12px;
}
.partner-warehouse-denied .material-symbols-outlined {
font-size: 36px;
font-variation-settings: 'FILL' 0;
} }
.partner-home--flush-top.partner-store-page, .partner-home--flush-top.partner-store-page,
+4
View File
@@ -17,6 +17,10 @@ export interface PartnerMe {
/** 子账号联系主账号用 */ /** 子账号联系主账号用 */
primaryPhone?: string; primaryPhone?: string;
primaryName?: string; primaryName?: string;
/** 主账号绑定的管仓 ID(有则本城酒单推送至该仓) */
managedWarehouseId?: string | null;
/** 是否已配置仓库管理(无则不展示/不推送酒订单) */
hasWarehouseAccess?: boolean;
} }
export interface UpdatePartnerMeRequest { export interface UpdatePartnerMeRequest {
@@ -103,10 +103,16 @@ export class PartnerCityService {
if (!primary.cityId) return { id: -1n }; if (!primary.cityId) return { id: -1n };
const warehouseIds = await this.resolveManagedWarehouseIds(primary.id); const warehouseIds = await this.resolveManagedWarehouseIds(primary.id);
if (warehouseIds.length > 0) { // 未配置管仓:不推送/不展示酒订单(同城无仓由总部履约)
return { fulfillmentWarehouseId: { in: warehouseIds } }; if (warehouseIds.length === 0) {
return { id: -1n };
} }
return { cityId: primary.cityId }; return { fulfillmentWarehouseId: { in: warehouseIds } };
}
async hasManagedWarehouse(partnerAccountId: bigint): Promise<boolean> {
const ids = await this.resolveManagedWarehouseIds(partnerAccountId);
return ids.length > 0;
} }
/** 合伙人可管仓库:主账号 managedWarehouseId + 绑定为管仓合伙人的仓 */ /** 合伙人可管仓库:主账号 managedWarehouseId + 绑定为管仓合伙人的仓 */
@@ -9,6 +9,7 @@ import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; import { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
import { PrismaService } from '../../common/prisma/prisma.module'; import { PrismaService } from '../../common/prisma/prisma.module';
import { PartnerCityService } from '../city-scope/partner-city.service';
class UpdatePartnerMeDto { class UpdatePartnerMeDto {
@IsOptional() @IsOptional()
@@ -243,7 +244,10 @@ export class AdminWineryBillController {
@Controller('partner/me') @Controller('partner/me')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
export class PartnerMeController { export class PartnerMeController {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly partnerCityService: PartnerCityService,
) {}
@Get() @Get()
async me(@CurrentUser() user: AuthUser) { async me(@CurrentUser() user: AuthUser) {
@@ -274,6 +278,7 @@ export class PartnerMeController {
where: { id: account.parentAccountId }, where: { id: account.parentAccountId },
}); });
} }
const hasWarehouseAccess = await this.partnerCityService.hasManagedWarehouse(primary.id);
return { return {
id: account.id.toString(), id: account.id.toString(),
name: account.name, name: account.name,
@@ -288,6 +293,8 @@ export class PartnerMeController {
hasWechat: !!account.wxOpenId, hasWechat: !!account.wxOpenId,
wxNickname: account.wxNickname ?? undefined, wxNickname: account.wxNickname ?? undefined,
wxAvatarUrl: account.wxAvatarUrl ?? undefined, wxAvatarUrl: account.wxAvatarUrl ?? undefined,
managedWarehouseId: primary.managedWarehouseId?.toString() ?? null,
hasWarehouseAccess,
}; };
} }
} }
@@ -509,6 +509,7 @@ export class TradeService {
async listPartnerOrders(partnerAccountId: bigint, page = 1, pageSize = 20) { async listPartnerOrders(partnerAccountId: bigint, page = 1, pageSize = 20) {
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId); const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
const hasWarehouseAccess = await this.partnerCityService.hasManagedWarehouse(primary.id);
const where = await this.partnerCityService.buildPartnerOrderWhere(primary.id); const where = await this.partnerCityService.buildPartnerOrderWhere(primary.id);
const [list, total] = await Promise.all([ const [list, total] = await Promise.all([
this.prisma.order.findMany({ this.prisma.order.findMany({
@@ -520,7 +521,14 @@ export class TradeService {
}), }),
this.prisma.order.count({ where }), this.prisma.order.count({ where }),
]); ]);
return { list: serializeBigInt(list.map(mapOrderCompat)), total, page, pageSize }; return {
list: serializeBigInt(list.map(mapOrderCompat)),
total,
page,
pageSize,
hasWarehouseAccess,
message: hasWarehouseAccess ? undefined : '未配置仓库管理权限,购酒订单由总部履约',
};
} }
async getPartnerOrder(partnerAccountId: bigint, orderId: bigint) { async getPartnerOrder(partnerAccountId: bigint, orderId: bigint) {