From f5b28dd09a5ee2891ee2f56434841bb37dafa24f Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 18 Apr 2026 19:13:25 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=90=8C=E6=AD=A5=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E4=B8=8A=E4=B8=8B=E6=96=87=E5=92=8C=E8=A7=84?= =?UTF-8?q?=E5=88=99=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - project.md:补充 AppConfig、FilterableColumnHeader、license/update/connectivity provider、批次追踪页面、GET /version 接口、并发安全规则、dev.sh stop 命令、 scripts/.logs gitignore、表格 UI 规范(hover + 列头筛选 + 客户端筛选模式) - CLAUDE.md:新增前端 URL 配置、平台判断、表格列头筛选、权限中间件、 并发安全(FOR UPDATE)等强制规则 Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 26 +++++++++ docs/context/project.md | 124 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 144 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fc6b57d..48ab34f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -186,3 +186,29 @@ security: 修复入库单多租户隔离漏洞 ### Go 命令 - 所有 Go 命令前须确保 PATH:`export PATH="/opt/homebrew/bin:$PATH"` - 或直接使用 `sh scripts/dev.sh` 脚本(已内置 PATH) + +### 前端 URL 配置 +- 所有后端 URL 必须通过 `AppConfig` 读取(`client/lib/core/config/app_config.dart`) +- **禁止**在任何 provider / repository / widget 中硬编码 `localhost:8080` 或任何 IP/域名 +- 使用:`AppConfig.apiBaseUrl`、`AppConfig.healthUrl`、`AppConfig.versionUrl` + +### 前端平台判断 +- 使用 `dart:io` 的 `Platform` 类前,必须先检查 `kIsWeb`(`import 'package:flutter/foundation.dart'`) +- Web 平台不支持 `dart:io`,直接使用会在 Web 构建时崩溃 + +### 表格列头筛选 +- 可筛选列使用 `FilterableColumnHeader`(来自 `widgets/multi_select_dropdown.dart`) +- 不要在 toolbar 放独立的筛选按钮,筛选入口应内嵌在列头 +- 列定义使用 `ColDef`,可隐藏列用 `ColumnToggleButton` 控制 + +### 权限中间件(已全局挂载) +- `middleware.ReadOnly()`:已挂载到主 `api` 路由组,只读用户(role=readonly)所有写操作自动返回 403 +- `middleware.AdminOnly()`:挂载在 `/users` 路由组,仅管理员可管理用户 +- **新增写操作路由**时,默认受 `ReadOnly()` 保护;若仅管理员可用,需加入 `AdminOnly()` 子组 + +### 并发安全(数据库操作) +- 在同一事务内读后写的操作,必须用 `FOR UPDATE` 锁行: + ```go + tx.Set("gorm:query_option", "FOR UPDATE").Where(...).First(&model) + ``` +- 适用场景:单号生成(`number_rules`)、库存扣减(`inventories`)、任何 check-then-act 模式 diff --git a/docs/context/project.md b/docs/context/project.md index 365a101..ad439ed 100644 --- a/docs/context/project.md +++ b/docs/context/project.md @@ -50,6 +50,8 @@ jiu/ │ │ │ ├── base.go # Base/TenantBase/Date/JSON 公共类型 │ │ │ └── *.go # 各业务模型 │ │ ├── middleware/auth.go # JWT 验证 + shop_id 注入(GetShopID/GetUserID) +│ │ │ # ReadOnly():只读用户禁止写操作(全局挂载) +│ │ │ # AdminOnly():仅管理员可访问(挂载在 /users 路由组) │ │ └── router/router.go # 路由注册 │ ├── testutil/setup.go # 测试工具包(SQLite DB、CreateTestXxx、GetAuthToken) │ ├── cmd/seed/main.go # 数据库工具(--reset 删表重建,--clear 清空数据) @@ -60,7 +62,8 @@ jiu/ ├── deploy/ │ └── docker-compose.yml # 本地开发:MySQL(3306) + Adminer(8888) ├── scripts/ -│ └── dev.sh # 一站式开发脚本(见下方用法) +│ ├── dev.sh # 一站式开发脚本(见下方用法) +│ └── .logs/ # 脚本运行日志(已加入 .gitignore,不提交) └── docs/ ├── context/project.md # 本文件(项目全貌) ├── dev-setup.md # 环境搭建指南 @@ -80,6 +83,7 @@ users: # 用户(含 shop_id,role: admin/operator/readonly) licenses: # 许可证(含 device_id 绑定,type: trial/monthly/annual/lifetime) product_categories: # 商品分类 products: # 商品(含 custom_fields JSON 动态扩展) + # UNIQUE KEY uk_product_code (shop_id, code) — 防并发重复 warehouses: # 仓库 partners: # 往来单位(type: supplier/customer) stock_in_orders: # 入库单(status: draft→pending→approved/rejected) @@ -101,6 +105,10 @@ number_rules: # 单号生成规则(前缀+日期+6位序号) 3. **出库前校验**:审核出库时校验库存充足,不足返回错误并回滚 4. **单号生成**:通过 `number_rules` 表事务安全生成,格式 `{前缀}{YYYYMMDD}{6位序号}` 5. **扩展字段**:所有业务主表含 `custom_fields JSON`,用于存储动态业务字段,避免频繁改表 +6. **并发安全**: + - `GenerateOrderNo` 在事务内用 `FOR UPDATE` 锁住 `number_rules` 行,防止并发生成重复单号 + - `ApproveStockOut`/`updateInventory` 在事务内用 `FOR UPDATE` 锁住库存行,防止超卖竞态 + - 商品编码 `products.code` 有 UNIQUE 约束,Create 时重试最多 5 次 ## 开发脚本(scripts/dev.sh) @@ -112,6 +120,7 @@ sh scripts/dev.sh run # 启动前后端(后端代码未变则 sh scripts/dev.sh run --force # 强制重启后端 sh scripts/dev.sh --backend-only # 仅启动后端 sh scripts/dev.sh --frontend-only # 仅启动前端 +sh scripts/dev.sh stop # 停止所有服务 # 数据库 sh scripts/dev.sh seed S001 # 清空并写入 S001 测试数据(执行 seeds/S001.sql) @@ -151,6 +160,10 @@ sh scripts/dev.sh seed S001 POST /api/v1/license/activate GET /api/v1/license/verify POST /api/v1/license/deactivate + GET /api/v1/license/info # 返回当前许可证详情(含到期日、类型) + +版本(无需 JWT): + GET /version # 返回最新版本信息(读取 version.yaml,缺失返回 500) 商品: GET/POST /api/v1/products @@ -181,8 +194,13 @@ sh scripts/dev.sh seed S001 库存: GET /api/v1/inventory GET /api/v1/inventory/logs + GET /api/v1/inventory/batch-tracking # 批次追踪(已审核入库商品含销售状态) POST/GET /api/v1/inventory/checks +用户管理(仅 admin 角色): + GET/POST /api/v1/users + PUT/DELETE /api/v1/users/:id + 数据导入: POST /api/v1/import/products (Excel/CSV) POST /api/v1/import/partners (Excel/CSV) @@ -197,24 +215,118 @@ client/lib/ │ ├── theme/app_theme.dart # 颜色常量 + ThemeData │ ├── auth/auth_state.dart # AuthUser, AuthState, AuthNotifier(shared_preferences 持久化) │ ├── api/api_client.dart # Dio 封装(401 自动刷新,_disposed 防悬空回调) -│ └── router/app_router.dart # go_router(_RouterNotifier + refreshListenable) +│ ├── config/app_config.dart # 全局 URL 配置(通过 --dart-define=BASE_URL=... 注入) +│ ├── router/app_router.dart # go_router(_RouterNotifier + refreshListenable) +│ ├── storage/login_history.dart # 登录历史(shared_preferences,支持候选词自动填充) +│ ├── models/page_result.dart # 通用分页结果模型 +│ └── exceptions.dart # 自定义异常类型 +├── models/ # 数据模型(与后端 JSON 对应) +├── repositories/ # 数据访问层(封装 API 调用) +│ └── auth_repository.dart # 登录/刷新 token(含历史记录保存) +├── providers/ # Riverpod 状态管理 +│ ├── license_provider.dart # 许可证状态(licenseInfoProvider) +│ ├── connectivity_provider.dart # 网络连通性探测(connectivityProvider) +│ ├── update_provider.dart # 版本更新检查(updateCheckProvider) +│ │ # 平台判断用 kIsWeb,避免 Web 上调用 dart:io +│ ├── product_provider.dart # 商品列表(AsyncNotifierProvider,含缓存) +│ ├── stock_in_provider.dart # 入库单列表 +│ ├── stock_out_provider.dart # 出库单列表 +│ ├── inventory_provider.dart # 库存列表 +│ ├── finance_provider.dart # 财务记录 +│ ├── partner_provider.dart # 往来单位 +│ ├── warehouse_provider.dart # 仓库列表 +│ ├── user_provider.dart # 用户管理 +│ └── number_rule_provider.dart # 编号规则 ├── screens/ -│ ├── auth/login_screen.dart # 登录页 -│ ├── shell/app_shell.dart # 主框架(顶栏 + 侧边栏 + 状态栏) +│ ├── auth/login_screen.dart # 登录页(含候选词下拉,150ms 延迟防止覆盖 onTap) +│ ├── shell/app_shell.dart # 主框架(顶栏 + 侧边栏 + 状态栏 + 更新提示 banner) │ ├── stock_in/ # 入库管理 │ ├── stock_out/ # 出库管理 -│ ├── inventory/ # 库存管理 +│ ├── inventory/ # 库存管理(含批次追踪 batch_tracking_screen.dart) │ ├── partners/ # 往来单位 │ ├── finance/ # 财务管理 │ ├── products/ # 商品管理(含分类) -│ └── settings/ # 系统设置(用户/仓库/编号规则) +│ └── settings/ # 系统设置(用户/仓库/编号规则/关于) └── widgets/ ├── page_scaffold.dart # Tab 页面封装 ├── data_table_card.dart # 含工具栏+分页的数据表格 + │ # StatefulWidget,用 Table + IntrinsicColumnWidth 实现 + │ # ValueNotifier _hoveredRow 驱动行 hover 高亮 + ├── multi_select_dropdown.dart # 多选筛选组件(多个导出): + │ # MultiSelectDropdown — 独立多选按钮(toolbar 使用) + │ # FilterableColumnHeader — 列头内嵌筛选图标 + │ # hover 显示,active 常显;图标 16px 靠右对齐 + │ # ColDef — 列定义(key, label, required, minWidth) + │ # ColumnToggleButton — 列显示/隐藏切换按钮 ├── status_badge.dart # 状态标签(草稿/待审/已审/拒绝) └── form_dialog.dart # 弹窗表单封装 ``` +## 前端配置管理 + +后端 URL 通过 `AppConfig` 统一管理,支持构建时注入: + +```dart +// client/lib/core/config/app_config.dart +class AppConfig { + static const _baseUrl = String.fromEnvironment( + 'BASE_URL', + defaultValue: 'http://localhost:8080', + ); + static String get baseUrl => _baseUrl; + static String get apiBaseUrl => '$_baseUrl/api/v1'; + static String get healthUrl => '$_baseUrl/health'; + static String get versionUrl => '$_baseUrl/version'; +} +``` + +**构建/运行时注入**: +```bash +flutter run --dart-define=BASE_URL=http://192.168.1.100:8080 +flutter build web --dart-define=BASE_URL=https://api.example.com +``` + +**规则**:所有硬编码 `localhost:8080` 的 URL 必须改用 `AppConfig` 对应属性,不得直接拼接字符串。 + +## 前端表格 UI 规范 + +所有数据表格使用 `DataTableCard` widget,遵循以下规范: + +### 行 Hover 效果 +`data_table_card.dart` 用 `Table` + `MouseRegion` + `ValueNotifier` 实现行级 hover: +- hover 时行背景色:`Color(0xFFF5F7FF)` +- 列头背景色:`Color(0xFFF0F4FF)` + +### 列头内嵌筛选(FilterableColumnHeader) +可筛选的列头使用 `FilterableColumnHeader`,作为 `DataColumn.label` 传入: +```dart +DataColumn( + label: FilterableColumnHeader( + text: '仓库', + options: warehouseOptions, + selected: _filterWarehouse, + onChanged: (v) => setState(() => _filterWarehouse = v), + ), +) +``` +- 鼠标 hover 时显示筛选图标(`filter_alt_outlined`,16px,右对齐) +- 已激活筛选时图标常驻(`filter_alt`,蓝色)+ 取消 icon +- 点击图标弹出多选对话框 + +### 客户端筛选模式 +部分表格(库存、商品、批次追踪、财务)采用客户端筛选:加载全量数据,在内存中过滤,派生 options: +```dart +final warehouseOptions = _records + .map((r) => r.warehouseName ?? '') + .where((s) => s.isNotEmpty) + .toSet().toList()..sort(); +``` + +### 列显示/隐藏(ColDef + ColumnToggleButton) +使用 `ColDef` 定义列,支持: +- `required: true` — 不可隐藏 +- `minWidth: 1000` — 屏幕宽度不足时自动隐藏 + ## 文档索引 | 文档 | 路径 | 说明 |