docs: 同步更新项目上下文和规则文档

- 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 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-18 19:13:25 +08:00
parent 5a3907e3f9
commit f5b28dd09a
2 changed files with 144 additions and 6 deletions
+26
View File
@@ -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 模式
+118 -6
View File
@@ -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_idrole: 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, AuthNotifiershared_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<int> _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<int>` 实现行级 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` — 屏幕宽度不足时自动隐藏
## 文档索引
| 文档 | 路径 | 说明 |