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 打款账单,可按门店筛选查看 + + +
{ + setFilters(v); + setPage(1); + }} + > + + ({ value, label }))} + /> + + + + + + + +
+ { + 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) -
{ setFilters(v); setPage(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 } }, }, }),