From 23ba639e9bfd8be79f2770c7a8ebe7b7663ecc0f Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Tue, 8 Sep 2026 10:07:34 +0800 Subject: [PATCH] =?UTF-8?q?v4.0.18=E7=89=88=E6=9C=AC=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../skills/dukang-coding/reference-backend.md | 2 +- apps/admin-web/src/App.tsx | 2 + apps/admin-web/src/layouts/AdminLayout.tsx | 2 + apps/admin-web/src/lib/hq-log.ts | 2 + apps/admin-web/src/pages/BankAccountsPage.tsx | 389 ++++++++++++++++++ .../admin-web/src/pages/HqPermissionsPage.tsx | 2 +- .../admin-web/src/pages/StoreAccountsPage.tsx | 243 ++++++++--- apps/mini-user/src/lib/store-display.ts | 32 +- .../src/pages/store-detail/index.tsx | 7 +- apps/mini-user/src/styles/store-detail.css | 1 + docs/企微API插件-配置手册.md | 4 +- docs/杜康好客-v3-现状对照.md | 3 +- docs/杜康好客-v3.5.15-开发文档.md | 4 +- docs/杜康好客-v3.5.16-开发文档.md | 2 +- docs/杜康好客-v3编码手册.md | 4 +- docs/杜康好客-v4-PRD.md | 25 +- docs/杜康好客-v4-现状对照.md | 6 +- docs/杜康好客-v4.0.18-开发文档.md | 80 +++- packages/domain/src/index.ts | 1 + packages/domain/src/store-address.test.ts | 31 ++ packages/domain/src/store-address.ts | 29 ++ packages/domain/src/wecom-report.test.ts | 15 + packages/domain/src/wecom-report.ts | 14 + packages/shared-types/src/hq-list-columns.ts | 1 + packages/shared-types/src/settlement.ts | 39 ++ packages/shared-types/src/shop.ts | 25 ++ .../shared-types/src/wecom-message-push.ts | 4 +- .../migrate-finance-bank-account-v4018.sql | 26 ++ server/dukang-api/prisma/schema.prisma | 36 ++ .../hq-operation/hq-operation.constants.ts | 4 + .../wecom/wecom-message-push.service.ts | 40 +- .../wecom/wecom-plugin-query.service.ts | 14 +- .../wecom/wecom-plugin.openapi.ts | 2 +- .../wecom/wecom-push-template.defaults.ts | 19 +- .../modules/ops/admin-stores.controller.ts | 33 ++ .../src/modules/ops/admin-stores.service.ts | 182 +++++++- .../ops/admin-wecom-reports.service.ts | 10 +- .../src/modules/ops/dto/admin-mutate.dto.ts | 57 +++ .../finance-bank-account.service.ts | 354 ++++++++++++++++ .../settlement/finance-bank-export.util.ts | 83 ++++ .../settlement/settlement.controller.ts | 88 +++- .../modules/settlement/settlement.module.ts | 5 +- .../modules/settlement/settlement.service.ts | 34 +- .../settlement/wecom-bill-digest.test.ts | 68 +++ .../modules/settlement/wecom-bill-digest.ts | 51 ++- .../src/modules/store/store.service.ts | 9 +- 46 files changed, 1922 insertions(+), 162 deletions(-) create mode 100644 apps/admin-web/src/pages/BankAccountsPage.tsx create mode 100644 packages/domain/src/store-address.test.ts create mode 100644 packages/domain/src/store-address.ts create mode 100644 server/dukang-api/prisma/migrate-finance-bank-account-v4018.sql create mode 100644 server/dukang-api/src/modules/settlement/finance-bank-account.service.ts create mode 100644 server/dukang-api/src/modules/settlement/finance-bank-export.util.ts create mode 100644 server/dukang-api/src/modules/settlement/wecom-bill-digest.test.ts diff --git a/.cursor/skills/dukang-coding/reference-backend.md b/.cursor/skills/dukang-coding/reference-backend.md index 2bf3cc1..98c2006 100644 --- a/.cursor/skills/dukang-coding/reference-backend.md +++ b/.cursor/skills/dukang-coding/reference-backend.md @@ -102,7 +102,7 @@ Admin 路由在 `modules/ops/` 下,前缀 `/admin/*`。 | `/admin/users` | C 端用户 | | `/admin/orders` | 订单 | | `/admin/stores` | 门店 | -| `/admin/store-accounts` | 门店账号 | +| `/admin/store-accounts` | 门店账号(含子账号 CRUD:`/:id/staff`) | | `/admin/store-media` | 门店媒体 | | `/admin/partners` | 合伙人 | | `/admin/partner-accounts` | 合伙人账号 | diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 8573032..204977c 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -36,6 +36,7 @@ import StoreBillsPage from './pages/StoreBillsPage'; import PartnerBillsPage from './pages/PartnerBillsPage'; import WineryBillsPage from './pages/WineryBillsPage'; import LogisticsBillsPage from './pages/LogisticsBillsPage'; +import BankAccountsPage from './pages/BankAccountsPage'; import TicketsPage from './pages/TicketsPage'; import SupportTicketsPage from './pages/SupportTicketsPage'; import InvoicesPage from './pages/InvoicesPage'; @@ -133,6 +134,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> = { + STORE: 'blue', + WINERY: 'gold', + PARTNER: 'purple', + LOGISTICS: 'cyan', + OTHER: 'default', +}; + +function otherNumericId(id: string) { + return id.startsWith('OTHER:') ? id.slice('OTHER:'.length) : id; +} + +export default function BankAccountsPage() { + const [filterForm] = Form.useForm<{ + type?: FinanceBankAccountType; + cityId?: string; + keyword?: string; + }>(); + const [otherForm] = Form.useForm(); + const [remarkForm] = Form.useForm<{ remark?: string }>(); + const [filters, setFilters] = useState({ type: '', cityId: '', keyword: '' }); + const [cities, setCities] = useState([]); + const [otherOpen, setOtherOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [remarkTarget, setRemarkTarget] = useState(null); + const [saving, setSaving] = useState(false); + const [exporting, setExporting] = useState<'xlsx' | 'pdf' | null>(null); + + useEffect(() => { + void request>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) + .then((res) => setCities(res.items ?? [])) + .catch(() => setCities([])); + }, []); + + const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( + '/admin/finance/bank-accounts', + () => { + const qs = new URLSearchParams(); + if (filters.type) qs.set('type', filters.type); + if (filters.cityId) qs.set('cityId', filters.cityId); + if (filters.keyword) qs.set('keyword', filters.keyword); + return qs; + }, + [filters.type, filters.cityId, filters.keyword], + ); + + function openCreate() { + setEditing(null); + otherForm.resetFields(); + setOtherOpen(true); + } + + function openEdit(row: FinanceBankAccountDto) { + setEditing(row); + otherForm.setFieldsValue({ + name: row.ownerName === row.bankAccountName ? undefined : row.ownerName, + bankAccountName: row.bankAccountName, + bankAccountNo: row.bankAccountNo, + bankBranch: row.bankBranch ?? undefined, + remark: row.remark ?? undefined, + }); + setOtherOpen(true); + } + + function openRemark(row: FinanceBankAccountDto) { + setRemarkTarget(row); + remarkForm.setFieldsValue({ remark: row.remark ?? undefined }); + } + + async function saveOther() { + const values = await otherForm.validateFields(); + setSaving(true); + try { + if (editing) { + await request(`/admin/finance/bank-accounts/other/${otherNumericId(editing.id)}`, { + method: 'PUT', + body: JSON.stringify(values), + }); + message.success('已保存'); + } else { + await request('/admin/finance/bank-accounts', { + method: 'POST', + body: JSON.stringify(values), + }); + message.success('已新增'); + } + setOtherOpen(false); + await reload(); + } catch (e) { + message.error(e instanceof Error ? e.message : '保存失败'); + } finally { + setSaving(false); + } + } + + async function saveRemark() { + if (!remarkTarget) return; + const values = await remarkForm.validateFields(); + setSaving(true); + try { + await request(`/admin/finance/bank-accounts/${encodeURIComponent(remarkTarget.id)}/remark`, { + method: 'PUT', + body: JSON.stringify({ remark: values.remark ?? '' }), + }); + message.success('备注已保存'); + setRemarkTarget(null); + await reload(); + } catch (e) { + message.error(e instanceof Error ? e.message : '保存失败'); + } finally { + setSaving(false); + } + } + + async function removeOther(row: FinanceBankAccountDto) { + try { + await request(`/admin/finance/bank-accounts/other/${otherNumericId(row.id)}`, { method: 'DELETE' }); + message.success('已删除'); + await reload(); + } catch (e) { + message.error(e instanceof Error ? e.message : '删除失败'); + } + } + + async function exportFile(format: 'xlsx' | 'pdf') { + setExporting(format); + try { + const qs = new URLSearchParams(); + qs.set('format', format); + if (filters.type) qs.set('type', filters.type); + if (filters.cityId) qs.set('cityId', filters.cityId); + if (filters.keyword) qs.set('keyword', filters.keyword); + const result = await request(`/admin/finance/bank-accounts/export?${qs}`); + downloadBase64File(result.contentBase64, result.filename, result.mimeType); + message.success(`已导出 ${result.count} 条`); + } catch (e) { + message.error(e instanceof Error ? e.message : '导出失败'); + } finally { + setExporting(null); + } + } + + const typeOptions = useMemo( + () => FINANCE_BANK_ACCOUNT_TYPES.map((value) => ({ value, label: FINANCE_BANK_ACCOUNT_TYPE_LABELS[value] })), + [], + ); + + const baseColumns: ColumnsType = [ + { + title: '类型', + dataIndex: 'type', + width: 88, + render: (type: FinanceBankAccountType) => ( + {FINANCE_BANK_ACCOUNT_TYPE_LABELS[type]} + ), + }, + { title: '归属', dataIndex: 'ownerName', width: 180 }, + { title: '城市', dataIndex: 'cityName', width: 100, render: (v?: string | null) => v || '—' }, + { title: '户名', dataIndex: 'bankAccountName', width: 140 }, + { title: '银行账号', dataIndex: 'bankAccountNo', width: 180 }, + { title: '开户行', dataIndex: 'bankBranch', width: 180, render: (v?: string | null) => v || '—' }, + { + title: '默认', + dataIndex: 'isDefault', + width: 72, + render: (v: boolean | undefined, row) => (row.type === 'STORE' ? (v ? '是' : '否') : '—'), + }, + { title: '备注', dataIndex: 'remark', width: 200, render: (v?: string | null) => v || '—' }, + { + title: '操作', + key: 'actions', + width: 160, + fixed: 'right', + render: (_, row) => ( + + + {row.editable ? ( + <> + + void removeOther(row)} + > + + + + ) : null} + + ), + }, + ]; + + const { columns, settingsButton, settingsModal } = useAdminListColumns('finance-bank-accounts', baseColumns, { + page, + pageSize, + }); + + return ( +
+ {settingsModal} + + 新增账户 + + } + /> + +
{ + setFilters({ + type: v.type || '', + cityId: v.cityId || '', + keyword: v.keyword?.trim() || '', + }); + setPage(1); + }} + > + + ({ value: c.id, label: c.name }))} + /> + + + + + + + + + + + + + + + + +
+ + { + setPage(p); + setPageSize(ps); + }, + }} + /> + + setOtherOpen(false)} + onOk={() => void saveOther()} + confirmLoading={saving} + destroyOnClose + > +
+ + + + + + + + + + + + + + + + +
+ + setRemarkTarget(null)} + onOk={() => void saveRemark()} + confirmLoading={saving} + destroyOnClose + > +
+ + + + +
+ + ); +} diff --git a/apps/admin-web/src/pages/HqPermissionsPage.tsx b/apps/admin-web/src/pages/HqPermissionsPage.tsx index 4b03d93..b4d087e 100644 --- a/apps/admin-web/src/pages/HqPermissionsPage.tsx +++ b/apps/admin-web/src/pages/HqPermissionsPage.tsx @@ -246,7 +246,7 @@ export default function HqPermissionsPage() { 按角色配置基础权限;按用户可追加或撤销。最终生效权限 =(角色权限 ∪ 追加)− 撤销。 超级管理员默认拥有除「危险操作」外的全部权限;删除用户/订单/城市、修改用户关联合伙人需在「按用户分配」或「按角色分配」中单独勾选(默认均无)。 运营/财务默认可删除门店分类;城市门店服务可新增分类,不可删除。 - 「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单。 + 「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单、物流对账与银行账户。 ; + staff?: AdminStoreStaffItem[]; }; type StoreOption = { id: string; name: string }; +const STAFF_ROLE_OPTIONS = Object.entries(STORE_STAFF_ROLE_LABELS).map(([value, label]) => ({ value, label })); +const STAFF_PERMISSION_OPTIONS = STORE_STAFF_DEFAULT_PERMISSIONS.map((value) => ({ + value, + label: STORE_STAFF_PERMISSION_LABELS[value], +})); + export default function StoreAccountsPage() { const [form] = Form.useForm(); const [createForm] = Form.useForm(); + const [staffForm] = Form.useForm(); + const [staffEditForm] = Form.useForm(); const [filters, setFilters] = useState>({}); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/store-accounts', @@ -50,9 +64,14 @@ export default function StoreAccountsPage() { const [detail, setDetail] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false); + const [staffOpen, setStaffOpen] = useState(false); + const [staffEditOpen, setStaffEditOpen] = useState(false); + const [editingStaffId, setEditingStaffId] = useState(null); const [stores, setStores] = useState([]); const [deletingStaffId, setDeletingStaffId] = useState(null); + const parentStoreOptions = (detail?.stores ?? []).map((s) => ({ value: s.id, label: s.name })); + async function loadStores() { const res = await request>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`); setStores(res.items); @@ -64,6 +83,34 @@ export default function StoreAccountsPage() { void reload(); } + function openAddStaff() { + if (!detail) return; + if (!detail.stores?.length) { + message.warning('请先为该主账号绑定门店'); + return; + } + staffForm.resetFields(); + staffForm.setFieldsValue({ + staffRole: StoreStaffRole.CASHIER, + permissions: [...STORE_STAFF_DEFAULT_PERMISSIONS], + storeIds: detail.stores.map((s) => s.id), + }); + setStaffOpen(true); + } + + function openEditStaff(staff: AdminStoreStaffItem) { + setEditingStaffId(staff.id); + staffEditForm.setFieldsValue({ + name: staff.name, + phone: staff.phone, + staffRole: staff.staffRole ?? StoreStaffRole.CASHIER, + status: staff.status, + permissions: staff.permissions?.length ? staff.permissions : [...STORE_STAFF_DEFAULT_PERMISSIONS], + storeIds: staff.storeIds?.length ? staff.storeIds : (staff.stores ?? []).map((s) => s.id), + }); + setStaffEditOpen(true); + } + async function deleteStaff(staffId: string) { if (!detail) return; setDeletingStaffId(staffId); @@ -151,13 +198,58 @@ export default function StoreAccountsPage() { const { columns, settingsButton, settingsModal } = useAdminListColumns('store-accounts', baseColumns, { page, pageSize }); + const staffColumns: ColumnsType = [ + { title: '姓名', dataIndex: 'name', width: 90 }, + { title: '手机', dataIndex: 'phone', width: 120 }, + { + title: '角色', + dataIndex: 'staffRole', + width: 80, + render: (role) => STORE_STAFF_ROLE_LABELS[role as StoreStaffRole] || role || '—', + }, + { + title: '门店', + render: (_, staff) => + staff.stores?.length ? staff.stores.map((s) => s.name).join('、') : '—', + }, + { + title: '状态', + dataIndex: 'status', + width: 70, + render: (s) => {ACCOUNT_STATUS_LABELS[s] || s}, + }, + { + title: '操作', + width: 120, + render: (_, staff) => ( + + + void deleteStaff(staff.id)} + > + + + + ), + }, + ]; + return (
{settingsModal} setDrawerOpen(false)} extra={ detail && (
{ACCOUNT_STATUS_LABELS[s] || s}, - }, - { - title: '操作', - width: 80, - render: (_, staff) => ( - void deleteStaff(staff.id)} - > - - - ), - }, - ]} - /> - - ) : ( - - 暂无子账号 - - )} +
+ + 子账号({detail.staff?.length ?? 0})· 仅主账号可添加,不可多级 + + +
+
)} @@ -321,6 +387,87 @@ export default function StoreAccountsPage() { + setStaffOpen(false)} + onOk={async () => { + if (!detail) return; + const v = await staffForm.validateFields(); + await request(`/admin/store-accounts/${detail.id}/staff`, { + method: 'POST', + body: JSON.stringify(v), + }); + message.success('子账号已创建'); + setStaffOpen(false); + await refreshDetail(detail.id); + }} + > +
+ + + + + + + + + + + +
+ setStaffEditOpen(false)} + onOk={async () => { + if (!detail || !editingStaffId) return; + const v = await staffEditForm.validateFields(); + await request(`/admin/store-accounts/${detail.id}/staff/${editingStaffId}`, { + method: 'PUT', + body: JSON.stringify(v), + }); + message.success('子账号已更新'); + setStaffEditOpen(false); + await refreshDetail(detail.id); + }} + > +
+ + + + + + ({ value, label }))} /> + + +