diff --git a/README.md b/README.md
index 67a40e4..c5dbbf6 100644
--- a/README.md
+++ b/README.md
@@ -47,8 +47,10 @@ pnpm dev:partner # http://localhost:5175
| 端 | 手机号 |
|----|--------|
| C端用户 | 13800000001 |
-| 门店 | 13900000001 |
| 合伙人 | 13700000001 |
+| HQ | 13600000001 |
+
+门店登录使用录入门店时绑定的手机号;seed 仅预置「郑州老城店」联调账号 `13910000001`。
## 主链路冒烟
diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx
index 0f3d4e7..544947b 100644
--- a/apps/admin-web/src/App.tsx
+++ b/apps/admin-web/src/App.tsx
@@ -21,7 +21,7 @@ import StoreMediaPage from './pages/StoreMediaPage';
import ProductsPage from './pages/ProductsPage';
import ProductDetailTemplatesPage from './pages/ProductDetailTemplatesPage';
import ResourcesPage from './pages/ResourcesPage';
-import StorePayoutsPage from './pages/StorePayoutsPage';
+import StoreBillsPage from './pages/StoreBillsPage';
import PartnerBillsPage from './pages/PartnerBillsPage';
import TicketsPage from './pages/TicketsPage';
import UserLogsPage from './pages/UserLogsPage';
@@ -60,7 +60,8 @@ export default function App() {
} />
} />
} />
- } />
+ } />
+ } />
} />
} />
} />
diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx
index 846f117..9285d0d 100644
--- a/apps/admin-web/src/layouts/AdminLayout.tsx
+++ b/apps/admin-web/src/layouts/AdminLayout.tsx
@@ -40,6 +40,7 @@ const MENU_ITEMS: MenuProps['items'] = [
children: [
{ key: '/stores', label: '门店列表' },
{ key: '/store-accounts', label: '门店账户' },
+ { key: '/store-bills', label: '门店账单' },
{ key: '/store-media', label: '门店资源' },
],
},
@@ -62,7 +63,6 @@ const MENU_ITEMS: MenuProps['items'] = [
{ key: '/benefit/ledgers', label: '流水' },
{ key: '/redeem-records', label: '核销记录' },
{ key: '/redeem/debug', label: '核销调试' },
- { key: '/store-payouts', label: '门店打款' },
],
},
{ key: '/partner-bills', icon: , label: '合伙人结算' },
diff --git a/apps/admin-web/src/pages/StoreBillsPage.tsx b/apps/admin-web/src/pages/StoreBillsPage.tsx
new file mode 100644
index 0000000..15e96ea
--- /dev/null
+++ b/apps/admin-web/src/pages/StoreBillsPage.tsx
@@ -0,0 +1,205 @@
+import { useEffect, useState } from 'react';
+import { Button, Descriptions, Drawer, Form, Select, Space, Table, Tag, Typography, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import { request, type Paginated } from '../lib/api';
+import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
+import { useAdminList } from '../lib/useAdminList';
+
+type Row = {
+ id: string;
+ redeemAmount: number;
+ payoutAmount: number;
+ settlementRate: number;
+ status: string;
+ expectedPayAt: string;
+ paidAt?: string;
+ createdAt: string;
+ store?: { id: string; name: string; cityName: string; phone?: string };
+ redeemRecord?: { redeemNo: string; amount?: number };
+};
+
+type StoreOption = { id: string; name: string; phone: string };
+
+const PAYOUT_STATUS_LABELS: Record = {
+ PENDING: '待打款',
+ PAID: '已打款',
+};
+
+const PAYOUT_STATUS_COLORS: Record = {
+ PENDING: 'orange',
+ PAID: 'green',
+};
+
+export default function StoreBillsPage() {
+ const [form] = Form.useForm();
+ const [filters, setFilters] = useState>({});
+ const [stores, setStores] = useState([]);
+ const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList(
+ '/admin/store-payouts',
+ () => {
+ const qs = new URLSearchParams();
+ if (filters.status) qs.set('status', filters.status);
+ if (filters.storeId) qs.set('storeId', filters.storeId);
+ return qs;
+ },
+ [filters],
+ );
+ const [detail, setDetail] = useState | null>(null);
+ const [drawerOpen, setDrawerOpen] = useState(false);
+
+ useEffect(() => {
+ void request>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
+ .then((res) => setStores(res.items))
+ .catch(() => {});
+ }, []);
+
+ async function confirmPayout(id: string) {
+ await request(`/admin/store-payouts/${id}/confirm`, {
+ method: 'POST',
+ body: JSON.stringify({ remark: '财务确认打款' }),
+ });
+ message.success('已确认打款');
+ reload();
+ }
+
+ const columns: ColumnsType = [
+ { title: '门店', dataIndex: ['store', 'name'], width: 140, ellipsis: true },
+ { title: '登录手机', dataIndex: ['store', 'phone'], width: 120, render: (v) => v || '—' },
+ { title: '城市', dataIndex: ['store', 'cityName'], width: 90 },
+ {
+ title: '核销单号',
+ dataIndex: ['redeemRecord', 'redeemNo'],
+ width: 160,
+ ellipsis: true,
+ render: (v) => v || '—',
+ },
+ { title: '核销面额', dataIndex: 'redeemAmount', width: 100, render: (v) => `¥${v}` },
+ {
+ title: '到账金额',
+ dataIndex: 'payoutAmount',
+ width: 100,
+ render: (v, row) => `¥${v}${row.settlementRate ? ` (${Math.round(row.settlementRate * 100)}%)` : ''}`,
+ },
+ {
+ title: '打款状态',
+ dataIndex: 'status',
+ width: 100,
+ render: (s) => {PAYOUT_STATUS_LABELS[s] || s},
+ },
+ { title: '预计打款', dataIndex: 'expectedPayAt', width: 160, render: fmtTime },
+ { title: '实际打款', dataIndex: 'paidAt', width: 160, render: (v) => (v ? fmtTime(String(v)) : '—') },
+ {
+ title: '操作',
+ width: 140,
+ fixed: 'right',
+ render: (_, row) => (
+
+
+ {row.status === 'PENDING' && (
+
+ )}
+
+ ),
+ },
+ ];
+
+ return (
+
+
+ 门店核销账单
+
+ 每笔核销对应一条 T+1 打款账单,可按门店筛选查看
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ setPage(p);
+ setPageSize(ps);
+ },
+ }}
+ />
+ setDrawerOpen(false)}>
+ {detail && (
+
+ {String((detail.store as { name?: string })?.name ?? '—')}
+
+ {String((detail.redeemRecord as { redeemNo?: string })?.redeemNo ?? '—')}
+
+ ¥{String(detail.redeemAmount)}
+ ¥{String(detail.payoutAmount)}
+
+ {detail.settlementRate ? `${Math.round(Number(detail.settlementRate) * 100)}%` : '—'}
+
+
+ {PAYOUT_STATUS_LABELS[String(detail.status)] || String(detail.status)}
+
+ {fmtTime(String(detail.expectedPayAt))}
+
+ {detail.paidAt ? fmtTime(String(detail.paidAt)) : '—'}
+
+ {fmtTime(String(detail.createdAt))}
+
+ )}
+
+
+ );
+}
diff --git a/apps/admin-web/src/pages/StorePayoutsPage.tsx b/apps/admin-web/src/pages/StorePayoutsPage.tsx
deleted file mode 100644
index ec4bf8c..0000000
--- a/apps/admin-web/src/pages/StorePayoutsPage.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { useState } from 'react';
-import { Button, Descriptions, Drawer, Form, Input, Select, Table, Typography, message } from 'antd';
-import type { ColumnsType } from 'antd/es/table';
-import { request } from '../lib/api';
-import { fmtTime } from '../lib/constants';
-import { useAdminList } from '../lib/useAdminList';
-
-type Row = {
- id: string;
- redeemAmount: number;
- payoutAmount: number;
- status: string;
- expectedPayAt: string;
- paidAt?: string;
- store?: { name: string; cityName: string };
- redeemRecord?: { redeemNo: string };
-};
-
-export default function StorePayoutsPage() {
- const [form] = Form.useForm();
- const [filters, setFilters] = useState>({});
- const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList(
- '/admin/store-payouts',
- () => {
- const qs = new URLSearchParams();
- if (filters.status) qs.set('status', filters.status);
- if (filters.storeId) qs.set('storeId', filters.storeId);
- return qs;
- },
- [filters],
- );
- const [detail, setDetail] = useState | null>(null);
- const [drawerOpen, setDrawerOpen] = useState(false);
-
- async function confirmPayout(id: string) {
- await request(`/admin/store-payouts/${id}/confirm`, {
- method: 'POST',
- body: JSON.stringify({ remark: '财务确认打款' }),
- });
- message.success('已确认打款');
- reload();
- }
-
- const columns: ColumnsType = [
- { title: '门店', dataIndex: ['store', 'name'] },
- { title: '城市', dataIndex: ['store', 'cityName'], width: 100 },
- { title: '核销额', dataIndex: 'redeemAmount', width: 90, render: (v) => `¥${v}` },
- { title: '打款额', dataIndex: 'payoutAmount', width: 90, render: (v) => `¥${v}` },
- { title: '状态', dataIndex: 'status', width: 90 },
- { title: '预计打款', dataIndex: 'expectedPayAt', width: 160, render: fmtTime },
- { title: '实际打款', dataIndex: 'paidAt', width: 160, render: (v) => (v ? fmtTime(String(v)) : '—') },
- {
- title: '操作', width: 140,
- render: (_, row) => (
- <>
-
- {row.status === 'PENDING' && (
-
- )}
- >
- ),
- },
- ];
-
- return (
-
-
门店打款(T+1)
-
-
-
-
-
-
-
{ setPage(p); setPageSize(ps); } }} />
- setDrawerOpen(false)}>
- {detail && (
-
- ¥{String(detail.redeemAmount)}
- ¥{String(detail.payoutAmount)}
- {String(detail.status)}
- {fmtTime(String(detail.expectedPayAt))}
-
- )}
-
-
- );
-}
diff --git a/apps/h5-shop/src/pages/LoginPage.tsx b/apps/h5-shop/src/pages/LoginPage.tsx
index 8215468..ca21971 100644
--- a/apps/h5-shop/src/pages/LoginPage.tsx
+++ b/apps/h5-shop/src/pages/LoginPage.tsx
@@ -9,7 +9,6 @@ function maskPhone(phone: string) {
return `${phone.slice(0, 3)} **** ${phone.slice(-4)}`;
}
-const DEV_DEFAULT_PHONE = import.meta.env.DEV ? '13900000001' : '';
const DEV_DEFAULT_CODE = import.meta.env.DEV ? '123456' : '';
export default function LoginPage() {
@@ -18,7 +17,7 @@ export default function LoginPage() {
const [params] = useSearchParams();
const quick = params.get('quick') === '1';
const savedProfile = getStoreProfile();
- const [phone, setPhone] = useState(getLastPhone() || DEV_DEFAULT_PHONE);
+ const [phone, setPhone] = useState(getLastPhone());
const [code, setCode] = useState(DEV_DEFAULT_CODE);
const [agreed, setAgreed] = useState(false);
const [loading, setLoading] = useState(false);
diff --git a/scripts/smoke-prev1.mjs b/scripts/smoke-prev1.mjs
index e90e02c..8e6157e 100644
--- a/scripts/smoke-prev1.mjs
+++ b/scripts/smoke-prev1.mjs
@@ -64,7 +64,7 @@ async function main() {
console.log('5. Shop login + redeem preview');
const shopLogin = await loginSms(
'SHOP_H5',
- '13900000001',
+ '13910000001',
'STORE_LOGIN',
'/shop/auth/login/sms',
'/shop/auth/sms/send',
diff --git a/scripts/smoke-v3.mjs b/scripts/smoke-v3.mjs
index 28999c2..005cba8 100644
--- a/scripts/smoke-v3.mjs
+++ b/scripts/smoke-v3.mjs
@@ -133,7 +133,7 @@ async function main() {
console.log('8. Shop confirm redeem');
const shopLogin = await loginSms(
'SHOP_H5',
- '13900000001',
+ '13910000001',
'STORE_LOGIN',
'/shop/auth/login/sms',
'/shop/auth/sms/send',
diff --git a/scripts/test-shop-auth.mjs b/scripts/test-shop-auth.mjs
index 35e0d6c..ce0c69a 100644
--- a/scripts/test-shop-auth.mjs
+++ b/scripts/test-shop-auth.mjs
@@ -40,17 +40,17 @@ async function main() {
console.log('2. STORE_LOGIN bound phone');
await req('SHOP_H5', '/shop/auth/sms/send', {
method: 'POST',
- body: JSON.stringify({ phone: '13900000001', scene: 'STORE_LOGIN' }),
+ body: JSON.stringify({ phone: '13910000001', scene: 'STORE_LOGIN' }),
});
const login = await req('SHOP_H5', '/shop/auth/login/sms', {
method: 'POST',
- body: JSON.stringify({ phone: '13900000001', code: MOCK_SMS_CODE }),
+ body: JSON.stringify({ phone: '13910000001', code: MOCK_SMS_CODE }),
});
if (!login.accessToken || !login.refreshToken) throw new Error('login missing tokens');
console.log('3. Shop session me + refresh');
const me = await req('SHOP_H5', '/shop/auth/me', { token: login.accessToken });
- if (me.phone !== '13900000001') throw new Error('me phone mismatch');
+ if (me.phone !== '13910000001') throw new Error('me phone mismatch');
const refreshed = await req('SHOP_H5', '/shop/auth/token/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken: login.refreshToken }),
@@ -69,7 +69,7 @@ async function main() {
const occupied = await expectFail('HQ_WEB', '/admin/stores/phone/sms/send', {
method: 'POST',
token: admin.accessToken,
- body: JSON.stringify({ phone: '13900000001' }),
+ body: JSON.stringify({ phone: '13910000001' }),
});
if (!occupied.includes('已绑定')) throw new Error(`unexpected: ${occupied}`);
diff --git a/server/dukang-api/prisma/seed-prev1.ts b/server/dukang-api/prisma/seed-prev1.ts
index 5769e3e..b6742a2 100644
--- a/server/dukang-api/prisma/seed-prev1.ts
+++ b/server/dukang-api/prisma/seed-prev1.ts
@@ -143,7 +143,7 @@ async function main() {
partnerId: partner.id,
categoryId: categories[0].id,
name: '郑州老城店',
- phone: '0371-88880001',
+ phone: '13910000001',
province: '河南省',
cityName: '郑州市',
district: '金水区',
@@ -165,7 +165,7 @@ async function main() {
partnerId: partner.id,
categoryId: categories[1].id,
name: '郑州美食城店',
- phone: '0371-88880002',
+ phone: '13910000002',
province: '河南省',
cityName: '郑州市',
district: '二七区',
@@ -179,11 +179,7 @@ async function main() {
});
await prisma.storeAccount.create({
- data: { storeId: store1.id, phone: '13900000001', name: '老城店店长' },
- });
-
- await prisma.storeAccount.create({
- data: { storeId: store2.id, phone: '13900000002', name: '美食城店长' },
+ data: { storeId: store1.id, phone: '13910000001', name: '郑州老城店' },
});
await prisma.user.create({
@@ -233,7 +229,7 @@ async function main() {
stores: 2,
testPhones: {
user: '13800000001',
- store: '13900000001',
+ store: '13910000001',
partner: '13700000001',
},
});
diff --git a/server/dukang-api/prisma/seed-v31.ts b/server/dukang-api/prisma/seed-v31.ts
index 792d36f..8bafbca 100644
--- a/server/dukang-api/prisma/seed-v31.ts
+++ b/server/dukang-api/prisma/seed-v31.ts
@@ -153,25 +153,23 @@ async function main() {
const storeDefs = [
{
name: '郑州老城店',
- phone: '0371-88880001',
+ phone: '13910000001',
district: '金水区',
address: '花园路100号',
intro: '正宗河南菜,欢迎核销好客权益',
img: 'https://picsum.photos/seed/store1/400/300',
categoryId: categories[0].id,
- accountPhone: '13900000001',
- accountName: '老城店店长',
+ withAccount: true,
},
{
name: '郑州美食城店',
- phone: '0371-88880002',
+ phone: '13910000002',
district: '二七区',
address: '大学路200号',
intro: '地方特色餐饮',
img: 'https://picsum.photos/seed/store2/400/300',
categoryId: categories[1].id,
- accountPhone: '13900000002',
- accountName: '美食城店长',
+ withAccount: false,
},
];
@@ -198,9 +196,11 @@ async function main() {
});
const cover = await createMockResource(ResourceOwnerType.STORE, store.id, ResourceBizType.COVER, def.img);
await prisma.store.update({ where: { id: store.id }, data: { coverResourceId: cover.id } });
- await prisma.storeAccount.create({
- data: { storeId: store.id, phone: def.accountPhone, name: def.accountName },
- });
+ if (def.withAccount) {
+ await prisma.storeAccount.create({
+ data: { storeId: store.id, phone: def.phone, name: def.name },
+ });
+ }
}
await prisma.user.create({
@@ -252,7 +252,6 @@ async function main() {
stores: storeDefs.length,
testPhones: {
user: '13800000001',
- store: '13900000001',
partner: '13700000001',
hq: '13600000001',
},
diff --git a/server/dukang-api/src/modules/settlement/settlement.service.ts b/server/dukang-api/src/modules/settlement/settlement.service.ts
index e76a949..80fe196 100644
--- a/server/dukang-api/src/modules/settlement/settlement.service.ts
+++ b/server/dukang-api/src/modules/settlement/settlement.service.ts
@@ -73,7 +73,7 @@ export class SettlementService {
skip: (page - 1) * pageSize,
take: pageSize,
include: {
- store: { select: { id: true, name: true, cityName: true } },
+ store: { select: { id: true, name: true, cityName: true, phone: true } },
redeemRecord: { select: { redeemNo: true, amount: true, userId: true } },
},
}),