From df251c2c3c95c687dd0d1a127fe163d6beabd412 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 30 Oct 2026 13:32:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=96=B0=E5=A2=9E=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E7=B3=BB=E7=BB=9F=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Phase1-验收报告.md | 6 +- docs/Phase2-系统配置验收.md | 110 +++++ docs/项目计划.md | 2 +- frontend/src/app/AppRouter.tsx | 7 +- .../features/admin/AdminSystemConfigsPage.tsx | 456 ++++++++++++++++++ frontend/src/features/admin/api.ts | 20 + frontend/src/features/admin/labels.ts | 26 + frontend/src/features/admin/types.ts | 15 + frontend/src/layouts/AdminLayout.tsx | 2 +- frontend/src/styles/global.css | 12 + 10 files changed, 650 insertions(+), 6 deletions(-) create mode 100644 docs/Phase2-系统配置验收.md create mode 100644 frontend/src/features/admin/AdminSystemConfigsPage.tsx diff --git a/docs/Phase1-验收报告.md b/docs/Phase1-验收报告.md index bb98e9a..27f3534 100644 --- a/docs/Phase1-验收报告.md +++ b/docs/Phase1-验收报告.md @@ -194,12 +194,12 @@ dist/assets/AdminLayout-*.js 36.87 kB │ gzip 13.02 kB ## 5. 已知限制 1. 移动端订单列表/详情与消息中心已接入真实接口;下单、支付、交接、归还、结账等操作仍待迁移。 -2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、公告管理、审计日志,其余页面仍为统一占位。 +2. 后台已完成仪表盘、订单列表/详情、退款审核、提现审核、商品列表/详情与审核、用户列表/详情与人工操作、申诉仲裁、财务看板、公告管理、审计日志、系统配置,其余页面仍为统一占位。 3. 分销后台已完成主题、Logo、登录页与 Shell 轻改,页面结构未重写。 -4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`,公告 `announcement:view` / `announcement:manage`,审计日志 `audit_log:view`;系统配置与分销站点仍需核对。 +4. 后台菜单权限码按后端逐个核对:订单 `order:list` / `order:view` / `order:close`,商品 `listing:view` / `listing:approve` / `listing:reject` / `listing:offline`,用户 `user:view` / `user:freeze` / `user:unfreeze` / `user:deposit_free` / `user:growth_points` / `user:manual_realname` / `user:revoke_realname` / `user:wallet_adjust`,提现 `withdrawal:list` / `withdrawal:detail` / `withdrawal:review` / `withdrawal:pay`,申诉 `dispute:view` / `dispute:arbitrate`,财务 `wallet:view`,公告 `announcement:view` / `announcement:manage`,审计日志 `audit_log:view`,系统配置 `system_config:view` / `system_config:update`;分销站点仍需核对。 5. SVG 中文尚未转曲。 6. 入口包已降至 495 kB(gzip 161 kB);AntD 共享基础设施仍在入口,后续可拆独立入口。 -7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理、审核、退款、提现、用户、申诉、财务、公告与审计日志已按批次提交。 +7. 移动端、订单、分销轻改与首屏优化均已提交;商品管理、审核、退款、提现、用户、申诉、财务、公告、审计日志与系统配置已按批次提交。 --- diff --git a/docs/Phase2-系统配置验收.md b/docs/Phase2-系统配置验收.md new file mode 100644 index 0000000..5c70506 --- /dev/null +++ b/docs/Phase2-系统配置验收.md @@ -0,0 +1,110 @@ +# Phase 2 验收报告(后台系统配置) + +> 日期:2026-10-30 +> 项目:解忧商行(hfb_jysh) +> 范围:`/console-jy8k3m/system-configs` +> 结论:**本切片通过,系统配置列表、筛选、JSON 格式化与更新已接真实接口** + +--- + +## 1. 接口与权限 + +| 能力 | 接口 | 权限码 | +| --- | --- | --- | +| 配置列表 | `GET /api/admin/system-configs` | `system_config:view` | +| 更新配置 | `PUT /api/admin/system-configs/:key` | `system_config:update` | + +更新请求体: + +```json +{ + "value": "配置值字符串", + "description": "配置说明" +} +``` + +后端行为: + +- 配置键不可新增或删除,只能在默认配置白名单内更新。 +- `backup.*` 只能走独立备份调度接口,不能从本页修改。 +- `integration.paddle_ocr_token` 允许保存空值。 +- `distribution.default_settlement_hold_days` 必须为 0–365 的整数。 +- `listing.publish_cooldown_minutes` 必须为不小于 0 的整数。 +- 发布选项、租客成长规则与短信模板由后端做结构校验和归一化;其余 JSON 配置主要由前端校验可解析,保存前需人工核对结构。 +- 每次更新写入 `audit_logs`,动作为 `system_config.update`。 + +## 2. 页面 + +`frontend/src/features/admin/AdminSystemConfigsPage.tsx` + +- 一次读取全部可见配置,前端按关键字与分类即时筛选 +- 分类:聊天与群组、分销、交接与超时、第三方集成、商品发布、移动端首页、订单、个人中心、租客成长、短信 +- 列:配置键/说明、分类、类型、当前值、更新时间、操作 +- 类型标签:`JSON` / `文本`,敏感键额外显示 `敏感` +- 敏感键(token、secret、password、api_key、private_key)在列表中只显示「已配置 / 未配置」,进入编辑后可见原值 +- 行点击或「编辑」打开配置弹窗 +- 无 `system_config:view` 时停止请求并显示权限提示;无 `system_config:update` 时只读 + +## 3. 编辑弹窗 + +- 只读展示配置键、分类、类型与敏感标记 +- 配置说明可编辑,最多 200 字 +- 配置值使用等宽字体文本域,JSON 值默认两空格缩进 +- 「格式化 JSON」仅在当前值可解析时可用 +- 保存前做前端校验: + - 非空(OCR token 除外) + - 分销结算冷却天数 0–365 + - 商品发布冷却分钟数不小于 0 + - 原值为 JSON 或新值以 `{` / `[` 开头时必须能解析 +- 保存前二次确认,提示配置会立即影响下一次读取并写入审计日志 +- 敏感配置额外提示不要将生产密钥写入开发库 + +## 4. 菜单与路由 + +- 路由:`frontend/src/app/AppRouter.tsx` 新增 `system-configs` +- 菜单:`frontend/src/layouts/AdminLayout.tsx` 权限码由 `systemconfig:view` 修正为 `system_config:view` +- 占位列表同步移除 `system-configs` + +## 5. 开发库配置概况 + +当前可见配置 27 项,其中 JSON 配置包含: + +```text +listing.publish_agreements +listing.publish_options +listing.sale_price_config +mobile.aw_recycle +mobile.cooperation_feedback +mobile.home_announcements +mobile.home_banners +order.agreements +profile.post_rental_notice +renter.growth_level_rules +sms.business_templates +``` + +## 6. 验证结果 + +```text +npx tsc --noEmit 通过 +npm run build 通过(入口 497.41 kB,gzip 161.38 kB) +GET /api/admin/system-configs ok,返回 27 项 +GET /console-jy8k3m/system-configs 200 +``` + +列表读取已实测;未执行 `PUT` 写操作,避免改动开发库运行参数。 + +## 7. 已知限制 + +1. 当前是原始键值编辑器,首页轮播、发布协议、短信模板等复杂 JSON 仍直接编辑原文,没有结构化表单。 +2. 不支持新增或删除配置键,新增配置需先在后端默认配置中登记。 +3. 备份调度不在本页,使用独立接口与后台入口。 +4. 没有配置变更历史对比,需到审计日志查看 `system_config.update` 明细。 +5. 敏感值仅在编辑弹窗展示,未接密钥轮换或加密存储改造。 + +## 8. 下一步建议 + +1. 对首页轮播、发布协议、短信模板做结构化编辑器。 +2. 线下出款与运营支出登记。 +3. 分销站点管理页。 +4. 移动端下单、支付与交接/归还/结账。 diff --git a/docs/项目计划.md b/docs/项目计划.md index 01f7866..072eb9e 100644 --- a/docs/项目计划.md +++ b/docs/项目计划.md @@ -395,7 +395,7 @@ hfb_jysh/ ### Phase 4:管理后台核心(4–5 周) -> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、公告管理与审计日志;线下出款/运营支出与系统配置待迁移。 +> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、公告管理、审计日志与系统配置;线下出款/运营支出待迁移。 - 后台登录、工作台、订单列表、订单详情。 - 商品列表、商品详情、审核。 diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index cffac23..89bad83 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -108,6 +108,11 @@ const AdminAuditLogsPage = lazy(() => default: module.AdminAuditLogsPage, })) ) +const AdminSystemConfigsPage = lazy(() => + import('@/features/admin/AdminSystemConfigsPage').then(module => ({ + default: module.AdminSystemConfigsPage, + })) +) const AdminLayout = lazy(() => import('@/layouts/AdminLayout').then(module => ({ default: module.AdminLayout })) ) @@ -117,7 +122,6 @@ const AdminSectionPlaceholder = lazy(() => const adminSections: Array<{ path: string; title: string; description: string }> = [ { path: 'distribution/sites', title: '分销站点', description: '管理分站、规则、结算与站点钱包。' }, - { path: 'system-configs', title: '系统配置', description: '维护首页、发布、订单与支付配置。' }, ] export function AppRouter() { @@ -159,6 +163,7 @@ export function AppRouter() { } /> } /> } /> + } /> {adminSections.map(section => ( 160 ? `${text.slice(0, 160)}…` : text || '空字符串' +} + +function validateValue(item: AdminSystemConfig, value: string) { + if (item.key !== EMPTY_TOKEN_KEY && !value.trim()) return '配置值不能为空' + if (item.key === 'distribution.default_settlement_hold_days') { + const days = Number(value) + if (!Number.isInteger(days) || days < 0 || days > 365) { + return '结算冷却天数必须是 0-365 的整数' + } + } + if (item.key === 'listing.publish_cooldown_minutes') { + const minutes = Number(value) + if (!Number.isInteger(minutes) || minutes < 0) { + return '发布冷却分钟数必须是不小于 0 的整数' + } + } + if (isJsonConfig(item) || value.trim().startsWith('{') || value.trim().startsWith('[')) { + try { + JSON.parse(value) + } catch { + return 'JSON 格式不正确,请先修正或点击「格式化 JSON」' + } + } + return null +} + +function buildColumns( + onEdit: (item: AdminSystemConfig) => void, + canUpdate: boolean +): TableColumnsType { + return [ + { + title: '配置键', + dataIndex: 'key', + width: 330, + fixed: 'left', + render: (_, row) => ( +
+ {row.key} + {formatText(row.description)} +
+ ), + }, + { + title: '分类', + key: 'category', + width: 120, + render: (_, row) => {systemConfigCategoryLabel(row.key)}, + }, + { + title: '类型', + key: 'type', + width: 110, + render: (_, row) => ( + + {isJsonConfig(row) ? JSON : 文本} + {isSensitive(row) ? 敏感 : null} + + ), + }, + { + title: '当前值', + key: 'value', + width: 380, + render: (_, row) => ( + + {isSensitive(row) ? ( + {previewValue(row)} + ) : ( + {previewValue(row)} + )} + + ), + }, + { + title: '更新时间', + dataIndex: 'updated_at', + width: 170, + render: (_, row) => ( +
+ {formatDateTime(row.updated_at)} + + {row.updated_by != null ? `操作人 #${row.updated_by}` : '系统默认值'} + +
+ ), + }, + { + title: '操作', + key: 'actions', + width: 96, + fixed: 'right', + render: (_, row) => + canUpdate ? ( + + ) : ( + 只读 + ), + }, + ] +} + +export function AdminSystemConfigsPage() { + const { message, modal } = App.useApp() + const [keyword, setKeyword] = useState('') + const [category, setCategory] = useState('') + const [editing, setEditing] = useState(null) + const [draftValue, setDraftValue] = useState('') + const [draftDescription, setDraftDescription] = useState('') + const [saving, setSaving] = useState(false) + const permissions = useAdminSessionStore(state => state.admin?.permissions) + const canView = Boolean(permissions?.includes('*') || permissions?.includes('system_config:view')) + const canUpdate = Boolean( + permissions?.includes('*') || permissions?.includes('system_config:update') + ) + + const { data, isPending, isFetching, isError, error, refetch } = useQuery({ + queryKey: ['admin-system-configs'], + queryFn: fetchAdminSystemConfigs, + enabled: canView, + }) + + const items = data ?? [] + + const filtered = useMemo(() => { + const text = keyword.trim().toLowerCase() + return items.filter(item => { + const matchesCategory = !category || systemConfigCategory(item.key) === category + const matchesKeyword = + !text || + item.key.toLowerCase().includes(text) || + (item.description ?? '').toLowerCase().includes(text) + return matchesCategory && matchesKeyword + }) + }, [items, keyword, category]) + + const jsonCount = items.filter(isJsonConfig).length + const sensitiveCount = items.filter(isSensitive).length + const errorText = error instanceof Error ? error.message : '请稍后重试或重新登录后台。' + const hasActiveFilters = Boolean(keyword || category) + + const openEditor = (item: AdminSystemConfig) => { + setEditing(item) + setDraftValue(isJsonConfig(item) ? formatJson(item.value) : item.value) + setDraftDescription(item.description) + } + + const closeEditor = () => { + if (saving) return + setEditing(null) + } + + const doSave = async () => { + if (!editing) return + setSaving(true) + try { + await updateAdminSystemConfig(editing.key, { + value: draftValue, + description: draftDescription.trim(), + }) + message.success('系统配置已更新,并已写入审计日志') + setEditing(null) + await refetch() + } catch (err) { + message.error(err instanceof Error ? err.message : '更新失败,请稍后重试') + } finally { + setSaving(false) + } + } + + const submit = () => { + if (!editing) return + const validation = validateValue(editing, draftValue) + if (validation) { + message.warning(validation) + return + } + modal.confirm({ + title: '确认更新系统配置?', + icon: , + content: ( +
+ + 配置键:{editing.key} + + 保存后前台或后台的下一次读取会立即使用新值,并记录一条审计日志。 + 请确认已经核对过 JSON 结构、金额单位与布尔值。 +
+ ), + okText: '确认更新', + cancelText: '再检查一下', + onOk: doSave, + }) + } + + const columns = useMemo(() => buildColumns(openEditor, canUpdate), [canUpdate]) + const editingJson = editing ? parseJson(draftValue).valid : false + + return ( +
+ + 共 {items.length} 项 + {jsonCount > 0 ? JSON {jsonCount} 项 : null} + {sensitiveCount > 0 ? 敏感 {sensitiveCount} 项 : null} + {isFetching && !isPending ? 正在更新 : null} + + } + extra={ + + } + /> + + {!canView ? ( + + ) : null} + + { + setKeyword('') + setCategory('') + } + : undefined + } + loading={isFetching} + extra={ + + {filtered.length === items.length + ? '显示全部配置' + : `筛选出 ${filtered.length} / ${items.length} 项`} + + } + > + + setKeyword(event.target.value)} + /> + + + setDraftDescription(event.target.value)} + /> +
+ +
+ 配置值 + setDraftValue(event.target.value)} + /> +
+ + {isSensitive(editing) ? ( + + ) : null} + + + 配置键不可修改;需要新增配置时先在后端默认配置中登记,再回到本页调整取值。 + + + ) : null} + + + ) +} diff --git a/frontend/src/features/admin/api.ts b/frontend/src/features/admin/api.ts index e9f8714..4f60031 100644 --- a/frontend/src/features/admin/api.ts +++ b/frontend/src/features/admin/api.ts @@ -19,6 +19,8 @@ import type { AdminOrderDetail, AdminOrderPage, AdminOrderQuery, + AdminSystemConfig, + AdminSystemConfigUpdatePayload, AdminUser, AdminUserItem, AdminUserPage, @@ -422,3 +424,21 @@ export async function fetchAdminAuditLogs(query: AdminAuditQuery) { } export type { AdminAuditLog } + +export async function fetchAdminSystemConfigs() { + const { data } = await adminClient.get>( + '/admin/system-configs' + ) + return data.data.items ?? [] +} + +export async function updateAdminSystemConfig( + key: string, + payload: AdminSystemConfigUpdatePayload +) { + const { data } = await adminClient.put>( + `/admin/system-configs/${encodeURIComponent(key)}`, + payload + ) + return data.data +} diff --git a/frontend/src/features/admin/labels.ts b/frontend/src/features/admin/labels.ts index 00c53f4..144e7dd 100644 --- a/frontend/src/features/admin/labels.ts +++ b/frontend/src/features/admin/labels.ts @@ -310,3 +310,29 @@ export const auditBizTypeOptions = Object.entries(auditBizTypeMap).map(([value, value, label, })) + +export const systemConfigCategoryMap: Record = { + chat: '聊天与群组', + distribution: '分销', + handoff: '交接与超时', + integration: '第三方集成', + listing: '商品发布', + mobile: '移动端首页', + order: '订单', + profile: '个人中心', + renter: '租客成长', + sms: '短信', +} + +export const systemConfigCategoryOptions = Object.entries(systemConfigCategoryMap).map( + ([value, label]) => ({ value, label }) +) + +export function systemConfigCategory(key: string) { + return key.split('.')[0] || 'other' +} + +export function systemConfigCategoryLabel(key: string) { + const category = systemConfigCategory(key) + return systemConfigCategoryMap[category] ?? category +} diff --git a/frontend/src/features/admin/types.ts b/frontend/src/features/admin/types.ts index 5c7df59..2f1a1a9 100644 --- a/frontend/src/features/admin/types.ts +++ b/frontend/src/features/admin/types.ts @@ -607,3 +607,18 @@ export interface AdminAuditPage { page: number page_size: number } + +export interface AdminSystemConfig { + id: number + key: string + value: string + description: string + updated_by?: number | null + created_at: string + updated_at: string +} + +export interface AdminSystemConfigUpdatePayload { + value: string + description: string +} diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx index 96985e5..30f5914 100644 --- a/frontend/src/layouts/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout.tsx @@ -106,7 +106,7 @@ const allNavGroups: NavGroup[] = [ label: '系统', icon: , children: [ - { key: '/system-configs', label: '系统配置', permission: 'systemconfig:view' }, + { key: '/system-configs', label: '系统配置', permission: 'system_config:view' }, { key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' }, ], }, diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index f0dbbfb..d92a1c3 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1489,6 +1489,18 @@ a { word-break: break-all; } +.admin-config-form { + display: flex; + flex-direction: column; + gap: 18px; +} + +.admin-config-editor { + font-family: 'Inter', monospace; + font-size: 12px; + line-height: 1.6; +} + .admin-detail-drawer .ant-drawer-header { align-items: flex-start; }