fix:sample/plate 之前的开发

This commit is contained in:
彭帅
2026-05-28 11:56:17 +08:00
parent fc36bc83e3
commit 8b65de36b8
367 changed files with 57752 additions and 947 deletions

155
.gitignore vendored
View File

@@ -1,10 +1,149 @@
.pydevproject # =============================================================================
target # Java / Maven
.classpath # =============================================================================
.project target/
.settings **/target/
.dbeaver-data-sources.xml *.class
/bin/ *.jar
*.war
*.ear
*.nar
hs_err_pid*
replay_pid*
.factorypath
.mvn/wrapper/maven-wrapper.jar
# Local Spring config (use *.template as the committed baseline)
**/application.properties
!**/application.properties.template
**/properties/application.properties
!**/properties/application.properties.template
# =============================================================================
# Node.js / Next.js (frontend)
# =============================================================================
node_modules/
**/node_modules/
.next/
**/.next/
out/
**/out/
dist/
**/dist/
build/
**/build/
.turbo/
.vercel/
# TypeScript / tooling caches
*.tsbuildinfo
.eslintcache
.stylelintcache
.parcel-cache
.cache/
# Package manager debug logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# OpenAPI codegen error dumps
openapi-ts-error-*.log
# =============================================================================
# Python (BrAPI test scripts)
# =============================================================================
__pycache__/
**/__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.venv/
venv/
ENV/
env/
.pytest_cache/
.mypy_cache/
.ruff_cache/
*.egg-info/
.eggs/
pip-log.txt
pip-delete-this-directory.txt
# =============================================================================
# Environment & secrets
# =============================================================================
.env
.env.local
.env.*.local
.env.development.local
.env.test.local
.env.production.local
!.env.example
!.env.template
!.env.*.example
*.pem
*.key
*.p12
*.pfx
credentials.json
secrets.json
**/secrets/
# =============================================================================
# IDE & editors
# =============================================================================
.idea/ .idea/
brapi-Java-TestServer.iml *.iml
*.ipr
*.iws
.vscode/
!.vscode/extensions.json
!.vscode/settings.json.example
*.swp
*.swo
*~
.project
.classpath
.settings/
.pydevproject
# =============================================================================
# Database tools
# =============================================================================
.dbeaver/
.dbeaver-data-sources.xml
# =============================================================================
# OS junk
# =============================================================================
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Thumbs.db
ehthumbs.db
Desktop.ini
# =============================================================================
# Logs & local runtime output
# =============================================================================
*.log
logs/
run*.log
run*.out.log
run*.err.log
# =============================================================================
# Local dev tools (bundled JDK/Maven, etc.)
# =============================================================================
.tools/ .tools/
# Legacy / Eclipse output
/bin/

5
AGENTS.md Normal file
View File

@@ -0,0 +1,5 @@
1.尽量不要动brapi自己写好的java代码不要改路径接口定义返回值
2.一般我只会让你加前端,或者加一些删除等简单的后端接口,千万不要破坏原接口的路径,入参定义,出参定义。
3.下拉框的数据要缓存下来,同一选项源不要重复请求接口。
4.前端首页(及同类入口页)加载首页数据时,副作用只触发一次查询,避免 Strict Mode 或重复 mount 导致多次请求。
5.保存、提交类操作要做防抖debounce防止连续点击重复提交。

View File

@@ -0,0 +1,227 @@
# BrAPI Test Server 总体数据架构图
本文档把 4 个模块串成一张总览图:
```text
Core -> Germplasm/Seed -> Phenotyping -> Genotyping
```
对应的模块文档:
| 模块 | 文档 | 核心作用 |
| --- | --- | --- |
| Core | `core-data-flow.md` | crop、program、trial、study、location、person 等基础上下文 |
| Germplasm/Seed | `04-germplasm-seed-data-flow.md` | germplasm、breeding_method、seed_lot、cross、pedigree、attribute |
| Phenotyping | `02-phenotyping-data-flow.md` | observation_unit、observation_variable、event、image、observation |
| Genotyping | `03-genotyping-data-flow.md` | sample、plate、reference、variantset、variant、callset、allele_call |
## 总体结论
整个数据模型的主干是:
```text
Core: crop -> program -> trial -> study
Germplasm: breeding_method -> germplasm -> cross / seed_lot / pedigree / attribute
Phenotyping: study + germplasm/seed_lot/cross -> observation_unit -> observation
Genotyping: observation_unit/study -> sample -> callset -> allele_call
Genotyping: reference_set -> variantset -> variant -> allele_call
```
`study` 是 Core 到 Phenotyping/Genotyping 的主桥;`germplasm` 是 Germplasm/Seed 到 Phenotyping/Genotyping 的主桥;`observation_unit` 是 Phenotyping 到 Genotyping 的主桥。
## 总架构图
```mermaid
flowchart TD
subgraph CORE["Core 基础上下文"]
CROP["crop<br/>作物"]
PERSON["person<br/>人员"]
PROGRAM["program<br/>项目"]
LOCATION["location<br/>地点"]
TRIAL["trial<br/>试验批次"]
SEASON["season<br/>季节"]
STUDY["study<br/>研究/试验实施单元"]
LIST["list / list_item<br/>通用列表"]
CROP --> PROGRAM
PERSON --> PROGRAM
PROGRAM --> TRIAL
CROP --> TRIAL
PROGRAM --> LOCATION
CROP --> LOCATION
TRIAL --> STUDY
PROGRAM --> STUDY
CROP --> STUDY
LOCATION --> STUDY
SEASON --> STUDY
PERSON --> LIST
end
subgraph GERM["Germplasm / Seed 种质与种子"]
BM["breeding_method<br/>育种方法"]
GERMPLASM["germplasm<br/>种质"]
GAD["germplasm_attribute_definition<br/>属性定义"]
GAV["germplasm_attribute_value<br/>属性值"]
CP["crossing_project<br/>杂交项目"]
CROSS["cross_entity<br/>Cross / PlannedCross"]
XP["cross_parent<br/>杂交亲本"]
PEDNODE["pedigree_node<br/>系谱节点"]
PEDEDGE["pedigree_edge<br/>系谱边"]
SEEDLOT["seed_lot<br/>种子批次"]
MIX["seed_lot_content_mixture<br/>批次组成"]
TX["seed_lot_transaction<br/>批次流转"]
BM --> GERMPLASM
GAD --> GAV
GERMPLASM --> GAV
CP --> CROSS
CROSS --> XP
GERMPLASM --> XP
CROSS --> CROSS_PLANNED["cross_entity<br/>planned cross 自关联"]
GERMPLASM --> PEDNODE
CP --> PEDNODE
PEDNODE --> PEDEDGE
PEDEDGE --> PEDNODE2["pedigree_node<br/>父本/子代节点"]
GERMPLASM --> MIX
CROSS --> MIX
MIX --> SEEDLOT
SEEDLOT --> TX
TX --> SEEDLOT
end
subgraph PHENO["Phenotyping 表型"]
ONTOLOGY["ontology<br/>本体"]
TRAIT["trait<br/>性状"]
METHOD["method<br/>方法"]
SCALE["scale<br/>标尺"]
OV["observation_variable<br/>观测变量"]
OU["observation_unit<br/>观测单元"]
EVENT["event<br/>事件"]
IMAGE["image<br/>图像"]
OBS["observation<br/>观测值"]
ONTOLOGY --> TRAIT
ONTOLOGY --> METHOD
ONTOLOGY --> SCALE
TRAIT --> OV
METHOD --> OV
SCALE --> OV
OU --> OBS
OV --> OBS
EVENT --> OU
OU --> IMAGE
IMAGE --> OBS
end
subgraph GENO["Genotyping 基因型"]
PLATE["plate<br/>样本板"]
SAMPLE["sample<br/>样本"]
REFSET["reference_set<br/>参考集"]
REF["reference<br/>参考序列"]
REFB["reference_bases<br/>参考片段"]
VARSET["variantset<br/>变异集合"]
VARIANT["variant<br/>变异位点"]
CALLSET["callset<br/>样本调用集合"]
CALL["allele_call<br/>基因型结果"]
GMAP["genome_map<br/>遗传图谱"]
LG["linkageGroup<br/>连锁群"]
MP["marker_position<br/>图谱位置"]
PLATE --> SAMPLE
SAMPLE --> CALLSET
CALLSET --> CALL
REFSET --> REF
REF --> REFB
REFSET --> VARSET
VARSET --> VARIANT
REFSET --> VARIANT
VARIANT --> CALL
GMAP --> LG
LG --> MP
VARIANT --> MP
end
CROP --> GERMPLASM
CROP --> GAD
TRAIT --> GAD
METHOD --> GAD
SCALE --> GAD
ONTOLOGY --> GAD
PROGRAM --> CP
PROGRAM --> SEEDLOT
LOCATION --> SEEDLOT
STUDY --> OU
TRIAL --> OU
PROGRAM --> OU
CROP --> OU
GERMPLASM --> OU
SEEDLOT --> OU
CROSS --> OU
STUDY --> EVENT
STUDY --> OBS
TRIAL --> OBS
PROGRAM --> OBS
CROP --> OBS
STUDY --> PLATE
TRIAL --> PLATE
PROGRAM --> PLATE
STUDY --> SAMPLE
TRIAL --> SAMPLE
PROGRAM --> SAMPLE
OU --> SAMPLE
GERMPLASM --> REFSET
STUDY --> VARSET
CROP --> GMAP
```
## 跨模块关键桥接关系
| 桥接点 | 连接模块 | 说明 |
| --- | --- | --- |
| `crop` | Core -> Germplasm/Pheno/Geno | 作物维度贯穿 program、trial、study、germplasm、变量、图谱 |
| `program` | Core -> Germplasm/Seed/Pheno/Geno | 项目维度连接 crossing_project、seed_lot、observation_unit、sample、plate |
| `trial` | Core -> Pheno/Geno | 试验批次维度连接 study、observation_unit、observation、sample、plate |
| `study` | Core -> Pheno/Geno | 最重要的实验上下文,连接 observation_unit、event、observation、sample、plate、variantset |
| `germplasm` | Germplasm -> Pheno/Geno | 种质可连接 observation_unit、cross_parent、seed_lot_content_mixture、reference_set |
| `seed_lot` | Germplasm/Seed -> Pheno | SeedLot 可作为 observation_unit 的材料来源 |
| `cross_entity` | Germplasm/Seed -> Pheno | Cross/PlannedCross 可作为 observation_unit 或 seed_lot_content_mixture 的来源 |
| `observation_unit` | Pheno -> Geno | 表型观测单元可生成或关联 genotyping sample |
| `sample` | Geno 内部入口 | 从 observation_unit/study/trial/program 进入 callset 和 allele_call |
| `variant` | Geno 内部位点 | 与 allele_call、marker_position 连接,承载基因型结果定位 |
## 推荐整体录入顺序
1. 录入 Core 基础上下文:`crop``person``program``location``trial``season``study`
2. 录入 Germplasm 上游:`breeding_method``germplasm_attribute_definition` 依赖的 `trait/method/scale/ontology`
3. 录入 `germplasm`,再补充 `germplasm_attribute_value`、donor、origin、institute、synonym、taxon 等扩展信息。
4. 如果涉及杂交,录入 `crossing_project``cross_entity``cross_parent`;计划杂交使用 `cross_entity.planned``planned_cross_id` 自关联表达。
5. 录入 Seed 数据:`seed_lot``seed_lot_content_mixture``seed_lot_transaction`
6. 录入 Phenotyping 定义:`ontology``trait``method``scale``observation_variable`
7. 录入 Phenotyping 实体与事实:`observation_unit``event``image``observation`
8. 录入 Genotyping 样本入口:`plate``sample`
9. 录入 Genotyping 参考和变异:`reference_set``reference``reference_bases``variantset``variant`
10. 录入 Genotyping 结果:`callset``callset_variant_sets``allele_call`
11. 如需遗传图谱定位,录入 `genome_map``linkageGroup``marker_position`
## 模块边界速记
| 模块 | 根节点 | 主要事实表 | 向外输出 |
| --- | --- | --- | --- |
| Core | `crop/program/trial/study` | `study` | 给所有业务模块提供上下文 |
| Germplasm/Seed | `germplasm` | `germplasm_attribute_value`, `seed_lot_content_mixture`, `seed_lot_transaction`, `cross_parent`, `pedigree_edge` | 给 Pheno 提供材料来源,给 Geno 提供 reference source |
| Phenotyping | `observation_unit` | `observation` | 给 Geno 提供 sample 的观测对象来源 |
| Genotyping | `sample`, `variant` | `allele_call` | 输出样本在位点上的 genotype 结果 |
## 关键注意点
1. `study` 是大多数实验数据的上下文入口;如果数据要进入 Pheno 或 Geno通常都应该能追溯到 `study`
2. `germplasm` 描述种质主数据,`seed_lot` 描述库存批次;二者通过 `seed_lot_content_mixture` 间接关联。
3. `plannedcross` 没有独立数据库表,落库在 `cross_entity`,通过 `planned``planned_cross_id` 表达。
4. `observation_unit` 可以关联 `germplasm``seed_lot``cross_entity`,是材料进入表型观测的入口。
5. `sample` 可以从 `observation_unit` 来,也冗余关联 `study/trial/program`,是基因型流程入口。
6. `allele_call` 是最终 genotype 结果表,连接 `callset``variant`
7. `additional_info``external_references` 是跨模块通用扩展表,主图中未展开,以免遮挡主干关系。

View File

@@ -0,0 +1,290 @@
# Phenotyping 模块数据流与表关系
本文档分析 Phenotyping 模块的数据录入顺序、核心表关系,以及它与 Core 模块 `study/trial/program/crop` 的衔接方式。
## 结论
Phenotyping 模块的数据主线是:
```text
core study -> ontology -> trait / method / scale -> observation_variable -> observation_unit -> event / image -> observation
```
更贴近业务录入的顺序可以理解为:
```text
1. 先有 Core 数据crop、program、trial、study
2. 录入 Ontology / Trait / Method / Scale
3. 组装 ObservationVariable
4. 录入 ObservationUnit
5. 录入 Event 和 Image
6. 录入 Observation
```
初始化脚本中与 Phenotyping 相关的执行顺序是:
```text
R__init_data_14_observation_units.sql
R__init_data_17_events.sql
R__init_data_18_images.sql
R__init_data_19_observation_variables.sql
R__init_data_20_observations.sql
R__init_data_26_observation_variables2.sql
R__init_data_26_observation_variables3.sql
```
注意:初始化脚本为了构造演示数据,`observation_unit` 早于 `observation_variable` 插入;从业务建模角度看,二者都依赖已存在的 `study`,而真正的观测值 `observation` 需要同时引用 `observation_unit``observation_variable`
## 核心表说明
| 表 | 作用 | 主要上游依赖 | 主要下游 |
| --- | --- | --- | --- |
| `ontology` | 本体信息,定义术语来源 | 无 | `trait``method``scale``observation_variable` |
| `ontology_ref` | 本体引用项 | 可独立录入 | `trait_ontology_reference``method_ontology_reference``scale_ontology_reference` |
| `trait` | 性状定义,描述“测什么” | 可选 `ontology` | `observation_variable` |
| `method` | 测量方法,描述“怎么测” | 可选 `ontology` | `observation_variable` |
| `scale` | 标尺/单位/数据类型,描述“用什么尺度表达” | 可选 `ontology` | `observation_variable``scale_valid_value_category` |
| `observation_variable` | 观测变量,由 trait/method/scale 组成 | `crop``trait``method``scale``ontology` | `observation``study_variable` |
| `observation_unit` | 观测对象,如 plot/plant/block | `crop``program``trial``study`,可选 germplasm/seed_lot/cross | `observation``event_observation_units``image` |
| `event` | 田间事件,如施肥、灌溉、采样等 | `study` | `event_param``event_observation_units` |
| `event_param` | 事件参数 | `event` | `event_parameter_entity_values_by_date` |
| `image` | 图片/影像数据 | 可选 `observation_unit``geojson` | `image_observations` |
| `observation` | 实际观测值 | `observation_unit``observation_variable``study`、可选 `season` | `image_observations` |
## 建议录入顺序
### 1. 准备 Core 上游数据
Phenotyping 数据通常挂在 Core 的层级下面:
```text
crop -> program -> trial -> study
```
其中 `study` 是 Phenotyping 的入口节点。`observation_unit``event``observation` 都会直接或间接关联到 `study`
### 2. 录入 Ontology
先录入 `ontology` 和需要的 `ontology_ref`
`ontology` 用来标识术语体系来源,后续 `trait``method``scale``observation_variable` 都可以挂载本体信息。
### 3. 录入 Trait / Method / Scale
这三类数据共同描述一个观测指标:
```text
Trait = 测什么,例如 plant height
Method = 怎么测,例如 ruler measurement
Scale = 用什么单位/数据类型表达,例如 cm、numeric
```
`scale` 如果有枚举或分类值,还会录入:
```text
scale_valid_value_category
```
### 4. 组装 ObservationVariable
录入 `observation_variable`,它会引用:
```text
crop
trait
method
scale
ontology
```
业务上它相当于“可被采集的一项指标”。例如“株高-尺测法-cm”。
`study_variable``study``observation_variable` 的多对多关系,表示某个 study 会采集哪些变量。
### 5. 录入 ObservationUnit
录入 `observation_unit`,它表示被观测对象,例如 field、block、plot、plant。
它通常会引用:
```text
crop
program
trial
study
```
并且可选关联:
```text
germplasm
seed_lot
cross
observation_unit_position
observation_unit_treatment
observation_unit_level
```
### 6. 录入 Event
录入 `event`,用于表达发生在 study 或 observation unit 上的事件。
常见关系:
```text
event -> study
event_observation_units -> observation_unit
event_param -> event
```
### 7. 录入 Image
录入 `image`,图片可以直接关联 `observation_unit`,也可以通过 `image_observations` 关联一个或多个 `observation`
图片坐标使用 `geojson/coordinate` 扩展。
### 8. 录入 Observation
最后录入 `observation`,这是 Phenotyping 模块的核心事实数据。
一条 observation 通常同时引用:
```text
observation_unit
observation_variable
study
trial
program
crop
season
```
代码里 `ObservationEntity.setObservationUnit(...)` 会从 observation unit 继承 study/trial/program/crop因此 observation 的上下文可以由 observation unit 自动带出。
## Phenotyping 数据流图
```mermaid
flowchart TD
C["Core: crop"] --> P["Core: program"]
P --> T["Core: trial"]
T --> S["Core: study"]
O["ontology 本体"] --> TR["trait 性状"]
O --> M["method 方法"]
O --> SC["scale 标尺"]
OR["ontology_ref 本体引用"] --> TR
OR --> M
OR --> SC
C --> OV["observation_variable 观测变量"]
TR --> OV
M --> OV
SC --> OV
O --> OV
S --> SV["study_variable 研究-变量"]
OV --> SV
C --> OU["observation_unit 观测单元"]
P --> OU
T --> OU
S --> OU
G["Germplasm/SeedLot/Cross 可选"] --> OU
OU --> OUP["position / treatment / level"]
S --> E["event 事件"]
E --> EP["event_param 事件参数"]
E --> EOU["event_observation_units"]
OU --> EOU
OU --> IMG["image 图像"]
GEO["geojson / coordinate"] --> IMG
OU --> OB["observation 观测值"]
OV --> OB
S --> OB
T --> OB
P --> OB
C --> OB
SE["Core: season"] --> OB
IMG --> IO["image_observations"]
OB --> IO
```
## Phenotyping ER 关系图
```mermaid
erDiagram
crop ||--o{ observation_variable : "crop_id"
crop ||--o{ observation_unit : "crop_id"
crop ||--o{ observation : "crop_id"
program ||--o{ observation_unit : "program_id"
program ||--o{ observation : "program_id"
trial ||--o{ observation_unit : "trial_id"
trial ||--o{ observation : "trial_id"
study ||--o{ observation_unit : "study_id"
study ||--o{ event : "study_id"
study ||--o{ observation : "study_id"
study ||--o{ study_variable : "study_db_id"
ontology ||--o{ trait : "ontology_id"
ontology ||--o{ method : "ontology_id"
ontology ||--o{ scale : "ontology_id"
ontology ||--o{ observation_variable : "ontology_id"
ontology_ref ||--o{ trait_ontology_reference : "ontology_reference_id"
ontology_ref ||--o{ method_ontology_reference : "ontology_reference_id"
ontology_ref ||--o{ scale_ontology_reference : "ontology_reference_id"
trait ||--o{ observation_variable : "trait_id"
method ||--o{ observation_variable : "method_id"
scale ||--o{ observation_variable : "scale_id"
scale ||--o{ scale_valid_value_category : "scale_id"
observation_variable ||--o{ observation : "observation_variable_id"
observation_variable ||--o{ study_variable : "variable_db_id"
observation_unit ||--o{ observation : "observation_unit_id"
observation_unit ||--o{ observation_unit_position : "observation_unit_id"
observation_unit ||--o{ observation_unit_treatment : "observation_unit_id"
observation_unit ||--o{ observation_unit_level : "observation_unit_id"
event ||--o{ event_param : "event_id"
event ||--o{ event_observation_units : "event_entity_id"
observation_unit ||--o{ event_observation_units : "observation_units_id"
image ||--o{ image_observations : "image_entity_id"
observation ||--o{ image_observations : "observations_id"
observation_unit ||--o{ image : "observation_unit_id"
season ||--o{ observation : "season_id"
```
## API 与表的对应关系
| API | 主表 | 说明 |
| --- | --- | --- |
| `/brapi/v2/ontologies` | `ontology` | 本体查询、新增 |
| `/brapi/v2/traits` | `trait` | 性状定义 |
| `/brapi/v2/methods` | `method` | 测量方法 |
| `/brapi/v2/scales` | `scale` | 标尺、单位、数据类型 |
| `/brapi/v2/variables` | `observation_variable` | 观测变量,由 trait/method/scale 组合 |
| `/brapi/v2/observationunits` | `observation_unit` | 观测单元 |
| `/brapi/v2/events` | `event` | 田间/实验事件 |
| `/brapi/v2/images` | `image` | 图像数据 |
| `/brapi/v2/observations` | `observation` | 实际观测值 |
## 关键注意点
1. `study` 是 Phenotyping 与 Core 的连接点,大多数表型数据都应挂到具体 study。
2. `observation_variable` 不是单独的数值,它是“性状 + 方法 + 标尺 + 本体”的指标定义。
3. `observation_unit` 是被观测对象,`observation` 是对这个对象在某个变量上的一次测量结果。
4. `event` 可以绑定多个 `observation_unit`,适合记录施肥、灌溉、采样等动作。
5. `image` 可以直接绑定 `observation_unit`,也可以通过 `image_observations` 与观测值关联。
6. `trait/method/scale/observation_variable` 都有 `*_additional_info``*_external_references` 扩展表,用于补充业务字段和外部引用。
7. `observation` 冗余保存了 `crop/program/trial/study` 上下文,代码中会从 `observation_unit``study` 向上继承这些上下文,方便查询。

View File

@@ -0,0 +1,313 @@
# Genotyping 模块数据流与表关系
本文档分析 Genotyping 模块的数据录入顺序、核心表关系,以及 Java 实体名与真实数据库表名之间的对应关系。
## 结论
Genotyping 模块的数据主线是:
```text
Core/Pheno 上游数据 -> sample / plate
ReferenceSet -> Reference -> ReferenceBases
ReferenceSet + Study -> VariantSet -> Variant
Sample -> CallSet
CallSet + Variant -> Call
GenomeMap -> LinkageGroup -> MarkerPosition -> Variant
```
更贴近业务录入的顺序是:
```text
1. 先有 Core/Phenotyping 上游crop、program、trial、study、observation_unit
2. 录入 Plate 和 Sample
3. 录入 ReferenceSet、Reference、ReferenceBases
4. 录入 VariantSet
5. 录入 Variant
6. 录入 CallSet
7. 录入 Call也就是 allele_call 表里的基因型结果
8. 录入 GenomeMap、LinkageGroup、MarkerPosition
```
初始化脚本中与 Genotyping 相关的执行顺序是:
```text
R__init_data_21_samples.sql
R__init_data_22_references.sql
R__init_data_23_variant_set_1.sql
R__init_data_24_genome_maps.sql
src/main/resources/db/sql/variant_set_4/variant_set_4.sql
src/main/resources/db/sql/variant_set_4/variant_set_4_alleles.sql
```
## 实体与真实表名
| 业务概念 | Java 实体 | 数据库表 | 说明 |
| --- | --- | --- | --- |
| Call | `CallEntity` | `allele_call` | 单个样本在某个 variant 上的 genotype 结果 |
| CallSet | `CallSetEntity` | `callset` | 某个 sample 的一组 call通常对应一个样本的基因型调用集合 |
| Sample | `SampleEntity` | `sample` | 送检样本/测序样本 |
| Plate | `PlateEntity` | `plate` | 样本板,包含多个 sample |
| MarkerPosition | `MarkerPositionEntity` | `marker_position` | variant 在 linkage group 上的位置 |
| Variant | `VariantEntity` | `variant` | 变异位点,如 SNP/Indel |
| ReferenceSet | `ReferenceSetEntity` | `reference_set` | 参考基因组集合 |
| GenomeMap | `GenomeMapEntity` | `genome_map` | 遗传图谱 |
| VariantSet | `VariantSetEntity` | `variantset` | 一批 variant 的集合 |
| Reference | `ReferenceEntity` | `reference` | 参考序列,如 chromosome/contig |
| ReferenceBases | `ReferenceBasesPageEntity` | `reference_bases` | reference 的序列分页 |
| LinkageGroup | `LinkageGroupEntity` | `linkageGroup` | 图谱中的连锁群;注意表名是驼峰 `linkageGroup` |
## 核心表说明
| 表 | 作用 | 主要上游依赖 | 主要下游 |
| --- | --- | --- | --- |
| `plate` | 样本板 | `program``trial``study`,可选 vendor submission | `sample` |
| `sample` | 样本 | `plate``observation_unit``program``trial``study` | `callset` |
| `reference_set` | 参考基因组集合 | 可选 `germplasm` | `reference``variantset``variant` |
| `reference` | 参考序列 | `reference_set` | `reference_bases` |
| `reference_bases` | 参考序列片段/分页 | `reference` | 无 |
| `variantset` | 变异集合 | `reference_set``study` | `variant``callset_variant_sets``variantset_analysis``variantset_format` |
| `variant` | 变异位点 | `reference_set``variantset` | `allele_call``marker_position` |
| `callset` | 样本的 call 集合 | `sample` | `allele_call``callset_variant_sets` |
| `allele_call` | genotype 调用结果 | `callset``variant` | 无 |
| `genome_map` | 遗传图谱 | `crop`,可关联 `study` | `linkageGroup` |
| `linkageGroup` | 连锁群 | `genome_map` | `marker_position` |
| `marker_position` | marker/variant 在图谱上的位置 | `linkageGroup``variant` | 无 |
## 建议录入顺序
### 1. 准备 Core/Phenotyping 上游数据
Genotyping 数据通常挂在 Core 和 Phenotyping 之上。
必须或常见上游包括:
```text
crop
program
trial
study
observation_unit
```
`sample` 可以关联 `observation_unit`,也会冗余关联 `program/trial/study`,用于查询和筛选。
### 2. 录入 Plate
先录入 `plate`,表示样本板。
`plate` 可关联:
```text
program
trial
study
plate_submission
```
如果样本不走板,也可以直接录入 `sample`;但当前模型中 sample 支持挂到 plate 上。
### 3. 录入 Sample
录入 `sample`,它是 genotyping 流程的样本入口。
主要关系:
```text
sample -> plate
sample -> observation_unit
sample -> program / trial / study
sample -> germplasm_taxon
```
### 4. 录入 ReferenceSet 和 Reference
录入 `reference_set`,表示参考基因组集合。
然后录入 `reference`,表示具体参考序列,例如 chromosome、contig。
如需保存具体序列片段,再录入:
```text
reference_bases
```
### 5. 录入 VariantSet
录入 `variantset`,它把一批 variant 组织成集合。
主要关系:
```text
variantset -> reference_set
variantset -> study
```
附属表包括:
```text
variantset_analysis
variantset_format
variantset_additional_info
variantset_external_references
```
### 6. 录入 Variant
录入 `variant`,表示具体变异位点。
主要关系:
```text
variant -> reference_set
variant -> variantset
```
附属表包括:
```text
variant_entity_alternate_bases
variant_entity_ciend
variant_entity_cipos
variant_entity_filters_failed
```
### 7. 录入 CallSet
录入 `callset`,表示某个样本的一组 genotype calls。
主要关系:
```text
callset -> sample
callset_variant_sets -> variantset
```
`callset_variant_sets``callset``variantset` 的多对多关系表。
### 8. 录入 Call
录入 `allele_call`,业务上就是 Call。
它是最终基因型调用结果,核心关系是:
```text
allele_call -> callset
allele_call -> variant
```
也就是说,一条 call 表示“某个 sample/callset 在某个 variant 上的 genotype、read depth、likelihood 等结果”。
### 9. 录入 GenomeMap 和 MarkerPosition
如果需要遗传图谱定位,录入:
```text
genome_map -> linkageGroup -> marker_position -> variant
```
`marker_position` 实际上把 variant 放到某个 linkage group 的具体位置上。
## Genotyping 数据流图
```mermaid
flowchart TD
C["Core: crop"] --> GM["genome_map 遗传图谱"]
C --> P["Core: program"]
P --> T["Core: trial"]
T --> ST["Core: study"]
ST --> PL["plate 样本板"]
ST --> VS["variantset 变异集合"]
ST --> SM["sample 样本"]
OU["Pheno: observation_unit"] --> SM
PL --> SM
GER["Germplasm 可选"] --> RS["reference_set 参考集合"]
RS --> R["reference 参考序列"]
R --> RB["reference_bases 参考序列分页"]
RS --> VS
VS --> V["variant 变异位点"]
RS --> V
SM --> CS["callset 样本调用集合"]
CS --> CSV["callset_variant_sets"]
VS --> CSV
CS --> CALL["allele_call / Call 基因型结果"]
V --> CALL
GM --> LG["linkageGroup 连锁群"]
LG --> MP["marker_position 图谱位置"]
V --> MP
VS --> VSA["variantset_analysis"]
VS --> VSF["variantset_format"]
```
## Genotyping ER 关系图
```mermaid
erDiagram
program ||--o{ plate : "program_id"
trial ||--o{ plate : "trial_id"
study ||--o{ plate : "study_id"
plate ||--o{ sample : "plate_id"
observation_unit ||--o{ sample : "observation_unit_id"
program ||--o{ sample : "program_id"
trial ||--o{ sample : "trial_id"
study ||--o{ sample : "study_id"
germplasm ||--o{ reference_set : "source_germplasm_id"
reference_set ||--o{ reference : "reference_set_id"
reference ||--o{ reference_bases : "reference_id"
reference_set ||--o{ variantset : "reference_set_id"
study ||--o{ variantset : "study_id"
variantset ||--o{ variant : "variant_set_id"
reference_set ||--o{ variant : "reference_set_id"
sample ||--o{ callset : "sample_id"
callset ||--o{ callset_variant_sets : "call_sets_id"
variantset ||--o{ callset_variant_sets : "variant_sets_id"
callset ||--o{ allele_call : "call_set_id"
variant ||--o{ allele_call : "variant_id"
crop ||--o{ genome_map : "crop_id"
genome_map ||--o{ linkageGroup : "genome_map_id"
linkageGroup ||--o{ marker_position : "linkage_group_id"
variant ||--o{ marker_position : "variant_id"
variantset ||--o{ variantset_analysis : "variant_set_id"
variantset ||--o{ variantset_format : "variant_set_id"
```
## API 与表的对应关系
| API | 主表 | 说明 |
| --- | --- | --- |
| `/brapi/v2/samples` | `sample` | 样本查询、新增、修改 |
| `/brapi/v2/plates` | `plate` | 样本板查询、新增、修改 |
| `/brapi/v2/callsets` | `callset` | 样本调用集合 |
| `/brapi/v2/calls` | `allele_call` | genotype 调用结果 |
| `/brapi/v2/variants` | `variant` | 变异位点 |
| `/brapi/v2/variantsets` | `variantset` | 变异集合 |
| `/brapi/v2/referencesets` | `reference_set` | 参考基因组集合 |
| `/brapi/v2/references` | `reference` | 参考序列 |
| `/brapi/v2/maps` | `genome_map` | 遗传图谱 |
| `/brapi/v2/markerpositions` | `marker_position` | variant/marker 在图谱上的位置 |
## 关键注意点
1. `CallEntity` 对应的数据库表不是 `call`,而是 `allele_call`
2. `CallSetEntity` 对应 `callset`,不是 `call_set`
3. `VariantSetEntity` 对应 `variantset`,不是 `variant_set`
4. `LinkageGroupEntity` 对应表名是 `linkageGroup`schema 里另有外键引用时大小写需要特别注意。
5. `sample` 是基因型流程的样本入口,向上关联 `plate/observation_unit/study/trial/program`
6. `variant` 是位点定义,`allele_call` 是样本在位点上的结果;不要把二者混成同一层数据。
7. `reference_set/reference/reference_bases` 是参考基因组侧;`variantset/variant/callset/allele_call` 是变异和结果侧。
8. `genome_map/linkageGroup/marker_position` 是遗传图谱定位侧,`marker_position` 通过 `variant_id` 与变异位点相连。
9. 与前两篇一样,`*_additional_info``*_external_references` 是通用扩展关系,用于补充业务字段和外部引用。

View File

@@ -0,0 +1,142 @@
# Germplasm 与 Seed 数据流及表关系
本文档整理 Germplasm 模块中以 `germplasm` 为核心,并覆盖 SeedLot、CrossingProject、GermplasmAttribute、Cross、Pedigree 等相关表的关系。重点表包括:
```text
germplasm
seed_lot
seed_lot_content_mixture
crossing_project
cross_entity / planned cross / cross_parent
germplasm_attribute_definition
germplasm_attribute_value
pedigree_node / pedigree_edge
breeding_method
```
## 结论
Germplasm 关系主线可以理解为:
```text
breeding_method -> germplasm -> germplasm_attribute_value -> germplasm_attribute_definition
program -> crossing_project -> cross_entity -> cross_parent -> germplasm
germplasm / cross_entity -> seed_lot_content_mixture -> seed_lot
germplasm -> pedigree_node -> pedigree_edge
```
其中 `plannedcross` 在数据库中不是独立表,而是 `cross_entity` 的自关联:`cross_entity.planned_cross_id -> cross_entity.id`,并通过 `planned` 字段区分计划杂交和实际杂交。
## 图 4 Germplasm 关系架构图
```mermaid
flowchart TD
BM["breeding_method<br/>育种方法"] -->|"breeding_method_id"| G["germplasm<br/>种质主表"]
CROP["crop<br/>作物"] -->|"crop_id"| G
GAD["germplasm_attribute_definition<br/>GermplasmAttribute 定义"] -->|"attribute_id"| GAV["germplasm_attribute_value<br/>GermplasmAttributeValue"]
G -->|"germplasm_id"| GAV
CROP -->|"crop_id"| GAD
TR["trait"] -->|"trait_id"| GAD
ME["method"] -->|"method_id"| GAD
SC["scale"] -->|"scale_id"| GAD
ONT["ontology"] -->|"ontology_id"| GAD
PR["program<br/>项目"] -->|"program_id"| CP["crossing_project<br/>CrossingProject"]
CP -->|"crossing_project_id"| CR["cross_entity<br/>Cross / PlannedCross"]
CP -->|"crossing_project_id"| XP["cross_parent<br/>CrossParent"]
CR -->|"cross_id"| XP
G -->|"germplasm_id"| XP
OU["observation_unit<br/>可选亲本来源"] -->|"observation_unit_id"| XP
CR -->|"planned_cross_id 自关联"| PCR["cross_entity<br/>planned cross"]
G -->|"germplasm_id"| SCM["seed_lot_content_mixture<br/>SeedLot 组成"]
CR -->|"cross_id"| SCM
SCM -->|"seed_lot_id"| SL["seed_lot<br/>SeedLot"]
PR -->|"program_id"| SL
LOC["location<br/>库位/地点"] -->|"location_id"| SL
SL -->|"from_seed_lot_id"| TX["seed_lot_transaction<br/>SeedLot 流转"]
TX -->|"to_seed_lot_id"| SL
CP -->|"crossing_project_id"| PN["pedigree_node<br/>PedigreeNode"]
G -->|"germplasm_id"| PN
PN -->|"this_node_id"| PE["pedigree_edge<br/>亲子/同胞关系"]
PE -->|"connceted_node_id"| PN2["pedigree_node<br/>父本/子代节点"]
```
## 图 4-2 Germplasm ER 关系图
```mermaid
erDiagram
crop ||--o{ germplasm : "crop_id"
breeding_method ||--o{ germplasm : "breeding_method_id"
germplasm ||--o{ germplasm_attribute_value : "germplasm_id"
germplasm ||--o{ germplasm_donor : "germplasm_id"
germplasm ||--o{ germplasm_institute : "germplasm_id"
germplasm ||--o{ germplasm_origin : "germplasm_id"
germplasm ||--o{ germplasm_synonym : "germplasm_id"
germplasm ||--o{ germplasm_taxon : "germplasm_id"
germplasm ||--o| pedigree_node : "germplasm_id"
pedigree_node ||--o{ pedigree_edge : "this_node_id"
pedigree_node ||--o{ pedigree_edge : "connceted_node_id"
crossing_project ||--o{ cross_entity : "crossing_project_id"
cross_entity ||--o{ cross_entity : "planned_cross_id"
cross_entity ||--o{ cross_parent : "cross_id"
crossing_project ||--o{ cross_parent : "crossing_project_id"
germplasm ||--o{ cross_parent : "germplasm_id"
seed_lot ||--o{ seed_lot_content_mixture : "seed_lot_id"
germplasm ||--o{ seed_lot_content_mixture : "germplasm_id"
cross_entity ||--o{ seed_lot_content_mixture : "cross_id"
location ||--o{ seed_lot : "location_id"
program ||--o{ seed_lot : "program_id"
seed_lot ||--o{ seed_lot_transaction : "from_seed_lot_id"
seed_lot ||--o{ seed_lot_transaction : "to_seed_lot_id"
```
## 核心表说明
| 表 | 作用 | 主要上游依赖 | 主要下游 |
| --- | --- | --- | --- |
| `germplasm` | 种质主表,保存 accession、PUI、物种、采集来源、种子来源等信息 | `crop`, `breeding_method` | 属性、机构、来源、系谱、SeedLot 组成、Cross 亲本 |
| `breeding_method` | 育种方法字典 | 无 | `germplasm` |
| `germplasm_attribute_definition` | GermplasmAttribute 定义,继承变量定义体系,可关联 trait/method/scale/ontology/crop | `crop`, `trait`, `method`, `scale`, `ontology` | `germplasm_attribute_value` |
| `germplasm_attribute_value` | GermplasmAttributeValue保存某个 germplasm 在某个属性上的取值 | `germplasm`, `germplasm_attribute_definition` | 属性查询 |
| `crossing_project` | CrossingProject杂交项目 | `program` | `cross_entity`, `cross_parent`, `pedigree_node` |
| `cross_entity` | Cross/PlannedCross 统一落库表;`planned_cross_id` 是对本表的自关联 | `crossing_project`, `cross_entity` | `cross_parent`, `seed_lot_content_mixture` |
| `cross_parent` | CrossParent连接 `cross_entity``germplasm` 或 observation unit | `cross_entity`, `crossing_project`, `germplasm`, `observation_unit` | 杂交亲本 |
| `seed_lot` | 种子批次/库存批次,保存数量、单位、库位、项目、创建和更新时间 | `location`, `program` | `seed_lot_content_mixture`, `seed_lot_transaction` |
| `seed_lot_content_mixture` | SeedLot 组成明细,连接 `seed_lot``germplasm``cross_entity` | `seed_lot`, `germplasm`, `cross_entity` | 表示批次内各成分占比 |
| `seed_lot_transaction` | SeedLot 流转记录,记录从一个批次到另一个批次的数量变化 | `from_seed_lot`, `to_seed_lot` | 库存流向追踪 |
| `pedigree_node` | 系谱节点,一个节点可关联一个 germplasm | `germplasm`, `crossing_project` | `pedigree_edge` |
| `pedigree_edge` | 系谱边,描述 parent/child/sibling 关系 | `pedigree_node` | 系谱查询 |
## 建议录入顺序
1. 先录入上游基础数据:`crop``breeding_method``program``location`,以及属性定义需要的 `trait/method/scale/ontology`
2. 录入 `germplasm_attribute_definition`,定义可采集的 GermplasmAttribute。
3. 录入 `germplasm` 主数据,并通过 `breeding_method_id` 关联育种方法。
4. 录入 `germplasm_attribute_value`,把 germplasm 与 attribute definition 连接起来并保存具体值。
5. 如果涉及杂交,录入 `crossing_project`,再录入计划杂交/实际杂交到 `cross_entity`;计划杂交通过 `planned=true``planned_cross_id` 自关联体现。
6. 录入 `cross_parent`,把 cross 与 parent germplasm 或 observation unit 关联起来。
7. 录入 `pedigree_node``pedigree_edge`,表达 germplasm 的 parent/child/sibling 系谱关系。
8. 录入 `seed_lot`,保存批次数量、单位、库位和项目归属。
9. 录入 `seed_lot_content_mixture`,把 seed lot 与一个或多个 `germplasm`/`cross_entity` 连接起来。
10. 后续出入库、分装、合并或转移时,录入 `seed_lot_transaction`,通过 `from_seed_lot_id``to_seed_lot_id` 追踪流向。
## 关键注意点
1. `germplasm.seedSource``germplasm.seedSourceDescription` 是种质主表上的描述字段,不等同于库存批次。
2. 真正表示库存批次的是 `seed_lot`,而批次与种质的关系在 `seed_lot_content_mixture` 中。
3. `seed_lot_content_mixture` 可以关联 `germplasm`,也可以关联 `cross_entity`,适合表达混合种子批次或由杂交产生的批次。
4. `seed_lot_transaction` 同时有 `fromSeedLot``toSeedLot`,因此它表达的是 seed lot 到 seed lot 的流转关系,而不是 seed lot 到 germplasm 的直接关系。
5. `plannedcross` 没有独立数据库表,统一使用 `cross_entity`,通过 `planned` 字段和 `planned_cross_id` 自关联表达。
6. `germplasm_attribute_definition` 是属性定义,`germplasm_attribute_value` 是种质上的实际属性值,两者通过 `attribute_id` 连接。
7. 系谱关系由 `pedigree_node``pedigree_edge` 表达;杂交流程由 `cross_entity``cross_parent` 表达,两者都可以回到 `germplasm` 主数据。

View File

@@ -0,0 +1,210 @@
# Core 模块数据流与表关系
本文档分析 `brapi-java` 项目 core 模块的数据录入顺序、主表关系,以及初始化脚本中的实际数据流。
## 结论
Core 模块的数据主线是:
```text
crop -> person -> program -> location -> trial -> season -> study -> study 附属信息
```
`list` 是相对独立的列表能力,可以较早录入;如果需要绑定列表所有人,则依赖 `person`
初始化脚本实际执行顺序是:
```text
R__init_data_01_crops.sql
R__init_data_02_lists.sql
R__init_data_03_locations.sql
R__init_data_04_people.sql
R__init_data_05_programs.sql
R__init_data_06_trials.sql
R__init_data_07_seasons.sql
R__init_data_08_studies.sql
```
其中 `R__init_data_05_programs.sql` 会插入 program 负责人到 `person` 表,并回填部分 `location.program_id/crop_id`
## 核心表说明
| 表 | 作用 | 主要上游依赖 | 主要下游 |
| --- | --- | --- | --- |
| `crop` | 作物字典Core 主根数据之一 | 无 | `program``location``trial``study` |
| `person` | 人员、联系人、负责人 | 无 | `program.lead_person_id``trial_contact``study_contact``list.list_owner_person_id` |
| `program` | 育种项目/业务项目 | `crop`、可选 `person` | `trial``study``location` |
| `location` | 地点/试验点 | 可选 `crop``program`、父级 `location``geojson` | `study` |
| `trial` | 试验批次/试验项目 | `crop``program` | `study``trial_contact``trial_publication``trial_dataset_authorship` |
| `season` | 季节/年度区间字典 | 无 | `study_season`、部分 observation |
| `study` | 具体研究/试验实施单元 | `crop``program``trial``location` | `study_contact``study_season``study_data_link``study_observation_level` 等 |
| `list` | 通用列表 | 可选 `person` | `list_item` |
| `list_item` | 列表明细项 | `list` | 无 |
## 建议录入顺序
### 1. 录入基础字典
先录入 `crop``person`
`crop` 是作物维度根数据,后续 `program``trial``study` 都会挂到它下面。`person` 是人员基础资料,后续会作为项目负责人、试验联系人、研究联系人、列表负责人使用。
### 2. 录入地点
录入 `location`,如果地点有坐标,需要先录入 `geojson``coordinate`
地点可以先不绑定 `program/crop`,初始化脚本里就是先插入地点,再在 program 初始化阶段回填 `program_id``crop_id`
### 3. 录入项目 Program
录入 `program` 时需要已有 `crop`。如果有负责人,需要已有 `person`
程序层面 `ProgramEntity.setCrop(...)` 直接绑定作物;后续 trial/study 设置 program 时,会同步继承 program 的 crop。
### 4. 录入 Trial
录入 `trial` 时需要已有 `program``crop`
`trial` 还可以同时录入:
```text
trial_contact
trial_dataset_authorship
trial_publication
trial_additional_info
trial_external_references
```
其中 `trial_contact``trial``person` 的多对多关系表。
### 5. 录入 Season
录入 `season`。它本身相对独立,但后续 `study` 会通过 `study_season` 关联多个 season。
### 6. 录入 Study
录入 `study` 时通常需要已有:
```text
crop
program
trial
location
```
`study` 是 core 模块向 pheno/geno 数据扩展的关键节点。后续 observation、observation_unit、sample、plate、variantset 等很多模块都会引用 `study`
### 7. 录入 Study 附属信息
录入 study 后,再录入依赖 `study_id` 的附属表:
```text
study_contact
study_data_link
study_environment_parameter
study_experimental_design
study_growth_facility
study_last_update
study_observation_level
study_season
study_variable
study_additional_info
study_external_references
```
## Core 数据流图
```mermaid
flowchart TD
A["1. crop 作物字典"] --> C["3. program 项目"]
B["2. person 人员"] --> C
B --> L["list 列表负责人,可选"]
L --> LI["list_item 列表项"]
G["geojson / coordinate 坐标"] --> D["2. location 地点"]
A --> D
C --> D
D --> E["6. study 研究"]
C --> F["4. trial 试验"]
A --> F
B --> FC["trial_contact 试验联系人"]
F --> FC
F --> FP["trial_publication / trial_dataset_authorship"]
S["5. season 季节"] --> SS["study_season 研究季节"]
F --> E
C --> E
A --> E
E --> SS
E --> SC["study_contact 研究联系人"]
B --> SC
E --> SA["study_data_link / environment / design / growth_facility / last_update / observation_level"]
E --> P1["pheno: observation_unit / observation"]
E --> G1["geno: sample / plate / variantset"]
```
## Core ER 关系图
```mermaid
erDiagram
crop ||--o{ program : "crop_id"
crop ||--o{ location : "crop_id"
crop ||--o{ trial : "crop_id"
crop ||--o{ study : "crop_id"
person ||--o{ program : "lead_person_id"
person ||--o{ trial_contact : "person_db_id"
person ||--o{ study_contact : "person_db_id"
person ||--o{ list : "list_owner_person_id"
program ||--o{ location : "program_id"
program ||--o{ trial : "program_id"
program ||--o{ study : "program_id"
location ||--o{ location : "parent_location_id"
location ||--o{ study : "location_id"
trial ||--o{ study : "trial_id"
trial ||--o{ trial_contact : "trial_db_id"
trial ||--o{ trial_publication : "trial_id"
trial ||--o{ trial_dataset_authorship : "trial_id"
study ||--o{ study_contact : "study_db_id"
study ||--o{ study_season : "study_db_id"
season ||--o{ study_season : "season_db_id"
study ||--o{ study_data_link : "study_id"
study ||--o{ study_environment_parameter : "study_id"
study ||--o{ study_experimental_design : "study_id"
study ||--o{ study_growth_facility : "study_id"
study ||--o{ study_last_update : "study_id"
study ||--o{ study_observation_level : "study_id"
list ||--o{ list_item : "list_id"
```
## API 与表的对应关系
| API | 主表 | 说明 |
| --- | --- | --- |
| `GET /brapi/v2/commoncropnames` | `crop` | 查询作物名称列表 |
| `GET/POST/PUT /brapi/v2/people` | `person` | 人员查询、新增、修改;无删除接口 |
| `GET/POST/PUT /brapi/v2/programs` | `program` | 项目依赖 crop可关联 lead person |
| `GET/POST/PUT /brapi/v2/locations` | `location` | 地点可关联 crop、program、parent location、geojson |
| `GET/POST/PUT /brapi/v2/trials` | `trial` | 试验依赖 program/crop可关联 contacts/publications |
| `GET/PUT /brapi/v2/seasons` | `season` | 季节字典 |
| `GET/POST/PUT /brapi/v2/studies` | `study` | 研究依赖 trial/program/crop/location |
| `GET/POST/PUT /brapi/v2/lists` | `list``list_item` | 列表及列表项 |
## 关键注意点
1. `crop` 是最重要的根字典之一,许多业务表都有 `crop_id`
2. `program` 是承上启下的业务节点,它依赖 `crop`,并被 `trial``study``location` 引用。
3. `trial` 是 study 的上级试验组织,`study` 是后续表型、基因型数据的核心入口。
4. `person``trial/study` 是多对多关系,通过 `trial_contact``study_contact` 连接。
5. `study_season``study``season` 的多对多关系。
6. `additional_info``external_reference` 是通用扩展表core 主表通过各自的 `*_additional_info``*_external_references` 关联它们。
7. 初始化脚本中 `list` 早于 `person` 插入,是因为初始 list 数据主要使用文本 owner 字段;如果业务上要设置 `list_owner_person_id`,应先有 `person`

View File

@@ -0,0 +1,67 @@
# 01 Core - crop 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`crop` 记录项目涉及的作物范围,是 Core 上下文的起点。后续 program、trial、study、germplasm、observation_variable、genome_map 等数据都需要回到作物维度筛选和解释。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `crop` |
| 前置依赖 | 无 |
| 下游引用 | `program``location``trial``study``germplasm``observation_variable``genome_map` |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Crop 列表页 | 支持关键词查询、新增、编辑、查看详情、停用 |
| Crop 新增/编辑页 | 轻量表单,重点录入作物名称 |
| Crop 详情页 | 展示该作物下的项目、试验、研究、种质、图谱入口 |
列表页表格字段:`crop_name`、下游 program 数、下游 study 数、状态。
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 作物主键,新增时系统生成,也可导入时指定 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `crop_name` | varchar(255) | 文本框 | 作物名称,用于所有下拉选择展示 | 必填、建议唯一 |
## 校验规则
1. `crop_name` 必填。
2. `crop_name` 建议唯一,新增和编辑时需要做重复提示。
3. 已被下游引用的作物不能物理删除,只能停用或提示引用关系。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /crops` | crop 分页查询 |
| `POST /crops` | 新增 crop |
| `GET /crops/{id}` | crop 详情 |
| `PUT /crops/{id}` | 编辑 crop |
| `DELETE /crops/{id}` | 删除或停用 crop需做引用检查 |
| `GET /selectors/crops` | crop 搜索下拉,参数 `keyword` |
## 导入导出
1. 支持通过 Core 导入流程导入 `crop`
2. 模板列名使用数据库字段名。
3. 可按 `id` 幂等更新;无 `id` 时新增。
4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。
## 删除/停用
`crop` 已被 `program``location``trial``study``germplasm``observation_variable``genome_map` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端弹窗展示后引导用户停用。
## 验收点
1. Crop 列表页支持分页、关键词搜索和基础筛选。
2. 外键选择器展示作物名称,提交保存 `crop.id`
3. 被引用的 crop 删除失败时,前端展示引用详情。

View File

@@ -0,0 +1,76 @@
# 01 Core - person 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`person` 记录项目负责人、trial 联系人、study 联系人和 list owner。它支撑责任追踪、联系人展示、权限和通知。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `person` |
| 前置依赖 | 无 |
| 下游引用 | `program.lead_person_id``trial_contact``study_contact``list.list_owner_person_id` |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Person 列表页 | 支持姓名、邮箱、机构筛选;新增、编辑、查看详情、停用 |
| Person 新增/编辑页 | 使用联系人表单维护人员信息 |
| Person 详情页 | 展示负责的 program、参与的 trial/study、拥有的 list |
列表页表格字段:姓名、邮箱、电话、机构、负责项目数、参与 study 数。
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 人员主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `description` | varchar(255) | 多行文本 | 人员说明、职责补充 | 可选 |
| `email_address` | varchar(255) | 邮箱输入框 | 邮箱地址 | 邮箱格式校验,建议唯一 |
| `first_name` | varchar(255) | 文本框 | 名 | 与 `last_name` 至少填写一个 |
| `institute_name` | varchar(255) | 文本框/选择器 | 所属机构 | 可选 |
| `last_name` | varchar(255) | 文本框 | 姓 | 与 `first_name` 至少填写一个 |
| `mailing_address` | varchar(255) | 多行文本 | 通讯地址 | 可选 |
| `middle_name` | varchar(255) | 文本框 | 中间名 | 可选 |
| `phone_number` | varchar(255) | 电话输入框 | 联系电话 | 可选,格式提示 |
| `userid` | varchar(255) | 文本框 | 外部用户 ID 或登录名 | 可选,建议唯一 |
## 校验规则
1. 姓名必填,`first_name``last_name` 至少填写一个。
2. `email_address` 需要校验邮箱格式。
3. 同一邮箱不建议重复录入。
4. 作为负责人或联系人被引用时,不允许物理删除。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /persons` | person 分页查询 |
| `POST /persons` | 新增 person |
| `GET /persons/{id}` | person 详情 |
| `PUT /persons/{id}` | 编辑 person |
| `DELETE /persons/{id}` | 删除或停用 person需做引用检查 |
| `GET /selectors/persons` | person 搜索下拉,参数 `keyword``instituteName` |
## 导入导出
1. 支持通过 Core 导入流程导入 `person`
2. 模板列名使用数据库字段名。
3. 可按 `id` 幂等更新;无 `id` 时新增。
4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。
## 删除/停用
`person` 已作为 program 负责人、trial 联系人、study 联系人或 list owner 时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。
## 验收点
1. Person 列表页支持分页、关键词搜索和机构筛选。
2. 邮箱格式错误时不能提交。
3. 作为负责人或联系人被引用的 person 删除失败时,前端展示引用详情。

View File

@@ -0,0 +1,77 @@
# 01 Core - program 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`program` 表示长期育种项目,是多个 trial、study、材料、样本和结果的聚合维度。创建 program 时必须明确所属 crop可选负责人。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `program` |
| 前置依赖 | `crop`,可选 `person` |
| 下游引用 | `trial``study``location``crossing_project``seed_lot``plate``sample` |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Program 列表页 | 支持 crop、负责人、program_type、关键词筛选新增、编辑、查看详情、停用 |
| Program 新增/编辑页 | 分为“基本信息”和“负责人/作物”两组 |
| Program 详情页 | Tab 展示 trial、study、location、seed lot、sample |
列表页表格字段:`name``abbreviation`、crop、负责人、trial 数、study 数。
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 项目主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `abbreviation` | varchar(255) | 文本框 | 项目缩写 | 可选 |
| `documentationurl` | varchar(255) | URL 输入框 | 项目文档链接 | URL 格式校验 |
| `funding_information` | varchar(255) | 多行文本 | 经费来源说明 | 可选 |
| `name` | varchar(255) | 文本框 | 项目名称 | 必填 |
| `objective` | varchar(255) | 多行文本 | 项目目标 | 可选 |
| `program_type` | integer | 下拉框 | 项目类型枚举 | 可选,按 BrAPI 枚举 |
| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` |
| `lead_person_id` | varchar(255) | 人员选择器 | 项目负责人 | 可选,来源 `person.id` |
## 校验规则
1. `name` 必填。
2. `crop_id` 必选,且必须引用已存在的 `crop.id`
3. 选择负责人时,`lead_person_id` 必须引用已存在的 `person.id`
4. `documentationurl` 需要做 URL 格式校验。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /programs` | program 分页查询 |
| `POST /programs` | 新增 program |
| `GET /programs/{id}` | program 详情 |
| `PUT /programs/{id}` | 编辑 program |
| `DELETE /programs/{id}` | 删除或停用 program需做引用检查 |
| `GET /programs/{id}/trials` | 查询 program 下 trial |
| `GET /programs/{id}/studies` | 查询 program 下 study |
| `GET /selectors/programs` | program 搜索下拉,参数 `keyword``cropId` |
## 导入导出
1. 支持通过 Core 导入流程导入 `program`
2. 外键列支持填写 ID可额外支持名称匹配但名称重复时必须报错。
3. 可按 `id` 幂等更新;无 `id` 时新增。
4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。
## 删除/停用
`program` 已有关联 `trial``study``location``crossing_project``seed_lot``plate``sample` 时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。
## 验收点
1. 创建 program 时必须选择 crop。
2. 负责人选择器只能保存已存在的 person ID。
3. Program 详情页能进入该项目下的 trial 和 study。

View File

@@ -0,0 +1,86 @@
# 01 Core - location 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`location` 记录试验实施地点,可作为公共地点,也可绑定 program 和 crop。study 创建时必须选择实施地点。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `location` |
| 前置依赖 | 可选 `crop``program`、父级 `location`、坐标 |
| 下游引用 | `study``seed_lot` |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Location 列表页 | 支持 crop、program、location_type、country、关键词筛选新增、编辑、查看详情、停用、地图查看 |
| Location 新增/编辑页 | 包含基本信息、行政区、坐标、父级地点 |
| Location 详情页 | 展示该地点下的 study 和 seed lot |
列表页支持地图/表格两种视图。列表页表格字段:`location_name`、location_type、country、program、crop、父级地点。
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 地点主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `abbreviation` | varchar(255) | 文本框 | 地点缩写 | 可选 |
| `coordinate_description` | varchar(255) | 多行文本 | 坐标描述 | 可选 |
| `coordinate_uncertainty` | varchar(255) | 文本框 | 坐标不确定性 | 可选 |
| `country_code` | varchar(255) | 国家代码选择器 | 国家代码 | 可选,建议 ISO 代码 |
| `country_name` | varchar(255) | 文本框 | 国家名称 | 可选,可由国家代码带出 |
| `documentationurl` | varchar(255) | URL 输入框 | 地点文档链接 | URL 格式校验 |
| `environment_type` | varchar(255) | 下拉框/文本框 | 环境类型 | 可选 |
| `exposure` | varchar(255) | 文本框 | 暴露条件 | 可选 |
| `institute_address` | varchar(255) | 多行文本 | 机构地址 | 可选 |
| `institute_name` | varchar(255) | 文本框 | 机构名称 | 可选 |
| `location_name` | varchar(255) | 文本框 | 地点名称 | 必填 |
| `location_type` | varchar(255) | 下拉框 | 地点类型,如 field、greenhouse、storage | 可选 |
| `site_status` | varchar(255) | 下拉框 | 地点状态 | 可选 |
| `slope` | varchar(255) | 文本框 | 坡度 | 可选 |
| `topography` | varchar(255) | 文本框 | 地形 | 可选 |
| `coordinates_id` | varchar(255) | 坐标选择器/地图取点 | 坐标对象 | 可选,来源 `geojson/coordinate` |
| `crop_id` | varchar(255) | 作物选择器 | 关联作物 | 可选,来源 `crop.id` |
| `parent_location_id` | varchar(255) | 地点选择器 | 父级地点 | 可选,不能选择自己 |
| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 可选,来源 `program.id` |
## 校验规则
1. `location_name` 必填。
2. 坐标字段格式合法。
3. `parent_location_id` 不能选择自己。
4. 选择 program 后可自动带出 crop但允许地点作为公共地点不绑定 program。
5. `documentationurl` 需要做 URL 格式校验。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /locations` | location 分页查询 |
| `POST /locations` | 新增 location |
| `GET /locations/{id}` | location 详情 |
| `PUT /locations/{id}` | 编辑 location |
| `GET /selectors/locations` | location 搜索下拉,参数 `keyword``programId``cropId``locationType` |
## 导入导出
1. 支持通过 Core 导入流程导入 `location`
2. 导入时如果 `parent_location_id` 指向自己,应报错并指出行号。
3. 外键列支持填写 ID可额外支持名称匹配但名称重复时必须报错。
4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。
## 删除/停用
`location` 已被 `study``seed_lot` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。
## 验收点
1. Location 列表页支持表格视图和地图查看。
2. 父级地点不能选择自己。
3. Study 新增时 location 选择器可按 program/crop 过滤。

View File

@@ -0,0 +1,78 @@
# 01 Core - trial 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`trial` 表示某一批试验、区域试验或年度试验,位于 program 与 study 之间。创建 trial 时选择 program并自动带出或校验 crop。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `trial` |
| 前置依赖 | `program``crop` |
| 下游引用 | `study``observation_unit``observation``plate``sample` |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Trial 列表页 | 支持 crop、program、active、起止日期、关键词筛选新增、编辑、查看详情、停用 |
| Trial 新增/编辑页 | 包含基本信息、项目作物、联系人、出版物四个区域 |
| Trial 详情页 | 展示 study 列表和 phenotyping/genotyping 入口 |
列表页表格字段:`trial_name`、program、crop、`start_date``end_date`、active。
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 试验主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `active` | boolean | 开关 | 是否启用 | 默认启用 |
| `documentationurl` | varchar(255) | URL 输入框 | 试验文档链接 | URL 格式校验 |
| `end_date` | timestamp | 日期选择器 | 结束日期 | 不早于 `start_date` |
| `start_date` | timestamp | 日期选择器 | 开始日期 | 可选 |
| `trial_description` | varchar(255) | 多行文本 | 试验描述 | 可选 |
| `trial_name` | varchar(255) | 文本框 | 试验名称 | 必填 |
| `trialpui` | varchar(255) | 文本框 | 试验永久标识 | 可选,建议唯一 |
| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` |
| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 必选,来源 `program.id` |
## 校验规则
1. `trial_name` 必填。
2. `program_id` 必选且必须存在。
3. `crop_id` 必选且必须与 `program.crop_id` 一致。
4. `end_date` 不能早于 `start_date`
5. `documentationurl` 需要做 URL 格式校验。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /trials` | trial 分页查询 |
| `POST /trials` | 新增 trial |
| `GET /trials/{id}` | trial 详情 |
| `PUT /trials/{id}` | 编辑 trial |
| `DELETE /trials/{id}` | 删除或停用 trial需检查 study 引用 |
| `GET /trials/{id}/studies` | 查询 trial 下 study |
| `GET /selectors/trials` | trial 搜索下拉,参数 `keyword``programId``cropId``active` |
## 导入导出
1. 支持通过 Core 导入流程导入 `trial`
2. 外键列支持填写 ID可额外支持名称匹配但名称重复时必须报错。
3. 导入时必须校验 trial 的 crop 与 program 的 crop 一致。
4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。
## 删除/停用
`trial` 已被 `study``observation_unit``observation``plate``sample` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。
## 验收点
1. 创建 trial 时,选择 program 后自动带出 crop。
2. Trial 列表页支持按 crop、program、active、日期筛选。
3. 已被 study 引用的 trial 不能直接删除。

View File

@@ -0,0 +1,66 @@
# 01 Core - season 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`season` 记录年度和季节,用于 study 的季节绑定,以及部分 observation 的季节维度统计。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `season` |
| 前置依赖 | 无 |
| 下游引用 | `study_season`、部分 `observation` |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Season 列表页 | 支持 year、season 筛选;新增、编辑、查看详情、停用 |
| Season 新增/编辑页 | 简单表单 |
| Season 详情页 | 展示关联 study |
列表页表格字段:`year``season`、关联 study 数。
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 季节主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `season` | varchar(255) | 文本框/下拉框 | 季节名称,如 Spring、Summer | 必填 |
| `year` | integer | 年份选择器 | 年份 | 必填,四位年份 |
## 校验规则
1. `season` 必填。
2. `year` 必填,使用四位年份。
3. 同一年份内 season 名称不建议重复。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /seasons` | season 查询 |
| `POST /seasons` | 新增 season |
| `PUT /seasons/{id}` | 编辑 season |
| `GET /selectors/seasons` | season 搜索下拉,参数 `year``keyword` |
## 导入导出
1. 支持通过 Core 导入流程导入 `season`
2. 模板列名使用数据库字段名。
3. 可按 `id` 幂等更新;无 `id` 时新增。
4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。
## 删除/停用
`season` 已被 `study_season``observation` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端展示后引导用户停用。
## 验收点
1. Season 列表页支持按 year 和 season 筛选。
2. 同一年份内重复季节名称需要提示。
3. Study 新增/编辑页可以多选 season。

View File

@@ -0,0 +1,103 @@
# 01 Core - study 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`study` 是一次真正落地执行的试验,是 Core 模块最关键的上下文单元。后续 observation_unit、event、observation、plate、sample、variantset 都会直接或间接挂到 study 上。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `study` |
| 前置依赖 | `crop``program``trial``location`,可选 `season` |
| 下游引用 | `observation_unit``event``observation``plate``sample``variantset` |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Study 列表页 | 支持 crop、program、trial、location、season、active、study_type、关键词筛选新增、编辑、查看详情、停用 |
| Study 新增/编辑页 | 分组表单:基本信息、上下文、地点季节、实验设计、联系人 |
| Study 工作台 | 点击 study 名称或“进入工作台”后进入,展示下游业务入口和聚合数量 |
Study 列表页表格字段:`study_name``study_code`、program、trial、location、`start_date``end_date`、active。
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | study 主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `active` | boolean | 开关 | 是否启用 | 默认启用 |
| `cultural_practices` | varchar(255) | 多行文本 | 栽培管理说明 | 可选 |
| `documentationurl` | varchar(255) | URL 输入框 | study 文档链接 | URL 格式校验 |
| `end_date` | timestamp | 日期选择器 | 结束日期 | 不早于 `start_date` |
| `license` | varchar(255) | 文本框 | 数据许可证 | 可选 |
| `observation_units_description` | varchar(255) | 多行文本 | 观测单元说明 | 可选 |
| `start_date` | timestamp | 日期选择器 | 开始日期 | 可选 |
| `study_code` | varchar(255) | 文本框 | study 编码 | 可选,建议同项目内唯一 |
| `study_description` | varchar(255) | 多行文本 | study 描述 | 可选 |
| `study_name` | varchar(255) | 文本框 | study 名称 | 必填 |
| `studypui` | varchar(255) | 文本框 | study 永久标识 | 可选,建议唯一 |
| `study_type` | varchar(255) | 下拉框 | study 类型 | 可选 |
| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` |
| `location_id` | varchar(255) | 地点选择器 | 实施地点 | 必选,来源 `location.id` |
| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 必选,来源 `program.id` |
| `trial_id` | varchar(255) | 试验选择器 | 所属 trial | 必选,来源 `trial.id` |
## 新增/编辑分区
| 区域 | 字段/功能 | 要求 |
| --- | --- | --- |
| 基本信息 | study_name、study_code、study_type、active、start_date、end_date | study_name 必填;结束日期不早于开始日期 |
| 上下文 | crop、program、trial、location、season | program、trial、crop、location 必选program -> trial 联动 |
| 说明信息 | study_description、cultural_practices、observation_units_description、license、documentationurl | URL 格式校验 |
| 联系人 | study_contact | 多选 person可增删 |
| 季节 | study_season | 可多选 season |
| 提交后 | 保存成功进入 Study 工作台 | 新建成功后自动跳转 |
## 校验规则
1. `study_name` 必填。
2. `program_id``trial_id``crop_id``location_id` 必选且必须存在。
3. `trial_id` 必须属于所选 `program_id`
4. 选择 program 后过滤 trial选择 trial 后自动带出 crop。
5. `end_date` 不能早于 `start_date`
6. 编辑 study 时,如果已有 observation_unit、sample、observation变更 program/trial/crop/location 前必须二次确认并检查一致性。
7. study 是下游核心引用,删除必须强提示。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /studies` | study 分页查询 |
| `POST /studies` | 新增 study |
| `GET /studies/{id}` | study 详情 |
| `PUT /studies/{id}` | 编辑 study |
| `DELETE /studies/{id}` | 删除或停用 study需检查下游引用 |
| `GET /studies/{id}/workbench` | Study 工作台聚合信息 |
| `GET /studies/{id}/contacts` | study 联系人 |
| `GET /studies/{id}/seasons` | study 季节 |
| `GET /selectors/studies` | study 搜索下拉,参数 `keyword``programId``trialId``locationId``active` |
## 导入导出
1. 支持通过 Core 导入流程导入 `study`
2. 导入时必须校验字段完整性、必填、格式、外键是否存在、联动关系是否一致。
3. 如果 trial 不属于 program应报错并指出行号。
4. study 导出应支持工作台摘要包括联系人、season、下游数据数量。
5. 大数据量导出时应走异步任务。
## 删除/停用
`study` 已被 `observation_unit``event``observation``plate``sample``variantset` 引用时,不允许物理删除。后端需要返回引用对象类型和数量,前端弹窗展示后引导用户停用。
## 验收点
1. Study 列表页支持按 crop、program、trial、location、season、active 筛选。
2. 点击 study 名称或“进入工作台”按钮后进入 Study 工作台。
3. Study 工作台可以看到观测单元、表型、样本、基因型入口。
4. study 创建成功后自动进入 Study 工作台。
5. 已被 observation_unit、sample、observation 引用的 study 不能直接删除。

View File

@@ -0,0 +1,73 @@
# 01 Core - list 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`list` 是通用分组列表的主表,用于维护一组业务对象或文本项。`list` 负责列表基本信息,`list_item` 负责列表明细。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `list` |
| 前置依赖 | 可选 `person` |
| 下游引用 | `list_item`、业务查询和分组 |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| List 列表页 | 展示 list支持查询、新增、编辑 |
| List 详情页 | 顶部展示 list 基本信息,下方内嵌 list_item 表格 |
List 详情页支持新增 item、批量导入 item、删除 item、排序。同一 list 内 item 不重复。
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 列表主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `date_created` | timestamp | 只读日期时间 | 创建时间 | 系统自动写入 |
| `date_modified` | timestamp | 只读日期时间 | 修改时间 | 系统自动更新 |
| `description` | varchar(255) | 多行文本 | 列表描述 | 可选 |
| `list_name` | varchar(255) | 文本框 | 列表名称 | 必填 |
| `list_owner_name` | varchar(255) | 文本框 | 列表 owner 名称 | 可选,可由 owner person 带出 |
| `list_source` | varchar(255) | 文本框 | 列表来源 | 可选 |
| `list_type` | integer | 下拉框 | 列表类型 | 必填,按 BrAPI 枚举 |
| `list_owner_person_id` | varchar(255) | 人员选择器 | 列表 owner | 可选,来源 `person.id` |
## 校验规则
1. `list_name` 必填。
2. `list_type` 必填,按 BrAPI 枚举选择。
3. 如果绑定 owner`list_owner_person_id` 必须引用已存在的 `person.id`
4. `date_created``date_modified` 由系统维护,前端只读。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /lists` | list 查询 |
| `POST /lists` | 新增 list |
| `PUT /lists/{id}` | 编辑 list |
| `POST /lists/{id}/items` | 给 list 添加 item |
| `DELETE /lists/{id}/items/{itemId}` | 删除 list item |
## 导入导出
1. 支持通过 Core 导入流程导入 `list`
2. 模板列名使用数据库字段名。
3. 外键列支持填写 `list_owner_person_id`;可额外支持名称匹配,但名称重复时必须报错。
4. 列表页支持导出当前筛选结果,导出文件应包含 ID 和展示名称。
## 删除/停用
`list` 已有 `list_item` 时,删除前必须提示。可以先清空明细再删除 list。
## 验收点
1. List 详情页顶部展示 list 基本信息,下方展示 list_item 表格。
2. 同一 list 内 item 不重复。
3. 绑定 owner 时只能保存已存在的 person ID。

View File

@@ -0,0 +1,62 @@
# 01 Core - list_item 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`list_item` 是 list 的明细项,是“给 list 添加成员”这个动作留下的记录。它可以保存目标对象 ID 或文本值,用于业务查询和分组。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `list_item` |
| 前置依赖 | `list` |
| 下游引用 | 业务查询和分组 |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| List 详情页 | 在 list 基本信息下方内嵌 list_item 表格 |
| 批量导入 | 支持给指定 list 批量导入 item |
新增 item 时可选择目标类型,例如 germplasm、study、sample。
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 列表项主键 | 必填、唯一 |
| `item` | varchar(255) | 文本框/对象选择器 | 列表项值,可存目标对象 ID 或文本 | 必填,同一 list 内不重复 |
| `list_id` | varchar(255) | List 选择器 | 所属列表 | 必选,来源 `list.id` |
## 校验规则
1. `list_id` 必选,且必须引用已存在的 `list.id`
2. `item` 必填。
3. 同一 `list_id``item` 不应重复。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `POST /lists/{id}/items` | 给 list 添加 item |
| `DELETE /lists/{id}/items/{itemId}` | 删除 list item |
## 导入导出
1. 支持通过 Core 导入流程导入 `list_item`
2. 导入时需要校验 `list_id` 是否存在。
3. 导入时需要校验同一 list 内 item 是否重复。
4. 错误报告返回行号、字段名、错误原因、建议修正方式。
## 删除/停用
`list_item` 是明细记录,可从 List 详情页删除。删除前需要确认,删除后不影响 list 基本信息。
## 验收点
1. List 详情页可新增 item、批量导入 item、删除 item、排序。
2. 同一 list 内重复 item 不能提交。
3. 批量导入错误能定位到行号和字段名。

View File

@@ -0,0 +1,57 @@
# 01 Core - trial_contact 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`trial_contact` 是给 trial 添加联系人的关系表,不是独立主数据。它记录 trial 与 person 的多对多关系,用于试验责任追踪、联系人展示、权限和通知。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `trial_contact` |
| 前置依赖 | `trial``person` |
| 下游引用 | trial 联系人展示、权限和通知 |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Trial 新增/编辑页 | 联系人区域使用可增删表格选择 `person` |
| Trial 详情页 | 展示 trial 联系人 |
| 批量导入 | 支持通过 Core 导入流程导入 trial 联系人关系 |
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `trial_db_id` | varchar(255) | Trial 选择器 | 所属 trial | 必选,来源 `trial.id` |
| `person_db_id` | varchar(255) | 人员选择器 | 联系人 | 必选,来源 `person.id` |
## 校验规则
1. `trial_db_id` 必选,且必须引用已存在的 `trial.id`
2. `person_db_id` 必选,且必须引用已存在的 `person.id`
3. 同一 trial 下不建议重复添加同一个 person。
## 接口能力
本文档原始需求未强制限定 trial_contact 的独立 URL。实现时可以随 trial 新增/编辑一起保存,也可以提供 trial 联系人子资源接口;前端需要具备新增、删除、查询 trial 联系人的能力。
## 导入导出
1. 支持通过 Core 导入流程导入 `trial_contact`
2. 外键列支持填写 ID可额外支持名称匹配但名称重复时必须报错。
3. 导入时需要校验 trial 和 person 是否存在。
4. 错误报告返回行号、字段名、错误原因、建议修正方式。
## 删除/停用
`trial_contact` 是关系记录,可在 Trial 新增/编辑页或详情页移除。删除关系不应删除 `trial``person` 主数据。
## 验收点
1. Trial 新增/编辑页可增删联系人。
2. 联系人选择器展示 person 名称,提交保存 `person.id`
3. 删除 trial_contact 后person 主数据仍保留。

View File

@@ -0,0 +1,56 @@
# 01 Core - trial_publication 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`trial_publication` 是给 trial 记录出版物、报告或文献引用的痕迹表。它挂在 trial 下,用于说明试验相关的外部文献和永久标识。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `trial_publication` |
| 前置依赖 | `trial` |
| 下游引用 | trial 出版物/报告展示 |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Trial 新增/编辑页 | 出版物区域可增删 publication 记录 |
| Trial 详情页 | 展示 trial 的出版物、报告或引用 |
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 出版物记录主键 | 必填、唯一 |
| `publicationpui` | varchar(255) | 文本框 | 出版物 PUI | 可选 |
| `publication_reference` | varchar(255) | 文本框/URL | 出版物引用 | 可选 |
| `trial_id` | varchar(255) | Trial 选择器 | 所属 trial | 必选,来源 `trial.id` |
## 校验规则
1. `id` 必填且唯一,新增时可由系统生成。
2. `trial_id` 必选,且必须引用已存在的 `trial.id`
3. `publication_reference` 如果录入 URL需要做 URL 格式校验。
4. `publicationpui` 建议唯一。
## 接口能力
本文档原始需求未强制限定 trial_publication 的独立 URL。实现时可以随 trial 新增/编辑一起保存,也可以提供 trial 出版物子资源接口;前端需要具备新增、删除、查询 trial 出版物的能力。
## 导入导出
原始导入对象清单未单独列出 `trial_publication`,但字段需求中包含该表。若纳入 Core 导入,应遵循通用导入流程:模板列名使用数据库字段名,外键校验 `trial_id`,错误报告返回行号、字段名和错误原因。
## 删除/停用
`trial_publication` 是 trial 的明细记录,可在 Trial 新增/编辑页或详情页移除。删除 publication 记录不应删除 `trial` 主数据。
## 验收点
1. Trial 新增/编辑页可维护出版物记录。
2. `trial_id` 不存在时不能提交。
3. 删除 publication 记录后trial 主数据仍保留。

View File

@@ -0,0 +1,61 @@
# 01 Core - study_contact 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`study_contact` 是给 study 添加联系人的关系表,不是独立主数据。它记录 study 与 person 的多对多关系,用于 Study 工作台联系人展示、责任追踪、权限和通知。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `study_contact` |
| 前置依赖 | `study``person` |
| 下游引用 | Study 工作台联系人展示、权限和通知 |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Study 新增/编辑页 | 联系人区域多选 person可增删 |
| Study 工作台 | 展示 study 联系人 |
| 批量导入 | 支持通过 Core 导入流程导入 study 联系人关系 |
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `study_db_id` | varchar(255) | Study 选择器 | 所属 study | 必选,来源 `study.id` |
| `person_db_id` | varchar(255) | 人员选择器 | 联系人 | 必选,来源 `person.id` |
## 校验规则
1. `study_db_id` 必选,且必须引用已存在的 `study.id`
2. `person_db_id` 必选,且必须引用已存在的 `person.id`
3. 同一 study 下不建议重复添加同一个 person。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /studies/{id}/contacts` | study 联系人,返回 person 列表 |
本文档原始需求未强制限定 study_contact 的写入 URL。实现时可以随 study 新增/编辑一起保存,也可以提供 study 联系人子资源接口;前端需要具备新增、删除、查询 study 联系人的能力。
## 导入导出
1. 支持通过 Core 导入流程导入 `study_contact`
2. 外键列支持填写 ID可额外支持名称匹配但名称重复时必须报错。
3. 导入时需要校验 study 和 person 是否存在。
4. 错误报告返回行号、字段名、错误原因、建议修正方式。
## 删除/停用
`study_contact` 是关系记录,可在 Study 新增/编辑页或工作台移除。删除关系不应删除 `study``person` 主数据。
## 验收点
1. Study 新增/编辑页可增删联系人。
2. Study 工作台能展示联系人列表。
3. 删除 study_contact 后person 主数据仍保留。

View File

@@ -0,0 +1,61 @@
# 01 Core - study_season 表录入说明
来源:`docs/requirements/01-core-data-entry-requirements.md`
## 录入目标
`study_season` 是给 study 绑定 season 的关系表。一个 study 可以关联多个 season用于多季节筛选、统计和 Study 工作台上下文展示。
## 前置依赖和下游引用
| 类型 | 内容 |
| --- | --- |
| 表 | `study_season` |
| 前置依赖 | `study``season` |
| 下游引用 | Study 工作台季节展示、多季节筛选和统计 |
## 页面入口
| 页面 | 录入要求 |
| --- | --- |
| Study 新增/编辑页 | 季节区域多选 season |
| Study 工作台 | 展示 study 关联 season |
| 批量导入 | 支持通过 Core 导入流程导入 study 与 season 关系 |
## 字段录入
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `study_db_id` | varchar(255) | Study 选择器 | 所属 study | 必选,来源 `study.id` |
| `season_db_id` | varchar(255) | Season 选择器 | 关联季节 | 必选,来源 `season.id` |
## 校验规则
1. `study_db_id` 必选,且必须引用已存在的 `study.id`
2. `season_db_id` 必选,且必须引用已存在的 `season.id`
3. 同一 study 下不建议重复绑定同一个 season。
## 接口能力
| 接口 | 用途 |
| --- | --- |
| `GET /studies/{id}/seasons` | study 季节,返回 season 列表 |
本文档原始需求未强制限定 study_season 的写入 URL。实现时可以随 study 新增/编辑一起保存,也可以提供 study season 子资源接口;前端需要具备新增、删除、查询 study season 的能力。
## 导入导出
1. 支持通过 Core 导入流程导入 `study_season`
2. 外键列支持填写 ID可额外支持名称匹配但名称重复时必须报错。
3. 导入时需要校验 study 和 season 是否存在。
4. 错误报告返回行号、字段名、错误原因、建议修正方式。
## 删除/停用
`study_season` 是关系记录,可在 Study 新增/编辑页或工作台移除。删除关系不应删除 `study``season` 主数据。
## 验收点
1. Study 新增/编辑页可多选 season。
2. Study 工作台能展示关联 season。
3. 删除 study_season 后season 主数据仍保留。

View File

@@ -0,0 +1,26 @@
# 02 Germplasm / Seed - breeding_method 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`breeding_method` 是育种方法字典,用来说明某个 germplasm 是通过什么方式形成的,例如杂交选育、回交、自交系选育、诱变、转基因、克隆选择等。它不是一次具体杂交动作,而是材料来源方法的分类。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------- | -------------------------------------------- | -------------------------------- | --------- | -------------------------------- |
| `id` | 育种方法主键,系统内部唯一标识 | 新增时系统生成;导入时可允许指定 | 隐藏/只读 | 必填、唯一;编辑时不允许修改 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许前端手填 |
| `abbreviation` | 方法缩写,如 MB、BC、DH | 用户录入 | 文本框 | 可选;建议同一用户下唯一 |
| `description` | 方法解释,如“回交用于恢复目标基因” | 用户录入 | 多行文本 | 可选,限制长度 |
| `name` | 方法名称,如 Male Backcross、Doubled Haploid | 用户录入 | 文本框 | 必填;建议唯一;作为下拉展示名称 |
## 页面与交互
- 列表页展示:方法名称、缩写、描述、使用材料数量。
- 新增页为简单字典表单。
- 删除前检查是否被 `germplasm.breeding_method_id` 引用;已引用时不允许物理删除,只允许停用。
---

View File

@@ -0,0 +1,54 @@
# 02 Germplasm / Seed - germplasm 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`germplasm` 是材料身份证,描述一个品种、品系、亲本、后代材料、遗传资源或研究材料“是谁”。它不表示库存数量,库存数量由 `seed_lot` 表达。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------------------------- | ------------------------------------------------------------ | -------------------------------------- | ----------------- | --------------------------------------------------------- |
| `id` | 种质主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一;编辑不可随意修改 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `accession_number` | 材料在种质库/机构内的 accession 编号,如 PI 113869 | 用户录入或导入 | 文本框 | 可选;建议同一 crop / institution 下唯一 |
| `acquisition_date` | 材料进入本系统或本机构的获取日期 | 用户录入 | 日期选择器 | 可选;不得晚于当前日期太多,导入时允许缺月/缺日需统一规则 |
| `acquisition_source_code` | 获取来源编码,如采集、引进、交换、繁殖等 | 用户选择 | 下拉框 | 可选;值来自 BrAPI/MCPD 枚举或系统字典 |
| `biological_status_of_accession_code` | 材料生物状态,如野生、地方品种、育种材料、改良品种、突变体等 | 用户选择 | 下拉框 | 可选;使用受控枚举,不建议自由输入 |
| `collection` | 材料所属集合、群体、panel 或 collection | 用户录入/选择 | 文本框/选择器 | 可选;可用于分组筛选 |
| `country_of_origin_code` | 原产国或育成/选育国家代码 | 用户选择 | 国家代码选择器 | 可选;建议使用 ISO 3166-1 三字母代码 |
| `default_display_name` | 系统默认展示名,给下拉框、表格、详情标题使用 | 用户录入,可由 germplasm_name 自动带出 | 文本框 | 与 `germplasm_name` 至少填一个;建议必填 |
| `documentationurl` | 材料说明文档、外部数据库页面或 DOI 链接 | 用户录入 | URL 输入框 | 可选;校验 URL 格式 |
| `genus` | 属名,如 Oryza、Triticum | 用户录入/字典选择 | 文本框/物种选择器 | 可选;建议首字母大写 |
| `germplasm_name` | 材料名称,可以是品种名、品系名、后代编号 | 用户录入 | 文本框 | 与 `default_display_name` 至少填一个;不强制全局唯一 |
| `germplasmpui` | 永久唯一标识,通常是 DOI、URI 或全局唯一编码 | 用户录入/外部导入 | 文本框/URL 输入框 | 可选;若填写必须唯一;建议用于跨系统交换 |
| `germplasm_preprocessing` | 材料用于试验前的统一处理说明,如消毒、催芽、低温处理 | 用户录入 | 文本框/多行文本 | 可选 |
| `mls_status` | 多边系统 MLS 状态,涉及植物遗传资源交换协议 | 用户选择 | 下拉框 | 可选;普通业务可隐藏到高级信息 |
| `seed_source` | 材料来源标识,如来源机构+accession或亲本组合描述 | 用户录入 | 文本框 | 可选;注意它不是库存批次,不等于 seed_lot |
| `seed_source_description` | 材料来源详细说明 | 用户录入 | 多行文本 | 可选 |
| `species` | 种名,如 sativa、aestivum | 用户录入/物种字典 | 文本框 | 可选;建议小写 |
| `species_authority` | 种名命名权威,如 L. | 用户录入 | 文本框 | 可选 |
| `subtaxa` | 亚种、变种、品种群、line 等更细分类 | 用户录入 | 文本框 | 可选 |
| `subtaxa_authority` | 亚种/变种命名权威 | 用户录入 | 文本框 | 可选 |
| `breeding_method_id` | 该材料形成所使用的育种方法 | 从 breeding_method 选择 | 搜索选择器 | 可选;必须引用存在的 breeding_method |
| `crop_id` | 所属作物 | 从 crop 选择 | 作物选择器 | 必填;后续 trial/study/attribute 应尽量同 crop |
## 录入建议
- 新建材料时,第一屏只放核心字段:`crop_id``germplasm_name``default_display_name``germplasmpui``accession_number``breeding_method_id`
- 分类与来源信息放在“高级信息”或“来源信息”分组。
- `germplasmpui``accession_number``germplasm_name` 三者不要混为一谈:
- `germplasm_name` 是人看的名字;
- `accession_number` 是机构内编号;
- `germplasmpui` 是跨系统长期唯一标识。
## 验收点
1. 新增 germplasm 时,必须选择 crop。
2. `germplasm_name``default_display_name` 至少填写一个。
3. 下拉选择材料时展示 `default_display_name`,辅助展示 accession number / PUI。
4. 如果 germplasm 已被 seed lot、cross parent、observation unit 引用,不允许物理删除。
---

View File

@@ -0,0 +1,48 @@
# 02 Germplasm / Seed - germplasm_attribute_definition 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
属性定义描述“材料可以有哪些稳定属性”。这些属性通常不是环境依赖的田间观测值,而是材料自身特征,例如籽粒颜色、抗病基因、硬度、熟期类型、特定 QTL、分子标记结果等。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ---------------------- | ------------------------------------------------------------ | ---------------------------- | ----------------- | ------------------------------------- |
| `id` | 属性定义主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `default_value` | 属性默认值 | 用户录入 | 动态输入框 | 可选;按 datatype / scale 校验 |
| `documentationurl` | 属性说明文档链接 | 用户录入 | URL 输入框 | 可选;校验 URL |
| `growth_stage` | 属性适用生长阶段,如 flowering | 用户录入/选择 | 下拉框/文本框 | 可选 |
| `institution` | 提交或维护该属性定义的机构 | 用户录入 | 文本框 | 可选 |
| `language` | 定义语言,如 zh、en | 用户选择 | 下拉框 | 可选;建议 ISO 639-1 |
| `scientist` | 提交该属性定义的科学家或负责人 | 用户录入/人员选择 | 文本框/人员选择器 | 可选 |
| `status` | 属性状态,如 recommended、obsolete、legacy | 用户选择 | 下拉框 | 可选;推荐使用枚举 |
| `submission_timestamp` | 属性定义提交时间 | 系统默认当前时间,可手动调整 | 日期时间选择器 | 可选;新增默认当前时间 |
| `crop_id` | 适用作物 | 从 crop 选择 | 作物选择器 | 可选;若填写,下游材料应同 crop |
| `method_id` | 属性测定方法 | 从 method 选择 | 方法选择器 | 可选;若填写,属性值录入按该方法解释 |
| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选 |
| `scale_id` | 值标尺/单位/有效值范围 | 从 scale 选择 | 标尺选择器 | 可选;若填写,属性值必须按 scale 校验 |
| `trait_id` | 关联性状 | 从 trait 选择 | 性状选择器 | 可选 |
| `attribute_category` | 属性分类,如 Morphological、Genetic、Quality | 用户选择/录入 | 下拉框/文本框 | 可选;建议字典化 |
| `code` | 属性代码,便于导入导出 | 用户录入 | 文本框 | 可选;建议唯一 |
| `datatype` | 属性值数据类型,如 text、numeric、date、boolean、categorical | 用户选择 | 下拉框 | 必填 |
| `description` | 属性解释 | 用户录入 | 多行文本 | 可选 |
| `name` | 属性名称 | 用户录入 | 文本框 | 必填;作为属性选择器展示名称 |
| `pui` | 属性永久标识 | 用户录入 | 文本框/URL 输入框 | 可选;建议唯一 |
| `uri` | 属性 URI | 用户录入 | URL 输入框 | 可选;校验 URL |
## 录入建议
- 属性定义页面本质是“属性字典配置”。
- 前端应根据 `datatype` 动态决定属性值录入控件:
- numeric数字输入框
- categorical下拉框
- date日期选择器
- boolean开关
- text文本框。
- 若绑定了 `scale_id`,则优先按 scale 的单位、上下限、有效分类值校验。
---

View File

@@ -0,0 +1,27 @@
# 02 Germplasm / Seed - germplasm_attribute_value 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
属性值是“某个 germplasm 在某个属性上的实际取值”。它不是属性定义,也不是 observation。它适合记录材料相对稳定、不强依赖环境的特征。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ----------------- | ------------------------------ | -------------------------------------- | ---------- | ---------------------------------------- |
| `id` | 属性值主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `determined_date` | 属性值被测定或确认的日期 | 用户录入 | 日期选择器 | 可选;多次测定时必须填写以区分记录 |
| `value` | 某个材料在某个属性上的实际取值 | 用户录入 | 动态控件 | 必填;按 attribute datatype / scale 校验 |
| `attribute_id` | 属性定义 | 从 germplasm_attribute_definition 选择 | 属性选择器 | 必选;必须存在 |
| `germplasm_id` | 所属材料 | 从 germplasm 选择 | 材料选择器 | 必选;必须存在 |
## 录入建议
- 推荐嵌入 Germplasm 详情页的“属性值”Tab。
- 支持批量导入,模板列建议为:`germplasm_id/germplasm_name``attribute_code/attribute_name``value``determined_date`
- 同一个 germplasm + attribute 可以允许多次测定,但页面必须显示测定日期、来源和最新值标记。
---

View File

@@ -0,0 +1,25 @@
# 02 Germplasm / Seed - crossing_project 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`crossing_project` 是某个育种项目下的一组杂交任务集合。它不是一次杂交而是一个杂交工作台例如“2026 抗倒伏杂交项目”。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------- | ------------------ | ------------------ | ---------- | ----------------------------- |
| `id` | 杂交项目主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `description` | 杂交项目说明 | 用户录入 | 多行文本 | 可选 |
| `name` | 杂交项目名称 | 用户录入 | 文本框 | 必填;同一 program 下建议唯一 |
| `program_id` | 所属育种项目 | 从 program 选择 | 项目选择器 | 必选;必须存在 |
## 页面与交互
- 详情页应展示计划杂交、实际杂交、潜在亲本、后代材料、产生的 seed lot。
- 创建 cross 时应自动带入 crossing_project_id。
---

View File

@@ -0,0 +1,30 @@
# 02 Germplasm / Seed - cross_entity 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`cross_entity` 统一承载计划杂交和实际杂交。通过 `planned` 字段区分计划与实际,通过 `planned_cross_id` 指向来源计划。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | ------------------------------------------- | ------------------------ | -------------- | --------------------------------------- |
| `id` | cross 主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `cross_type` | 杂交类型,如 biparental、self、backcross 等 | 用户选择 | 下拉框 | 可选;值来自枚举字典 |
| `name` | cross 名称,如 A × B、A/B、Cross-2026-001 | 用户录入或自动生成 | 文本框 | 必填;同一 crossing project 下建议唯一 |
| `planned` | 是否为计划杂交 | 页面根据入口自动设置 | 开关/分段控件 | 必填;计划杂交为 true实际杂交为 false |
| `status` | 状态,如 TODO、DONE、SKIPPED、FAILED | 用户选择/系统更新 | 下拉框 | 可选;计划杂交常用 TODO/DONE/SKIPPED |
| `crossing_project_id` | 所属杂交项目 | 从 crossing_project 选择 | 杂交项目选择器 | 必选 |
| `planned_cross_id` | 实际杂交来源的计划杂交 | 从 cross_entity 选择 | Cross 选择器 | 可选;不能选择自己;实际杂交建议填写 |
## 录入建议
- 页面上分成“计划杂交”和“实际杂交”两个入口,但后端都保存到 `cross_entity`
- 创建计划杂交时:`planned=true``planned_cross_id=null`
- 完成实际杂交时:`planned=false``planned_cross_id=原计划杂交 id`
- 亲本不要直接塞在 cross 主表字段中,应通过 `cross_parent` 维护,便于支持多亲本和 observation_unit 亲本来源。
---

View File

@@ -0,0 +1,26 @@
# 02 Germplasm / Seed - cross_parent 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`cross_parent` 表示某个 cross 的亲本。亲本可以来自 `germplasm`,也可以来自某个 `observation_unit`,例如田间某一株实际被选作父本/母本。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | --------------------------------------------------- | ---------------------------------- | ----------------- | ------------------------------------- |
| `id` | 亲本记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `parent_type` | 亲本角色,如 MALE、FEMALE、SELF、POPULATION、CLONAL | 用户选择 | 下拉框 | 必填;使用枚举 |
| `cross_id` | 所属 cross | 从 cross_entity 选择或由详情页带入 | Cross 选择器/隐藏 | 必选 |
| `crossing_project_id` | 所属 crossing project | 由 cross 自动带出 | 只读/隐藏 | 可选;如填写必须与 cross 一致 |
| `germplasm_id` | 亲本材料 | 从 germplasm 选择 | 材料选择器 | 与 `observation_unit_id` 至少一个必填 |
| `observation_unit_id` | 亲本观测单元 | 从 observation_unit 选择 | 观测单元选择器 | 与 `germplasm_id` 至少一个必填 |
## 录入建议
- 在 Cross 详情页内嵌“亲本列表”。
- 常见快捷录入Parent1 / Parent2。
- 对于田间选株杂交,优先记录 observation_unit_id同时可带出 germplasm_id保证可追溯到具体植株。
---

View File

@@ -0,0 +1,28 @@
# 02 Germplasm / Seed - pedigree_node 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`pedigree_node` 是系谱图中的节点,通常对应一个 germplasm。它用于描述材料在系谱树中的位置不等同于一次杂交记录。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | --------------------- | ------------------------ | -------------- | ----------------------------------------- |
| `id` | 系谱节点主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `crossing_year` | 亲本最初杂交年份 | 用户录入 | 年份选择器 | 可选;四位年份 |
| `family_code` | 家系编号 | 用户录入 | 文本框 | 可选;同一 crossing_project 下建议唯一 |
| `pedigree_string` | 系谱字符串,如 A/B//C | 用户录入/系统生成 | 文本框 | 可选;建议支持 Purdy notation |
| `crossing_project_id` | 产生该节点的杂交项目 | 从 crossing_project 选择 | 杂交项目选择器 | 可选 |
| `germplasm_id` | 该系谱节点对应的材料 | 从 germplasm 选择 | 材料选择器 | 建议必填;同一 germplasm 不建议重复建节点 |
## 录入建议
- Germplasm 详情页提供“系谱”Tab。
- 支持两种维护方式:树图拖拽维护、表格维护节点和边。
- 如果 cross 完成后产生后代 germplasm应自动或半自动创建 pedigree_node。
---

View File

@@ -0,0 +1,27 @@
# 02 Germplasm / Seed - pedigree_edge 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`pedigree_edge` 是系谱图中的边,描述节点之间的父子、同胞等关系。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------- | ------------------------------------------ | --------------------- | ---------- | ------------------------------------ |
| `id` | 系谱边主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `edge_type` | 边类型,如 parent、child、sibling | 用户选择 | 下拉框 | 必填 |
| `parent_type` | 如果是亲本关系,标识 MALE、FEMALE、SELF 等 | 用户选择 | 下拉框 | 可选;当 edge_type=parent 时建议必填 |
| `connceted_node_id` | 被连接节点 | 从 pedigree_node 选择 | 节点选择器 | 必选 |
| `this_node_id` | 当前节点 | 从 pedigree_node 选择 | 节点选择器 | 必选;不能等于 connected node |
## 录入建议
- 前端展示时不要暴露“this_node_id / connected_node_id”这种技术词应该显示为“当前材料”和“关联材料”。
- 添加父本/母本时,系统自动创建 edge_type=parent。
- 需要避免明显循环,例如 A 是 B 的父本,同时 B 又是 A 的父本。
---

View File

@@ -0,0 +1,33 @@
# 02 Germplasm / Seed - seed_lot 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`seed_lot` 是实物库存批次,描述某一批种子当前有多少、放在哪里、属于哪个项目。它不是 germplasm 身份;同一个 germplasm 可以有多个 seed_lot。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------- | ---------------------------------------------------- | --------------------- | ------------------- | ----------------------------- |
| `id` | SeedLot 主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `amount` | 当前库存数量,可以是粒数、重量、株数等 | 用户录入/交易自动更新 | 数字输入框 | 必填;非负;交易后自动更新 |
| `created_date` | 批次创建时间 | 系统默认,可导入 | 日期时间选择器/只读 | 默认当前时间 |
| `description` | 批次说明 | 用户录入 | 多行文本 | 可选 |
| `last_updated` | 最后更新时间,包含交易变化 | 系统自动更新 | 只读 | 不允许手动改 |
| `name` | 批次名称,如 华占-2026-荆门-扩繁批 | 用户录入或自动生成 | 文本框 | 必填;同一 program 下建议唯一 |
| `source_collection` | 原始来源 collection如野外采集、nursery、种质库集合 | 用户录入 | 文本框 | 可选 |
| `storage_location` | 具体库位描述,如 冰箱A-2层-盒03 | 用户录入 | 文本框 | 可选 |
| `units` | 数量单位,如 seeds、g、kg、plants | 用户选择 | 下拉框/文本框 | 必填;交易单位需一致或可换算 |
| `location_id` | 库存所在地点 | 从 location 选择 | 地点选择器 | 可选 |
| `program_id` | 所属项目 | 从 program 选择 | 项目选择器 | 可选;用于项目库存筛选 |
## 录入建议
- 创建 seed_lot 后必须进入“批次组成”Tab至少录入一条 `seed_lot_content_mixture`
- 普通用户不要直接编辑 amountamount 应通过入库、出库、转移、分装等交易动作更新。
- 支持库存状态:充足、低库存、耗尽,可由 amount 和阈值计算。
---

View File

@@ -0,0 +1,26 @@
# 02 Germplasm / Seed - seed_lot_content_mixture 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`seed_lot_content_mixture` 描述一个 seed_lot 由哪些材料或 cross 组成。单一材料批次也需要有一条组成记录,比例为 100%。混合批次则多条记录占比合计为 100%。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------------- | ----------------------------- | -------------------- | ------------------- | ---------------------------------------- |
| `id` | 批次组成主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `mixture_percentage` | 该材料或 cross 在批次中的占比 | 用户录入 | 百分比输入框 | 0 到 100同一 seed lot 总和建议等于 100 |
| `cross_id` | 来源 cross | 从 cross_entity 选择 | Cross 选择器 | 与 `germplasm_id` 至少一个必填 |
| `germplasm_id` | 来源 germplasm | 从 germplasm 选择 | 材料选择器 | 与 `cross_id` 至少一个必填 |
| `seed_lot_id` | 所属 seed lot | 由详情页带入或选择 | SeedLot 选择器/隐藏 | 必选 |
## 录入建议
- 新建 seed_lot 时,如果用户选择了单个 germplasm系统自动生成一条 mixture`germplasm_id=所选材料``mixture_percentage=100`
- 如果来源是某次杂交产生的种子,优先填写 `cross_id`
- 如果既能追溯 cross 又能追溯 germplasm可按系统设计决定是否允许同时填写若允许同时展示“来源杂交”和“当前材料身份”。
---

View File

@@ -0,0 +1,42 @@
# 02 Germplasm / Seed - seed_lot_transaction 表录入说明
来源:`docs/requirements/02-germplasm-seed-entry-requirements.md`
## 录入目标
`seed_lot_transaction` 记录库存变化。它不应该由用户像普通表单一样手动维护,而应该由“入库、出库、转移、分装、合并、消耗”等业务动作自动生成。
## 字段录入
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------ | -------------------------------------------- | ----------------------- | ------------------- | ------------------------------------------------- |
| `id` | 流转记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `amount` | 流转数量 | 用户在业务动作中录入 | 数字输入框 | 必填;大于 0 |
| `description` | 流转说明,如用于某 study、分装原因、报废原因 | 用户录入 | 多行文本 | 可选;出库/报废建议必填 |
| `timestamp` | 流转发生时间 | 默认当前时间,可调整 | 日期时间选择器 | 必填 |
| `units` | 流转单位 | 默认继承 seed_lot.units | 下拉框/只读 | 必填;需与 seed_lot 单位一致或有换算关系 |
| `from_seed_lot_id` | 来源批次 | 按动作自动设置 | SeedLot 选择器/隐藏 | 与 `to_seed_lot_id` 至少一个存在 |
| `to_seed_lot_id` | 目标批次 | 按动作自动设置 | SeedLot 选择器/隐藏 | 与 `from_seed_lot_id` 至少一个存在;不能等于 from |
## 业务动作映射
| 动作 | from_seed_lot_id | to_seed_lot_id | amount 对库存影响 |
| --------- | ---------------- | -------------- | -------------------------------------------- |
| 入库 | 空 | 目标批次 | 目标批次增加 |
| 出库 | 来源批次 | 空 | 来源批次减少 |
| 转移 | 来源批次 | 目标批次 | 来源减少,目标增加 |
| 分装 | 原批次 | 新批次 | 原批次减少,新批次增加 |
| 合并 | 多个来源批次 | 目标批次 | 来源减少,目标增加;可能生成多条 transaction |
| 消耗/报废 | 来源批次 | 空 | 来源减少,并记录原因 |
## 验收点
1. amount 必须大于 0。
2. 出库/消耗时amount 不得超过来源批次当前库存。
3. from 和 to 不能相同。
4. transaction 创建后应自动更新 seed_lot.amount 和 last_updated。
5. 已生成的 transaction 原则上不允许随意修改;如需纠错,应通过反向交易或更正记录处理。
---

View File

@@ -0,0 +1,48 @@
# 03 Genotyping - plate 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`plate` 表示样本板,是批量送检和测序前的样本组织单元。一个 plate 可以包含多个 `sample`,也可以关联 vendor submission。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `plate` |
| Java 实体 | `PlateEntity` |
| 前置依赖 | `program``trial``study`,可选 `plate_submission` |
| 下游引用 | `sample` |
| API | `/brapi/v2/plates` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | plate 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `plate_name` | 样本板名称 | 用户录入 | 建议必填,同一 study 下建议唯一 |
| `plate_barcode` | 样本板条码 | 扫码/手填 | 建议唯一 |
| `client_plate_db_id` | 客户侧 plate ID | 用户录入/导入 | 可选 |
| `client_plate_barcode` | 客户侧 plate 条码 | 用户录入/导入 | 可选 |
| `plate_format` | plate 规格,如 96/384 孔板 | 枚举选择 | 可选;与样本孔位校验联动 |
| `sample_type` | plate 中样本类型 | 枚举选择 | 可选 |
| `sample_submission_format` | 提交给 vendor 的样本板格式 | 枚举选择 | 可选 |
| `status_time_stamp` | 状态时间 | 系统写入或导入 | 可选 |
| `program_id` | 所属项目 | 项目选择器 | 可选;若填需存在 |
| `trial_id` | 所属 trial | Trial 选择器 | 可选;若填需存在 |
| `study_id` | 所属 study | Study 选择器 | 可选;若填需存在 |
| `submission_id` | vendor plate submission | Vendor submission 选择器 | 可选 |
## 页面与交互
- Plate 列表页支持按 program、trial、study、barcode、plateName 查询。
- 详情页展示 plate 基本信息和下属 sample 列表。
- 新增 sample 时如果从 plate 详情进入,默认带出 `plate_id`
## 关键校验
1. 如果录入 `program_id/trial_id/study_id`,需要校验 Core 上下文一致性。
2. 如果使用 `plate_format`,样本 `plate_row/plate_column/well` 不能超出规格。
3. 已有关联 `sample` 的 plate 删除前必须提示,通常只允许停用或先迁移样本。

View File

@@ -0,0 +1,56 @@
# 03 Genotyping - sample 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`sample` 是 Genotyping 流程的样本入口,表示送检样本或测序样本。它可以挂到 `plate`,也可以直接关联 `observation_unit`,并冗余保存 program/trial/study 方便查询。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `sample` |
| Java 实体 | `SampleEntity` |
| 前置依赖 | `plate``observation_unit``program``trial``study`,可选 `germplasm_taxon` |
| 下游引用 | `callset` |
| API | `/brapi/v2/samples` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | sample 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `sample_name` | 样本名称 | 用户录入/批量导入 | 建议必填 |
| `sample_barcode` | 样本条码 | 扫码/手填 | 建议唯一 |
| `samplepui` | 样本永久标识 | 用户录入/导入 | 可选,建议唯一 |
| `sample_description` | 样本说明 | 用户录入 | 可选 |
| `sample_type` | 样本类型 | 下拉/文本 | 可选 |
| `tissue_type` | 组织类型 | 下拉/文本 | 可选 |
| `sample_timestamp` | 取样时间 | 日期时间选择器 | 可选 |
| `taken_by` | 取样人 | 文本/人员选择 | 可选 |
| `sample_group_db_id` | 样本分组 ID | 文本/导入 | 可选 |
| `concentration` | 样本浓度 | 文本/数值 | 可选,建议带单位 |
| `volume` | 样本体积 | 文本/数值 | 可选,建议带单位 |
| `plate_id` | 所在样本板 | Plate 选择器 | 可选,若填需存在 |
| `plate_row` | 板行 | 文本 | 与 plate format 联动校验 |
| `plate_column` | 板列 | 数字输入 | 与 plate format 联动校验 |
| `well` | 孔位,如 A01 | 文本/自动生成 | 同一 plate 内建议唯一 |
| `observation_unit_id` | 对应观测单元 | ObservationUnit 选择器 | 可选,若填需存在 |
| `program_id` | 所属项目 | 项目选择器 | 可选,若填需存在 |
| `trial_id` | 所属 trial | Trial 选择器 | 可选,若填需存在 |
| `study_id` | 所属 study | Study 选择器 | 可选,若填需存在 |
| `taxon_id_id` | germplasm taxon | Taxon 选择器 | 可选 |
## 页面与交互
- Sample 列表页支持按 sampleName、barcode、plate、study、trial、program、observationUnit 查询。
- 从 Study 工作台创建 sample 时默认继承 `study_id/trial_id/program_id`
- 从 ObservationUnit 详情创建 sample 时默认带出 `observation_unit_id`
## 关键校验
1. `sample` 若关联 `observation_unit`,需要检查 observation unit 所属 study 与 sample 的 study 一致。
2. 同一 plate 内 `well` 不应重复。
3. 删除 sample 前必须检查是否已有 `callset`

View File

@@ -0,0 +1,44 @@
# 03 Genotyping - reference_set 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`reference_set` 表示参考基因组集合,是 reference、variantset、variant 的参考侧上游。它可以关联来源 germplasm。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `reference_set` |
| Java 实体 | `ReferenceSetEntity` |
| 前置依赖 | 可选 `germplasm` |
| 下游引用 | `reference``variantset``variant` |
| API | `/brapi/v2/referencesets` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | reference set 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `reference_set_name` | 参考集合名称 | 用户录入 | 建议必填 |
| `assemblypui` | assembly 永久标识 | 用户录入/导入 | 可选,建议唯一 |
| `description` | 参考集合说明 | 多行文本 | 可选 |
| `is_derived` | 是否派生参考 | 开关 | 可选 |
| `md5checksum` | 校验值 | 文本 | 可选,格式提示 |
| `sourceuri` | 来源 URI | URL 输入 | 可选URL 格式校验 |
| `species_ontology_term` | 物种本体 term | 文本/本体选择器 | 可选 |
| `species_ontology_termuri` | 物种本体 URI | URL 输入 | 可选URL 格式校验 |
| `source_germplasm_id` | 来源 germplasm | Germplasm 选择器 | 可选,若填需存在 |
## 页面与交互
- 列表页展示 referenceSetName、assemblyPUI、species、sourceGermplasm、reference 数、variantset 数。
- 详情页展示 reference、variantset、variant 入口。
## 关键校验
1. 删除 reference_set 前检查 `reference``variantset``variant` 引用。
2. 若填写 `source_germplasm_id`,必须引用已存在 germplasm。
3. md5 checksum 建议做格式提示,不强行阻断历史数据。

View File

@@ -0,0 +1,40 @@
# 03 Genotyping - reference 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`reference` 表示具体参考序列,例如 chromosome、contig 或 scaffold。它属于一个 `reference_set`,下游可以分页保存序列片段。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `reference` |
| Java 实体 | `ReferenceEntity` |
| 前置依赖 | `reference_set` |
| 下游引用 | `reference_bases` |
| API | `/brapi/v2/references` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | reference 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `reference_name` | 参考序列名称 | 用户录入 | 建议必填 |
| `reference_set_id` | 所属 reference_set | ReferenceSet 选择器 | 必选,必须存在 |
| `length` | 序列长度 | 数字输入/导入 | 可选,非负 |
| `md5checksum` | 序列校验值 | 文本 | 可选 |
| `source_divergence` | 与来源差异度 | 数字输入 | 可选 |
## 页面与交互
- Reference 列表页支持按 referenceSet、referenceName 查询。
- Reference 详情页展示 reference_bases 分页和长度、checksum 信息。
## 关键校验
1. `reference_set_id` 必须存在。
2. 删除 reference 前检查 `reference_bases`
3. 若维护 `reference_bases`,建议校验分页总长度与 `length` 的一致性。

View File

@@ -0,0 +1,37 @@
# 03 Genotyping - reference_bases 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`reference_bases` 保存 reference 的序列片段或分页内容。它是参考序列的明细表,通常通过导入或后端任务写入。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `reference_bases` |
| Java 实体 | `ReferenceBasesPageEntity` |
| 前置依赖 | `reference` |
| 下游引用 | 无 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | reference bases 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `reference_id` | 所属 reference | Reference 选择器/导入 | 必选,必须存在 |
| `page_number` | 分页序号 | 数字输入/导入 | 同一 reference 内建议唯一 |
| `bases` | 碱基序列片段 | 文本/文件导入 | 最大 2048 字符;建议仅允许 A/C/G/T/N 等字符 |
## 页面与交互
- 一般不单独提供复杂 CRUD可在 Reference 详情页查看或导入。
- 长序列建议走文件导入或异步任务,不建议手工逐页录入。
## 关键校验
1. `reference_id` 必须存在。
2. `bases` 长度不能超过数据库字段限制。
3. 同一 reference 下 `page_number` 不应重复。

View File

@@ -0,0 +1,39 @@
# 03 Genotyping - variantset 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`variantset` 表示一批 variant 的集合通常对应一次测序、芯片、DArTSeq 或某个 study 下的位点集合。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `variantset` |
| Java 实体 | `VariantSetEntity` |
| 前置依赖 | `reference_set``study` |
| 下游引用 | `variant``callset_variant_sets``variantset_analysis``variantset_format` |
| API | `/brapi/v2/variantsets` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | variantset 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `variant_set_name` | 变异集合名称 | 用户录入/导入 | 建议必填 |
| `reference_set_id` | 参考基因组集合 | ReferenceSet 选择器 | 建议必选,若填需存在 |
| `study_id` | 所属 study | Study 选择器 | 可选,若填需存在 |
## 页面与交互
- VariantSet 列表页支持按 referenceSet、study、variantSetName 查询。
- 详情页展示 variants、callsets、analysis、available formats。
- 从 Study 工作台创建时默认带出 `study_id`
## 关键校验
1. `reference_set_id` 与下属 `variant.reference_set_id` 应保持一致。
2. 删除 variantset 前检查 `variant``callset_variant_sets``variantset_analysis``variantset_format`
3. 导入大型 variantset 时建议先建 variantset再异步导入 variants 和 calls。

View File

@@ -0,0 +1,57 @@
# 03 Genotyping - variant 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`variant` 表示具体变异位点,例如 SNP、Indel 或结构变异。它是位点定义,不是某个样本的结果;样本上的 genotype 结果写入 `allele_call`
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `variant` |
| Java 实体 | `VariantEntity` |
| 前置依赖 | `reference_set``variantset` |
| 下游引用 | `allele_call``marker_position` |
| API | `/brapi/v2/variants` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | variant 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `variant_name` | 位点名称/marker 名称 | 用户录入/导入 | 建议必填 |
| `variant_type` | 位点类型,如 SNP/INDEL/SV | 下拉/导入 | 可选,建议枚举 |
| `reference_set_id` | 参考集合 | ReferenceSet 选择器 | 可选,若填需存在 |
| `variant_set_id` | 所属 variantset | VariantSet 选择器 | 建议必选,若填需存在 |
| `reference_bases` | 参考碱基 | 文本/导入 | 可选 |
| `variant_start` | 起始位置 | 数字输入/导入 | 可选,非负 |
| `variant_end` | 结束位置 | 数字输入/导入 | 可选,不能小于 start |
| `svlen` | 结构变异长度 | 数字输入/导入 | 可选 |
| `filters_applied` | 是否做过过滤 | 开关/导入 | 可选 |
| `filters_passed` | 是否通过过滤 | 开关/导入 | 可选 |
| `created` | 创建时间 | 系统写入/导入 | 可选 |
| `updated` | 更新时间 | 系统写入/导入 | 可选 |
## 附属集合字段
| 附属表 | 内容 |
| --- | --- |
| `variant_entity_alternate_bases` | alternateBases 列表 |
| `variant_entity_ciend` | CIEND 区间 |
| `variant_entity_cipos` | CIPOS 区间 |
| `variant_entity_filters_failed` | 未通过的 filter 列表 |
## 页面与交互
- Variant 列表页支持按 variantSet、referenceSet、variantName、variantType 查询。
- 大批量位点建议通过文件导入,不建议普通表单逐条录入。
- 详情页展示 allele_call 数量和 marker_position 入口。
## 关键校验
1. `variant` 是位点定义,不能把样本 genotype 写在本表。
2. `variant_set_id``reference_set_id` 应与所属 variantset 保持一致。
3. 删除 variant 前检查 `allele_call``marker_position` 引用。

View File

@@ -0,0 +1,40 @@
# 03 Genotyping - callset 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`callset` 表示某个 sample 的一组 genotype calls通常对应一个样本在一个或多个 variantset 上的调用集合。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `callset` |
| Java 实体 | `CallSetEntity` |
| 前置依赖 | `sample` |
| 下游引用 | `allele_call``callset_variant_sets` |
| API | `/brapi/v2/callsets` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | callset 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `call_set_name` | 调用集合名称 | 用户录入/导入 | 建议必填,同一 sample 下建议唯一 |
| `sample_id` | 所属 sample | Sample 选择器/导入 | 必选,必须存在 |
| `created` | 创建时间 | 系统写入/导入 | 可选 |
| `updated` | 更新时间 | 系统写入/导入 | 可选 |
## 页面与交互
- CallSet 列表页支持按 sample、variantSet、callSetName 查询。
- 从 Sample 详情创建时默认带出 `sample_id`
- 需要通过 `callset_variant_sets` 绑定参与的 variantset。
## 关键校验
1. `sample_id` 必须存在。
2. 删除 callset 前检查 `allele_call``callset_variant_sets`
3. 如果 callset 绑定多个 variantset查询和导出时要明确当前 variantset 范围。

View File

@@ -0,0 +1,42 @@
# 03 Genotyping - allele_call 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`allele_call` 是最终 genotype 调用结果。业务上它就是 Call一条记录表示某个 sample/callset 在某个 variant 上的 genotype、read depth、likelihood 等结果。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `allele_call` |
| Java 实体 | `CallEntity` |
| 前置依赖 | `callset``variant` |
| 下游引用 | 无 |
| API | `/brapi/v2/calls` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | call 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `call_set_id` | 所属 callset | CallSet 选择器/导入 | 必选,必须存在 |
| `variant_id` | 对应 variant | Variant 选择器/导入 | 必选,必须存在 |
| `genotype` | genotype 值,如 0/1、A/T | 文本/导入 | 建议必填 |
| `read_depth` | 测序深度 | 数字输入/导入 | 可选,非负 |
| `genotype_likelihood` | genotype likelihood | 数字输入/导入 | 可选 |
| `phase_set` | phase set | 文本/导入 | 可选 |
## 页面与交互
- 通常不做逐条手工录入,主要通过 VCF/HapMap/矩阵文件导入。
- 列表页支持按 callset、sample、variantset、variant 查询。
- 在 Variant 详情页可查看该位点下的 calls在 Sample/CallSet 详情页可查看该样本的 calls。
## 关键校验
1. `call_set_id``variant_id` 必须存在。
2. 同一 callset 对同一 variant 不应重复。
3. `allele_call` 不应直接承担 variant 定义字段;位点信息应来自 `variant`

View File

@@ -0,0 +1,45 @@
# 03 Genotyping - genome_map 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`genome_map` 表示遗传图谱,用于组织 linkage group并通过 marker_position 将 variant 放到图谱坐标上。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `genome_map` |
| Java 实体 | `GenomeMapEntity` |
| 前置依赖 | `crop`,可关联 `study` |
| 下游引用 | `linkageGroup` / `linkage_group` |
| API | `/brapi/v2/maps` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | map 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `map_name` | 图谱名称 | 用户录入 | 建议必填 |
| `mappui` | map 永久标识 | 用户录入/导入 | 可选,建议唯一 |
| `crop_id` | 所属作物 | Crop 选择器 | 可选,若填需存在 |
| `scientific_name` | 学名 | 文本 | 可选 |
| `type` | 图谱类型 | 下拉/文本 | 可选 |
| `unit` | 图谱单位,如 cM | 文本/下拉 | 可选 |
| `published_date` | 发表日期 | 日期选择器 | 可选 |
| `documentationurl` | 文档链接 | URL 输入 | 可选URL 格式校验 |
| `comments` | 备注 | 多行文本 | 可选 |
## 页面与交互
- GenomeMap 列表页支持按 crop、mapName、type 查询。
- 详情页展示 linkage groups 和 marker positions。
- 可通过图谱详情批量导入 linkage group 与 marker position。
## 关键校验
1. 删除 genome_map 前检查 linkage group 引用。
2. 若关联 study应检查 study 的 crop 与 map 的 crop 一致。
3. `unit` 应与 marker_position 的 position 语义保持一致。

View File

@@ -0,0 +1,42 @@
# 03 Genotyping - linkageGroup / linkage_group 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 命名注意
架构文档标注 `LinkageGroupEntity` 对应表名是驼峰 `linkageGroup`。当前初始 schema DDL 中实际表名为 `linkage_group`。做 Atlas/Flyway 迁移时,必须先确认目标数据库中的真实表名,避免生成大小写或命名不一致的重复表。
## 录入目标
连锁群是 genome_map 下的分组,例如 chromosome 或 linkage group。`marker_position` 会把 variant 放到具体 linkage group 的位置上。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| Java 实体 | `LinkageGroupEntity` |
| 表 | 架构文档:`linkageGroup`;当前 DDL`linkage_group` |
| 前置依赖 | `genome_map` |
| 下游引用 | `marker_position` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | linkage group 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `genome_map_id` | 所属 genome_map | GenomeMap 选择器 | 必选,必须存在 |
| `linkage_group_name` | 连锁群名称 | 用户录入/导入 | 建议必填,同一 map 下唯一 |
| `max_marker_position` | 最大 marker 位置 | 数字输入/导入 | 可选,非负 |
## 页面与交互
- 通常作为 GenomeMap 详情页的子表维护。
- 支持批量导入 linkage group。
- 详情页展示该 linkage group 下的 marker_position 列表。
## 关键校验
1. `genome_map_id` 必须存在。
2. 同一 genome_map 下 `linkage_group_name` 不应重复。
3. 删除 linkage group 前检查 `marker_position` 引用。

View File

@@ -0,0 +1,39 @@
# 03 Genotyping - marker_position 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`marker_position` 记录 marker/variant 在 linkage group 上的位置,是遗传图谱定位侧与 variant 位点侧的连接表。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `marker_position` |
| Java 实体 | `MarkerPositionEntity` |
| 前置依赖 | `linkageGroup` / `linkage_group``variant` |
| 下游引用 | 无 |
| API | `/brapi/v2/markerpositions` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | marker position 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `linkage_group_id` | 所属 linkage group | LinkageGroup 选择器/导入 | 必选,必须存在 |
| `variant_id` | 对应 variant | Variant 选择器/导入 | 必选,必须存在 |
| `position` | 图谱位置 | 数字输入/导入 | 必填,非负 |
## 页面与交互
- 可在 GenomeMap/LinkageGroup 详情页批量维护。
- 列表页支持按 map、linkageGroup、variant 查询。
- Variant 详情页可展示 marker_position 信息。
## 关键校验
1. `linkage_group_id``variant_id` 必须存在。
2. 同一 linkage group 下同一 variant 不应重复。
3. `position` 不应超过 linkage group 的 `max_marker_position`

View File

@@ -0,0 +1,34 @@
# 03 Genotyping - callset_variant_sets 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`callset_variant_sets``callset``variantset` 的多对多关系表,表示某个样本调用集合覆盖了哪些 variantset。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `callset_variant_sets` |
| 前置依赖 | `callset``variantset` |
| 下游引用 | 查询、导出和 allele matrix 范围过滤 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `call_sets_id` | callset 主键 | CallSet 选择器/导入 | 必选,必须存在 |
| `variant_sets_id` | variantset 主键 | VariantSet 选择器/导入 | 必选,必须存在 |
## 页面与交互
- 通常在 CallSet 详情页或 VariantSet 详情页维护,不建议独立做主菜单。
- 创建 callset 后可选择一个或多个 variantset。
- 大批量导入 calls 时可以由导入任务自动创建关系。
## 关键校验
1. `call_sets_id``variant_sets_id` 必须存在。
2. 同一 callset 与 variantset 关系不应重复。
3. 删除关系不应删除 callset 或 variantset 主数据。

View File

@@ -0,0 +1,45 @@
# 03 Genotyping - variantset_analysis 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`variantset_analysis` 记录 variantset 的分析或 QC 信息,例如分析名称、软件、类型、描述和时间。它是 variantset 的附属明细表。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `variantset_analysis` |
| Java 实体 | `VariantSetAnalysisEntity` |
| 前置依赖 | `variantset` |
| 下游引用 | 分析信息展示 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | analysis 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `variant_set_id` | 所属 variantset | VariantSet 选择器/导入 | 必选,必须存在 |
| `analysis_name` | 分析名称 | 用户录入 | 建议必填 |
| `description` | 分析说明 | 多行文本 | 可选 |
| `type` | 分析类型,如 QC | 下拉/文本 | 可选 |
| `created` | 创建时间 | 日期时间 | 可选 |
| `updated` | 更新时间 | 日期时间 | 可选 |
## 附属集合字段
| 附属表 | 内容 |
| --- | --- |
| `variant_set_analysis_entity_software` | software 列表,如软件名称、版本或 URL |
## 页面与交互
- 在 VariantSet 详情页以 Analysis Tab 维护。
- 支持添加多条分析记录,每条可维护多个 software。
## 关键校验
1. `variant_set_id` 必须存在。
2. 删除 variantset 时需要先处理或级联处理 analysis。
3. `software` 如果是 URL前端可做 URL 格式提示。

View File

@@ -0,0 +1,42 @@
# 03 Genotyping - variantset_format 表录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
## 录入目标
`variantset_format` 记录 variantset 可用的数据格式和文件地址,例如 allele matrix、VCF、HapMap 或 CSV。它用于下载、导出和矩阵读取。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `variantset_format` |
| Java 实体 | `VariantSetAvailableFormatEntity` |
| 前置依赖 | `variantset` |
| 下游引用 | 文件下载、allele matrix 展示、导出 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | format 主键 | 系统生成;导入时可指定 | 必填、唯一 |
| `variant_set_id` | 所属 variantset | VariantSet 选择器/导入 | 必选,必须存在 |
| `data_format` | 数据格式,如 VCF/HapMap/矩阵 | 枚举选择 | 建议必填 |
| `file_format` | MIME/文件格式 | 枚举选择 | 建议必填 |
| `fileurl` | 文件 URL | URL 输入/导入 | 可选URL 格式校验 |
| `expand_homozygotes` | 是否展开纯合位点 | 开关 | 可选 |
| `sep_phased` | phased genotype 分隔符 | 文本 | 可选 |
| `sep_unphased` | unphased genotype 分隔符 | 文本 | 可选 |
| `unknown_string` | 缺失值字符串 | 文本 | 可选 |
## 页面与交互
- 在 VariantSet 详情页以 Formats Tab 维护。
- 支持一组 variantset 配置多个可下载格式。
- 点击 `fileurl` 可下载或预览,对 allele matrix URL 可进入矩阵查看。
## 关键校验
1. `variant_set_id` 必须存在。
2. `fileurl` 如填写需通过 URL 格式校验。
3. 对矩阵格式,`sep_phased/sep_unphased/unknown_string` 会影响解析,应在导入预览时展示。

View File

@@ -0,0 +1,45 @@
# 03 Genotyping 开发录入说明
来源:`docs/architecture/03-genotyping-data-flow.md`
本目录按 Genotyping 模块的数据录入顺序拆分开发说明。主线是:
```text
Core/Phenotyping 上游数据 -> plate / sample
reference_set -> reference -> reference_bases
reference_set + study -> variantset -> variant
sample -> callset
callset + variantset -> callset_variant_sets
callset + variant -> allele_call
genome_map -> linkageGroup/linkage_group -> marker_position -> variant
```
## 文档清单
| 顺序 | 文档 | 表 | 作用 |
| --- | --- | --- | --- |
| 01 | `01-plate.md` | `plate` | 样本板 |
| 02 | `02-sample.md` | `sample` | 送检样本/测序样本 |
| 03 | `03-reference_set.md` | `reference_set` | 参考基因组集合 |
| 04 | `04-reference.md` | `reference` | 参考序列 |
| 05 | `05-reference_bases.md` | `reference_bases` | 参考序列片段/分页 |
| 06 | `06-variantset.md` | `variantset` | 变异集合 |
| 07 | `07-variant.md` | `variant` | 变异位点 |
| 08 | `08-callset.md` | `callset` | 样本调用集合 |
| 09 | `09-allele_call.md` | `allele_call` | genotype 调用结果 |
| 10 | `10-genome_map.md` | `genome_map` | 遗传图谱 |
| 11 | `11-linkage_group.md` | `linkageGroup` / `linkage_group` | 连锁群 |
| 12 | `12-marker_position.md` | `marker_position` | marker/variant 图谱位置 |
| 13 | `13-callset_variant_sets.md` | `callset_variant_sets` | callset 与 variantset 关系 |
| 14 | `14-variantset_analysis.md` | `variantset_analysis` | variantset 分析信息 |
| 15 | `15-variantset_format.md` | `variantset_format` | variantset 可用文件/矩阵格式 |
## 全局注意点
1. `CallEntity` 对应真实业务表是 `allele_call`,不要新建 `call` 表。
2. `CallSetEntity` 对应 `callset`,不是 `call_set`
3. `VariantSetEntity` 对应 `variantset`,不是 `variant_set`
4. 架构文档标注 `LinkageGroupEntity` 的表名为 `linkageGroup`;当前初始 DDL 中表名为 `linkage_group`。做迁移前必须以实际数据库 catalog 为准。
5. `sample` 是 Genotyping 样本入口,向上关联 `plate/observation_unit/study/trial/program`
6. `variant` 是位点定义,`allele_call` 是某个样本/callset 在位点上的结果。
7. `reference_set/reference/reference_bases` 是参考基因组侧;`variantset/variant/callset/allele_call` 是变异和结果侧。

View File

@@ -0,0 +1,37 @@
# 04 Germplasm / Seed - breeding_method 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`breeding_method` 是育种方法字典,用来说明 germplasm 是通过什么方式形成的例如杂交选育、回交、自交系选育、诱变、DH、克隆选择等。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `breeding_method` |
| Java 实体 | `BreedingMethodEntity` |
| 前置依赖 | 无 |
| 下游引用 | `germplasm.breeding_method_id` |
| API | `/brapi/v2/breedingmethods` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | 育种方法主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `name` | 方法名称 | 用户录入 | 必填,建议唯一 |
| `abbreviation` | 方法缩写 | 用户录入 | 可选,建议唯一 |
| `description` | 方法说明 | 多行文本 | 可选 |
## 页面与交互
- 列表页展示 name、abbreviation、description、引用 germplasm 数。
- 新增/编辑页为简单字典表单。
## 关键校验
1. `name` 必填。
2. 已被 `germplasm` 引用时不允许物理删除,只允许停用或提示引用关系。

View File

@@ -0,0 +1,68 @@
# 04 Germplasm / Seed - germplasm 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`germplasm` 是种质主表,保存 accession、PUI、分类、采集、来源、育种方法和展示名称等身份信息。它不是库存批次库存批次由 `seed_lot` 表达。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `germplasm` |
| Java 实体 | `GermplasmEntity` |
| 前置依赖 | `crop`,可选 `breeding_method` |
| 下游引用 | `germplasm_attribute_value``germplasm_donor``germplasm_institute``germplasm_origin``germplasm_synonym``germplasm_taxon``pedigree_node``cross_parent``seed_lot_content_mixture` |
| API | `/brapi/v2/germplasm` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | germplasm 主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `germplasm_name` | 种质名称 | 用户录入/导入 | 建议必填 |
| `default_display_name` | 默认展示名称 | 用户录入/自动生成 | 可选,缺省用 germplasm_name |
| `germplasmpui` | 种质永久标识 | 用户录入/导入 | 可选,建议唯一 |
| `accession_number` | accession 编号 | 用户录入/导入 | 可选,建议唯一 |
| `crop_id` | 所属作物 | Crop 选择器 | 建议必选,若填需存在 |
| `breeding_method_id` | 育种方法 | BreedingMethod 选择器 | 可选,若填需存在 |
| `genus` | 属 | 用户录入 | 可选 |
| `species` | 种 | 用户录入 | 可选 |
| `species_authority` | 种命名人 | 用户录入 | 可选 |
| `subtaxa` | 亚种/变种 | 用户录入 | 可选 |
| `subtaxa_authority` | 亚种命名人 | 用户录入 | 可选 |
| `country_of_origin_code` | 来源国家代码 | 国家选择器 | 可选 |
| `collection` | collection | 用户录入 | 可选 |
| `acquisition_date` | 获取日期 | 日期选择器 | 可选 |
| `acquisition_source_code` | 获取来源编码 | 枚举选择 | 可选 |
| `biological_status_of_accession_code` | 生物状态编码 | 枚举选择 | 可选 |
| `mls_status` | MLS 状态 | 枚举选择 | 可选 |
| `germplasm_preprocessing` | 种质预处理说明 | 文本 | 可选 |
| `seed_source` | 种子来源描述字段 | 文本 | 可选,不等于 seed_lot |
| `seed_source_description` | 种子来源补充说明 | 多行文本 | 可选 |
| `documentationurl` | 文档链接 | URL 输入 | 可选URL 格式校验 |
## 附属信息
| 附属表 | 内容 |
| --- | --- |
| `germplasm_donor` | donor accession、donor institute |
| `germplasm_institute` | host、donor、breeding、collecting、redundant 等机构 |
| `germplasm_origin` | 来源坐标和坐标不确定性 |
| `germplasm_synonym` | 别名及别名类型 |
| `germplasm_taxon` | taxon 来源和值 |
| `germplasm_entity_type_of_germplasm_storage_code` | storage type code 列表 |
## 页面与交互
- Germplasm 列表页支持按 crop、germplasmName、accessionNumber、PUI、synonym 查询。
- 详情页以 Tab 展示 attributes、donors、institutes、origin、synonyms、taxon、pedigree、seed lots、cross parent 记录。
- 从 Study/ObservationUnit 或 SeedLot 进入时应能回到 germplasm 详情。
## 关键校验
1. `germplasm_name` 建议必填,`accession_number``germplasmpui` 建议唯一。
2. 删除 germplasm 前必须检查属性值、seed lot 组成、cross parent、pedigree、sample/taxon 等引用。
3. 不要用 `seed_source` 表达库存;库存必须走 `seed_lot`

View File

@@ -0,0 +1,39 @@
# 04 Germplasm / Seed - germplasm_donor 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`germplasm_donor` 记录某个 germplasm 的 donor accession 和 donor institute 信息,是 germplasm 的附属明细。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `germplasm_donor` |
| Java 实体 | `DonorEntity` |
| 前置依赖 | `germplasm` |
| 下游引用 | MCPD 展示、来源追踪 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | donor 记录主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 |
| `donor_accession_number` | donor accession 编号 | 用户录入 | 可选 |
| `donor_institute_code` | donor 机构代码 | 用户录入/选择 | 可选 |
| `donor_institute_name` | donor 机构名称 | 用户录入/选择 | 可选 |
| `germplasmpui` | donor germplasm PUI | 用户录入 | 可选 |
## 页面与交互
- 在 Germplasm 详情页 Donor Tab 内维护。
- 支持一条 germplasm 维护多个 donor 记录。
## 关键校验
1. `germplasm_id` 必须存在。
2. 同一 germplasm 下 donor accession + institute code 不建议重复。
3. 删除 donor 记录不应删除 germplasm 主数据。

View File

@@ -0,0 +1,38 @@
# 04 Germplasm / Seed - germplasm_institute 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`germplasm_institute` 记录 germplasm 相关机构,包括 host、donor、breeding、collecting、redundant 等类型。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `germplasm_institute` |
| Java 实体 | `GermplasmInstituteEntity` |
| 前置依赖 | `germplasm` |
| 下游引用 | Germplasm MCPD、机构筛选 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | 机构记录主键 | 系统生成 | 必填、唯一 |
| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 |
| `institute_type` | 机构类型 | 枚举选择 | HOST、DONOR、BREEDING、COLLECTING、REDUNDANT |
| `institute_code` | 机构代码 | 用户录入 | 可选 |
| `institute_name` | 机构名称 | 用户录入 | 建议必填 |
| `institute_address` | 机构地址 | 多行文本 | 可选 |
## 页面与交互
- 在 Germplasm 详情页 Institute Tab 内维护。
- 可把 HOST institute 作为 germplasm 主信息摘要展示。
## 关键校验
1. `germplasm_id` 必须存在。
2. 同一 germplasm 下同类型、同 code 的机构不建议重复。
3. 删除 institute 记录不应删除 germplasm 主数据。

View File

@@ -0,0 +1,36 @@
# 04 Germplasm / Seed - germplasm_origin 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`germplasm_origin` 记录 germplasm 的来源地坐标和坐标不确定性,适合表达采集地点或原产地的空间信息。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `germplasm_origin` |
| Java 实体 | `GermplasmOriginEntity` |
| 前置依赖 | `germplasm`,可选 `geojson` |
| 下游引用 | 地理来源展示、MCPD |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | origin 记录主键 | 系统生成 | 必填、唯一 |
| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 |
| `coordinate_uncertainty` | 坐标不确定性 | 文本/数字 | 可选 |
| `coordinates_id` | GeoJSON 坐标对象 | 地图取点/GeoJSON 导入 | 可选,若填需存在 |
## 页面与交互
- 在 Germplasm 详情页 Origin Tab 内维护。
- 支持地图取点和 GeoJSON 查看。
## 关键校验
1. `germplasm_id` 必须存在。
2. 坐标格式需要合法。
3. 删除 origin 记录不应删除 germplasm 主数据。

View File

@@ -0,0 +1,36 @@
# 04 Germplasm / Seed - germplasm_synonym 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`germplasm_synonym` 记录 germplasm 的别名、旧名、商品名或本地名,用于检索和展示。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `germplasm_synonym` |
| Java 实体 | `GermplasmSynonymEntity` |
| 前置依赖 | `germplasm` |
| 下游引用 | Germplasm 搜索、详情展示 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | synonym 记录主键 | 系统生成 | 必填、唯一 |
| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 |
| `synonym` | 别名 | 用户录入 | 必填 |
| `type` | 别名类型 | 下拉/文本 | 可选,如 local、commercial、old name |
## 页面与交互
- 在 Germplasm 详情页 Synonym Tab 内维护。
- Germplasm 列表搜索应支持 synonym 命中。
## 关键校验
1. `germplasm_id` 必须存在。
2. 同一 germplasm 下同一个 synonym 不应重复。
3. 删除 synonym 不应删除 germplasm 主数据。

View File

@@ -0,0 +1,36 @@
# 04 Germplasm / Seed - germplasm_taxon 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`germplasm_taxon` 记录外部 taxon 标识和来源,用于把 germplasm 连接到分类数据库或外部生物分类体系。该表由 `TaxonEntity` 映射。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `germplasm_taxon` |
| Java 实体 | `TaxonEntity` |
| 前置依赖 | `germplasm` |
| 下游引用 | `sample.taxon_id_id`、分类展示 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | taxon 记录主键 | 系统生成 | 必填、唯一 |
| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 |
| `source_name` | taxon 来源名称 | 文本/下拉 | 可选,如 NCBI Taxonomy |
| `taxon_id` | taxon 标识 | 用户录入 | 必填 |
## 页面与交互
- 在 Germplasm 详情页 Taxon Tab 内维护。
- Sample 表可引用 taxon 记录。
## 关键校验
1. `germplasm_id` 必须存在。
2. 同一 source 下 taxon_id 不建议重复。
3. 删除 taxon 前检查 sample 引用。

View File

@@ -0,0 +1,55 @@
# 04 Germplasm / Seed - germplasm_attribute_definition 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`germplasm_attribute_definition` 定义 germplasm 可维护的属性。它继承变量定义体系,可关联 crop、trait、method、scale、ontology。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `germplasm_attribute_definition` |
| Java 实体 | `GermplasmAttributeDefinitionEntity` |
| 前置依赖 | `crop``trait``method``scale``ontology` |
| 下游引用 | `germplasm_attribute_value` |
| API | `/brapi/v2/attributes` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | 属性定义主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `name` | 属性名称 | 用户录入 | 必填 |
| `code` | 属性编码 | 用户录入 | 可选,建议唯一 |
| `pui` | 永久标识 | 用户录入 | 可选,建议唯一 |
| `attribute_category` | 属性分类 | 下拉/文本 | 可选 |
| `datatype` | 数据类型 | 下拉 | 建议必填 |
| `description` | 属性说明 | 多行文本 | 可选 |
| `uri` | 外部 URI | URL 输入 | 可选URL 格式校验 |
| `crop_id` | 作物 | Crop 选择器 | 可选 |
| `trait_id` | 性状 | Trait 选择器 | 可选 |
| `method_id` | 方法 | Method 选择器 | 可选 |
| `scale_id` | 标尺 | Scale 选择器 | 可选 |
| `ontology_id` | 本体 | Ontology 选择器 | 可选 |
| `documentationurl` | 文档链接 | URL 输入 | 可选 |
| `status` | 状态 | 下拉/文本 | 可选 |
| `default_value` | 默认值 | 文本 | 可选 |
| `growth_stage` | 生育期 | 文本 | 可选 |
| `institution` | 机构 | 文本 | 可选 |
| `language` | 语言 | 文本 | 可选 |
| `scientist` | 科学家/维护人 | 文本 | 可选 |
| `submission_timestamp` | 提交时间 | 日期时间 | 可选 |
## 页面与交互
- Attribute Definition 列表页支持按 crop、category、datatype、keyword 查询。
- 创建 attribute value 前必须先有 definition。
## 关键校验
1. `name` 必填。
2. 已被 `germplasm_attribute_value` 引用时不允许物理删除。
3. `datatype` 要与 value 输入控件联动。

View File

@@ -0,0 +1,39 @@
# 04 Germplasm / Seed - germplasm_attribute_value 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`germplasm_attribute_value` 保存某个 germplasm 在某个属性定义上的实际取值。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `germplasm_attribute_value` |
| Java 实体 | `GermplasmAttributeValueEntity` |
| 前置依赖 | `germplasm``germplasm_attribute_definition` |
| 下游引用 | 属性查询、详情展示 |
| API | `/brapi/v2/attributevalues` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | 属性值主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `germplasm_id` | 所属 germplasm | Germplasm 选择器/详情页带出 | 必选,必须存在 |
| `attribute_id` | 属性定义 | Attribute 选择器 | 必选,必须存在 |
| `value` | 属性值 | 根据 definition.datatype 选择控件 | 必填或按业务要求 |
| `determined_date` | 测定日期 | 日期选择器 | 可选 |
## 页面与交互
- 在 Germplasm 详情页 Attributes Tab 维护。
- 批量导入时按 germplasm + attribute 组成矩阵录入更高效。
## 关键校验
1. `germplasm_id``attribute_id` 必须存在。
2. 同一 germplasm 下同一 attribute 不建议重复,若允许多次测定,需要用 determined_date 区分。
3. `value` 必须符合 attribute definition 的 datatype。

View File

@@ -0,0 +1,38 @@
# 04 Germplasm / Seed - crossing_project 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`crossing_project` 表示杂交项目,是 cross、planned cross、cross parent 和 pedigree node 的上游组织维度,通常挂在 Core 的 `program` 下。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `crossing_project` |
| Java 实体 | `CrossingProjectEntity` |
| 前置依赖 | `program` |
| 下游引用 | `cross_entity``cross_parent``pedigree_node` |
| API | `/brapi/v2/crossingprojects` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | crossing project 主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `name` | 杂交项目名称 | 用户录入 | 必填,建议同一 program 下唯一 |
| `description` | 项目说明 | 多行文本 | 可选 |
| `program_id` | 所属 program | Program 选择器 | 必选,必须存在 |
## 页面与交互
- 列表页支持按 program、keyword 查询。
- 详情页展示 crosses、planned crosses、potential parents、pedigree nodes。
## 关键校验
1. `program_id` 必须存在。
2. 删除 crossing_project 前检查 cross、cross_parent、pedigree_node 引用。
3. 如果 program 有 crop创建 cross 时亲本 germplasm 建议与 program crop 一致。

View File

@@ -0,0 +1,50 @@
# 04 Germplasm / Seed - cross_entity 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`cross_entity` 是 Cross 和 PlannedCross 的统一落库表。`plannedcross` 不是独立数据库表,计划杂交通过 `planned=true` 表达;实际杂交可以通过 `planned_cross_id` 关联计划杂交。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `cross_entity` |
| Java 实体 | `CrossEntity` |
| 前置依赖 | `crossing_project`,可选自关联 `cross_entity` |
| 下游引用 | `cross_parent``cross_pollination_event``seed_lot_content_mixture` |
| API | `/brapi/v2/crosses``/brapi/v2/plannedcrosses` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | cross 主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `name` | cross 名称 | 用户录入/自动生成 | 建议必填 |
| `cross_type` | 杂交类型 | 枚举选择 | 可选 |
| `crossing_project_id` | 所属 crossing project | CrossingProject 选择器 | 建议必选,若填需存在 |
| `status` | planned cross 状态 | 枚举选择 | 计划杂交常用 |
| `planned` | 是否为计划杂交 | 开关 | 必填,默认 false |
| `planned_cross_id` | 关联计划杂交 | Cross 选择器 | 可选,必须指向 `planned=true` 的 cross |
## 附属集合字段
| 附属表 | 内容 |
| --- | --- |
| `cross_entity_cross_attributes` | crossAttributes 列表 |
| `cross_parent` | cross 的亲本 |
| `cross_pollination_event` | 授粉事件 |
## 页面与交互
- Cross 列表页展示实际杂交PlannedCross 列表页展示 `planned=true` 数据。
- 新建实际 cross 时可选择 planned cross并继承 planned cross 的亲本作为初始值。
- 详情页展示 parents、pollination events 和 seed lot 组成入口。
## 关键校验
1. `plannedcross` 不新建表,所有 planned cross 走 `cross_entity`
2. `planned_cross_id` 不能指向自己。
3. 删除 cross 前检查 `cross_parent``cross_pollination_event``seed_lot_content_mixture` 引用。

View File

@@ -0,0 +1,39 @@
# 04 Germplasm / Seed - cross_parent 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`cross_parent` 连接 cross 与亲本来源。亲本可以是 germplasm也可以是 observation_unit用来表达实际田间单株或 plot 来源。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `cross_parent` |
| Java 实体 | `CrossParentEntity` |
| 前置依赖 | `cross_entity``crossing_project``germplasm``observation_unit` |
| 下游引用 | 杂交亲本展示和校验 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | cross parent 主键 | 系统生成 | 必填、唯一 |
| `cross_id` | 所属 cross | Cross 选择器/详情页带出 | 必选,必须存在 |
| `crossing_project_id` | 所属 crossing project | 自动带出/选择器 | 建议与 cross 一致 |
| `germplasm_id` | 亲本 germplasm | Germplasm 选择器 | 与 observation_unit 至少填一个 |
| `observation_unit_id` | 亲本 observation unit | ObservationUnit 选择器 | 与 germplasm 至少填一个 |
| `parent_type` | 亲本类型 | 枚举选择 | FEMALE、MALE、SELF、POPULATION、CLONAL 等 |
## 页面与交互
- 在 Cross 或 PlannedCross 详情页 Parents Tab 内维护。
- 创建 cross 时至少添加一个亲本;常见杂交需要 FEMALE 和 MALE。
## 关键校验
1. `cross_id` 必须存在。
2. `germplasm_id``observation_unit_id` 至少填写一个,不建议同时为空。
3. 同一 cross 下相同 parentType + germplasm/observationUnit 不应重复。
4. 如果填 `crossing_project_id`,应与 `cross.crossing_project_id` 一致。

View File

@@ -0,0 +1,37 @@
# 04 Germplasm / Seed - cross_pollination_event 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`cross_pollination_event` 记录实际杂交过程中的授粉事件,包括授粉编号、时间和是否成功。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `cross_pollination_event` |
| Java 实体 | `CrossPollinationEventEntity` |
| 前置依赖 | `cross_entity` |
| 下游引用 | Cross 过程追踪 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | 授粉事件主键 | 系统生成 | 必填、唯一 |
| `cross_id` | 所属 cross | Cross 选择器/详情页带出 | 必选,必须存在 |
| `pollination_number` | 授粉编号 | 用户录入/自动生成 | 可选,建议同一 cross 下唯一 |
| `pollination_successful` | 是否授粉成功 | 开关 | 可选 |
| `pollination_time_stamp` | 授粉时间 | 日期时间选择器 | 可选 |
## 页面与交互
- 在 Cross 详情页 Pollination Events Tab 内维护。
- 可以按授粉时间排序展示。
## 关键校验
1. `cross_id` 必须存在。
2. 同一 cross 下 `pollination_number` 不建议重复。
3. 删除授粉事件不应删除 cross 主数据。

View File

@@ -0,0 +1,40 @@
# 04 Germplasm / Seed - pedigree_node 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`pedigree_node` 表示系谱中的一个节点,一个节点通常关联一个 germplasm并可归属 crossing_project。节点之间的 parent/child/sibling 关系由 `pedigree_edge` 记录。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `pedigree_node` |
| Java 实体 | `PedigreeNodeEntity` |
| 前置依赖 | `germplasm`、可选 `crossing_project` |
| 下游引用 | `pedigree_edge` |
| API | `/brapi/v2/pedigree` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | pedigree node 主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `germplasm_id` | 对应 germplasm | Germplasm 选择器 | 建议必选,若填需存在 |
| `crossing_project_id` | 所属 crossing project | CrossingProject 选择器 | 可选 |
| `crossing_year` | 杂交年份 | 年份输入 | 可选 |
| `family_code` | 家系编号 | 文本 | 可选 |
| `pedigree_string` | 系谱字符串 | 文本 | 可选 |
## 页面与交互
- 可从 Germplasm 详情页进入 Pedigree Tab。
- 支持图谱视图和表格视图。
## 关键校验
1. 同一 germplasm 通常只应有一个 pedigree node。
2. 删除 pedigree_node 前检查 `pedigree_edge` 中 this_node 和 connceted_node 引用。
3. 导入 pedigree 时需要先创建所有节点,再创建边。

View File

@@ -0,0 +1,43 @@
# 04 Germplasm / Seed - pedigree_edge 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`pedigree_edge` 描述两个 pedigree_node 之间的 parent、child 或 sibling 关系,并标注亲本类型。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `pedigree_edge` |
| Java 实体 | `PedigreeEdgeEntity` |
| 前置依赖 | `pedigree_node` |
| 下游引用 | 系谱查询、亲本/后代展示 |
| API | `/brapi/v2/pedigree` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | pedigree edge 主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `this_node_id` | 当前节点 | PedigreeNode 选择器 | 必选,必须存在 |
| `connceted_node_id` | 关联节点 | PedigreeNode 选择器 | 必选,必须存在 |
| `edge_type` | 关系类型 | 枚举选择 | parent、child、sibling |
| `parent_type` | 亲本类型 | 枚举选择 | 可选,如 FEMALE、MALE |
## 命名注意
实体字段是 `conncetedNode`,拼写与 `connected` 不同;开发 DTO、SQL、迁移时要留意当前代码和数据库列名。
## 页面与交互
- 在 Germplasm/Pedigree 图谱视图中维护。
- 创建 parent 关系时可自动补齐反向 child 关系,具体以业务实现为准。
## 关键校验
1. `this_node_id``connceted_node_id` 必须存在。
2. 两个节点不能相同。
3. 同一节点之间同一种 edge_type 不应重复。

View File

@@ -0,0 +1,46 @@
# 04 Germplasm / Seed - seed_lot 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`seed_lot` 是真实库存批次,描述某一批种子当前数量、单位、库位和项目归属。同一个 germplasm 可以有多个 seed lot。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `seed_lot` |
| Java 实体 | `SeedLotEntity` |
| 前置依赖 | 可选 `location``program` |
| 下游引用 | `seed_lot_content_mixture``seed_lot_transaction` |
| API | `/brapi/v2/seedlots` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | seed lot 主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `name` | 批次名称 | 用户录入/自动生成 | 必填,建议同一 program 下唯一 |
| `amount` | 当前库存数量 | 交易自动更新/用户录入初始值 | 非负 |
| `units` | 数量单位 | 下拉/文本 | 建议必填 |
| `created_date` | 创建时间 | 系统写入/导入 | 可选 |
| `last_updated` | 最后更新时间 | 系统更新 | 不建议手填 |
| `description` | 批次说明 | 多行文本 | 可选 |
| `source_collection` | 来源 collection | 文本 | 可选 |
| `storage_location` | 具体库位 | 文本 | 可选 |
| `location_id` | 所在地点/库位 | Location 选择器 | 可选,若填需存在 |
| `program_id` | 所属项目 | Program 选择器 | 可选,若填需存在 |
## 页面与交互
- SeedLot 列表页支持按 program、location、name、库存状态查询。
- 详情页展示 content mixture 和 transactions。
- 创建 seed lot 后建议至少维护一条 `seed_lot_content_mixture`
## 关键校验
1. `amount` 不允许为负。
2. 普通用户不应直接编辑 amount库存变化应通过 `seed_lot_transaction`
3. 删除 seed lot 前检查组成明细和交易引用。

View File

@@ -0,0 +1,38 @@
# 04 Germplasm / Seed - seed_lot_content_mixture 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`seed_lot_content_mixture` 表示 seed lot 的组成明细。它可以把 seed lot 连接到 germplasm也可以连接到 cross_entity适合表达混合批次或杂交产生的批次。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `seed_lot_content_mixture` |
| Java 实体 | `SeedLotContentMixtureEntity` |
| 前置依赖 | `seed_lot`、可选 `germplasm`、可选 `cross_entity` |
| 下游引用 | Seed lot 组成展示 |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | 组成明细主键 | 系统生成 | 必填、唯一 |
| `seed_lot_id` | 所属 seed lot | SeedLot 选择器/详情页带出 | 必选,必须存在 |
| `germplasm_id` | 组成 germplasm | Germplasm 选择器 | 与 cross_id 至少填一个 |
| `cross_id` | 组成来源 cross | Cross 选择器 | 与 germplasm_id 至少填一个 |
| `mixture_percentage` | 组成比例 | 数字输入 | 0-100 |
## 页面与交互
- 在 SeedLot 详情页 Content Mixture Tab 内维护。
- 支持多个组成明细,合计比例可展示为进度或校验提示。
## 关键校验
1. `seed_lot_id` 必须存在。
2. `germplasm_id``cross_id` 至少填写一个。
3. 同一 seed lot 下 mixture_percentage 合计建议为 100。
4. 删除组成明细不应删除 seed lot、germplasm 或 cross 主数据。

View File

@@ -0,0 +1,43 @@
# 04 Germplasm / Seed - seed_lot_transaction 表录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
## 录入目标
`seed_lot_transaction` 记录 seed lot 之间的库存流转,例如入库、出库、转移、分装、合并或消耗。它表达 from seed lot 到 to seed lot 的数量变化。
## 上下游关系
| 类型 | 内容 |
| --- | --- |
| 表 | `seed_lot_transaction` |
| Java 实体 | `SeedLotTransactionEntity` |
| 前置依赖 | `from_seed_lot``to_seed_lot` |
| 下游引用 | 库存追踪、库存数量更新 |
| API | `/brapi/v2/seedlots/transactions``/brapi/v2/seedlots/{seedLotDbId}/transactions` |
## 字段录入
| 字段 | 业务意义 | 录入方式 | 校验规则 |
| --- | --- | --- | --- |
| `id` | transaction 主键 | 系统生成;导入可指定 | 必填、唯一 |
| `auth_user_id` | 数据所属用户 | 登录上下文自动写入 | 不允许前端手填 |
| `from_seed_lot_id` | 来源 seed lot | SeedLot 选择器 | 入库场景可为空,若填需存在 |
| `to_seed_lot_id` | 目标 seed lot | SeedLot 选择器 | 出库/消耗场景可为空,若填需存在 |
| `amount` | 流转数量 | 数字输入 | 必填,正数 |
| `units` | 数量单位 | 下拉/文本 | 必填,应与批次单位一致或可换算 |
| `timestamp` | 流转时间 | 日期时间选择器 | 默认当前时间 |
| `description` | 流转说明 | 多行文本 | 可选 |
## 页面与交互
- 在 SeedLot 详情页 Transactions Tab 展示相关流转。
- 新增交易后应更新 from/to seed lot 的 amount 和 last_updated。
- 支持按时间、from/to seed lot、program、location 查询。
## 关键校验
1. `from_seed_lot_id``to_seed_lot_id` 至少填写一个。
2. `from_seed_lot_id` 不能等于 `to_seed_lot_id`
3. 出库或转移时,来源批次数量不能被扣成负数。
4. transaction 是业务动作痕迹,原则上不允许随意物理删除。

View File

@@ -0,0 +1,46 @@
# 04 Germplasm / Seed 开发录入说明
来源:`docs/architecture/04-germplasm-seed-data-flow.md`
本目录按 Germplasm / Seed 模块的数据录入顺序拆分开发说明。主线是:
```text
breeding_method -> germplasm
germplasm_attribute_definition -> germplasm_attribute_value
program -> crossing_project -> cross_entity -> cross_parent
germplasm / cross_entity -> seed_lot_content_mixture -> seed_lot
seed_lot -> seed_lot_transaction
germplasm -> pedigree_node -> pedigree_edge
```
## 文档清单
| 顺序 | 文档 | 表 | 作用 |
| --- | --- | --- | --- |
| 01 | `01-breeding_method.md` | `breeding_method` | 育种方法字典 |
| 02 | `02-germplasm.md` | `germplasm` | 种质主表 |
| 03 | `03-germplasm_donor.md` | `germplasm_donor` | donor 信息 |
| 04 | `04-germplasm_institute.md` | `germplasm_institute` | 机构信息 |
| 05 | `05-germplasm_origin.md` | `germplasm_origin` | 来源地/坐标 |
| 06 | `06-germplasm_synonym.md` | `germplasm_synonym` | 别名 |
| 07 | `07-germplasm_taxon.md` | `germplasm_taxon` | taxon 标识 |
| 08 | `08-germplasm_attribute_definition.md` | `germplasm_attribute_definition` | 属性定义 |
| 09 | `09-germplasm_attribute_value.md` | `germplasm_attribute_value` | 属性值 |
| 10 | `10-crossing_project.md` | `crossing_project` | 杂交项目 |
| 11 | `11-cross_entity.md` | `cross_entity` | Cross / PlannedCross |
| 12 | `12-cross_parent.md` | `cross_parent` | 杂交亲本 |
| 13 | `13-cross_pollination_event.md` | `cross_pollination_event` | 授粉事件 |
| 14 | `14-pedigree_node.md` | `pedigree_node` | 系谱节点 |
| 15 | `15-pedigree_edge.md` | `pedigree_edge` | 系谱边 |
| 16 | `16-seed_lot.md` | `seed_lot` | 种子库存批次 |
| 17 | `17-seed_lot_content_mixture.md` | `seed_lot_content_mixture` | 批次组成 |
| 18 | `18-seed_lot_transaction.md` | `seed_lot_transaction` | 批次流转 |
## 全局注意点
1. `plannedcross` 没有独立数据库表,统一落在 `cross_entity`,通过 `planned``planned_cross_id` 自关联表达。
2. `germplasm.seedSource` / `seedSourceDescription` 是种质来源描述,不等于真实库存批次。
3. 真正表达库存的是 `seed_lot`,批次与种质或杂交来源的关系在 `seed_lot_content_mixture`
4. `seed_lot_transaction` 表达 seed lot 到 seed lot 的流转,不直接表达 seed lot 到 germplasm。
5. 属性定义和值要分开:`germplasm_attribute_definition` 定义“可填什么”,`germplasm_attribute_value` 记录“某个 germplasm 填了什么”。
6. 系谱关系用 `pedigree_node` / `pedigree_edge`;杂交流程用 `cross_entity` / `cross_parent`,两条线都可以回到 `germplasm`

533
docs/dev/backend/auth.md Normal file
View File

@@ -0,0 +1,533 @@
# 后端登录注册接口说明
本文档描述 `common-br-api` 项目中用户认证注册、登录、Token 校验)的后端实现,便于前后端联调与部署排查。
---
## 1. 总览
| 项目 | 说明 |
|------|------|
| 路由前缀 | `/auth` |
| 框架 | FastAPI |
| 用户表 | `sys_users` |
| 会话表 | `user_sessions` |
| 密码算法 | PBKDF2-HMAC-SHA256210000 次迭代) |
| Token 形式 | 自实现 JWT 风格 Bearer TokenHS256 签名) |
| 默认有效期 | 24 小时1440 分钟) |
**当前已实现的 HTTP 接口:**
| 方法 | 路径 | 说明 |
|------|------|------|
| `POST` | `/auth/register` | 注册并自动登录 |
| `POST` | `/auth/login` | 账号密码登录 |
**尚未实现的接口(前端 UI 可能已预留):**
- `/auth/logout`(登出 / 服务端吊销会话)
- `/auth/refresh`(刷新 Token
- 手机号 / 短信验证码登录
---
## 2. 代码结构
```
app/
├── api/
│ └── auth.py # 注册、登录、get_current_user 依赖
├── core/
│ └── security.py # 密码哈希、Token 签发、Token 哈希
├── models/
│ └── auth.py # User、UserSession ORM 模型
├── config/
│ └── settings.py # JWT 密钥、过期时间等配置
└── main.py # 挂载 auth 路由
migrations/versions/
└── 7c91a64f2b2e_create_auth_tables.py # 建表迁移
```
路由挂载(`app/main.py`
```python
app.include_router(auth_router) # 无前缀叠加router 自身 prefix="/auth"
```
因此完整 URL 为:
```text
http://<host>:8000/auth/register
http://<host>:8000/auth/login
```
经 Next.js 代理后(开发环境)也可访问:
```text
http://localhost:3000/auth/register
http://localhost:3000/auth/login
```
---
## 3. 认证流程
### 3.1 注册 / 登录发 Token
```mermaid
sequenceDiagram
participant Client as 客户端
participant API as /auth/*
participant DB as PostgreSQL
Client->>API: POST /auth/register 或 /auth/login
API->>DB: 查重 / 校验密码
API->>API: create_access_token(sub, account, jti)
API->>DB: INSERT user_sessions(jti, token_hash, expires_at)
API-->>Client: TokenResponse(access_token, expires_at, user)
```
### 3.2 受保护接口校验 Token
```mermaid
sequenceDiagram
participant Client as 客户端
participant API as 业务接口
participant DB as PostgreSQL
Client->>API: Authorization: Bearer <token>
API->>API: 校验 JWT 签名与 exp
API->>DB: JOIN sys_users + user_sessions<br/>匹配 user_id, jti, token_hash
alt 会话有效
API-->>Client: 200 + 业务数据
else 无效或过期
API-->>Client: 401 / 403
end
```
**要点:**
1. Token 不仅校验 JWT 签名和过期时间,还必须在 `user_sessions` 表中有对应且未吊销的记录。
2. 数据库存的是 Token 的 **SHA256 哈希**,不是明文 Token。
3. 每个 Token 有唯一 `jti`JWT ID用于会话追踪。
---
## 4. 数据库设计
### 4.1 `sys_users`
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | UUID | PK | 用户 ID |
| `account` | VARCHAR(100) | UNIQUE, NOT NULL | 登录账号 |
| `password_hash` | TEXT | NOT NULL | PBKDF2 哈希串 |
| `name` | VARCHAR(100) | NOT NULL | 显示名称 |
| `email` | VARCHAR(255) | UNIQUE, NOT NULL | 邮箱(注册时转小写) |
| `phone` | VARCHAR(50) | NULL | 手机号(可选) |
| `created_at` | TIMESTAMPTZ | NOT NULL | 创建时间 |
| `updated_at` | TIMESTAMPTZ | NOT NULL | 更新时间 |
索引:
- `ux_users_account`account 唯一)
- `ux_users_email`email 唯一)
### 4.2 `user_sessions`
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | UUID | PK | 会话记录 ID |
| `user_id` | UUID | FK → sys_users.id, CASCADE | 所属用户 |
| `jti` | VARCHAR(64) | UNIQUE, NOT NULL | Token 会话 ID |
| `token_hash` | TEXT | NOT NULL | SHA256(access_token) |
| `expires_at` | TIMESTAMPTZ | NOT NULL | 过期时间 |
| `revoked_at` | TIMESTAMPTZ | NULL | 吊销时间(预留,当前无 logout 接口写入) |
| `created_at` | TIMESTAMPTZ | NOT NULL | 创建时间 |
| `updated_at` | TIMESTAMPTZ | NOT NULL | 更新时间 |
---
## 5. 环境配置
配置类:`app/config/settings.py`
| 环境变量 | 默认值 | 说明 |
|----------|--------|------|
| `JWT_SECRET_KEY` | `change-me-in-production` | HMAC 签名密钥,**生产必须修改** |
| `JWT_ALGORITHM` | `HS256` | 写入 JWT Header实际签名为 HMAC-SHA256 |
| `ACCESS_TOKEN_EXPIRE_MINUTES` | `1440`24h | Token 有效期(分钟) |
| `DATABASE_URL` | 见 `.env.example` | PostgreSQL 连接串 |
示例(`.env`
```env
JWT_SECRET_KEY=your-long-random-secret
DATABASE_URL=postgresql+asyncpg://user:pass@postgres:5432/brapi-python
```
---
## 6. 密码安全
实现文件:`app/core/security.py`
### 6.1 哈希格式
```
pbkdf2_sha256$210000$<salt_hex>$<digest_hex>
```
- 算法:`PBKDF2-HMAC-SHA256`
- 迭代次数:`210000`
- Salt16 字节随机 hex`secrets.token_hex(16)`
### 6.2 注册时
```python
password_hash=hash_password(payload.password)
```
### 6.3 登录时
```python
verify_password(payload.password, user.password_hash)
```
校验失败统一返回 `401 Invalid account or password`,不区分「账号不存在」与「密码错误」。
---
## 7. Access Token 结构
Token 由 `create_access_token()` 生成,格式为三段式 Base64URL
```text
<base64url(header)>.<base64url(payload)>.<base64url(signature)>
```
### 7.1 Header
```json
{"alg": "HS256", "typ": "JWT"}
```
### 7.2 Payload
```json
{
"sub": "<user_uuid>",
"account": "<login_account>",
"jti": "<32_hex_chars>",
"iat": 1710000000,
"exp": 1710086400
}
```
| Claim | 含义 |
|-------|------|
| `sub` | 用户 UUID 字符串 |
| `account` | 登录账号 |
| `jti` | 会话唯一 ID |
| `iat` | 签发时间Unix 秒) |
| `exp` | 过期时间Unix 秒) |
### 7.3 签名
```python
HMAC-SHA256(key=JWT_SECRET_KEY, msg=f"{header_b64}.{payload_b64}")
```
> 注意:项目使用**自实现**的 JWT 编解码(`app/api/auth.py` + `app/core/security.py`),未依赖 PyJWT 库。
---
## 8. HTTP 接口详情
### 8.1 注册 `POST /auth/register`
**请求体 `RegisterRequest`**
| 字段 | 类型 | 必填 | 校验 |
|------|------|------|------|
| `account` | string | 是 | 长度 3100 |
| `password` | string | 是 | 长度 6128 |
| `name` | string | 是 | 长度 1100 |
| `email` | string | 是 | 长度 3255入库前转小写 |
| `phone` | string | 否 | 最大 50 |
**业务逻辑:**
1. `account``email` 去空格 / 小写处理
2. 查询 `sys_users`,若 `account``email` 已存在 → `409 Conflict`
3. 创建用户并 `flush`
4. 调用 `_issue_token()` 写入 `user_sessions` 并返回 Token
**成功响应:** `201 Created`Body 为 `TokenResponse`
**curl 示例:**
```bash
curl -X POST http://127.0.0.1:8000/auth/register \
-H "Content-Type: application/json" \
-d '{
"account": "demo_user",
"password": "demo123456",
"name": "演示用户",
"email": "demo@example.com",
"phone": "13800138000"
}'
```
---
### 8.2 登录 `POST /auth/login`
**请求体 `LoginRequest`**
| 字段 | 类型 | 必填 | 校验 |
|------|------|------|------|
| `account` | string | 是 | 长度 1100 |
| `password` | string | 是 | 长度 1128 |
**业务逻辑:**
1.`account` 查用户
2. `verify_password` 失败 → `401 Unauthorized`
3. 成功则 `_issue_token()` 并返回
**成功响应:** `200 OK`Body 为 `TokenResponse`
**curl 示例:**
```bash
curl -X POST http://127.0.0.1:8000/auth/login \
-H "Content-Type: application/json" \
-d '{
"account": "demo_user",
"password": "demo123456"
}'
```
---
### 8.3 统一成功响应 `TokenResponse`
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
"token_type": "bearer",
"expires_at": "2026-05-27T10:00:00+00:00",
"user": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"account": "demo_user",
"name": "演示用户",
"email": "demo@example.com",
"phone": "13800138000"
}
}
```
| 字段 | 说明 |
|------|------|
| `access_token` | Bearer Token 字符串 |
| `token_type` | 固定 `"bearer"` |
| `expires_at` | ISO8601 UTC 过期时间 |
| `user` | 当前用户公开信息(不含密码) |
---
### 8.4 错误响应格式
认证相关错误通过 `ApiError` 抛出,由 `app/main.py` 统一处理:
```json
{
"message": "错误摘要",
"detail": "错误摘要或详细信息"
}
```
| HTTP 状态码 | 场景 | message 示例 |
|-------------|------|----------------|
| `401` | 登录失败 | `Invalid account or password` |
| `401` | 缺少 Token | `Missing bearer token` |
| `401` | Token 无效 | `Invalid token signature` / `Session invalid or expired` |
| `403` | Token 过期 | `Token expired` |
| `409` | 注册冲突 | `Account or email already exists` |
| `422` | 参数校验失败 | `Validation failed`detail 为字段错误列表) |
---
## 9. 受保护接口:`get_current_user`
定义位置:`app/api/auth.py`
```python
async def get_current_user(
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
session: AsyncSession = Depends(get_db_session),
) -> User:
...
```
### 9.1 客户端如何携带 Token
```http
Authorization: Bearer <access_token>
```
### 9.2 校验步骤
1. 检查 `Authorization` 头是否存在且 scheme 为 `bearer`
2. `_decode_access_token()`验证格式、HMAC 签名、`exp` 未过期
3. 从 payload 读取 `sub`user_id`jti`
4. 数据库查询:
```sql
SELECT sys_users.*
FROM sys_users
JOIN user_sessions ON user_sessions.user_id = sys_users.id
WHERE sys_users.id = :user_id
AND user_sessions.jti = :jti
AND user_sessions.token_hash = sha256(:token)
AND user_sessions.revoked_at IS NULL
AND user_sessions.expires_at > now()
```
5. 查不到用户 → `401 Session invalid or expired`
### 9.3 哪些接口使用了 `get_current_user`
#### `/auth` 模块
register/login 本身不需要 Token
#### `/api/dictionaries` 模块(`app/api/dictionaries.py`
| 接口 | 是否需要登录 |
|------|-------------|
| `GET /api/dictionaries/crops` | 否 |
| `POST /api/dictionaries/crops` | **是** |
| `GET /api/dictionaries/persons` | 否 |
| `POST /api/dictionaries/persons` | 否 |
| `POST /api/dictionaries/countries` | **是** |
| `PUT /api/dictionaries/countries/{code}` | **是** |
#### `/brapi/v2` 模块(`app/api/router.py`
- 多数 **GET** 接口:接收 `Authorization` 头但**不强制校验**BrAPI 兼容占位)
- 多数 **POST / PUT / DELETE** 写操作:通过 `Depends(get_current_user)` **强制登录**
写操作成功后会将 `current_user.id` 写入业务表的 `auth_user_id` 字段,用于记录数据创建者。
> 上传接口 `POST /brapi/v2/upload/image` 当前**未**接入 `get_current_user`,前端可选择性携带 Token。
---
## 10. `_issue_token` 内部流程
```python
async def _issue_token(session: AsyncSession, user: User) -> TokenResponse:
jti = uuid4().hex
token, expires_at = create_access_token(
subject=str(user.id),
account=user.account,
jti=jti,
)
session.add(UserSession(
user_id=user.id,
jti=jti,
token_hash=hash_token(token),
expires_at=expires_at,
))
await session.commit()
return TokenResponse(...)
```
每次登录/注册都会**新增一条** `user_sessions` 记录;旧会话不会自动吊销(除非手动清理数据库或未来实现 logout
---
## 11. 前端对接(简要)
| 文件 | 作用 |
|------|------|
| `frontend/src/services/authService.ts` | 封装 `loginWithPassword``registerAccount` |
| `frontend/src/lib/api/sdk.gen.ts` | OpenAPI 生成的 `/auth/login``/auth/register` 客户端 |
| `frontend/src/stores/modules/auth.ts` | Zustand 持久化 Token 与用户信息 |
| `frontend/src/utils/token.ts` | 从 `localStorage` 读取 `auth_token` |
| `frontend/src/lib/client.ts` | 请求拦截器自动附加 `Authorization: Bearer ...` |
登录成功后前端会:
1. 调用 `setSession(session)` 写入 Zustand + `localStorage.auth_token`
2. 跳转到 `/central-config/user/employee``redirect` 参数指定页
登出:仅前端 `logout()` 清除本地存储,**不会**通知后端吊销会话。
---
## 12. 本地调试
### 12.1 启动后端
```bash
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload
```
### 12.2 Swagger 文档
```text
http://127.0.0.1:8000/docs
```
在 Swagger 中可找到 `auth` 分组下的 `register``login`
### 12.3 用 Token 访问受保护接口
```bash
TOKEN="<access_token>"
curl http://127.0.0.1:8000/api/dictionaries/crops \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"crop_name": "maize"}'
```
---
## 13. 部署注意事项
1. **必须修改** `JWT_SECRET_KEY`,不要使用默认值 `change-me-in-production`
2. 首次部署需执行数据库迁移:`uv run alembic upgrade head`(包含 `sys_users``user_sessions` 建表)。
3. Docker / K8s 环境中,容器内 `API_BASE_URL=http://127.0.0.1:8000` 供 Next.js 代理;客户端浏览器仍通过同源 `/auth/*` 访问。
4. 当前无服务端 logoutToken 在过期前始终有效(只要 `user_sessions` 记录存在)。如需强制下线,需扩展 logout 接口并设置 `revoked_at`
---
## 14. 后续可扩展项
| 功能 | 建议实现 |
|------|----------|
| 登出 | `POST /auth/logout`,将当前 `jti``revoked_at` 设为 now |
| 刷新 Token | `POST /auth/refresh`,验证旧 Token 后签发新 Token |
| 当前用户 | `GET /auth/me`,返回 `UserResponse` |
| 修改密码 | `POST /auth/change-password` |
| 手机号登录 | 独立短信验证码表 + 新 login 分支 |
| 上传鉴权 | `upload.py` 增加 `Depends(get_current_user)` |
---
## 15. 相关源码索引
| 内容 | 路径 |
|------|------|
| 路由与 DTO | `app/api/auth.py` |
| 密码 / Token 工具 | `app/core/security.py` |
| ORM 模型 | `app/models/auth.py` |
| 配置 | `app/config/settings.py` |
| 建表迁移 | `migrations/versions/7c91a64f2b2e_create_auth_tables.py` |
| 应用入口 | `app/main.py` |
| 前端封装 | `frontend/src/services/authService.ts` |

View File

@@ -0,0 +1,89 @@
# 数据录入需求总览
本文档基于 `docs/architecture` 下 5 份架构文档拆解,目标是把 BrAPI Test Server 的数据录入需求按上游到下游梳理清楚:
```text
Core 基础数据 -> Germplasm/Seed 种质种子 -> Phenotyping 表型 -> Genotyping 基因型
```
## 文档索引
| 顺序 | 文档 | 说明 |
| --- | --- | --- |
| 1 | `01-core-data-entry-requirements.md` | crop、person、program、location、trial、season、study、list |
| 2 | `02-germplasm-seed-entry-requirements.md` | breeding_method、germplasm、attribute、cross、seed_lot、pedigree |
| 3 | `03-phenotyping-entry-requirements.md` | ontology、trait、method、scale、observation_variable、observation_unit、event、image、observation |
| 4 | `04-genotyping-entry-requirements.md` | plate、sample、reference、variantset、variant、callset、allele_call、genome_map |
## 总体录入链路
```mermaid
flowchart TD
CROP["crop 作物"] --> PROGRAM["program 项目"]
PERSON["person 人员"] --> PROGRAM
PROGRAM --> TRIAL["trial 试验批次"]
PROGRAM --> LOCATION["location 地点"]
TRIAL --> STUDY["study 研究/试验单元"]
LOCATION --> STUDY
CROP --> BM["breeding_method 育种方法"]
BM --> G["germplasm 种质"]
G --> GAV["germplasm_attribute_value 种质属性值"]
PROGRAM --> CP["crossing_project 杂交项目"]
CP --> CROSS["cross_entity Cross/PlannedCross"]
G --> CROSSP["cross_parent 亲本"]
CROSS --> CROSSP
G --> MIX["seed_lot_content_mixture 批次组成"]
CROSS --> MIX
MIX --> SL["seed_lot 种子批次"]
STUDY --> OU["observation_unit 观测单元"]
G --> OU
SL --> OU
CROSS --> OU
OU --> OBS["observation 观测值"]
OU --> SAMPLE["sample 样本"]
STUDY --> SAMPLE
SAMPLE --> CALLSET["callset 调用集合"]
VARIANT["variant 变异位点"] --> CALL["allele_call 基因型结果"]
CALLSET --> CALL
```
## 全局界面原则
| 页面类型 | 用途 | 建议界面 |
| --- | --- | --- |
| 列表页 | 查询、筛选、分页、批量操作 | 顶部筛选区 + 表格 + 新增按钮 + 行内查看/编辑 |
| 新增/编辑页 | 单表主数据录入 | 分组表单,必填字段靠前,外键使用搜索选择器 |
| 详情页 | 查看主表及下游数据 | 顶部摘要 + Tab基本信息、关联数据、扩展信息、外部引用 |
| 关系选择器 | 选择上游依赖 | 支持按 ID、名称、作物、项目、study 搜索 |
| 批量导入 | 初始化和大批量数据 | 上传 CSV/Excel + 字段映射 + 预校验 + 导入结果 |
## 全局字段规则
| 字段类型 | 录入规则 |
| --- | --- |
| `id` / `DbId` | 系统生成或用户提供;必须唯一;编辑时不可随意修改 |
| 名称字段 | 必填或强建议必填;用于下拉选择展示 |
| 外键字段 | 页面展示名称,提交保存实际 ID |
| 枚举字段 | 使用下拉框或单选控件 |
| 日期时间 | 使用日期/时间选择器,保存前统一格式 |
| `additional_info` | 作为高级配置,用键值编辑器录入 |
| `external_references` | 作为外部系统引用,用可增删表格录入 |
## 总体录入顺序
1. 录入 Core`crop``person``program``location``trial``season``study``list`
2. 录入 Germplasm/Seed`breeding_method``germplasm_attribute_definition``germplasm``germplasm_attribute_value``crossing_project``cross_entity``cross_parent``pedigree_node``pedigree_edge``seed_lot``seed_lot_content_mixture``seed_lot_transaction`
3. 录入 Phenotyping`ontology``trait``method``scale``observation_variable``observation_unit``event``image``observation`
4. 录入 Genotyping`plate``sample``reference_set``reference``reference_bases``variantset``variant``callset``allele_call``genome_map``linkageGroup``marker_position`
## 通用验收标准
1. 每张表都有列表、新增、编辑、详情能力。
2. 每个外键字段都能通过名称搜索选择,而不是要求用户手填数据库 ID。
3. 下游表新增时必须校验上游数据是否存在。
4. 删除或禁用上游数据时,需要提示被哪些下游表引用。
5. 支持按模块导入 CSV/Excel并在导入前给出错误行和错误原因。
6. 详情页能看到关键下游关联,例如 `study` 下的 observation unit、sample、variantset。

View File

@@ -0,0 +1,755 @@
# Core 模块业务需求文档
## 1. 模块定位
Core 是项目、试验、地点、人员、季节的基础上下文模块,是所有业务数据的总上游。它不是普通字典维护,而是为后续 Germplasm/Seed、Phenotyping、Genotyping 提供“这批数据属于哪个项目、哪次试验、哪个地点、哪个年度、谁负责”的业务台账。
推荐先完成 Core 录入,再录入 Germplasm、Phenotyping、Genotyping。
## 2. 真实业务理解
Core 不只是“基础字典”,它在真实育种业务里是项目台账:回答“谁在什么项目、什么地点、什么年度、做哪一次试验”。后续所有材料、田间布置、表型采集、样本检测、基因型结果,都需要回到 Core 上下文里解释。
最常见的业务链路是:
```text
crop 作物
-> program 长期育种项目
-> trial 某一批试验/区域试验
-> study 一次真正落地执行的试验
```
例如:
```text
crop: 水稻
program: 水稻抗倒伏育种项目
trial: 2026 湖北区域试验
location: 荆门试验站
season: 2026 春季
study: 2026 荆门水稻抗倒伏田间试验
```
这里最关键的是 `study``program` 更像长期项目,`trial` 是试验批次,`study` 才是具体执行单元。后续 `observation_unit``event``observation``plate``sample``variantset` 都会直接或间接挂到 `study` 上。
## 3. 用户角色和使用场景
| 角色 | 主要职责 | 典型入口 | 权限重点 |
| --- | --- | --- | --- |
| 项目管理员 | 维护作物、项目、人员、地点等基础台账 | crop、person、program、location 列表页 | 可新增/编辑基础数据,可停用未被引用的数据 |
| 试验负责人 | 创建 trial 和 study维护联系人、季节、试验设计 | trial 列表、study 列表、Study 工作台 | 可创建试验上下文,可配置 study 联系人、season、变量清单 |
| 数据录入员 | 进入 Study 工作台继续录观测单元、表型、样本 | Study 工作台 | 主要使用下游入口,一般不能删除 Core 主数据 |
| 数据管理员 | 处理导入、导出、停用、删除校验、数据修复 | 导入导出页、数据质量页 | 可批量导入、导出、停用,有删除前引用检查权限 |
### 3.1 项目管理员场景
项目管理员先建立 `crop``person``program``location`。例如创建“水稻抗倒伏育种项目”,选择 crop=水稻,负责人=张三。录完后,试验负责人才能在该项目下创建 trial 和 study。
### 3.2 试验负责人场景
试验负责人创建“2026 湖北区域试验”和“2026 荆门水稻抗倒伏田间试验”。创建 study 时选择 program、trial、location、season保存后进入 Study 工作台。
### 3.3 数据录入员场景
数据录入员通常不从 crop/program 开始,而是从 Study 工作台进入,继续录 observation unit、observation、sample 等下游数据。
### 3.4 数据管理员场景
数据管理员负责批量导入 Core 数据、处理重复数据、做删除/停用前检查。例如某 trial 已有关联 study 时,不允许物理删除,只能停用。
## 用户为什么要录 Core
| 用户问题 | 由哪些表回答 | 录完能做什么 |
| --- | --- | --- |
| 我们做什么作物? | `crop` | 限定项目、材料、变量、图谱的作物范围 |
| 谁负责项目和试验? | `person``program.lead_person_id``trial_contact``study_contact` | 项目责任追踪、联系人展示、权限和通知 |
| 这是哪个长期育种项目? | `program` | 聚合多个 trial/study、材料、样本、结果 |
| 这是哪一批试验? | `trial` | 管理区域试验、年度试验、批次试验 |
| 试验在哪里、哪一季做? | `location``season``study_season` | 支持多地点、多季节筛选和统计 |
| 哪一次试验真正落地执行? | `study` | 进入 Study 工作台,继续录观测单元、表型、样本、基因型 |
## 6. 页面需求
Core 的核心页面不应该只是表单 CRUD而应该围绕 `study` 做“Study 工作台”。
### 6.1 Study 工作台
```text
Study 工作台
├─ 基本信息:名称、编号、类型、起止日期、负责人
├─ 上下文crop、program、trial、location、season
├─ 试验设计experimental design、observation level、growth facility
├─ 联系人/附件study_contact、data_link、external_references
├─ 观测单元:跳转 observation_unit
├─ 表型数据:跳转 observation / matrix
├─ 样本与样本板:跳转 sample / plate
├─ 基因型数据:跳转 variantset / callset / allele_call
└─ 导入导出:按 study 导入观测单元、表型、样本、基因型
```
### 6.2 Crop 列表页
| 项目 | 需求 |
| --- | --- |
| 功能 | 查询 crop新增、编辑、查看详情、停用 |
| 筛选 | 关键词搜索,按 crop_name 模糊匹配 |
| 表格字段 | `crop_name`、下游 program 数、下游 study 数、状态 |
| 操作 | 查看、编辑、停用 |
| 验收标准 | crop 已被 program/germplasm/study 引用时,不允许物理删除 |
### 6.3 Person 列表页
| 项目 | 需求 |
| --- | --- |
| 功能 | 查询人员;新增、编辑、查看详情、停用 |
| 筛选 | 姓名、邮箱、机构 |
| 表格字段 | 姓名、邮箱、电话、机构、负责项目数、参与 study 数 |
| 操作 | 查看、编辑、停用 |
| 验收标准 | 邮箱格式正确;作为负责人或联系人被引用时,不允许物理删除 |
### 6.4 Program 列表页
| 项目 | 需求 |
| --- | --- |
| 功能 | 查询 program新增、编辑、查看详情、停用 |
| 筛选 | crop、负责人、program_type、关键词 |
| 表格字段 | `name``abbreviation`、crop、负责人、trial 数、study 数 |
| 操作 | 查看、编辑、停用、查看 trial、查看 study |
| 验收标准 | 创建 program 时必须选择 crop负责人来自 person 选择器 |
### 6.5 Trial 列表页
| 项目 | 需求 |
| --- | --- |
| 功能 | 查询 trial新增、编辑、查看详情、停用 |
| 筛选 | crop、program、active、起止日期、关键词 |
| 表格字段 | `trial_name`、program、crop、`start_date``end_date`、active |
| 操作 | 查看、编辑、停用、查看 study |
| 验收标准 | 选择 program 后自动带出 crop已被 study 引用时不允许物理删除 |
### 6.6 Location 列表页
| 项目 | 需求 |
| --- | --- |
| 功能 | 查询 location新增、编辑、查看详情、停用 |
| 筛选 | crop、program、location_type、country、关键词 |
| 表格字段 | `location_name`、location_type、country、program、crop、父级地点 |
| 操作 | 查看、编辑、停用、地图查看 |
| 验收标准 | 父级地点不能选择自己;坐标字段格式合法 |
### 6.7 Season 列表页
| 项目 | 需求 |
| --- | --- |
| 功能 | 查询 season新增、编辑、查看详情、停用 |
| 筛选 | year、season |
| 表格字段 | `year``season`、关联 study 数 |
| 操作 | 查看、编辑、停用 |
| 验收标准 | 同一年份内 season 名称不建议重复 |
### 6.8 Study 列表页
| 项目 | 需求 |
| --- | --- |
| 功能 | 查询 study新增、编辑、查看详情、停用点击 study 名称进入 Study 工作台 |
| 筛选 | crop、program、trial、location、season、active、study_type、关键词 |
| 表格字段 | `study_name``study_code`、program、trial、location、`start_date``end_date`、active |
| 操作 | 查看、编辑、停用、进入工作台 |
| 验收标准 | 选择 program 后只展示该 program 下的 trial选择 trial 后自动带出 crop已有 observation_unit/sample/observation 时不能物理删除 |
### 6.9 Study 新增/编辑页
| 区域 | 字段/功能 | 要求 |
| --- | --- | --- |
| 基本信息 | study_name、study_code、study_type、active、start_date、end_date | study_name 必填;结束日期不早于开始日期 |
| 上下文 | crop、program、trial、location、season | program、trial、crop、location 必选program -> trial 联动 |
| 说明信息 | study_description、cultural_practices、observation_units_description、license、documentationurl | URL 格式校验 |
| 联系人 | study_contact | 多选 person可增删 |
| 季节 | study_season | 可多选 season |
| 提交后 | 保存成功进入 Study 工作台 | 新建成功后自动跳转 |
### 6.10 List 详情页
| 项目 | 需求 |
| --- | --- |
| 功能 | 维护 list 和 list_item |
| 页面形态 | 顶部 list 基本信息,下方 list_item 表格 |
| 操作 | 新增 item、批量导入 item、删除 item、排序 |
| 验收标准 | 同一 list 内 item 不重复 |
## 哪些表是业务动作痕迹
| 动作 | 痕迹表 | 说明 |
| --- | --- | --- |
| 给 trial 添加联系人 | `trial_contact` | trial 与 person 的关系,不是独立主数据 |
| 给 study 添加联系人 | `study_contact` | study 与 person 的关系 |
| 给 study 绑定季节 | `study_season` | 一个 study 可关联多个 season |
| 给 study 绑定观测变量 | `study_variable` | 表示该 study 要采集哪些指标 |
| 给 trial 记录出版物 | `trial_publication` | trial 的文献/报告引用 |
| 给 list 添加成员 | `list_item` | list 的明细项,是分组动作留下的记录 |
## 4. 核心业务流程
### 4.1 新建 Core 试验上下文流程
1. 用户创建 `crop`
2. 用户创建 `person`
3. 用户创建 `program`,并选择 `crop` 和负责人 `lead_person_id`
4. 用户创建 `location`,可选绑定 `program``crop`
5. 用户创建 `trial`,选择 `program` 后自动带出 `crop`
6. 用户创建 `season`
7. 用户创建 `study`,按 `program -> trial -> location -> season` 的顺序选择上下文。
8. `study` 创建成功后,系统自动进入 Study 工作台。
### 4.2 业务流程图
```mermaid
flowchart TD
A["创建 crop"] --> B["创建 person"]
B --> C["创建 program<br/>选择 crop / lead person"]
C --> D["创建 location<br/>可选 program / crop"]
C --> E["创建 trial<br/>选择 program 自动带出 crop"]
E --> F["创建 season"]
D --> G["创建 study<br/>选择 program -> trial -> location -> season"]
F --> G
G --> H["进入 Study 工作台"]
```
## 5. 核心对象说明
### 5.1 crop 作物
#### 数据库录入
| 项目 | 要求 |
| --- | --- |
| 表 | `crop` |
| 前置依赖 | 无 |
| 主要字段 | 作物 ID、作物名称、公共作物名、描述 |
| 下游引用 | `program``location``trial``study``germplasm``observation_variable``genome_map` |
#### 界面形态
列表页展示作物名称、描述、更新时间、下游引用数量。新增页是轻量表单,重点是作物名称唯一校验。详情页展示该作物下的项目、试验、研究、种质、图谱入口。
#### 校验规则
1. 作物名称必填且唯一。
2. 已被下游引用的作物不能直接删除,只允许停用或提示引用关系。
### 5.2 person 人员
#### 数据库录入
| 项目 | 要求 |
| --- | --- |
| 表 | `person` |
| 前置依赖 | 无 |
| 主要字段 | 姓名、邮箱、机构、联系方式、角色 |
| 下游引用 | `program.lead_person_id``trial_contact``study_contact``list.list_owner_person_id` |
#### 界面形态
列表页支持姓名、邮箱、机构筛选。新增/编辑页用联系人表单。详情页展示负责的 program、参与的 trial/study、拥有的 list。
#### 校验规则
1. 姓名必填。
2. 邮箱格式正确。
3. 同一邮箱不建议重复录入。
### 5.3 program 项目
#### 数据库录入
| 项目 | 要求 |
| --- | --- |
| 表 | `program` |
| 前置依赖 | `crop`,可选 `person` |
| 主要字段 | 项目名称、作物、负责人、缩写、目标、描述 |
| 下游引用 | `trial``study``location``crossing_project``seed_lot``plate``sample` |
#### 界面形态
新增页分为“基本信息”和“负责人/作物”两组。作物和负责人使用搜索选择器。详情页 Tab 展示 trial、study、location、seed lot、sample。
#### 校验规则
1. 项目名称必填。
2. 作物必选。
3. 选择负责人时必须存在于 `person`
### 5.4 location 地点
#### 数据库录入
| 项目 | 要求 |
| --- | --- |
| 表 | `location` |
| 前置依赖 | 可选 `crop``program`、父级 `location`、坐标 |
| 主要字段 | 地点名称、地点类型、国家/地区、经纬度、父级地点、所属项目 |
| 下游引用 | `study``seed_lot` |
#### 界面形态
列表页支持地图/表格两种视图。新增页包含基本信息、行政区、坐标、父级地点。详情页展示该地点下的 study 和 seed lot。
#### 校验规则
1. 地点名称必填。
2. 经纬度格式合法。
3. 父级地点不能选择自己。
### 5.5 trial 试验批次
#### 数据库录入
| 项目 | 要求 |
| --- | --- |
| 表 | `trial` |
| 前置依赖 | `program``crop` |
| 主要字段 | 试验名称、项目、作物、开始/结束日期、联系人、出版物 |
| 下游引用 | `study``observation_unit``observation``plate``sample` |
#### 界面形态
新增页包含基本信息、项目作物、联系人、出版物四个区域。联系人用可增删表格选择 `person`。详情页展示 study 列表和 phenotyping/genotyping 入口。
#### 校验规则
1. 试验名称必填。
2. program 与 crop 必须匹配。
3. 结束日期不能早于开始日期。
### 5.6 season 季节
#### 数据库录入
| 项目 | 要求 |
| --- | --- |
| 表 | `season` |
| 前置依赖 | 无 |
| 主要字段 | 季节名称、年份、开始日期、结束日期 |
| 下游引用 | `study_season`、部分 `observation` |
#### 界面形态
列表页按年份和季节筛选。新增页是简单表单。详情页展示关联 study。
#### 校验规则
1. 名称和年份必填。
2. 同一年份内季节名称不建议重复。
### 5.7 study 研究/试验实施单元
#### 数据库录入
| 项目 | 要求 |
| --- | --- |
| 表 | `study` |
| 前置依赖 | `crop``program``trial``location`,可选 `season` |
| 主要字段 | study 名称、study 类型、项目、试验、地点、季节、开始/结束日期、实验设计 |
| 下游引用 | `observation_unit``event``observation``plate``sample``variantset` |
#### 界面形态
新增页是分组表单:基本信息、上下文、地点季节、实验设计、联系人。上下文字段按顺序联动:选择 program 后过滤 trial 和 crop选择 trial 后带出 crop。详情页作为工作台展示 observation unit、observation、sample、variantset。
#### 校验规则
1. study 名称必填。
2. program、trial、crop、location 必选。
3. trial 必须属于所选 program。
4. study 是下游核心引用,删除必须强提示。
### 5.8 list / list_item 通用列表
#### 数据库录入
| 项目 | 要求 |
| --- | --- |
| 表 | `list``list_item` |
| 前置依赖 | 可选 `person` |
| 主要字段 | 列表名称、列表类型、owner、项目项 ID、项目项名称 |
| 下游引用 | 业务查询和分组 |
#### 界面形态
列表页展示 list。详情页内嵌 list item 表格,支持手动添加、批量导入、排序。新增 item 时可选择目标类型,例如 germplasm、study、sample。
#### 校验规则
1. list 名称必填。
2. list item 在同一 list 内不应重复。
3. 如果绑定 ownerowner 必须存在于 `person`
## 7. 字段需求
### crop
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 作物主键,新增时系统生成,也可导入时指定 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `crop_name` | varchar(255) | 文本框 | 作物名称,用于所有下拉选择展示 | 必填、建议唯一 |
### person
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 人员主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `description` | varchar(255) | 多行文本 | 人员说明、职责补充 | 可选 |
| `email_address` | varchar(255) | 邮箱输入框 | 邮箱地址 | 邮箱格式校验,建议唯一 |
| `first_name` | varchar(255) | 文本框 | 名 | 与 `last_name` 至少填写一个 |
| `institute_name` | varchar(255) | 文本框/选择器 | 所属机构 | 可选 |
| `last_name` | varchar(255) | 文本框 | 姓 | 与 `first_name` 至少填写一个 |
| `mailing_address` | varchar(255) | 多行文本 | 通讯地址 | 可选 |
| `middle_name` | varchar(255) | 文本框 | 中间名 | 可选 |
| `phone_number` | varchar(255) | 电话输入框 | 联系电话 | 可选,格式提示 |
| `userid` | varchar(255) | 文本框 | 外部用户 ID 或登录名 | 可选,建议唯一 |
### program
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 项目主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `abbreviation` | varchar(255) | 文本框 | 项目缩写 | 可选 |
| `documentationurl` | varchar(255) | URL 输入框 | 项目文档链接 | URL 格式校验 |
| `funding_information` | varchar(255) | 多行文本 | 经费来源说明 | 可选 |
| `name` | varchar(255) | 文本框 | 项目名称 | 必填 |
| `objective` | varchar(255) | 多行文本 | 项目目标 | 可选 |
| `program_type` | integer | 下拉框 | 项目类型枚举 | 可选,按 BrAPI 枚举 |
| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` |
| `lead_person_id` | varchar(255) | 人员选择器 | 项目负责人 | 可选,来源 `person.id` |
### location
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 地点主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `abbreviation` | varchar(255) | 文本框 | 地点缩写 | 可选 |
| `coordinate_description` | varchar(255) | 多行文本 | 坐标描述 | 可选 |
| `coordinate_uncertainty` | varchar(255) | 文本框 | 坐标不确定性 | 可选 |
| `country_code` | varchar(255) | 国家代码选择器 | 国家代码 | 可选,建议 ISO 代码 |
| `country_name` | varchar(255) | 文本框 | 国家名称 | 可选,可由国家代码带出 |
| `documentationurl` | varchar(255) | URL 输入框 | 地点文档链接 | URL 格式校验 |
| `environment_type` | varchar(255) | 下拉框/文本框 | 环境类型 | 可选 |
| `exposure` | varchar(255) | 文本框 | 暴露条件 | 可选 |
| `institute_address` | varchar(255) | 多行文本 | 机构地址 | 可选 |
| `institute_name` | varchar(255) | 文本框 | 机构名称 | 可选 |
| `location_name` | varchar(255) | 文本框 | 地点名称 | 必填 |
| `location_type` | varchar(255) | 下拉框 | 地点类型,如 field、greenhouse、storage | 可选 |
| `site_status` | varchar(255) | 下拉框 | 地点状态 | 可选 |
| `slope` | varchar(255) | 文本框 | 坡度 | 可选 |
| `topography` | varchar(255) | 文本框 | 地形 | 可选 |
| `coordinates_id` | varchar(255) | 坐标选择器/地图取点 | 坐标对象 | 可选,来源 `geojson/coordinate` |
| `crop_id` | varchar(255) | 作物选择器 | 关联作物 | 可选,来源 `crop.id` |
| `parent_location_id` | varchar(255) | 地点选择器 | 父级地点 | 可选,不能选择自己 |
| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 可选,来源 `program.id` |
### trial
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 试验主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `active` | boolean | 开关 | 是否启用 | 默认启用 |
| `documentationurl` | varchar(255) | URL 输入框 | 试验文档链接 | URL 格式校验 |
| `end_date` | timestamp | 日期选择器 | 结束日期 | 不早于 `start_date` |
| `start_date` | timestamp | 日期选择器 | 开始日期 | 可选 |
| `trial_description` | varchar(255) | 多行文本 | 试验描述 | 可选 |
| `trial_name` | varchar(255) | 文本框 | 试验名称 | 必填 |
| `trialpui` | varchar(255) | 文本框 | 试验永久标识 | 可选,建议唯一 |
| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` |
| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 必选,来源 `program.id` |
### season
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 季节主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `season` | varchar(255) | 文本框/下拉框 | 季节名称,如 Spring、Summer | 必填 |
| `year` | integer | 年份选择器 | 年份 | 必填,四位年份 |
### study
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | study 主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `active` | boolean | 开关 | 是否启用 | 默认启用 |
| `cultural_practices` | varchar(255) | 多行文本 | 栽培管理说明 | 可选 |
| `documentationurl` | varchar(255) | URL 输入框 | study 文档链接 | URL 格式校验 |
| `end_date` | timestamp | 日期选择器 | 结束日期 | 不早于 `start_date` |
| `license` | varchar(255) | 文本框 | 数据许可证 | 可选 |
| `observation_units_description` | varchar(255) | 多行文本 | 观测单元说明 | 可选 |
| `start_date` | timestamp | 日期选择器 | 开始日期 | 可选 |
| `study_code` | varchar(255) | 文本框 | study 编码 | 可选,建议同项目内唯一 |
| `study_description` | varchar(255) | 多行文本 | study 描述 | 可选 |
| `study_name` | varchar(255) | 文本框 | study 名称 | 必填 |
| `studypui` | varchar(255) | 文本框 | study 永久标识 | 可选,建议唯一 |
| `study_type` | varchar(255) | 下拉框 | study 类型 | 可选 |
| `crop_id` | varchar(255) | 作物选择器 | 所属作物 | 必选,来源 `crop.id` |
| `location_id` | varchar(255) | 地点选择器 | 实施地点 | 必选,来源 `location.id` |
| `program_id` | varchar(255) | 项目选择器 | 所属项目 | 必选,来源 `program.id` |
| `trial_id` | varchar(255) | 试验选择器 | 所属 trial | 必选,来源 `trial.id` |
### list
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 列表主键 | 必填、唯一 |
| `auth_user_id` | varchar(255) | 隐藏 | 数据所属用户 | 登录上下文自动写入 |
| `date_created` | timestamp | 只读日期时间 | 创建时间 | 系统自动写入 |
| `date_modified` | timestamp | 只读日期时间 | 修改时间 | 系统自动更新 |
| `description` | varchar(255) | 多行文本 | 列表描述 | 可选 |
| `list_name` | varchar(255) | 文本框 | 列表名称 | 必填 |
| `list_owner_name` | varchar(255) | 文本框 | 列表 owner 名称 | 可选,可由 owner person 带出 |
| `list_source` | varchar(255) | 文本框 | 列表来源 | 可选 |
| `list_type` | integer | 下拉框 | 列表类型 | 必填,按 BrAPI 枚举 |
| `list_owner_person_id` | varchar(255) | 人员选择器 | 列表 owner | 可选,来源 `person.id` |
### list_item
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 列表项主键 | 必填、唯一 |
| `item` | varchar(255) | 文本框/对象选择器 | 列表项值,可存目标对象 ID 或文本 | 必填,同一 list 内不重复 |
| `list_id` | varchar(255) | List 选择器 | 所属列表 | 必选,来源 `list.id` |
### trial_contact
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `trial_db_id` | varchar(255) | Trial 选择器 | 所属 trial | 必选,来源 `trial.id` |
| `person_db_id` | varchar(255) | 人员选择器 | 联系人 | 必选,来源 `person.id` |
### trial_publication
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `id` | varchar(255) | 隐藏/只读 | 出版物记录主键 | 必填、唯一 |
| `publicationpui` | varchar(255) | 文本框 | 出版物 PUI | 可选 |
| `publication_reference` | varchar(255) | 文本框/URL | 出版物引用 | 可选 |
| `trial_id` | varchar(255) | Trial 选择器 | 所属 trial | 必选,来源 `trial.id` |
### study_contact
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `study_db_id` | varchar(255) | Study 选择器 | 所属 study | 必选,来源 `study.id` |
| `person_db_id` | varchar(255) | 人员选择器 | 联系人 | 必选,来源 `person.id` |
### study_season
| 字段 | 类型 | 控件 | 录入说明 | 校验/来源 |
| --- | --- | --- | --- | --- |
| `study_db_id` | varchar(255) | Study 选择器 | 所属 study | 必选,来源 `study.id` |
| `season_db_id` | varchar(255) | Season 选择器 | 关联季节 | 必选,来源 `season.id` |
## 8. 接口需求
接口命名可以按项目现有 BrAPI Controller 适配;本节描述前端页面需要的能力,不强制限定最终 URL 必须完全一致。
### 8.1 CRUD 接口
| 对象 | 接口 | 用途 |
| --- | --- | --- |
| crop | `GET /crops` | crop 分页查询 |
| crop | `POST /crops` | 新增 crop |
| crop | `GET /crops/{id}` | crop 详情 |
| crop | `PUT /crops/{id}` | 编辑 crop |
| crop | `DELETE /crops/{id}` | 删除或停用 crop需做引用检查 |
| person | `GET /persons` | person 分页查询 |
| person | `POST /persons` | 新增 person |
| person | `GET /persons/{id}` | person 详情 |
| person | `PUT /persons/{id}` | 编辑 person |
| person | `DELETE /persons/{id}` | 删除或停用 person需做引用检查 |
| program | `GET /programs` | program 分页查询 |
| program | `POST /programs` | 新增 program |
| program | `GET /programs/{id}` | program 详情 |
| program | `PUT /programs/{id}` | 编辑 program |
| program | `DELETE /programs/{id}` | 删除或停用 program需做引用检查 |
| location | `GET /locations` | location 分页查询 |
| location | `POST /locations` | 新增 location |
| location | `GET /locations/{id}` | location 详情 |
| location | `PUT /locations/{id}` | 编辑 location |
| trial | `GET /trials` | trial 分页查询 |
| trial | `POST /trials` | 新增 trial |
| trial | `GET /trials/{id}` | trial 详情 |
| trial | `PUT /trials/{id}` | 编辑 trial |
| trial | `DELETE /trials/{id}` | 删除或停用 trial需检查 study 引用 |
| season | `GET /seasons` | season 查询 |
| season | `POST /seasons` | 新增 season |
| season | `PUT /seasons/{id}` | 编辑 season |
| study | `GET /studies` | study 分页查询 |
| study | `POST /studies` | 新增 study |
| study | `GET /studies/{id}` | study 详情 |
| study | `PUT /studies/{id}` | 编辑 study |
| study | `DELETE /studies/{id}` | 删除或停用 study需检查下游引用 |
| list | `GET /lists` | list 查询 |
| list | `POST /lists` | 新增 list |
| list | `PUT /lists/{id}` | 编辑 list |
| list item | `POST /lists/{id}/items` | 给 list 添加 item |
| list item | `DELETE /lists/{id}/items/{itemId}` | 删除 list item |
### 8.2 详情聚合接口
| 接口 | 用途 | 返回重点 |
| --- | --- | --- |
| `GET /programs/{id}/trials` | 查询 program 下 trial | trial 基础信息、active |
| `GET /programs/{id}/studies` | 查询 program 下 study | study 基础信息、location、trial |
| `GET /trials/{id}/studies` | 查询 trial 下 study | study 列表 |
| `GET /studies/{id}/workbench` | Study 工作台聚合信息 | study 基本信息、联系人、season、observation_unit 数、observation 数、sample 数、variantset 数 |
| `GET /studies/{id}/contacts` | study 联系人 | person 列表 |
| `GET /studies/{id}/seasons` | study 季节 | season 列表 |
### 8.3 选择器接口
| 接口 | 用途 | 参数 |
| --- | --- | --- |
| `GET /selectors/crops` | crop 搜索下拉 | `keyword` |
| `GET /selectors/persons` | person 搜索下拉 | `keyword``instituteName` |
| `GET /selectors/programs` | program 搜索下拉 | `keyword``cropId` |
| `GET /selectors/trials` | trial 搜索下拉 | `keyword``programId``cropId``active` |
| `GET /selectors/locations` | location 搜索下拉 | `keyword``programId``cropId``locationType` |
| `GET /selectors/seasons` | season 搜索下拉 | `year``keyword` |
| `GET /selectors/studies` | study 搜索下拉 | `keyword``programId``trialId``locationId``active` |
### 8.4 导入导出接口
| 接口 | 用途 |
| --- | --- |
| `GET /imports/templates/core/{objectType}` | 下载 Core 对象导入模板 |
| `POST /imports/core/{objectType}/preview` | 上传文件并预校验,不落库 |
| `POST /imports/core/{objectType}/commit` | 确认导入 |
| `GET /exports/core/{objectType}` | 导出当前筛选结果 |
| `GET /imports/{jobId}/errors` | 获取导入错误报告 |
## 9. 联动规则
| 场景 | 联动规则 |
| --- | --- |
| 创建 program | 必须先选择 crop负责人从 person 选择器选择 |
| 创建 trial | 选择 program 后自动带出 croptrial 的 crop 必须与 program.crop_id 一致 |
| 创建 location | 选择 program 后可自动带出 crop但允许地点作为公共地点不绑定 program |
| 创建 study | 先选 program再按 program 过滤 trial选择 trial 后自动带出 crop |
| 创建 study | location 可按 program/crop 过滤season 支持多选 |
| 编辑 study | 如果已有 observation_unit/sample/observation变更 program/trial/crop/location 前必须二次确认并检查一致性 |
| Study 工作台 | 入口必须携带 study_id下游新增 observation_unit、sample、variantset 默认继承该 study 上下文 |
| 外键显示 | 前端展示名称,提交保存 ID表格列需要同时支持名称展示和 ID 调试查看 |
## 10. 删除/停用规则
| 对象 | 删除/停用规则 |
| --- | --- |
| crop | 已被 program、location、trial、study、germplasm、observation_variable、genome_map 引用时,不允许物理删除 |
| person | 已作为 program 负责人、trial 联系人、study 联系人、list owner 时,不允许物理删除 |
| program | 已有关联 trial、study、location、crossing_project、seed_lot、plate、sample 时,不允许物理删除 |
| location | 已被 study 或 seed_lot 引用时,不允许物理删除 |
| trial | 已被 study、observation_unit、observation、plate、sample 引用时,不允许物理删除 |
| season | 已被 study_season 或 observation 引用时,不允许物理删除 |
| study | 已被 observation_unit、event、observation、plate、sample、variantset 引用时,不允许物理删除 |
| list | 已有 list_item 时,删除前必须提示;可先清空明细再删除 |
删除失败时,后端应返回被引用对象类型和数量,前端弹窗展示,例如:
```text
该 study 已被以下数据引用,不能删除:
- observation_unit: 120
- observation: 560
- sample: 96
请先处理下游数据,或将 study 停用。
```
## 11. 导入导出需求
### 11.1 导入对象
Core 模块至少支持以下对象导入:
```text
crop
person
program
location
trial
season
study
list
list_item
trial_contact
study_contact
study_season
```
### 11.2 导入流程
1. 用户选择对象类型,例如 `study`
2. 下载 CSV/Excel 模板。
3. 用户填写模板并上传。
4. 系统做预校验:字段完整性、必填、格式、外键是否存在、联动关系是否一致。
5. 预览页展示可导入行、错误行、警告行。
6. 用户确认后提交导入。
7. 导入完成后生成导入报告。
### 11.3 导入模板要求
| 要求 | 说明 |
| --- | --- |
| 字段名 | 使用数据库字段名作为模板列名 |
| 外键列 | 支持填写 ID可额外支持名称匹配但名称重复时必须报错 |
| 错误报告 | 返回行号、字段名、错误原因、建议修正方式 |
| 幂等策略 | 可按 ID 更新;无 ID 时新增 |
| 权限 | 数据录入员可导入 study 下游数据Core 主数据导入建议仅项目管理员/数据管理员可操作 |
### 11.4 导出需求
1. 所有列表页支持导出当前筛选结果。
2. 导出文件应包含 ID 和展示名称。
3. study 导出应支持导出工作台摘要包括联系人、season、下游数据数量。
4. 导出大数据量时应走异步任务。
## 12. 验收标准
### 12.1 前端验收标准
1. 所有列表页支持分页、关键词搜索、基础筛选。
2. 所有外键字段前端展示名称,提交保存 ID。
3. 创建 program 时,必须选择 crop。
4. 创建 trial 时,选择 program 后自动带出 crop。
5. 创建 study 时,必须选择 program、trial、crop、location。
6. Study 列表页支持按 crop、program、trial、location、season、active 筛选。
7. 点击 study 名称或“进入工作台”按钮后进入 Study 工作台。
8. Study 工作台可以看到观测单元、表型、样本、基因型入口。
9. 删除被引用数据时,前端展示引用详情,不允许静默失败。
10. 导入预校验结果能展示错误行、错误字段、错误原因。
### 12.2 后端验收标准
1. 创建 program 时校验 `crop_id` 存在。
2. 创建 trial 时校验 `program_id` 存在,且 trial.crop_id 与 program.crop_id 一致。
3. 创建 study 时校验 `program_id``trial_id``crop_id``location_id` 存在。
4. 创建 study 时校验 trial 属于所选 program。
5. 删除 crop/program/trial/study 前必须检查下游引用。
6. `GET /studies/{id}/workbench` 能返回工作台所需聚合数量。
7. 选择器接口支持 keyword 模糊搜索和上游过滤参数。
8. 导入接口支持 preview 和 commit 两阶段。
### 12.3 测试点
1. 选择 program 后trial 下拉框只展示该 program 下的数据。
2. 选择 trial 后,系统自动带出 crop。
3. 已被 study 引用的 trial 不能直接删除。
4. 已被 observation_unit、sample、observation 引用的 study 不能直接删除。
5. study 创建成功后自动进入 Study 工作台。
6. 修改 study 的 program/trial/crop 时,如果已有下游数据,必须提示风险。
7. 导入 study 时,如果 trial 不属于 program应报错并指出行号。
8. 导入 location 时,如果 parent_location_id 指向自己,应报错。
9. 导出列表时,筛选条件必须生效。
10. 所有接口返回的分页字段一致,便于前端表格复用。

View File

@@ -0,0 +1,609 @@
# Germplasm / Seed 模块专业数据录入需求文档 V2
## 1. 文档目的
本文档用于指导 Germplasm / Seed 模块的前端页面、后端接口、字段校验、数据导入、测试验收设计。本文档不再只描述数据库表关系,而是从真实育种业务出发,解释每个字段的业务意义、录入方式、控件建议、校验规则和上下游影响。
## 2. 模块定位
Germplasm / Seed 模块描述育种材料的生命周期:
```text
材料身份 -> 材料属性 -> 杂交计划/实际杂交 -> 亲本 -> 系谱 -> 种子批次 -> 库存流转 -> 被 study / observation_unit 使用
```
核心概念如下:
| 概念 | 业务含义 | 主要表 |
| ---------------------------- | --------------------------------------------------------- | ------------------------------------------------------------ |
| Germplasm 材料身份 | 一个品种、品系、后代材料、种质资源的身份信息 | `germplasm` |
| Germplasm Attribute 材料属性 | 材料自身稳定特征,如抗性、熟期、籽粒硬度、基因/QTL 标记等 | `germplasm_attribute_definition``germplasm_attribute_value` |
| Cross 杂交动作 | 一次计划杂交或实际杂交,包含亲本、状态、项目归属 | `crossing_project``cross_entity``cross_parent` |
| Pedigree 系谱 | 材料之间的亲子、同胞、后代关系 | `pedigree_node``pedigree_edge` |
| SeedLot 种子批次 | 某个材料或杂交组合对应的一批实物种子,有数量、单位、库位 | `seed_lot``seed_lot_content_mixture` |
| Transaction 库存流转 | 入库、出库、转移、分装、合并、消耗等动作流水 | `seed_lot_transaction` |
## 3. 推荐业务流程
```text
1. 维护育种方法 breeding_method
2. 维护材料属性定义 germplasm_attribute_definition
3. 创建材料 germplasm
4. 给材料补充属性值 germplasm_attribute_value
5. 如涉及杂交,创建 crossing_project
6. 创建计划杂交 cross_entity(planned=true)
7. 给计划杂交录入亲本 cross_parent
8. 实际完成杂交后,创建实际杂交 cross_entity(planned=false, planned_cross_id=计划杂交)
9. 如产生后代材料,创建新的 germplasm并补充 pedigree_node / pedigree_edge
10. 如产生实物种子,创建 seed_lot
11. 在 seed_lot_content_mixture 中描述批次组成
12. 后续库存变化通过 seed_lot_transaction 记录
13. seed_lot / germplasm / cross_entity 后续可作为 observation_unit 的材料来源
```
## 4. 页面总体设计
### 4.1 Germplasm 材料详情页
```text
Germplasm 详情页
├─ 基本信息名称、默认显示名、PUI、accession、作物、育种方法
├─ 分类信息genus、species、subtaxa、country_of_origin_code
├─ 来源信息acquisition_date、seed_source、seed_source_description、collection
├─ 属性值germplasm_attribute_value
├─ 系谱pedigree_node / pedigree_edge 树图
├─ 作为亲本cross_parent
├─ 种子批次seed_lot_content_mixture -> seed_lot
└─ 下游使用observation_unit、reference_set、sample 追踪
```
### 4.2 SeedLot 库存详情页
```text
SeedLot 详情页
├─ 当前库存amount、units、location、storage_location
├─ 批次组成germplasm / cross / mixture_percentage
├─ 出入库操作:入库、出库、转移、分装、合并、消耗
├─ 流转流水seed_lot_transaction
└─ 下游使用:哪些 study / observation_unit 使用了该批种子
```
### 4.3 CrossingProject 杂交项目工作台
```text
CrossingProject 工作台
├─ 项目信息name、program、description
├─ 计划杂交cross_entity(planned=true)
├─ 实际杂交cross_entity(planned=false)
├─ 亲本cross_parent
├─ 后代材料germplasm / pedigree_node
└─ 产生种子批次seed_lot
```
---
# 5. 字段级专业录入需求
## 5.1 breeding_method 育种方法
### 业务说明
`breeding_method` 是育种方法字典,用来说明某个 germplasm 是通过什么方式形成的,例如杂交选育、回交、自交系选育、诱变、转基因、克隆选择等。它不是一次具体杂交动作,而是材料来源方法的分类。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------- | -------------------------------------------- | -------------------------------- | --------- | -------------------------------- |
| `id` | 育种方法主键,系统内部唯一标识 | 新增时系统生成;导入时可允许指定 | 隐藏/只读 | 必填、唯一;编辑时不允许修改 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许前端手填 |
| `abbreviation` | 方法缩写,如 MB、BC、DH | 用户录入 | 文本框 | 可选;建议同一用户下唯一 |
| `description` | 方法解释,如“回交用于恢复目标基因” | 用户录入 | 多行文本 | 可选,限制长度 |
| `name` | 方法名称,如 Male Backcross、Doubled Haploid | 用户录入 | 文本框 | 必填;建议唯一;作为下拉展示名称 |
### 页面与交互
- 列表页展示:方法名称、缩写、描述、使用材料数量。
- 新增页为简单字典表单。
- 删除前检查是否被 `germplasm.breeding_method_id` 引用;已引用时不允许物理删除,只允许停用。
---
## 5.2 germplasm 种质 / 材料身份
### 业务说明
`germplasm` 是材料身份证,描述一个品种、品系、亲本、后代材料、遗传资源或研究材料“是谁”。它不表示库存数量,库存数量由 `seed_lot` 表达。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------------------------- | ------------------------------------------------------------ | -------------------------------------- | ----------------- | --------------------------------------------------------- |
| `id` | 种质主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一;编辑不可随意修改 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `accession_number` | 材料在种质库/机构内的 accession 编号,如 PI 113869 | 用户录入或导入 | 文本框 | 可选;建议同一 crop / institution 下唯一 |
| `acquisition_date` | 材料进入本系统或本机构的获取日期 | 用户录入 | 日期选择器 | 可选;不得晚于当前日期太多,导入时允许缺月/缺日需统一规则 |
| `acquisition_source_code` | 获取来源编码,如采集、引进、交换、繁殖等 | 用户选择 | 下拉框 | 可选;值来自 BrAPI/MCPD 枚举或系统字典 |
| `biological_status_of_accession_code` | 材料生物状态,如野生、地方品种、育种材料、改良品种、突变体等 | 用户选择 | 下拉框 | 可选;使用受控枚举,不建议自由输入 |
| `collection` | 材料所属集合、群体、panel 或 collection | 用户录入/选择 | 文本框/选择器 | 可选;可用于分组筛选 |
| `country_of_origin_code` | 原产国或育成/选育国家代码 | 用户选择 | 国家代码选择器 | 可选;建议使用 ISO 3166-1 三字母代码 |
| `default_display_name` | 系统默认展示名,给下拉框、表格、详情标题使用 | 用户录入,可由 germplasm_name 自动带出 | 文本框 | 与 `germplasm_name` 至少填一个;建议必填 |
| `documentationurl` | 材料说明文档、外部数据库页面或 DOI 链接 | 用户录入 | URL 输入框 | 可选;校验 URL 格式 |
| `genus` | 属名,如 Oryza、Triticum | 用户录入/字典选择 | 文本框/物种选择器 | 可选;建议首字母大写 |
| `germplasm_name` | 材料名称,可以是品种名、品系名、后代编号 | 用户录入 | 文本框 | 与 `default_display_name` 至少填一个;不强制全局唯一 |
| `germplasmpui` | 永久唯一标识,通常是 DOI、URI 或全局唯一编码 | 用户录入/外部导入 | 文本框/URL 输入框 | 可选;若填写必须唯一;建议用于跨系统交换 |
| `germplasm_preprocessing` | 材料用于试验前的统一处理说明,如消毒、催芽、低温处理 | 用户录入 | 文本框/多行文本 | 可选 |
| `mls_status` | 多边系统 MLS 状态,涉及植物遗传资源交换协议 | 用户选择 | 下拉框 | 可选;普通业务可隐藏到高级信息 |
| `seed_source` | 材料来源标识,如来源机构+accession或亲本组合描述 | 用户录入 | 文本框 | 可选;注意它不是库存批次,不等于 seed_lot |
| `seed_source_description` | 材料来源详细说明 | 用户录入 | 多行文本 | 可选 |
| `species` | 种名,如 sativa、aestivum | 用户录入/物种字典 | 文本框 | 可选;建议小写 |
| `species_authority` | 种名命名权威,如 L. | 用户录入 | 文本框 | 可选 |
| `subtaxa` | 亚种、变种、品种群、line 等更细分类 | 用户录入 | 文本框 | 可选 |
| `subtaxa_authority` | 亚种/变种命名权威 | 用户录入 | 文本框 | 可选 |
| `breeding_method_id` | 该材料形成所使用的育种方法 | 从 breeding_method 选择 | 搜索选择器 | 可选;必须引用存在的 breeding_method |
| `crop_id` | 所属作物 | 从 crop 选择 | 作物选择器 | 必填;后续 trial/study/attribute 应尽量同 crop |
### 录入建议
- 新建材料时,第一屏只放核心字段:`crop_id``germplasm_name``default_display_name``germplasmpui``accession_number``breeding_method_id`
- 分类与来源信息放在“高级信息”或“来源信息”分组。
- `germplasmpui``accession_number``germplasm_name` 三者不要混为一谈:
- `germplasm_name` 是人看的名字;
- `accession_number` 是机构内编号;
- `germplasmpui` 是跨系统长期唯一标识。
### 验收标准
1. 新增 germplasm 时,必须选择 crop。
2. `germplasm_name``default_display_name` 至少填写一个。
3. 下拉选择材料时展示 `default_display_name`,辅助展示 accession number / PUI。
4. 如果 germplasm 已被 seed lot、cross parent、observation unit 引用,不允许物理删除。
---
## 5.3 germplasm_attribute_definition 属性定义
### 业务说明
属性定义描述“材料可以有哪些稳定属性”。这些属性通常不是环境依赖的田间观测值,而是材料自身特征,例如籽粒颜色、抗病基因、硬度、熟期类型、特定 QTL、分子标记结果等。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ---------------------- | ------------------------------------------------------------ | ---------------------------- | ----------------- | ------------------------------------- |
| `id` | 属性定义主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `default_value` | 属性默认值 | 用户录入 | 动态输入框 | 可选;按 datatype / scale 校验 |
| `documentationurl` | 属性说明文档链接 | 用户录入 | URL 输入框 | 可选;校验 URL |
| `growth_stage` | 属性适用生长阶段,如 flowering | 用户录入/选择 | 下拉框/文本框 | 可选 |
| `institution` | 提交或维护该属性定义的机构 | 用户录入 | 文本框 | 可选 |
| `language` | 定义语言,如 zh、en | 用户选择 | 下拉框 | 可选;建议 ISO 639-1 |
| `scientist` | 提交该属性定义的科学家或负责人 | 用户录入/人员选择 | 文本框/人员选择器 | 可选 |
| `status` | 属性状态,如 recommended、obsolete、legacy | 用户选择 | 下拉框 | 可选;推荐使用枚举 |
| `submission_timestamp` | 属性定义提交时间 | 系统默认当前时间,可手动调整 | 日期时间选择器 | 可选;新增默认当前时间 |
| `crop_id` | 适用作物 | 从 crop 选择 | 作物选择器 | 可选;若填写,下游材料应同 crop |
| `method_id` | 属性测定方法 | 从 method 选择 | 方法选择器 | 可选;若填写,属性值录入按该方法解释 |
| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选 |
| `scale_id` | 值标尺/单位/有效值范围 | 从 scale 选择 | 标尺选择器 | 可选;若填写,属性值必须按 scale 校验 |
| `trait_id` | 关联性状 | 从 trait 选择 | 性状选择器 | 可选 |
| `attribute_category` | 属性分类,如 Morphological、Genetic、Quality | 用户选择/录入 | 下拉框/文本框 | 可选;建议字典化 |
| `code` | 属性代码,便于导入导出 | 用户录入 | 文本框 | 可选;建议唯一 |
| `datatype` | 属性值数据类型,如 text、numeric、date、boolean、categorical | 用户选择 | 下拉框 | 必填 |
| `description` | 属性解释 | 用户录入 | 多行文本 | 可选 |
| `name` | 属性名称 | 用户录入 | 文本框 | 必填;作为属性选择器展示名称 |
| `pui` | 属性永久标识 | 用户录入 | 文本框/URL 输入框 | 可选;建议唯一 |
| `uri` | 属性 URI | 用户录入 | URL 输入框 | 可选;校验 URL |
### 录入建议
- 属性定义页面本质是“属性字典配置”。
- 前端应根据 `datatype` 动态决定属性值录入控件:
- numeric数字输入框
- categorical下拉框
- date日期选择器
- boolean开关
- text文本框。
- 若绑定了 `scale_id`,则优先按 scale 的单位、上下限、有效分类值校验。
---
## 5.4 germplasm_attribute_value 属性值
### 业务说明
属性值是“某个 germplasm 在某个属性上的实际取值”。它不是属性定义,也不是 observation。它适合记录材料相对稳定、不强依赖环境的特征。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ----------------- | ------------------------------ | -------------------------------------- | ---------- | ---------------------------------------- |
| `id` | 属性值主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `determined_date` | 属性值被测定或确认的日期 | 用户录入 | 日期选择器 | 可选;多次测定时必须填写以区分记录 |
| `value` | 某个材料在某个属性上的实际取值 | 用户录入 | 动态控件 | 必填;按 attribute datatype / scale 校验 |
| `attribute_id` | 属性定义 | 从 germplasm_attribute_definition 选择 | 属性选择器 | 必选;必须存在 |
| `germplasm_id` | 所属材料 | 从 germplasm 选择 | 材料选择器 | 必选;必须存在 |
### 录入建议
- 推荐嵌入 Germplasm 详情页的“属性值”Tab。
- 支持批量导入,模板列建议为:`germplasm_id/germplasm_name``attribute_code/attribute_name``value``determined_date`
- 同一个 germplasm + attribute 可以允许多次测定,但页面必须显示测定日期、来源和最新值标记。
---
## 5.5 crossing_project 杂交项目
### 业务说明
`crossing_project` 是某个育种项目下的一组杂交任务集合。它不是一次杂交而是一个杂交工作台例如“2026 抗倒伏杂交项目”。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------- | ------------------ | ------------------ | ---------- | ----------------------------- |
| `id` | 杂交项目主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `description` | 杂交项目说明 | 用户录入 | 多行文本 | 可选 |
| `name` | 杂交项目名称 | 用户录入 | 文本框 | 必填;同一 program 下建议唯一 |
| `program_id` | 所属育种项目 | 从 program 选择 | 项目选择器 | 必选;必须存在 |
### 页面与交互
- 详情页应展示计划杂交、实际杂交、潜在亲本、后代材料、产生的 seed lot。
- 创建 cross 时应自动带入 crossing_project_id。
---
## 5.6 cross_entity 计划杂交 / 实际杂交
### 业务说明
`cross_entity` 统一承载计划杂交和实际杂交。通过 `planned` 字段区分计划与实际,通过 `planned_cross_id` 指向来源计划。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | ------------------------------------------- | ------------------------ | -------------- | --------------------------------------- |
| `id` | cross 主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `cross_type` | 杂交类型,如 biparental、self、backcross 等 | 用户选择 | 下拉框 | 可选;值来自枚举字典 |
| `name` | cross 名称,如 A × B、A/B、Cross-2026-001 | 用户录入或自动生成 | 文本框 | 必填;同一 crossing project 下建议唯一 |
| `planned` | 是否为计划杂交 | 页面根据入口自动设置 | 开关/分段控件 | 必填;计划杂交为 true实际杂交为 false |
| `status` | 状态,如 TODO、DONE、SKIPPED、FAILED | 用户选择/系统更新 | 下拉框 | 可选;计划杂交常用 TODO/DONE/SKIPPED |
| `crossing_project_id` | 所属杂交项目 | 从 crossing_project 选择 | 杂交项目选择器 | 必选 |
| `planned_cross_id` | 实际杂交来源的计划杂交 | 从 cross_entity 选择 | Cross 选择器 | 可选;不能选择自己;实际杂交建议填写 |
### 录入建议
- 页面上分成“计划杂交”和“实际杂交”两个入口,但后端都保存到 `cross_entity`
- 创建计划杂交时:`planned=true``planned_cross_id=null`
- 完成实际杂交时:`planned=false``planned_cross_id=原计划杂交 id`
- 亲本不要直接塞在 cross 主表字段中,应通过 `cross_parent` 维护,便于支持多亲本和 observation_unit 亲本来源。
---
## 5.7 cross_parent 杂交亲本
### 业务说明
`cross_parent` 表示某个 cross 的亲本。亲本可以来自 `germplasm`,也可以来自某个 `observation_unit`,例如田间某一株实际被选作父本/母本。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | --------------------------------------------------- | ---------------------------------- | ----------------- | ------------------------------------- |
| `id` | 亲本记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `parent_type` | 亲本角色,如 MALE、FEMALE、SELF、POPULATION、CLONAL | 用户选择 | 下拉框 | 必填;使用枚举 |
| `cross_id` | 所属 cross | 从 cross_entity 选择或由详情页带入 | Cross 选择器/隐藏 | 必选 |
| `crossing_project_id` | 所属 crossing project | 由 cross 自动带出 | 只读/隐藏 | 可选;如填写必须与 cross 一致 |
| `germplasm_id` | 亲本材料 | 从 germplasm 选择 | 材料选择器 | 与 `observation_unit_id` 至少一个必填 |
| `observation_unit_id` | 亲本观测单元 | 从 observation_unit 选择 | 观测单元选择器 | 与 `germplasm_id` 至少一个必填 |
### 录入建议
- 在 Cross 详情页内嵌“亲本列表”。
- 常见快捷录入Parent1 / Parent2。
- 对于田间选株杂交,优先记录 observation_unit_id同时可带出 germplasm_id保证可追溯到具体植株。
---
## 5.8 pedigree_node 系谱节点
### 业务说明
`pedigree_node` 是系谱图中的节点,通常对应一个 germplasm。它用于描述材料在系谱树中的位置不等同于一次杂交记录。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | --------------------- | ------------------------ | -------------- | ----------------------------------------- |
| `id` | 系谱节点主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `crossing_year` | 亲本最初杂交年份 | 用户录入 | 年份选择器 | 可选;四位年份 |
| `family_code` | 家系编号 | 用户录入 | 文本框 | 可选;同一 crossing_project 下建议唯一 |
| `pedigree_string` | 系谱字符串,如 A/B//C | 用户录入/系统生成 | 文本框 | 可选;建议支持 Purdy notation |
| `crossing_project_id` | 产生该节点的杂交项目 | 从 crossing_project 选择 | 杂交项目选择器 | 可选 |
| `germplasm_id` | 该系谱节点对应的材料 | 从 germplasm 选择 | 材料选择器 | 建议必填;同一 germplasm 不建议重复建节点 |
### 录入建议
- Germplasm 详情页提供“系谱”Tab。
- 支持两种维护方式:树图拖拽维护、表格维护节点和边。
- 如果 cross 完成后产生后代 germplasm应自动或半自动创建 pedigree_node。
---
## 5.9 pedigree_edge 系谱边
### 业务说明
`pedigree_edge` 是系谱图中的边,描述节点之间的父子、同胞等关系。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------- | ------------------------------------------ | --------------------- | ---------- | ------------------------------------ |
| `id` | 系谱边主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `edge_type` | 边类型,如 parent、child、sibling | 用户选择 | 下拉框 | 必填 |
| `parent_type` | 如果是亲本关系,标识 MALE、FEMALE、SELF 等 | 用户选择 | 下拉框 | 可选;当 edge_type=parent 时建议必填 |
| `connceted_node_id` | 被连接节点 | 从 pedigree_node 选择 | 节点选择器 | 必选 |
| `this_node_id` | 当前节点 | 从 pedigree_node 选择 | 节点选择器 | 必选;不能等于 connected node |
### 录入建议
- 前端展示时不要暴露“this_node_id / connected_node_id”这种技术词应该显示为“当前材料”和“关联材料”。
- 添加父本/母本时,系统自动创建 edge_type=parent。
- 需要避免明显循环,例如 A 是 B 的父本,同时 B 又是 A 的父本。
---
## 5.10 seed_lot 种子批次
### 业务说明
`seed_lot` 是实物库存批次,描述某一批种子当前有多少、放在哪里、属于哪个项目。它不是 germplasm 身份;同一个 germplasm 可以有多个 seed_lot。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------- | ---------------------------------------------------- | --------------------- | ------------------- | ----------------------------- |
| `id` | SeedLot 主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `amount` | 当前库存数量,可以是粒数、重量、株数等 | 用户录入/交易自动更新 | 数字输入框 | 必填;非负;交易后自动更新 |
| `created_date` | 批次创建时间 | 系统默认,可导入 | 日期时间选择器/只读 | 默认当前时间 |
| `description` | 批次说明 | 用户录入 | 多行文本 | 可选 |
| `last_updated` | 最后更新时间,包含交易变化 | 系统自动更新 | 只读 | 不允许手动改 |
| `name` | 批次名称,如 华占-2026-荆门-扩繁批 | 用户录入或自动生成 | 文本框 | 必填;同一 program 下建议唯一 |
| `source_collection` | 原始来源 collection如野外采集、nursery、种质库集合 | 用户录入 | 文本框 | 可选 |
| `storage_location` | 具体库位描述,如 冰箱A-2层-盒03 | 用户录入 | 文本框 | 可选 |
| `units` | 数量单位,如 seeds、g、kg、plants | 用户选择 | 下拉框/文本框 | 必填;交易单位需一致或可换算 |
| `location_id` | 库存所在地点 | 从 location 选择 | 地点选择器 | 可选 |
| `program_id` | 所属项目 | 从 program 选择 | 项目选择器 | 可选;用于项目库存筛选 |
### 录入建议
- 创建 seed_lot 后必须进入“批次组成”Tab至少录入一条 `seed_lot_content_mixture`
- 普通用户不要直接编辑 amountamount 应通过入库、出库、转移、分装等交易动作更新。
- 支持库存状态:充足、低库存、耗尽,可由 amount 和阈值计算。
---
## 5.11 seed_lot_content_mixture 批次组成
### 业务说明
`seed_lot_content_mixture` 描述一个 seed_lot 由哪些材料或 cross 组成。单一材料批次也需要有一条组成记录,比例为 100%。混合批次则多条记录占比合计为 100%。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------------- | ----------------------------- | -------------------- | ------------------- | ---------------------------------------- |
| `id` | 批次组成主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `mixture_percentage` | 该材料或 cross 在批次中的占比 | 用户录入 | 百分比输入框 | 0 到 100同一 seed lot 总和建议等于 100 |
| `cross_id` | 来源 cross | 从 cross_entity 选择 | Cross 选择器 | 与 `germplasm_id` 至少一个必填 |
| `germplasm_id` | 来源 germplasm | 从 germplasm 选择 | 材料选择器 | 与 `cross_id` 至少一个必填 |
| `seed_lot_id` | 所属 seed lot | 由详情页带入或选择 | SeedLot 选择器/隐藏 | 必选 |
### 录入建议
- 新建 seed_lot 时,如果用户选择了单个 germplasm系统自动生成一条 mixture`germplasm_id=所选材料``mixture_percentage=100`
- 如果来源是某次杂交产生的种子,优先填写 `cross_id`
- 如果既能追溯 cross 又能追溯 germplasm可按系统设计决定是否允许同时填写若允许同时展示“来源杂交”和“当前材料身份”。
---
## 5.12 seed_lot_transaction 批次流转
### 业务说明
`seed_lot_transaction` 记录库存变化。它不应该由用户像普通表单一样手动维护,而应该由“入库、出库、转移、分装、合并、消耗”等业务动作自动生成。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------ | -------------------------------------------- | ----------------------- | ------------------- | ------------------------------------------------- |
| `id` | 流转记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `amount` | 流转数量 | 用户在业务动作中录入 | 数字输入框 | 必填;大于 0 |
| `description` | 流转说明,如用于某 study、分装原因、报废原因 | 用户录入 | 多行文本 | 可选;出库/报废建议必填 |
| `timestamp` | 流转发生时间 | 默认当前时间,可调整 | 日期时间选择器 | 必填 |
| `units` | 流转单位 | 默认继承 seed_lot.units | 下拉框/只读 | 必填;需与 seed_lot 单位一致或有换算关系 |
| `from_seed_lot_id` | 来源批次 | 按动作自动设置 | SeedLot 选择器/隐藏 | 与 `to_seed_lot_id` 至少一个存在 |
| `to_seed_lot_id` | 目标批次 | 按动作自动设置 | SeedLot 选择器/隐藏 | 与 `from_seed_lot_id` 至少一个存在;不能等于 from |
### 业务动作映射
| 动作 | from_seed_lot_id | to_seed_lot_id | amount 对库存影响 |
| --------- | ---------------- | -------------- | -------------------------------------------- |
| 入库 | 空 | 目标批次 | 目标批次增加 |
| 出库 | 来源批次 | 空 | 来源批次减少 |
| 转移 | 来源批次 | 目标批次 | 来源减少,目标增加 |
| 分装 | 原批次 | 新批次 | 原批次减少,新批次增加 |
| 合并 | 多个来源批次 | 目标批次 | 来源减少,目标增加;可能生成多条 transaction |
| 消耗/报废 | 来源批次 | 空 | 来源减少,并记录原因 |
### 验收标准
1. amount 必须大于 0。
2. 出库/消耗时amount 不得超过来源批次当前库存。
3. from 和 to 不能相同。
4. transaction 创建后应自动更新 seed_lot.amount 和 last_updated。
5. 已生成的 transaction 原则上不允许随意修改;如需纠错,应通过反向交易或更正记录处理。
---
# 6. 跨表联动与关键校验
## 6.1 选择器联动
| 场景 | 联动规则 |
| ------------------------- | ------------------------------------------------------------ |
| 创建 germplasm | 必须先选择 cropbreeding_method 可选 |
| 创建 attribute definition | crop 可选;若选择 trait/method/scale/ontology则必须引用存在记录 |
| 创建 attribute value | 选择 germplasm 后attribute 选择器优先展示同 crop 或未限定 crop 的属性 |
| 创建 crossing project | 必须选择 program |
| 创建 cross | 必须先选择 crossing_project计划杂交和实际杂交使用不同入口 |
| 创建 cross parent | 从 cross 详情页进入时自动带出 cross 和 crossing_project |
| 创建 seed lot | 可选择 program、location保存后必须维护 content mixture |
| 创建 transaction | 从 seed lot 详情页进入时自动带出 from/to seed lot |
## 6.2 删除规则
| 对象 | 删除限制 |
| -------------------- | ------------------------------------------------------------ |
| breeding_method | 已被 germplasm 引用时不可删除 |
| germplasm | 已被 attribute value、cross parent、seed lot mixture、pedigree、observation unit 引用时不可删除 |
| attribute definition | 已有 attribute value 时不可删除 |
| crossing_project | 已有 cross / cross parent / pedigree node 时不可删除 |
| cross_entity | 已有 parent、seed lot mixture、observation unit 引用时不可删除 |
| seed_lot | 已有 mixture、transaction、observation unit 引用时不可删除 |
| transaction | 原则上不可物理删除;只能冲销或作废 |
## 6.3 批量导入要求
### Germplasm 导入
必需列建议:
```text
crop_id 或 crop_name
germplasm_name 或 default_display_name
```
强烈建议列:
```text
accession_number
germplasmpui
breeding_method_name
country_of_origin_code
genus
species
seed_source
```
### SeedLot 导入
必需列建议:
```text
seed_lot_name
amount
units
germplasm_id/germplasm_name 或 cross_id/cross_name
mixture_percentage
```
### Attribute Value 导入
必需列建议:
```text
germplasm_id 或 germplasm_name
attribute_id 或 attribute_name/code
value
```
可选列:
```text
determined_date
source
remark
```
导入必须先预校验,错误报告至少包含:行号、字段、错误原因、建议修复方式。
---
# 7. 后端接口建议
## 7.1 主数据接口
```text
GET /germplasm
POST /germplasm
GET /germplasm/{id}
PUT /germplasm/{id}
DELETE /germplasm/{id}
GET /breeding-methods
POST /breeding-methods
GET /germplasm-attributes
POST /germplasm-attributes
GET /germplasm/{id}/attribute-values
POST /germplasm/{id}/attribute-values
GET /crossing-projects
POST /crossing-projects
GET /crossing-projects/{id}/crosses
GET /crosses
POST /crosses
GET /crosses/{id}/parents
POST /crosses/{id}/parents
GET /seed-lots
POST /seed-lots
GET /seed-lots/{id}/mixtures
POST /seed-lots/{id}/mixtures
GET /seed-lots/{id}/transactions
POST /seed-lots/{id}/transactions
```
## 7.2 选择器接口
```text
GET /selectors/crops
GET /selectors/breeding-methods
GET /selectors/germplasm?cropId=&keyword=
GET /selectors/germplasm-attributes?cropId=&datatype=&keyword=
GET /selectors/crossing-projects?programId=&keyword=
GET /selectors/crosses?crossingProjectId=&planned=&keyword=
GET /selectors/seed-lots?programId=&locationId=&keyword=
GET /selectors/observation-units?studyId=&germplasmId=&keyword=
```
---
# 8. 测试验收清单
1. 创建 germplasm 时,未选择 crop 不允许保存。
2. `germplasm_name``default_display_name` 都为空时不允许保存。
3. `germplasmpui` 重复时提示唯一性冲突。
4. attribute value 的 value 必须按 attribute datatype / scale 校验。
5. 创建 cross 时必须选择 crossing project。
6. 实际杂交选择 planned_cross_id 时不能选择自身。
7. cross parent 必须选择 parent_type。
8. cross parent 的 germplasm_id 和 observation_unit_id 至少填写一个。
9. seed lot 的 amount 不允许小于 0。
10. seed lot 的 units 必填。
11. seed lot content mixture 的 percentage 必须在 0 到 100 之间。
12. 同一 seed lot 的 mixture_percentage 总和不为 100 时,保存前应提示或禁止保存,具体取决于业务配置。
13. 出库数量不能超过当前库存。
14. transaction 创建后自动更新 seed_lot.amount。
15. 已有关联下游数据的 germplasm、cross、seed_lot 不允许物理删除。

View File

@@ -0,0 +1,778 @@
# Phenotyping 模块专业数据录入需求文档 V2
## 1. 文档目的
本文档用于指导 Phenotyping 表型模块的前端页面、后端接口、字段校验、数据导入和测试验收设计。本文档不只描述表关系,而是从真实育种表型采集业务出发,解释每个对象和字段的业务意义、录入方式、控件建议、校验规则和上下游影响。
## 2. 模块定位
Phenotyping 模块用于管理田间、温室、实验室或高通量设备采集到的表型数据。它的核心不是“录一张 observation 表”,而是完整描述:
```text
在哪个 study 里
对哪个 observation_unit
按照哪个 observation_variable
在什么时间、由谁、用什么方法
采集到了什么 value
并能关联事件、图片、坐标和上下文
```
## 3. 核心业务概念
| 概念 | 业务含义 | 关键表 |
| ---------------------------- | ---------------------------------------------------------- | ------------------------------------------------- |
| Ontology 本体 | 性状、方法、标尺、变量的术语来源,可以是本地本体或公开本体 | `ontology` |
| Trait 性状 | 测什么。例如株高、穗长、病害等级、籽粒颜色 | `trait` |
| Method 方法 | 怎么测。例如尺测、人工评分、无人机图像分析、实验室检测 | `method` |
| Scale 标尺 | 用什么单位或尺度表达。例如 cm、kg、0-5 等级、文本、布尔值 | `scale``scale_valid_value_category` |
| ObservationVariable 观测变量 | Trait + Method + Scale 的组合,表示一个可采集指标 | `observation_variable` |
| StudyVariable 研究采集指标 | 某个 study 计划采集哪些 observation variable | `study_variable` |
| ObservationUnit 观测单元 | 被观测对象。通常是 plot、plant、block、field、sample | `observation_unit` |
| Event 事件 | 管理动作或环境事件,如施肥、灌溉、打药、移栽、极端天气 | `event``event_param``event_observation_units` |
| Image 图像 | 田间图片、无人机图片、病害图片、长势图像等 | `image``image_observations` |
| Observation 观测值 | 某个观测单元在某个变量上的一次实际测量结果 | `observation` |
## 4. 推荐业务流程
```text
1. 准备 Core 上下文crop、program、trial、study、location、season
2. 维护或导入本体 ontology
3. 定义 trait / method / scale
4. 组合 observation_variable
5. 把 observation_variable 绑定到 study_variable形成本 study 的采集指标清单
6. 根据田间设计生成 observation_unit例如 block / plot / plant
7. 给 observation_unit 绑定材料来源germplasm / seed_lot / cross
8. 记录 event例如播种、移栽、施肥、灌溉、打药、采样、极端天气
9. 通过矩阵方式批量录入 observation
10. 上传 image并关联 observation_unit 或 observation
11. 做数据质控:缺失值、异常值、重复采集、单位和分类值校验
```
## 5. 推荐页面形态
### 5.1 Study 表型工作台
```text
Study 表型工作台
├─ 试验上下文study / trial / program / crop / location / season
├─ 指标清单study_variable
├─ 田间设计observation_unit 层级、block、plot、plant、row、column
├─ 材料布置germplasm、seed_lot、cross
├─ 表型矩阵observation_unit × observation_variable
├─ 田间事件event + event_param + event_observation_units
├─ 图片证据image + image_observations
├─ 数据质控:缺失值、异常值、重复值、单位校验、分类值校验
└─ 导入导出:观测单元模板、表型采集模板、图片元数据模板
```
### 5.2 表型矩阵录入
真实采集时不应只提供单条 observation 表单,而应支持矩阵录入:
| observation_unit | germplasm | 株高 cm | 穗长 cm | 倒伏等级 | 备注 |
| ---------------- | --------- | ------: | ------: | -------: | ---------- |
| Plot-001 | 华占 | 112.5 | 24.1 | 1 | 正常 |
| Plot-002 | 抗倒伏A | 98.3 | 22.8 | 0 | 正常 |
| Plot-003 | F2-001 | 101.6 | 23.0 | 4 | 台风后倒伏 |
保存时,系统按每个非空单元格生成一条 `observation`,每一列对应一个 `observation_variable`
---
# 6. 字段级专业录入需求
## 6.1 ontology 本体
### 业务说明
`ontology` 表示术语体系来源,用来说明 trait、method、scale、observation_variable 的定义来自哪里。它可以是本地维护的术语库,也可以是公开 Crop Ontology、Trait Ontology 或机构内部标准。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------ | ----------------------------------------- | -------------------------------- | --------------- | -------------------------------- |
| `id` | 本体主键,系统内部唯一标识 | 新增时系统生成;导入时可允许指定 | 隐藏/只读 | 必填、唯一;编辑时不可修改 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许前端手填 |
| `authors` | 本体作者或维护者 | 用户录入 | 文本框 | 可选;多个作者可用分号或数组扩展 |
| `copyright` | 本体版权说明 | 用户录入 | 文本框/多行文本 | 可选 |
| `description` | 本体用途说明 | 用户录入 | 多行文本 | 可选,建议填写 |
| `documentationurl` | 本体文档链接 | 用户录入 | URL 输入框 | 可选;若填写必须是合法 URL |
| `licence` | 本体许可证,例如 CC-BY、MIT、机构内部许可 | 用户录入/选择 | 文本框/下拉框 | 可选 |
| `ontology_name` | 本体名称,例如 Rice Trait Ontology | 用户录入 | 文本框 | 必填;同一用户下建议唯一 |
| `version` | 本体版本,例如 2024.1、v2.0 | 用户录入 | 文本框 | 可选;建议与外部本体版本一致 |
### 录入建议
- 若系统只是内部使用,可先创建一个“默认本体”或“本地表型本体”。
- 若对接 Crop Ontology建议保存 ontology 名称、版本、文档 URL。
- 本体删除前必须检查是否被 trait、method、scale、observation_variable 引用。
---
## 6.2 trait 性状
### 业务说明
`trait` 描述“测什么”。例如株高、叶长、穗长、倒伏等级、病害严重度、籽粒颜色。专业上 trait 可以拆成 `entity``attribute`
```text
Trait = Entity + Attribute
例如grain colour
entity = grain
attribute = colour
```
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------- | ----------------------------------------------------- | -------------------- | ----------------- | ---------------------------------------- |
| `id` | 性状主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `attribute` | 性状属性,即被观察的特征,如 height、colour、severity | 用户录入/本体选择 | 文本框/本体选择器 | 可选;若填写 PUI建议同步填写 attribute |
| `attributepui` | 属性永久唯一标识,通常是 URI | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 |
| `entity` | 性状实体,即观察对象部位,如 plant、leaf、grain、root | 用户录入/本体选择 | 文本框/本体选择器 | 可选 |
| `entitypui` | 实体永久唯一标识,通常是 URI | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 |
| `main_abbreviation` | 性状主缩写,如 PH、GY、DTH | 用户录入 | 文本框 | 可选;同一 ontology 下建议唯一 |
| `status` | 性状状态,如 recommended、obsolete、legacy | 用户选择 | 下拉框 | 可选;推荐使用枚举 |
| `trait_class` | 性状分类,如 phenology、morphology、disease、quality | 用户选择/录入 | 下拉框/文本框 | 可选;建议字典化 |
| `trait_description` | 性状定义和说明 | 用户录入 | 多行文本 | 可选,但专业使用强烈建议填写 |
| `trait_name` | 性状名称,给用户看的名称 | 用户录入 | 文本框 | 必填;作为下拉展示名称 |
| `traitpui` | 性状永久唯一标识 | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写必须唯一 |
| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选;必须引用存在的 ontology |
### 录入建议
- 简单业务可只填:`trait_name``trait_description``trait_class`
- 专业数据交换场景建议填写:`entity``attribute``traitpui``ontology_id`
- 不要把单位写进 trait。比如“株高 cm”不应该作为 trait正确拆分是 trait=株高scale=cm。
---
## 6.3 method 方法
### 业务说明
`method` 描述“怎么测”。同一个 trait 用不同方法测,会形成不同 observation_variable。例如
```text
Trait: plant height
Method 1: tape measure
Method 2: drone image processing
Scale: cm
```
这两个变量不能混为一谈,因为它们的数据来源、误差和可比性不同。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------- | ------------------------------------------------------------ | ------------------ | ----------------- | -------------------------------- |
| `id` | 方法主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `description` | 方法说明,如测量位置、工具、采样规则 | 用户录入 | 多行文本 | 可选;建议必填 |
| `formula` | 如果该方法通过计算得到结果,填写公式 | 用户录入 | 文本框/公式编辑器 | 可选;需要格式提示 |
| `method_class` | 方法分类,如 measurement、estimation、computed、image_analysis | 用户选择/录入 | 下拉框/文本框 | 可选;建议字典化 |
| `methodpui` | 方法永久唯一标识 | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 |
| `name` | 方法名称 | 用户录入 | 文本框 | 必填;同一 ontology 下不建议重复 |
| `reference` | 方法参考文献、SOP、标准链接 | 用户录入 | 文本框/URL 输入框 | 可选;若为 URL 需校验格式 |
| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选;必须引用存在的 ontology |
### 录入建议
- 方法页面应重点让用户说明“测量步骤”,否则后续数据不可复现。
- 对于人工评分,必须在 description 中说明评分标准,并通过 scale 配置有效分类值。
- 对于高通量图像方法reference 建议填写算法版本、模型版本或 SOP 链接。
---
## 6.4 scale 标尺
### 业务说明
`scale` 描述“用什么尺度表达结果”。它决定 observation.value 的合法值、单位、数据类型、小数位和分类值。常见类型:
```text
Numerical: 株高 cm、产量 kg/ha
Ordinal: 倒伏等级 0-5、病害等级 1-9
Categorical: 籽粒颜色 red/yellow/white
Text: 描述性备注
Boolean: 是否开花、是否倒伏
Date: 开花日期
```
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ----------------- | ----------------------------------------------------- | ------------------ | ----------------- | --------------------------------- |
| `id` | 标尺主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `data_type` | 数据类型,决定 observation.value 的输入控件和校验方式 | 用户选择 | 下拉框 | 必填;使用系统枚举 |
| `decimal_places` | 小数位数 | 用户录入 | 数字输入框 | 数值型可填;必须为非负整数 |
| `scale_name` | 标尺名称,如 cm、0-5 lodging score、disease score 1-9 | 用户录入 | 文本框 | 必填;同一 ontology 下建议唯一 |
| `scalepui` | 标尺永久唯一标识 | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 |
| `units` | 单位,如 cm、mm、kg/ha、days | 用户选择/录入 | 单位选择器/文本框 | 数值型建议必填;分类型可为空 |
| `valid_value_max` | 最大有效值 | 用户录入 | 数字输入框/文本框 | 数值/等级型校验;必须大于等于 min |
| `valid_value_min` | 最小有效值 | 用户录入 | 数字输入框/文本框 | 数值/等级型校验;必须小于等于 max |
| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选;必须引用存在 ontology |
### 录入建议
- `data_type` 是动态表单核心字段。前端要根据它渲染 observation.value
- Numerical数字输入框按 min/max/decimal_places 校验;
- Ordinal数字或下拉通常有 min/max
- Categorical下拉框选项来自 `scale_valid_value_category`
- Text文本框
- Boolean开关/单选;
- Date日期选择器。
- 如果 data_type 是分类型,必须至少配置一个 `scale_valid_value_category`
- 如果 data_type 是数值型,建议填写单位和有效范围。
---
## 6.5 scale_valid_value_category 分类有效值
### 业务说明
`scale_valid_value_category` 是分类型或等级型 scale 的合法取值表。例如倒伏等级 0-5、病害等级 1-9、颜色 red/yellow/white。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ---------- | --------------------------------- | --------------------------- | ----------------- | ------------------------ |
| `id` | 分类值主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `label` | 用户界面展示文案,如 “0 - 不倒伏” | 用户录入 | 文本框 | 必填 |
| `value` | 实际保存值,如 0、1、red | 用户录入 | 文本框 | 必填;同一 scale 内唯一 |
| `scale_id` | 所属 scale | 从 scale 选择或由详情页带出 | Scale 选择器/隐藏 | 必选;必须引用存在 scale |
### 录入建议
- 在 scale 详情页中以内嵌表格维护。
- 保存 observation 时,如果 scale 是分类型value 必须在该表中存在。
- label 可以给用户看value 用于保存和导出,二者可以不同。
---
## 6.6 observation_variable 观测变量
### 业务说明
`observation_variable` 是真正进入采集模板的指标。它由 trait、method、scale 组合而成。
```text
observation_variable = trait + method + scale
例如:株高 + 尺子测量 + cm = 株高-尺测法-cm
```
它不是观测结果,而是“可以被测的一列数据”。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ---------------------- | ------------------------------------------ | ------------------------ | ----------------- | ---------------------------------------- |
| `id` | 观测变量主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `default_value` | 默认观测值,例如默认等级、默认文本 | 用户录入 | 动态控件 | 可选;按 scale 校验 |
| `documentationurl` | 变量说明文档链接 | 用户录入 | URL 输入框 | 可选;若填写必须合法 URL |
| `growth_stage` | 建议采集生育期,如 flowering、maturity | 用户录入/选择 | 下拉框/文本框 | 可选;建议字典化 |
| `institution` | 定义该变量的机构 | 用户录入 | 文本框 | 可选 |
| `language` | 变量语言,如 zh、en | 用户选择 | 下拉框 | 可选;建议 ISO 639-1 |
| `scientist` | 提交或维护该变量的科学家/负责人 | 用户录入/人员选择 | 文本框/选择器 | 可选 |
| `status` | 变量状态,如 recommended、obsolete、legacy | 用户选择 | 下拉框 | 可选obsolete 不建议用于新 study |
| `submission_timestamp` | 变量提交时间 | 系统默认当前时间,可调整 | 日期时间选择器 | 可选;新增默认当前时间 |
| `crop_id` | 适用作物 | 从 crop 选择 | 作物选择器 | 可选;如果填写,下游 study 应尽量同 crop |
| `method_id` | 测定方法 | 从 method 选择 | 方法选择器 | 必选 |
| `ontology_id` | 所属本体 | 从 ontology 选择 | 本体选择器 | 可选 |
| `scale_id` | 标尺 | 从 scale 选择 | 标尺选择器 | 必选 |
| `trait_id` | 被测性状 | 从 trait 选择 | 性状选择器 | 必选 |
| `name` | 变量名称,采集矩阵列名 | 自动生成后允许修改 | 文本框 | 必填;同一 crop/ontology 下建议唯一 |
| `pui` | 变量永久唯一标识 | 用户录入/本体带出 | 文本框/URL 输入框 | 可选;若填写建议唯一 |
### 录入建议
- 新建变量采用“三段式选择”:先选 trait再选 method再选 scale。
- 系统自动生成 name例如`trait_name + method_name + scale_name`,用户可修改。
- 变量详情页应展示:被哪些 study 使用、已有多少 observation、是否仍推荐使用。
- 如果变量已经产生 observation不建议修改 trait/method/scale只能停用旧变量并创建新变量。
---
## 6.7 study_variable 研究采集指标
### 业务说明
`study_variable` 是 study 和 observation_variable 的多对多关系,表示某个 study 计划采集哪些指标。它是“配置采集模板”的动作痕迹。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ---------------- | --------------------- | ---------------------------- | ----------------- | --------------------------- |
| `study_db_id` | 所属 study | 从 study 选择或由工作台带出 | Study 选择器/隐藏 | 必选;必须引用存在 study |
| `variable_db_id` | 该 study 要采集的变量 | 从 observation_variable 选择 | 变量多选器 | 必选;必须引用存在 variable |
### 录入建议
- 在 Study 表型工作台的“指标清单”Tab 中维护。
- 支持批量选择变量和从历史 study 复制变量清单。
- 若某变量已有 observation不允许从 study_variable 中直接移除,除非先处理已有观测数据。
---
## 6.8 observation_unit 观测单元
### 业务说明
`observation_unit` 是“被观测对象”。通常是 plot 或 plant也可以是 field、block、sub-plot、sample 等。它连接了 Core 上下文和材料来源,是表型数据的入口,也可以成为后续 genotyping sample 的来源。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ----------------------- | ------------------------------------ | ------------------------- | ----------------- | -------------------------------------------------- |
| `id` | 观测单元主键 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `observation_unit_name` | 观测单元名称,如 Plot-001、Plant-003 | 用户录入/批量生成 | 文本框 | 必填;同一 study 内建议唯一 |
| `observation_unitpui` | 观测单元永久唯一标识 | 用户录入/系统生成 | 文本框/URL 输入框 | 可选;若填写建议唯一 |
| `crop_id` | 作物 | 由 study 自动带出,可只读 | 作物选择器/只读 | 可选但建议有;需与 study.crop 一致 |
| `cross_id` | 材料来源 cross | 从 cross_entity 选择 | Cross 选择器 | 可选;与 germplasm/seed_lot 至少建议选一个材料来源 |
| `germplasm_id` | 材料来源 germplasm | 从 germplasm 选择 | 材料选择器 | 可选;常用字段 |
| `program_id` | 项目 | 由 study 自动带出 | 项目选择器/只读 | 可选但建议有;需与 study.program 一致 |
| `seed_lot_id` | 种子批次来源 | 从 seed_lot 选择 | SeedLot 选择器 | 可选;田间播种建议填写 |
| `study_id` | 所属 study | 从 study 选择或工作台带出 | Study 选择器/隐藏 | 必选 |
| `trial_id` | 所属 trial | 由 study 自动带出 | Trial 选择器/只读 | 可选但建议有;需与 study.trial 一致 |
### 录入建议
- 推荐从 Study 表型工作台批量生成,不建议逐条手工建。
- 必须支持田间布局字段field、block、rep、plot、row、column、plant。若当前表没有这些字段应通过 position/level 附属表或 additional_info 保存。
- `germplasm_id` 代表材料身份;`seed_lot_id` 代表实际播种批次;二者建议都保留。
- 选择 seed_lot 时可自动带出 germplasm但不应强行覆盖用户选择。
### 观测层级建议
| 层级 | 含义 | 例子 |
| ------ | -------------- | ---------- |
| field | 田块或试验场 | Field-01 |
| block | 区组 | Block-01 |
| rep | 重复 | Rep-01 |
| plot | 小区 | Plot-001 |
| plant | 单株 | Plant-001 |
| sample | 样本级观测对象 | Sample-001 |
---
## 6.9 event 事件
### 业务说明
`event` 记录发生在 study 或 observation_unit 上的离散事件。它既可以是处理的一部分,也可以是影响结果的外部背景。例如播种、移栽、施肥、灌溉、打药、收获、采样、暴雨、台风、病害暴发等。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------- | ---------------------------------------------------------- | ------------------------- | ----------------- | -------------------------- |
| `id` | 事件主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `event_description` | 事件说明,如“台风后倒伏严重” | 用户录入 | 多行文本 | 可选;异常事件建议必填 |
| `event_type` | 事件类型名称,如 fertilizer、irrigation、planting、harvest | 用户选择/录入 | 下拉框/文本框 | 必填;建议使用事件类型字典 |
| `event_type_db_id` | 事件类型 ID可引用标准事件类型字典 | 用户选择/系统带出 | 选择器/隐藏 | 可选 |
| `study_id` | 事件所属 study | 从 study 选择或工作台带出 | Study 选择器/隐藏 | 必选 |
### 录入建议
- 在 Study 表型工作台的“事件”Tab 中维护。
- 新增事件时先选择作用范围:整个 study 或部分 observation_unit。
- 如果只作用于部分 plot/plant必须通过 `event_observation_units` 记录范围。
- 事件发生时间字段在你当前表结构中未显式列出,但业务上非常重要;如果数据库已有 event_date/time 字段,应强制录入;如果没有,建议补字段或放入 additional_info。
---
## 6.10 event_param 事件参数
### 业务说明
`event_param` 是事件的参数明细。例如施肥事件可以有肥料类型、剂量、单位;灌溉事件可以有水量、持续时间;打药事件可以有药剂名称、浓度、剂量。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------- | ------------------------------------- | ----------------- | ----------------- | ------------------------- |
| `id` | 事件参数主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `code` | 参数代码,如 N_RATE、IRR_VOL | 用户录入/字典带出 | 文本框 | 可选;同一事件内建议唯一 |
| `description` | 参数解释 | 用户录入 | 多行文本 | 可选 |
| `key` | 参数键,如 fertilizerType、dose、unit | 用户录入/字典选择 | 文本框 | 必填;同一事件内不应重复 |
| `name` | 参数名称,如 氮肥施用量 | 用户录入 | 文本框 | 可选 |
| `rdf_value` | RDF 或本体语义值 | 用户录入/系统带出 | 文本框 | 可选;普通业务可隐藏 |
| `units` | 参数单位,如 kg/ha、mm、L | 用户选择/录入 | 单位选择器 | 可选;数值参数建议必填 |
| `value` | 参数值 | 用户录入 | 动态控件 | 可选;若 key 需要值则必填 |
| `value_description` | 参数值说明 | 用户录入 | 多行文本 | 可选 |
| `event_id` | 所属事件 | 由事件详情页带出 | Event 选择器/隐藏 | 必选 |
### 录入建议
- 前端以键值表格方式维护。
- 可以为常用事件类型配置参数模板,例如 fertilizer 事件默认出现 fertilizerType、amount、units。
- 参数值是否必填取决于事件类型配置。
---
## 6.11 event_observation_units 事件作用范围
### 业务说明
`event_observation_units` 记录事件作用到哪些 observation_unit。比如一次施肥作用于整个 study则可以不逐个绑定如果某个处理只作用于部分 plot则必须明确绑定。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ---------------------- | ------------ | --------------------------------------- | ----------------- | ------------------------ |
| `event_entity_id` | 事件 | 由事件详情页带出 | Event 选择器/隐藏 | 必选 |
| `observation_units_id` | 事件作用对象 | 从当前 study 的 observation_unit 中选择 | 多选器/表格 | 必选;必须属于同一 study |
### 录入建议
- 支持按 block、rep、plot 范围批量选择。
- 如果事件作用于整个 study可以在 event 上记录 scope=study不必生成大量关系记录。
- 如果 event_observation_units 有记录,所有 observation_units 必须属于 event.study_id。
---
## 6.12 image 图像
### 业务说明
`image` 用于保存图片或图片元数据。图片可以是人工拍摄、无人机、固定相机、显微图、病害图片等。图片可以直接关联 observation_unit也可以通过 `image_observations` 关联具体 observation。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | ------------------------------------- | ------------------------ | ------------------- | -------------------------------------- |
| `id` | 图片主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `copyright` | 图片版权或授权说明 | 用户录入 | 文本框 | 可选 |
| `description` | 图片说明如“Plot-003 台风后倒伏照片” | 用户录入 | 多行文本 | 可选,建议填写 |
| `image_data` | 图片二进制内容 | 用户上传 | 文件上传 | 与 `imageurl` 至少一个;限制大小和格式 |
| `image_file_name` | 图片文件名 | 上传时自动填充 | 只读/文本框 | 可选;上传时自动生成 |
| `image_file_size` | 文件大小 | 上传时自动识别 | 只读 | 可选;应限制最大大小 |
| `image_height` | 图片高度 | 上传后自动识别 | 只读/数字框 | 可选;必须为非负整数 |
| `imagemimetype` | MIME 类型,如 image/jpeg、image/png | 上传后自动识别 | 只读/下拉框 | 可选;只允许图片 MIME 类型 |
| `imageurl` | 外部图片 URL 或对象存储 URL | 用户填写/上传后生成 | URL 输入框 | 与 `image_data` 至少一个URL 格式校验 |
| `image_width` | 图片宽度 | 上传后自动识别 | 只读/数字框 | 可选;必须为非负整数 |
| `name` | 图片名称 | 用户录入/文件名带出 | 文本框 | 必填 |
| `time_stamp` | 拍摄或上传时间 | 用户录入/系统默认 | 日期时间选择器 | 可选;建议填写拍摄时间 |
| `coordinates_id` | 图片坐标 | 地图取点/坐标对象选择 | 地图控件/坐标选择器 | 可选;建议支持 Point/Polygon |
| `observation_unit_id` | 所属观测单元 | 从 observation_unit 选择 | 观测单元选择器 | 可选;若填写应属于同一 study 上下文 |
### 录入建议
- 推荐图片文件存对象存储,数据库只存 URL 和元数据,避免 bytea 过大。
- 手机拍照或无人机导入时应自动提取文件名、大小、宽高、MIME、拍摄时间和 GPS 信息。
- 对无人机俯拍图,可保存图像中心点,也可保存图像覆盖范围 Polygon。
---
## 6.13 image_observations 图片与观测值关系
### 业务说明
`image_observations` 表示某张图片支持或关联哪些 observation。比如一张倒伏照片可以关联一个 plot 的倒伏等级 observation。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ----------------- | ---------- | ------------------- | ------------------ | ---------------------------------------- |
| `image_entity_id` | 图片 | 由图片详情页带出 | Image 选择器/隐藏 | 必选 |
| `observations_id` | 关联观测值 | 从 observation 选择 | Observation 选择器 | 必选;建议与图片的 observation_unit 一致 |
### 录入建议
- 图片详情页支持关联多个 observation。
- observation 详情页也支持反向查看相关图片。
- 如果图片已绑定 observation_unit则关联 observation 时应过滤为同一 observation_unit 下的 observation。
---
## 6.14 observation 观测值
### 业务说明
`observation` 是 Phenotyping 模块最核心的事实表。它表示“某个 observation_unit 在某个 observation_variable 上的一次实际采集结果”。
```text
observation_unit = Plot-003
observation_variable = 株高-尺测法-cm
value = 112.5
observation_time_stamp = 2026-07-12 09:30
```
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------------- | ------------------ | ---------------------------------------- | ------------------- | ------------------------------------------ |
| `id` | 观测值主键 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `collector` | 采集人 | 用户选择/登录用户带出 | 人员选择器/文本框 | 可选;人工采集建议填写 |
| `observation_time_stamp` | 观测时间 | 用户录入/采集设备带出 | 日期时间选择器 | 建议必填;按 ISO 8601 保存 |
| `uploaded_by` | 上传人 | 登录用户自动写入 | 只读 | 可选;批量导入时自动填 |
| `value` | 实际观测值 | 用户录入/导入/设备上传 | 动态控件 | 必填;按 variable.scale 校验 |
| `crop_id` | 作物上下文 | 由 observation_unit 或 study 自动带出 | 只读/隐藏 | 可选但建议保存;需与 observation_unit 一致 |
| `geo_coordinates_id` | 观测发生坐标 | 地图取点/设备 GPS | 地图控件/坐标选择器 | 可选;坐标需合法 |
| `observation_unit_id` | 被观测对象 | 从 observation_unit 选择或矩阵行带出 | 观测单元选择器/隐藏 | 必选 |
| `observation_variable_id` | 观测变量 | 从 observation_variable 选择或矩阵列带出 | 变量选择器/隐藏 | 必选 |
| `program_id` | 项目上下文 | 由 observation_unit/study 自动带出 | 只读/隐藏 | 可选但建议保存 |
| `season_id` | 季节 | 从 study season 或用户选择 | Season 选择器 | 可选;多季节 study 建议填写 |
| `study_id` | 所属 study | 由 observation_unit 自动带出 | 只读/隐藏 | 必选;需与 observation_unit.study 一致 |
| `trial_id` | 所属 trial | 由 observation_unit/study 自动带出 | 只读/隐藏 | 可选但建议保存 |
### 录入建议
- 普通录入优先走矩阵:行是 observation_unit列是 observation_variable。
- 单条表单只用于补录、纠错、详情编辑。
- value 必须使用 observation_variable.scale 决定控件和校验:
- 数值型:数字、小数位、范围;
- 分类型:只能从 valid category 中选;
- 日期型:日期格式;
- 布尔型true/false
- 文本型:长度限制。
- 同一 `observation_unit_id + observation_variable_id + observation_time_stamp` 可允许重复,但必须在页面展示重复标记、采集人和上传来源。
---
# 7. 跨表联动规则
| 场景 | 联动规则 |
| ------------------------- | ------------------------------------------------------------ |
| 创建 observation_variable | 必须先选择 trait、method、scale系统自动生成变量名称 |
| 配置 study_variable | 变量选择器优先展示同 crop 或未限定 crop 的变量 |
| 创建 observation_unit | 选择 study 后自动带出 crop、program、trial |
| 选择 seed_lot | 可自动带出 germplasm但允许用户确认 |
| 录入 observation | 选择 observation_unit 后自动带出 study、trial、program、crop |
| 录入 observation.value | 根据 observation_variable.scale 动态渲染控件和校验 |
| 新增 event | 从 study 工作台进入时自动带出 study_id |
| 选择事件作用范围 | 只能选择同一 study 下的 observation_unit |
| 上传 image | 如果从 observation_unit 详情页上传,自动绑定 observation_unit_id |
| 关联 image_observations | 如果图片已绑定 observation_unit只能关联同 observation_unit 的 observation |
---
# 8. 删除和停用规则
| 对象 | 删除限制 |
| -------------------- | ------------------------------------------------------------ |
| ontology | 已被 trait/method/scale/variable 引用时不可物理删除 |
| trait | 已被 observation_variable 或 germplasm_attribute_definition 引用时不可物理删除 |
| method | 已被 observation_variable 引用时不可物理删除 |
| scale | 已被 observation_variable 或 attribute definition 引用时不可物理删除 |
| observation_variable | 已产生 observation 时不可删除,只能停用或设为 obsolete |
| study_variable | 若已有该变量的 observation不允许直接移除 |
| observation_unit | 已有 observation、image、event、sample 时不可删除 |
| event | 已有关联 event_param 或 event_observation_units 时删除需级联确认或禁止 |
| image | 已关联 observation 时不可直接删除,应先解除关系或作废 |
| observation | 原始数据原则上不建议物理删除;建议保留修改历史或作废状态 |
---
# 9. 批量导入要求
## 9.1 ObservationUnit 导入模板
必需列建议:
```text
study_id 或 study_name
observation_unit_name
observation_level_name
observation_level_code
```
强烈建议列:
```text
germplasm_id 或 germplasm_name
seed_lot_id 或 seed_lot_name
block
rep
plot
row
column
```
## 9.2 ObservationVariable 导入模板
必需列建议:
```text
trait_name
method_name
scale_name
variable_name
```
可选列:
```text
trait_pui
method_pui
scale_pui
variable_pui
crop_name
growth_stage
status
```
## 9.3 Observation 矩阵导入模板
推荐格式:
```text
observation_unit_name,germplasm_name,collection_time,collector,PlantHeight_cm,LodgingScore_0_5,DiseaseScore_1_9
Plot-001,华占,2026-07-12T09:30:00+08:00,张三,112.5,1,2
Plot-002,抗倒伏A,2026-07-12T09:35:00+08:00,张三,98.3,0,1
```
导入规则:
1. 每个变量列必须能映射到一个 observation_variable。
2. 每一行必须能匹配一个 observation_unit。
3. 每个非空单元格生成一条 observation。
4. 空值不生成 observation除非用户选择“导入为空观测”。
5. 导入前必须预校验,错误报告包含行号、列名、错误原因、建议修复方式。
## 9.4 Image 导入模板
必需列建议:
```text
image_name
image_url 或 image_file
```
可选列:
```text
observation_unit_name
observation_id
capture_time
longitude
latitude
polygon
copyright
description
```
---
# 10. 后端接口建议
## 10.1 主数据接口
```text
GET /ontologies
POST /ontologies
GET /ontologies/{id}
PUT /ontologies/{id}
DELETE /ontologies/{id}
GET /traits
POST /traits
GET /traits/{id}
PUT /traits/{id}
GET /methods
POST /methods
GET /methods/{id}
PUT /methods/{id}
GET /scales
POST /scales
GET /scales/{id}
PUT /scales/{id}
GET /scales/{id}/valid-values
POST /scales/{id}/valid-values
GET /observation-variables
POST /observation-variables
GET /observation-variables/{id}
PUT /observation-variables/{id}
GET /studies/{studyId}/variables
POST /studies/{studyId}/variables
DELETE /studies/{studyId}/variables/{variableId}
GET /studies/{studyId}/observation-units
POST /studies/{studyId}/observation-units/batch-generate
POST /observation-units
PUT /observation-units/{id}
GET /studies/{studyId}/events
POST /studies/{studyId}/events
GET /events/{id}/params
POST /events/{id}/params
POST /events/{id}/observation-units
GET /images
POST /images
POST /images/upload
POST /images/{id}/observations
GET /observations
POST /observations
POST /observations/matrix
POST /observations/import
```
## 10.2 选择器接口
```text
GET /selectors/ontologies
GET /selectors/traits?ontologyId=&keyword=
GET /selectors/methods?ontologyId=&keyword=
GET /selectors/scales?ontologyId=&dataType=&keyword=
GET /selectors/observation-variables?cropId=&studyId=&keyword=
GET /selectors/studies?programId=&trialId=&keyword=
GET /selectors/observation-units?studyId=&level=&germplasmId=&keyword=
GET /selectors/events?studyId=&eventType=&keyword=
GET /selectors/images?observationUnitId=&keyword=
```
---
# 11. 测试验收清单
1. 创建 trait 时,`trait_name` 必填。
2. 创建 method 时,`name` 必填。
3. 创建 scale 时,`scale_name``data_type` 必填。
4. 分类型 scale 没有 valid value 时,应提示或禁止用于 observation_variable。
5. 创建 observation_variable 时trait、method、scale 必选。
6. observation_variable 名称可自动生成,也允许用户手动修改。
7. 配置 study_variable 时,不能重复绑定同一个 variable。
8. 创建 observation_unit 时study 必选。
9. 选择 study 后crop、program、trial 自动带出。
10. 同一 study 内 observation_unit_name 重复时提示。
11. observation_unit 至少建议绑定 germplasm、seed_lot、cross 中一个材料来源。
12. 创建 event 时study 和 event_type 必填。
13. event 绑定 observation_unit 时,所有 observation_unit 必须属于该 event 的 study。
14. 图片上传时image_data 和 imageurl 至少一个存在。
15. 图片 MIME 类型必须是允许的图片类型。
16. observation 创建时observation_unit、observation_variable、value 必填。
17. observation.value 必须按 scale.data_type 校验。
18. 数值型 value 必须在 valid_value_min 和 valid_value_max 范围内。
19. 分类型 value 必须属于 scale_valid_value_category。
20. observation 的 study/trial/program/crop 必须与 observation_unit 上下文一致。
21. 矩阵录入保存时,每个有效单元格生成一条 observation。
22. 导入失败时必须返回行号、字段、错误原因和修复建议。
23. 已产生 observation 的 observation_variable 不允许直接删除。
24. 已被 observation、image、event、sample 引用的 observation_unit 不允许物理删除。
25. 所有日期时间字段按 ISO 8601 保存,并保留时区或统一转换为 UTC。
---
# 12. 开发实现重点
1. `observation_variable` 是表型指标定义,不是观测值。
2. `observation_unit` 是被观测对象,不是实际测量值。
3. `observation` 才是真正的表型事实数据。
4. `value` 字段虽然数据库是字符串,但前端和后端必须按 scale 做类型校验。
5. `study` 是 Phenotyping 的核心上下文入口,几乎所有表型数据都应能追溯到 study。
6. 表型采集页面应优先做矩阵录入,不要只做单条 CRUD。
7. 图片不要直接塞数据库大字段为主,建议走对象存储 URL + 元数据。
8. Event 不只是备注,它是解释表型差异的重要上下文,应支持作用范围和参数化。

View File

@@ -0,0 +1,890 @@
# Genotyping 模块专业数据录入需求文档 V2
## 1. 文档目的
本文档用于指导 Genotyping 基因型模块的前端页面、后端接口、字段校验、文件导入、矩阵展示和测试验收设计。本文档不只描述数据库表关系,而是从真实基因型检测业务出发,解释每个对象和字段的业务意义、录入方式、控件建议、校验规则和上下游影响。
## 2. 模块定位
Genotyping 模块描述从田间或实验对象取样,到实验室检测,再到导入基因型结果矩阵的完整流程。
它的核心业务不是“录 allele_call 表”,而是:
```text
从哪个 study / observation_unit 取样
样本放在哪块 plate 的哪个 well
检测结果基于哪个 reference_set
结果文件包含哪些 variantset / variant
每个 sample 在每个 variant 上的 genotype 是什么
这些位点是否还映射到 genome_map / linkage_group
```
## 3. 核心业务主线
Genotyping 有三条主线:
```text
样本线study / observation_unit -> plate -> sample -> callset
位点线reference_set -> reference -> reference_bases -> variantset -> variant
结果线callset + variant -> allele_call
图谱线crop -> genome_map -> linkage_group -> marker_position -> variant
```
### 3.1 样本线
样本线回答:
```text
从哪个材料、哪个 plot、哪株植物或哪个样本来源取样
样本叫什么?
谁采的?什么时候采的?
放在哪块样本板哪个孔?
```
对应核心表:
```text
plate
sample
callset
```
### 3.2 位点线
位点线回答:
```text
检测结果基于哪个参考基因组?
参考序列有哪些 chromosome / scaffold / contig
检测了哪些 SNP / Indel / SV 位点?
这些位点属于哪个 variantset
```
对应核心表:
```text
reference_set
reference
reference_bases
variantset
variant
```
### 3.3 结果线
结果线回答:
```text
某个 sample 在某个 variant 上的 genotype 是什么?
测序深度是多少?
是否 phased
是否有 likelihood
```
对应核心表:
```text
callset
allele_call
```
### 3.4 图谱线
图谱线回答:
```text
某个位点在遗传图谱或物理图谱上的位置是什么?
它属于哪个 linkage group / chromosome / scaffold
单位是 cM 还是 Mb
```
对应核心表:
```text
genome_map
linkage_group
marker_position
```
---
# 4. 推荐业务流程
```text
1. 准备 Core / Phenotyping 上游数据crop、program、trial、study、observation_unit
2. 从 observation_unit 或 study 批量生成 sample
3. 创建 plate并把 sample 分配到 well / row / column
4. 送检或接收实验室结果文件
5. 创建 reference_set维护 reference / reference_bases
6. 创建 variantset导入 variant
7. 为 sample 生成 callset
8. 绑定 callset 与 variantset
9. 导入 allele_call形成 sample × variant 基因型矩阵
10. 如有遗传图谱,创建 genome_map / linkage_group / marker_position
11. 做结果质控缺失率、重复位点、重复样本、read depth、genotype 格式、reference 一致性
```
---
# 5. 推荐页面形态
## 5.1 Genotyping 工作台
建议以 study 或 genotyping project 为入口组织页面:
```text
Genotyping 工作台
├─ 样本管理sample 列表、从 observation_unit 生成 sample
├─ 样本板plate 列表、96/384 孔位布局
├─ 参考基因组reference_set、reference、reference_bases
├─ 变异集合variantset、variant、analysis、format
├─ 检测结果callset、allele_call、genotype matrix
├─ 遗传图谱genome_map、linkage_group、marker_position
├─ 导入导出sample 模板、VCF/Hapmap/CSV、allele matrix
└─ 质控:缺失率、重复率、深度、位点过滤、样本过滤
```
## 5.2 Plate 孔位布局页
样本板不建议只做普通 CRUD应提供 96 孔 / 384 孔布局视图:
```text
Plate-96-001
01 02 03 04
A SAMPLE-001 SAMPLE-002 SAMPLE-003 SAMPLE-004
B SAMPLE-013 SAMPLE-014 SAMPLE-015 SAMPLE-016
C SAMPLE-025 SAMPLE-026 EMPTY EMPTY
...
```
用户在孔位上放置 sample系统保存
```text
sample.plate_id
sample.well
sample.plate_row
sample.plate_column
```
## 5.3 Genotype Matrix 结果页
`allele_call` 是基因型矩阵中的一个格子:
| sample / callset | SNP001 | SNP002 | SNP003 |
| ---------------- | ------ | ------ | ------ |
| SAMPLE-001 | A/G | C/C | T/G |
| SAMPLE-002 | A/A | C/T | T/T |
| SAMPLE-003 | ./ . | C/C | T/G |
导入时:
```text
样本列 / 样本行 -> sample / callset
位点列 / 位点行 -> variant
单元格 genotype -> allele_call.genotype
```
---
# 6. 字段级专业录入需求
## 6.1 plate 样本板
### 业务说明
`plate` 用于管理承载样本的样本板或样本容器组。真实业务里,它通常是一块 96 孔板、384 孔板也可能是一组试管或实验室提交批次。plate 的核心价值是支持实验室送样、扫码、孔位追踪和结果回填。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------------------- | ----------------------------------------------- | ------------------------------ | ----------------- | -------------------------------------------- |
| `id` | 样本板主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一;编辑不可修改 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许前端手填 |
| `client_plate_barcode` | 客户侧或外部实验室给出的板条码 | 用户录入/扫码/导入 | 文本框/扫码框 | 可选;建议唯一 |
| `client_plate_db_id` | 客户侧系统中的 plate ID | 用户录入/导入 | 文本框 | 可选;用于外部系统对接 |
| `plate_barcode` | 本系统样本板条码 | 系统生成/扫码录入 | 文本框/扫码框 | 可选;建议唯一 |
| `plate_format` | 样本板规格,如 96、384 | 用户选择 | 下拉框 | 可选;若填写,应限制为系统支持规格 |
| `plate_name` | 样本板名称,如 Plate-2026-001 | 用户录入/系统生成 | 文本框 | 与 barcode 至少一个必填 |
| `sample_submission_format` | 样本提交格式,如 DNA、tissue、seed、tube、plate | 用户选择 | 下拉框 | 可选;建议枚举化 |
| `sample_type` | 该板默认样本类型 | 用户选择 | 下拉框 | 可选;可作为 sample 默认值 |
| `status_time_stamp` | 样本板状态更新时间 | 系统自动写入 | 只读日期时间 | 可选;状态变化时自动更新 |
| `program_id` | 所属项目 | 从 program 选择或由 study 带出 | 项目选择器/只读 | 可选;若 study 已选,应与 study.program 一致 |
| `study_id` | 所属 study | 从 study 选择 | Study 选择器 | 可选;若选择则带出 trial/program |
| `submission_id` | 外部 vendor submission 或送检单 ID | 用户录入/选择 | 选择器/文本框 | 可选;用于实验室送检对接 |
| `trial_id` | 所属 trial | 从 trial 选择或由 study 带出 | Trial 选择器/只读 | 可选;若 study 已选,应与 study.trial 一致 |
### 录入建议
- 创建 plate 时,用户先选择 `plate_format`,系统生成孔位矩阵。
- 如果从 study 工作台创建,`program_id``trial_id``study_id` 自动带出。
- 如果通过实验室返回文件创建,应优先用 `plate_barcode``client_plate_barcode` 匹配。
### 验收标准
1. `plate_name``plate_barcode` 至少填写一个。
2. 同一系统中 `plate_barcode` 不应重复。
3. 选择 study 后trial/program 自动带出且保持一致。
4. plate_format 为 96 时,只允许 A01-H12384 时只允许 A01-P24。
---
## 6.2 sample 样本
### 业务说明
`sample` 是 genotyping 流程的样本入口表示从田间、温室、实验室或库存材料中取出的一个物理生物样本例如叶片、种子、DNA、组织、提取液等。sample 应尽可能关联 observation_unit这样才能把表型和基因型打通。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | ------------------------------------ | -------------------------------------- | ---------------------- | --------------------------------------------------------- |
| `id` | 样本主键,系统内部唯一标识 | 系统生成;导入可指定 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `concentration` | 样本浓度,如 DNA 浓度 50 ng/µL | 用户录入/仪器导入 | 数字+单位输入 | 可选;建议拆分数值和单位,当前字段可暂存文本 |
| `plate_column` | 样本在 plate 中的列号 | 孔位选择自动生成 | 数字输入/孔位控件 | 绑定 plate 时必填或由 well 解析;范围受 plate_format 限制 |
| `plate_row` | 样本在 plate 中的行号 | 孔位选择自动生成 | 下拉/孔位控件 | 绑定 plate 时必填或由 well 解析;范围受 plate_format 限制 |
| `sample_barcode` | 样本条码 | 扫码/系统生成/导入 | 文本框/扫码框 | 可选;建议唯一 |
| `sample_description` | 样本说明 | 用户录入 | 多行文本 | 可选 |
| `sample_group_db_id` | 样本组 ID用于批次、送检组或处理组 | 用户选择/导入 | 选择器/文本框 | 可选 |
| `sample_name` | 样本名称,如 SAMPLE-001 | 用户录入/系统生成 | 文本框 | 必填;同一 study 下建议唯一 |
| `samplepui` | 样本永久唯一标识 | 用户录入/系统生成 | 文本框/URL 输入框 | 可选;若填写建议唯一 |
| `sample_timestamp` | 采样时间 | 用户录入/设备带出 | 日期时间选择器 | 可选;建议必填 |
| `sample_type` | 样本类型,如 tissue、DNA、seed、leaf | 用户选择 | 下拉框 | 可选;建议枚举化 |
| `taken_by` | 采样人 | 用户选择/登录用户带出 | 人员选择器/文本框 | 可选 |
| `tissue_type` | 组织类型,如 leaf、root、grain、stem | 用户选择/录入 | 下拉框/文本框 | 可选;样本为组织时建议填写 |
| `volume` | 样本体积,如 50 µL | 用户录入/仪器导入 | 数字+单位输入 | 可选;当前字段可暂存文本 |
| `well` | 孔位,如 A01、H12 | 孔位布局选择 | 孔位选择器 | 绑定 plate 时建议必填;同一 plate 内唯一 |
| `observation_unit_id` | 来源观测单元 | 从 observation_unit 选择或批量生成带出 | ObservationUnit 选择器 | 可选但强烈建议填写;必须存在 |
| `plate_id` | 所属样本板 | 从 plate 选择 | Plate 选择器 | 可选;若填写需校验孔位唯一 |
| `program_id` | 项目上下文 | 由 observation_unit/study 带出 | 只读/隐藏 | 可选但建议保存;需与 study 一致 |
| `study_id` | 所属 study | 由 observation_unit 带出 | Study 选择器/只读 | 可选但建议保存;若有 observation_unit 必须一致 |
| `taxon_id_id` | 分类单元 ID | 从 taxon 选择 | Taxon 选择器 | 可选 |
| `trial_id` | 所属 trial | 由 observation_unit/study 带出 | Trial 选择器/只读 | 可选但建议保存 |
### 录入建议
- 推荐从 observation_unit 批量生成 sample一行 observation_unit 生成一个或多个 sample。
- 如果样本放入 plate应通过孔位布局页设置 well不建议手填 A01。
- `well``plate_row``plate_column` 应保持一致well=A01则 row=Acolumn=1。
- sample 是物理样本,不是 genotype 结果;结果必须进入 callset / allele_call。
### 验收标准
1. `sample_name` 必填。
2. 同一 plate 中 `well` 不允许重复。
3. 绑定 observation_unit 时study/trial/program 自动带出。
4. 绑定 plate 时well 必须符合 plate_format。
5. 样本已有 callset 时,不允许物理删除。
---
## 6.3 reference_set 参考集
### 业务说明
`reference_set` 表示一个参考基因组集合,也就是一套 reference genome assembly。它定义 variant 坐标体系。不同参考基因组版本之间的位点坐标不可随意混用。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------------------- | ---------------------------------------------- | ------------------ | ----------------- | ---------------------------- |
| `id` | 参考集主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `assemblypui` | 参考组装永久标识,如 DOI、INSDC accession、URI | 用户录入/导入 | 文本框/URL 输入框 | 可选;建议唯一 |
| `description` | 参考集说明 | 用户录入 | 多行文本 | 可选 |
| `is_derived` | 是否由其他参考集派生 | 用户选择 | 开关 | 可选 |
| `md5checksum` | 参考集校验值 | 用户录入/文件计算 | 文本框 | 可选;若填写应符合 MD5 格式 |
| `reference_set_name` | 参考集名称,如 IRGSP-1.0、B73 RefGen_v5 | 用户录入 | 文本框 | 必填;建议唯一 |
| `sourceuri` | 参考集来源 URI 或下载地址 | 用户录入 | URL 输入框 | 可选;校验 URL |
| `species_ontology_term` | 物种本体术语名称 | 用户录入/选择 | 文本框/本体选择器 | 可选 |
| `species_ontology_termuri` | 物种本体 URI | 用户录入/本体带出 | URL 输入框 | 可选;校验 URL |
| `source_germplasm_id` | 参考基因组来源材料 | 从 germplasm 选择 | Germplasm 选择器 | 可选;必须引用存在 germplasm |
### 录入建议
- reference_set 是 variantset 和 variant 的上游,不能随意改。
- 如果导入 VCF必须先明确该 VCF 的 reference_set。
- 同一 crop 可能存在多个 reference_set应在页面上清楚显示版本和来源。
---
## 6.4 reference 参考序列
### 业务说明
`reference` 是 reference_set 中的一条参考序列,例如 chromosome、contig、scaffold。variant 的坐标通常是基于某条 reference 的位置。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------- | --------------------------------------------- | --------------------- | ------------------- | ------------------------------- |
| `id` | 参考序列主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `length` | 序列长度 | 用户录入/FASTA 导入 | 数字输入框 | 必填或建议必填;非负整数 |
| `md5checksum` | 单条 reference 的 MD5 校验值 | 用户录入/文件计算 | 文本框 | 可选;若填写应符合 MD5 格式 |
| `reference_name` | 参考序列名称,如 chr1、1、Chr01、scaffold_001 | 用户录入/FASTA 导入 | 文本框 | 必填;同一 reference_set 内唯一 |
| `source_divergence` | 与来源序列的差异程度 | 用户录入/导入 | 数字输入框 | 可选;范围建议 0-1 或按业务定义 |
| `reference_set_id` | 所属 reference_set | 从 reference_set 选择 | ReferenceSet 选择器 | 必选 |
### 录入建议
- 建议通过 FASTA 索引文件批量导入 chromosome / contig。
- 同一 reference_set 下 reference_name 必须唯一。
- variant 导入时,文件中的 CHROM 必须能匹配 reference.reference_name。
---
## 6.5 reference_bases 参考片段
### 业务说明
`reference_bases` 保存 reference 的序列片段或分页。通常不建议人工录入,而是由 FASTA 导入或接口生成。对于大型基因组,不建议把完整序列全部塞入普通业务表,除非系统确实需要序列查询。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------- | ------------------ | ------------------- | --------------------- | --------------------------------------- |
| `id` | 参考片段主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `bases` | 碱基序列片段 | FASTA 导入/接口生成 | 多行文本/只读 | 必填;只能包含合法碱基字符 A/C/G/T/N 等 |
| `page_number` | 分页编号 | 系统生成 | 数字输入/只读 | 必填;非负整数;同一 reference 下唯一 |
| `reference_id` | 所属 reference | 由导入带出 | Reference 选择器/隐藏 | 必选 |
### 录入建议
- 不建议前端做手工新增入口,只提供查看和导入。
- 每页 bases 长度应固定或可配置,例如 2KB、10KB、100KB。
- 导入时校验页码连续性和字符合法性。
---
## 6.6 variantset 变异集合
### 业务说明
`variantset` 是一批 variant 和相关 call 的集合,通常对应一个 VCF 文件、一个 SNP 芯片结果、一个测序分析批次或一个 study 的基因型数据集。它必须明确 reference_set否则 variant 坐标无意义。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------ | ---------------------------------------------- | --------------------- | ------------------- | ----------------------------------------- |
| `id` | 变异集合主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `variant_set_name` | 变异集合名称,如 Rice-2026-SNPSet、VCF-Run-001 | 用户录入/文件名带出 | 文本框 | 必填;建议唯一 |
| `reference_set_id` | 使用的参考集 | 从 reference_set 选择 | ReferenceSet 选择器 | 必选 |
| `study_id` | 关联 study | 从 study 选择 | Study 选择器 | 可选;若填写,应与样本来源 study 保持一致 |
### 录入建议
- 导入 VCF/CSV 前必须先选择 reference_set。
- 如果 variantset 来源于某次 study 的样本检测,应填写 study_id。
- variantset 详情页应展示variant 数量、callset 数量、分析信息、文件格式、导入状态。
---
## 6.7 variant 变异位点
### 业务说明
`variant` 表示一个遗传序列上的位点或区间,可以是 SNP、INDEL、SV也可以是传统 marker。它通常由 reference_set、reference_name、start/end、reference_bases、alternate_bases 共同定义。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------ | ------------------------------------ | ------------------------------------- | ------------------------ | ------------------------------------------------ |
| `id` | 变异主键 | 系统生成/导入 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `created` | 创建时间 | 系统默认/文件导入 | 日期时间只读 | 可选;默认当前时间 |
| `variant_end` | 变异结束位置 | 文件导入/用户录入 | 数字输入框 | 可选;非负整数;不小于 start |
| `filters_applied` | 是否执行过过滤 | 文件导入/系统写入 | 开关/只读 | 可选 |
| `filters_passed` | 是否通过过滤 | 文件导入/系统写入 | 开关/只读 | 可选 |
| `reference_bases` | 参考等位基因或参考碱基 | 文件导入/用户录入 | 文本框 | 可选;合法碱基字符 |
| `variant_start` | 变异起始位置 | 文件导入/用户录入 | 数字输入框 | 必填;非负整数 |
| `svlen` | 结构变异长度 | 文件导入/用户录入 | 数字输入框 | 可选SV 类型时建议填写 |
| `updated` | 更新时间 | 系统自动写入 | 只读日期时间 | 自动更新 |
| `variant_name` | 变异名称,如 SNP001、chr1_123456_A_G | 用户录入/自动生成 | 文本框 | 必填或由位置自动生成;同一 variantset 下建议唯一 |
| `variant_type` | 变异类型,如 SNP、INDEL、SV、marker | 用户选择/文件导入 | 下拉框 | 可选;建议枚举化 |
| `reference_set_id` | 所属参考集 | 从 reference_set 选择/variantset 带出 | ReferenceSet 选择器/隐藏 | 必选;需与 variantset.reference_set 一致 |
| `variant_set_id` | 所属变异集合 | 从 variantset 选择 | VariantSet 选择器/隐藏 | 必选 |
### 录入建议
- 大量 variant 应通过 VCF、HapMap、CSV 批量导入,不建议手工录入。
- 如果当前表缺少 alternate_bases、reference_name 字段,应在导入逻辑或扩展表中补齐,否则变异定义不完整。
- variant 的唯一性建议使用:`reference_set_id + reference_name + start + reference_bases + alternate_bases`
---
## 6.8 callset 调用集合
### 业务说明
`callset` 表示某个 sample 参与一次测序、芯片或分析事件后形成的一组 genotype calls。多数情况下一个 sample 对应一个 callset但如果同一个 sample 多次送检或使用不同分析流程,则可以有多个 callset。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------- | -------------------------------- | ----------------------- | ------------- | ---------------------------- |
| `id` | CallSet 主键 | 系统生成/导入 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `call_set_name` | 调用集合名称,如 SAMPLE-001_Run1 | 用户录入/系统生成 | 文本框 | 必填;同一 sample 下建议唯一 |
| `created` | 创建时间 | 系统默认/导入 | 日期时间 | 默认当前时间 |
| `updated` | 更新时间 | 系统自动写入 | 只读日期时间 | 自动更新 |
| `sample_id` | 所属样本 | 从 sample 选择/导入匹配 | Sample 选择器 | 必选 |
### 录入建议
- 从 genotype 文件导入时,可以按样本列自动创建 callset。
- callset 需要通过 `callset_variant_sets` 绑定它覆盖的 variantset。
- sample 详情页应展示 callset 列表和每个 callset 的 variantset、call 数量、导入时间。
---
## 6.9 callset_variant_sets 调用集合与变异集合关系
### 业务说明
`callset_variant_sets` 表示某个 callset 覆盖哪些 variantset。它是一次检测结果和一批位点集合之间的关系。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ----------------- | -------- | ------------------------------- | ---------------------- | -------- |
| `call_sets_id` | 调用集合 | 从 callset 选择/导入自动创建 | CallSet 选择器/隐藏 | 必选 |
| `variant_sets_id` | 变异集合 | 从 variantset 选择/导入自动绑定 | VariantSet 选择器/隐藏 | 必选 |
### 录入建议
- 从 variantset 导入 genotype matrix 时,系统应自动绑定所有新建 callset 到该 variantset。
- 同一 callset + variantset 不允许重复。
---
## 6.10 allele_call 基因型结果
### 业务说明
`allele_call` 是最终基因型事实数据,表示某个 callset 在某个 variant 上的 genotype 结果。它是 genotype matrix 的一个格子。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | --------------------------------------------------------- | ------------------------ | --------------------- | ------------------------------ |
| `id` | 基因型结果主键 | 系统生成/导入 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `genotype` | genotype 结果,如 A/G、0/1、0 | 1、AA、./. | 文件导入/少量手工修正 | 文本框/基因型控件 |
| `genotype_likelihood` | genotype likelihood表示不同 genotype 的可能性或置信信息 | 文件导入 | 数字输入框 | 可选;范围和含义按导入格式定义 |
| `phase_set` | phase set用于 phased genotype 分组 | 文件导入 | 文本框 | 可选phased 数据时使用 |
| `read_depth` | 测序深度 | 文件导入/用户录入 | 数字输入框 | 可选;非负整数 |
| `call_set_id` | 所属 callset | 从 callset 选择/导入匹配 | CallSet 选择器/隐藏 | 必选 |
| `variant_id` | 对应 variant | 从 variant 选择/导入匹配 | Variant 选择器/隐藏 | 必选 |
### 录入建议
- allele_call 不建议大量手工新增,主入口应该是 VCF/HapMap/CSV 文件导入。
- 同一 `call_set_id + variant_id` 不应重复。
- `genotype` 需要兼容常见格式:
- Allele 格式A/G、C/C、T/G
- VCF 编码0/0、0/1、1/1、0|1
- 缺失:./.、NA、N、--,具体缺失字符串由 variantset_format 配置。
- 导入后应支持按 sample/callset 或 variant 两种视角查询。
---
## 6.11 variantset_analysis 变异集合分析信息
### 业务说明
`variantset_analysis` 记录 variantset 的分析流程、软件、版本、参数、时间等信息。它是数据可追溯的重要来源。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ---------------- | ------------------------------------------------------------ | ------------------ | ---------------------- | -------------- |
| `id` | 分析记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `analysis_name` | 分析名称,如 GATK SNP Calling Run 2026-01 | 用户录入/导入带出 | 文本框 | 必填 |
| `created` | 创建时间 | 系统默认 | 日期时间 | 默认当前时间 |
| `description` | 分析说明,如软件版本、过滤参数 | 用户录入 | 多行文本 | 可选,建议填写 |
| `type` | 分析类型,如 SNP calling、imputation、filtering、chip genotyping | 用户选择/录入 | 下拉框/文本框 | 可选 |
| `updated` | 更新时间 | 系统自动写入 | 只读日期时间 | 自动更新 |
| `variant_set_id` | 所属 variantset | 从 variantset 选择 | VariantSet 选择器/隐藏 | 必选 |
### 录入建议
- 导入结果文件时,应生成或要求填写 analysis 信息。
- 推荐记录:软件名称、版本、参数、过滤阈值、执行人、执行日期。
---
## 6.12 variantset_format 变异集合文件格式
### 业务说明
`variantset_format` 描述 variantset 数据文件格式,例如 VCF、HapMap、CSV、TSV、DArTSeq 等。它影响 genotype 的解析方式、缺失值、phased/unphased 分隔符等。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| -------------------- | --------------------------------------------- | ------------------- | ---------------------- | ------------------------ |
| `id` | 格式记录主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `data_format` | 数据内部结构,如 VCF、HapMap、DArTSeq、matrix | 用户选择/文件识别 | 下拉框 | 可选;建议枚举化 |
| `expand_homozygotes` | 是否展开纯合 genotype例如 A -> A/A | 用户选择 | 开关 | 可选 |
| `file_format` | 文件 MIME 或外层格式,如 csv、tsv、zip、excel | 用户选择/文件识别 | 下拉框 | 可选 |
| `fileurl` | 原始结果文件链接 | 上传后生成/用户填写 | URL 输入框 | 可选URL 格式校验 |
| `sep_phased` | phased genotype 分隔符,如 `|` | 用户录入/默认 | 文本框 | 可选;通常为 `|` |
| `sep_unphased` | unphased genotype 分隔符,如 `/` | 用户录入/默认 | 文本框 | 可选;通常为 `/` |
| `unknown_string` | 缺失值字符串,如 `./.`、NA、-- | 用户录入/默认 | 文本框 | 可选;导入时用于缺失识别 |
| `variant_set_id` | 所属 variantset | 从 variantset 选择 | VariantSet 选择器/隐藏 | 必选 |
### 录入建议
- 文件上传时应自动识别 file_format 和 data_format。
- 缺失值字符串必须参与 allele_call 导入解析。
- 如果格式是 VCF应支持 phased/unphased 分隔符。
---
## 6.13 genome_map 遗传图谱
### 业务说明
`genome_map` 描述遗传图谱或物理图谱。遗传图谱通常单位为 cM物理图谱通常单位为 Mb 或 bp。它用于把 variant / marker 映射到 linkage group 上。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------ | ------------------------------ | ------------------ | ----------------- | ------------------------------------------ |
| `id` | 图谱主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `comments` | 图谱备注 | 用户录入 | 多行文本 | 可选 |
| `documentationurl` | 图谱说明文档或论文链接 | 用户录入 | URL 输入框 | 可选URL 格式校验 |
| `map_name` | 图谱名称 | 用户录入 | 文本框 | 必填;建议唯一 |
| `mappui` | 图谱永久唯一标识 | 用户录入 | 文本框/URL 输入框 | 可选;若填写建议唯一 |
| `published_date` | 发布时间 | 用户录入 | 日期选择器 | 可选 |
| `scientific_name` | 物种学名 | 用户录入 | 文本框 | 可选 |
| `type` | 图谱类型,如 Genetic、Physical | 用户选择 | 下拉框 | 可选;建议枚举 |
| `unit` | 图谱单位,如 cM、Mb、bp | 用户选择 | 单位选择器 | 可选Genetic 常用 cMPhysical 常用 Mb/bp |
| `crop_id` | 所属作物 | 从 crop 选择 | Crop 选择器 | 必选 |
### 录入建议
- 图谱命名应能体现是 consensus map、mapping population map、reference genome map 还是 pan-genome map。
- 图谱类型和单位必须匹配Genetic + cMPhysical + Mb/bp。
---
## 6.14 linkage_group 连锁群
### 业务说明
`linkage_group` 是 genome_map 中的分组。它可能对应 chromosome、scaffold、contig 或传统遗传连锁群。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| --------------------- | -------------------------------------- | ------------------ | --------------------- | ---------------------------- |
| `id` | 连锁群主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `linkage_group_name` | 连锁群名称,如 Chr1、LG1、scaffold_001 | 用户录入/导入 | 文本框 | 必填;同一 genome_map 下唯一 |
| `max_marker_position` | 该连锁群最大 marker 位置 | 用户录入/导入计算 | 数字输入框 | 可选;非负数 |
| `genome_map_id` | 所属 genome_map | 从 genome_map 选择 | GenomeMap 选择器/隐藏 | 必选 |
### 录入建议
- 在 genome_map 详情页维护。
- 支持从 marker_position 文件批量导入时自动创建 linkage_group。
- 若填写 max_marker_positionmarker_position.position 不得超过该值。
---
## 6.15 marker_position 图谱位置
### 业务说明
`marker_position` 把 variant 映射到某个 linkage_group 上的位置。它用于遗传图谱定位、QTL、marker 辅助选择和图谱展示。
### 字段说明
| 字段 | 业务意义 | 录入方式 | 控件建议 | 校验规则 |
| ------------------ | --------------------------------- | --------------------- | ------------------- | ---------------------------------------- |
| `id` | 图谱位置主键 | 系统生成 | 隐藏/只读 | 必填、唯一 |
| `auth_user_id` | 数据所属用户或租户 | 登录上下文自动写入 | 隐藏 | 不允许手填 |
| `position` | variant 在 linkage_group 上的位置 | 用户录入/文件导入 | 数字输入框 | 必填;非负;不得超过 max_marker_position |
| `linkage_group_id` | 所属 linkage_group | 从 linkage_group 选择 | LinkageGroup 选择器 | 必选 |
| `variant_id` | 对应 variant | 从 variant 选择 | Variant 选择器 | 必选 |
### 录入建议
- 不建议逐条手工录入,优先通过 map 文件批量导入。
- 同一 genome_map 下,同一 variant 不应重复映射,除非业务允许多图谱位置。
---
# 7. 跨表联动规则
| 场景 | 联动规则 |
| ------------------------------- | ------------------------------------------------------------ |
| 从 observation_unit 生成 sample | 自动带出 study、trial、program可带出 germplasm 信息用于展示 |
| 给 sample 分配 plate | 选择 well 后自动生成 plate_row、plate_column |
| 创建 plate | 选择 study 后自动带出 trial/program |
| 创建 variantset | 必须先选择 reference_set |
| 导入 variant | variant.reference_set 必须与 variantset.reference_set 一致 |
| 创建 callset | 必须选择 sample可按 sample_name 自动生成 call_set_name |
| 导入 allele_call | 先匹配 callset再匹配 variant匹配不到时进入错误报告 |
| 绑定 callset_variant_sets | 导入 genotype matrix 时自动绑定 callset 与 variantset |
| 创建 genome_map | 必须选择 crop |
| 创建 marker_position | variant 与 linkage_group 最好来自同一 crop/reference 背景 |
---
# 8. 删除和停用规则
| 对象 | 删除限制 |
| --------------- | ------------------------------------------------------- |
| plate | 已有 sample 时不可删除 |
| sample | 已有 callset 时不可删除 |
| reference_set | 已有 reference、variantset、variant 时不可删除 |
| reference | 已有 reference_bases 或 variant 引用时不可删除 |
| variantset | 已有 variant、callset 绑定或 allele_call 结果时不可删除 |
| variant | 已有 allele_call 或 marker_position 时不可删除 |
| callset | 已有 allele_call 时不可删除 |
| allele_call | 原始检测结果原则上不建议物理删除,应支持作废或版本化 |
| genome_map | 已有 linkage_group 时不可删除 |
| linkage_group | 已有 marker_position 时不可删除 |
| marker_position | 可删除,但需记录操作日志 |
---
# 9. 批量导入要求
## 9.1 Sample 导入模板
必需列建议:
```text
sample_name
study_id 或 study_name
```
强烈建议列:
```text
observation_unit_id 或 observation_unit_name
sample_barcode
sample_type
tissue_type
sample_timestamp
taken_by
plate_name
well
```
导入规则:
1. 如果提供 observation_unit系统自动带出 study/trial/program。
2. 如果提供 plate + well需校验同板孔位唯一。
3. 如果 plate 不存在,可根据配置自动创建或报错。
## 9.2 Plate 导入模板
```text
plate_name
plate_barcode
plate_format
study_name
sample_name
well
```
导入规则:
1. plate_format 决定合法 well 范围。
2. 同一 plate + well 不允许重复。
3. sample_name 不存在时,可配置是否自动创建 sample。
## 9.3 Variant 导入模板
建议支持 VCF / HapMap / CSV。
CSV 必需列建议:
```text
variant_name
reference_name
variant_start
reference_bases
alternate_bases
variant_type
variant_set_name
reference_set_name
```
导入规则:
1. 必须先选择或创建 reference_set。
2. CHROM / reference_name 必须能匹配 reference。
3. variant_start 必须非负。
4. alternate_bases 不能为空,除非只是导入传统 marker。
## 9.4 Genotype Matrix / Allele Call 导入模板
推荐矩阵格式:
```text
variant_name,SAMPLE-001,SAMPLE-002,SAMPLE-003
SNP001,A/G,A/A,./.
SNP002,C/C,C/T,C/C
SNP003,T/G,T/T,T/G
```
导入规则:
1. 每个样本列必须能匹配 sample 或 callset。
2. 每个 variant 行必须能匹配 variant。
3. 每个非空 genotype 单元格生成一条 allele_call。
4. 缺失值按 variantset_format.unknown_string 处理。
5. 同一 callset + variant 已存在时,根据导入策略选择覆盖、跳过或报错。
## 9.5 Genome Map 导入模板
```text
map_name
linkage_group_name
variant_name
position
unit
```
导入规则:
1. map_name 不存在时可配置自动创建 genome_map。
2. linkage_group 不存在时可配置自动创建。
3. variant 必须存在。
4. position 必须非负且不超过 max_marker_position。
---
# 10. 后端接口建议
## 10.1 主数据接口
```text
GET /plates
POST /plates
GET /plates/{id}
PUT /plates/{id}
GET /plates/{id}/layout
POST /plates/{id}/assign-samples
GET /samples
POST /samples
POST /samples/batch-from-observation-units
GET /samples/{id}
PUT /samples/{id}
GET /samples/{id}/callsets
GET /reference-sets
POST /reference-sets
GET /reference-sets/{id}
GET /reference-sets/{id}/references
POST /reference-sets/{id}/references
GET /references
POST /references
GET /references/{id}/bases
POST /references/{id}/bases/import
GET /variantsets
POST /variantsets
GET /variantsets/{id}
GET /variantsets/{id}/variants
POST /variantsets/{id}/variants/import
GET /variantsets/{id}/formats
POST /variantsets/{id}/formats
GET /variantsets/{id}/analyses
POST /variantsets/{id}/analyses
GET /variants
POST /variants
GET /variants/{id}
GET /callsets
POST /callsets
GET /callsets/{id}
POST /callsets/{id}/variantsets
GET /allele-calls
POST /allele-calls/import
GET /allele-matrix
GET /genome-maps
POST /genome-maps
GET /genome-maps/{id}/linkage-groups
POST /genome-maps/{id}/linkage-groups
POST /marker-positions/import
```
## 10.2 选择器接口
```text
GET /selectors/plates?studyId=&keyword=
GET /selectors/samples?studyId=&plateId=&keyword=
GET /selectors/reference-sets?cropId=&keyword=
GET /selectors/references?referenceSetId=&keyword=
GET /selectors/variantsets?referenceSetId=&studyId=&keyword=
GET /selectors/variants?variantSetId=&referenceName=&start=&end=&keyword=
GET /selectors/callsets?sampleId=&variantSetId=&keyword=
GET /selectors/genome-maps?cropId=&keyword=
GET /selectors/linkage-groups?genomeMapId=&keyword=
```
---
# 11. 测试验收清单
1. 创建 plate 时plate_name 和 plate_barcode 至少填写一个。
2. plate_barcode 重复时提示唯一性冲突。
3. plate_format=96 时,只允许 A01-H12plate_format=384 时,只允许 A01-P24。
4. sample_name 必填。
5. sample 绑定 plate 时,同一 plate + well 不允许重复。
6. sample 绑定 observation_unit 后study/trial/program 自动带出。
7. reference_set_name 必填。
8. reference_name 在同一 reference_set 内不允许重复。
9. reference.length 必须为非负整数。
10. reference_bases.bases 只能包含合法碱基字符。
11. variantset 必须选择 reference_set。
12. variant.reference_set_id 必须与 variantset.reference_set_id 一致。
13. variant_start 必须非负。
14. variant_end 不得小于 variant_start。
15. variant_name 为空时系统能按 reference + position + allele 自动生成。
16. callset 必须绑定 sample。
17. 同一 sample 下 call_set_name 不建议重复。
18. callset + variantset 关系不允许重复。
19. allele_call 必须绑定 callset 和 variant。
20. 同一 callset + variant 不允许重复,除非系统支持版本化。
21. read_depth 必须为非负整数。
22. genotype 必须符合导入格式规则。
23. 缺失 genotype 必须按 unknown_string 统一处理。
24. 导入 genotype matrix 时,无法匹配的 sample、variant 必须返回错误报告。
25. 导入失败报告必须包含行号、列名、错误原因、建议修复方式。
26. genome_map 必须选择 crop。
27. linkage_group_name 在同一 genome_map 内不允许重复。
28. marker_position.position 必须非负,且不超过 max_marker_position。
29. 已有关联下游数据的 sample、variant、callset、variantset 不允许物理删除。
30. 所有导入操作必须记录导入批次、上传人、上传时间和原始文件链接。
---
# 12. 开发实现重点
1. `sample` 是物理样本,不是基因型结果。
2. `callset` 是 sample 参与一次检测/分析形成的结果集合。
3. `allele_call` 才是某个 sample/callset 在某个 variant 上的 genotype 结果。
4. `variantset` 必须绑定 reference_set否则位点坐标没有意义。
5. 大量 variant 和 allele_call 不应手工录入,主入口必须是文件导入。
6. plate 必须做孔位布局,不要只做普通表单。
7. genotype matrix 页面必须支持按 sample 和按 variant 两种方向查看。
8. VCF/HapMap/CSV 的解析规则要独立成导入服务,不要散落在页面逻辑里。
9. reference_set、variantset、callset、analysis、format 都是结果可追溯的关键上下文。
10. 已导入的基因型结果建议支持版本化或作废,不建议直接物理删除。

3
frontend/.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

23
frontend/README.md Normal file
View File

@@ -0,0 +1,23 @@
# Frontend
## Start
```bash
cd frontend
npm install
npm run dev
```
Default URL: http://localhost:3010
## Routes
- /imports
- /imports/new
- /imports/[taskId]/mapping
- /imports/[taskId]/validate
- /imports/[taskId]/execute
- /imports/[taskId]/errors
- /imports/[taskId]/detail
- /imports/rollback
- /imports/history

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

21
frontend/components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

6
frontend/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

22
frontend/next.config.js Normal file
View File

@@ -0,0 +1,22 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
const apiBaseUrl = process.env.API_BASE_URL || "http://localhost:8081";
return [
{
source: "/auth/:path*",
destination: `${apiBaseUrl}/auth/:path*`,
},
{
source: "/brapi/v2/:path*",
destination: `${apiBaseUrl}/brapi/v2/:path*`,
},
{
source: "/api/:path*",
destination: `${apiBaseUrl}/api/:path*`,
},
];
},
};
module.exports = nextConfig;

View File

@@ -0,0 +1,37 @@
import { defineConfig } from "@hey-api/openapi-ts";
import { config } from "dotenv";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
const envPath = resolve(process.cwd(), ".env");
if (existsSync(envPath)) {
config({ path: envPath });
}
const envLocalPath = resolve(process.cwd(), ".env.local");
if (existsSync(envLocalPath)) {
config({ path: envLocalPath });
}
const apiBaseUrl =
process.env.API_BASE_URL ||
process.env.NEXT_PUBLIC_API_BASE_URL ||
"http://localhost:8081";
const openApiUrl =
process.env.OPENAPI_URL ||
process.env.REACT_APP_OPENAPI_URL ||
`${apiBaseUrl.replace(/\/$/, "")}/brapi/v2/openapi.json`;
export default defineConfig({
client: "@hey-api/client-fetch",
input: openApiUrl,
output: "./src/lib/api",
schemas: {
name: "types.gen.ts"
},
services: {
name: "sdk.gen.ts"
},
clientName: "client.gen.ts"
});

8394
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

61
frontend/package.json Normal file
View File

@@ -0,0 +1,61 @@
{
"name": "agri-data-standard-ui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3010",
"build": "next build",
"start": "next start -p 3010",
"lint": "next lint",
"api:generate": "node scripts/generate-api.cjs"
},
"dependencies": {
"@hey-api/openapi-ts": "^0.87.1",
"@hookform/resolvers": "^4.1.3",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.2",
"axios": "^1.8.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.2.1",
"lucide-react": "^0.475.0",
"next": "^16.2.6",
"next-themes": "^0.4.6",
"openapi-fetch": "^0.15.0",
"react": "19.1.0",
"react-day-picker": "^10.0.1",
"react-dom": "19.1.0",
"react-hook-form": "^7.76.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.25.76",
"zustand": "5.0.5"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.11",
"@types/node": "22.15.21",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"dotenv": "^16.4.7",
"eslint": "9.27.0",
"eslint-config-next": "15.3.2",
"tailwindcss": "^4.1.11",
"typescript": "5.8.3"
}
}

View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};

View File

@@ -0,0 +1,42 @@
const { execSync } = require("node:child_process");
const { existsSync } = require("node:fs");
const path = require("node:path");
const dotenv = require("dotenv");
dotenv.config({ path: ".env" });
dotenv.config({ path: ".env.local", override: true });
const apiBaseUrl =
process.env.API_BASE_URL ||
process.env.NEXT_PUBLIC_API_BASE_URL ||
"http://localhost:8081";
const openApiUrl =
process.env.OPENAPI_URL ||
process.env.REACT_APP_OPENAPI_URL ||
`${apiBaseUrl.replace(/\/$/, "")}/brapi/v2/openapi.json`;
process.env.API_BASE_URL = apiBaseUrl;
if (!process.env.OPENAPI_URL && !process.env.REACT_APP_OPENAPI_URL) {
process.env.OPENAPI_URL = openApiUrl;
}
console.log("API_BASE_URL:", apiBaseUrl);
console.log("OPENAPI_URL:", openApiUrl);
console.log("开始生成 OpenAPI 客户端...");
execSync("npx @hey-api/openapi-ts", {
stdio: "inherit",
env: process.env
});
const apiDir = path.resolve(process.cwd(), "src/lib/api");
const required = ["types.gen.ts", "sdk.gen.ts", "index.ts", "client.gen.ts"];
const missing = required.filter((file) => !existsSync(path.join(apiDir, file)));
if (missing.length > 0) {
console.error("生成失败,缺失文件:", missing.join(", "));
process.exit(1);
}
console.log("OpenAPI 客户端生成完成src/lib/api");

View File

@@ -0,0 +1,5 @@
import { PageHeader } from "@/components/common/PageHeader";
export default function CatalogPage() {
return <PageHeader title="资产目录" description="三级菜单页面占位,一级与二级菜单目前为示例占位结构。" />;
}

View File

@@ -0,0 +1,5 @@
import { PageHeader } from "@/components/common/PageHeader";
export default function SupplierPage() {
return <PageHeader title="供应商档案" description="三级菜单页面占位,一级与二级菜单目前为示例占位结构。" />;
}

View File

@@ -0,0 +1,31 @@
import { FormEvent } from "react";
import { Button } from "@/components/ui/button";
import type { CropFormValues } from "./types";
interface CropFormProps {
values: CropFormValues;
submitting: boolean;
onChange: (key: keyof CropFormValues, value: string) => void;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
}
export function CropForm({ values, submitting, onChange, onSubmit }: CropFormProps) {
return (
<form className="card-agriculture grid gap-5 p-5 md:p-6" onSubmit={onSubmit}>
<div className="flex items-start gap-3">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-2xl bg-secondary text-sm font-black text-secondary-foreground">01</span>
<div>
<h2 className="text-xl font-black tracking-tight text-card-foreground"></h2>
<p className="mt-1 text-sm leading-6 text-muted-foreground"></p>
</div>
</div>
<label className="grid gap-2 text-sm font-black text-card-foreground">
<span></span>
<input className="input-agriculture" value={values.cropName} onChange={(event) => onChange("cropName", event.target.value)} placeholder="例如maize / rice / wheat" />
</label>
<Button variant="agriculture" size="lg" type="submit" disabled={submitting || !values.cropName.trim()}>
{submitting ? "正在写入 crop 表..." : "保存作物"}
</Button>
</form>
);
}

View File

@@ -0,0 +1,17 @@
interface CropGuideProps {
error: string | null;
}
export function CropGuide({ error }: CropGuideProps) {
return (
<div className="card-agriculture p-5 md:p-6">
<h3 className="text-lg font-black text-card-foreground"></h3>
<ul className="mt-4 grid gap-3 text-sm leading-6 text-muted-foreground">
<li className="rounded-2xl bg-muted px-4 py-3">使</li>
<li className="rounded-2xl bg-muted px-4 py-3"> ProgramTrialStudy</li>
<li className="rounded-2xl bg-muted px-4 py-3"></li>
</ul>
{error ? <div className="mt-4 rounded-2xl border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm font-bold text-destructive">{error}</div> : null}
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { Button } from "@/components/ui/button";
const quickCrops = ["rice", "maize", "wheat", "soybean", "cotton"];
interface CropHeaderProps {
total: number;
loading: boolean;
onSelectQuickCrop: (name: string) => void;
}
export function CropHeader({ total, loading, onSelectQuickCrop }: CropHeaderProps) {
return (
<div className="relative grid gap-6 overflow-hidden rounded-[34px] bg-[radial-gradient(circle_at_12%_15%,rgba(187,247,208,0.94),transparent_28%),linear-gradient(135deg,var(--primary),#0f766e_48%,#84cc16)] p-6 text-primary-foreground shadow-agriculture lg:grid-cols-[1fr_240px] lg:p-8">
<div className="pointer-events-none absolute -right-16 -top-16 h-56 w-56 rounded-full bg-primary-foreground/15 blur-sm" />
<div className="relative z-10 max-w-3xl">
<div className="mb-4 inline-flex rounded-full border border-primary-foreground/25 bg-primary-foreground/15 px-3 py-1 text-xs font-black uppercase tracking-[0.2em]">
/ Crop
</div>
<h1 className="text-3xl font-black tracking-tight md:text-5xl"></h1>
<p className="mt-4 max-w-2xl text-sm leading-7 text-primary-foreground/90 md:text-base">
BrAPI <strong className="font-black">crop</strong> ProgramTrialStudyGermplasm
</p>
<div className="mt-5 flex flex-wrap gap-2">
{quickCrops.map((name) => (
<Button key={name} type="button" variant="outline" className="rounded-full border-primary-foreground/20 bg-primary-foreground/15 text-primary-foreground hover:bg-primary-foreground/25" onClick={() => onSelectQuickCrop(name)}>
{name}
</Button>
))}
</div>
</div>
<div className="relative z-10 flex min-h-44 flex-col justify-between rounded-3xl border border-primary-foreground/20 bg-primary-foreground/15 p-5 shadow-2xl shadow-emerald-950/15 backdrop-blur">
<span className="text-sm font-black text-primary-foreground/85"></span>
<strong className="text-6xl font-black leading-none">{total}</strong>
<em className="not-italic text-sm font-bold text-primary-foreground/85">{loading ? "正在同步数据库..." : "已连接 crop 表"}</em>
</div>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { Button } from "@/components/ui/button";
import type { CropItem } from "./types";
interface CropTableProps {
rows: CropItem[];
loading: boolean;
onRefresh: () => void;
}
export function CropTable({ rows, loading, onRefresh }: CropTableProps) {
return (
<div className="card-agriculture overflow-hidden">
<div className="flex flex-col gap-3 border-b border-border p-5 md:flex-row md:items-center md:justify-between md:p-6">
<div className="flex items-center gap-3">
<span className="grid h-10 w-10 place-items-center rounded-2xl bg-accent text-sm font-black text-accent-foreground">02</span>
<h2 className="text-xl font-black tracking-tight text-card-foreground">Crop </h2>
</div>
<Button variant="agriculture" type="button" onClick={onRefresh}>{loading ? "刷新中..." : "刷新数据"}</Button>
</div>
{loading ? (
<div className="p-8 text-center text-sm font-bold text-muted-foreground"> crop ...</div>
) : rows.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full border-collapse text-left text-sm">
<thead className="bg-muted text-xs uppercase tracking-[0.16em] text-muted-foreground">
<tr>
<th className="px-5 py-4 font-black">ID</th>
<th className="px-5 py-4 font-black"></th>
<th className="px-5 py-4 font-black"></th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{rows.map((row) => (
<tr key={row.id} className="transition hover:bg-muted/70">
<td className="px-5 py-4 text-muted-foreground">{row.id}</td>
<td className="px-5 py-4"><strong className="font-black text-card-foreground">{row.crop_name || "-"}</strong></td>
<td className="px-5 py-4 text-muted-foreground">{row.user_name || "-"}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="p-8 text-center text-sm font-bold text-muted-foreground"> maize</div>
)}
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { createCrop, listCrops } from "@/services/dictionaryService";
import type { CropFormValues, CropItem } from "./types";
export async function fetchCropList(): Promise<CropItem[]> {
return listCrops();
}
export async function saveCrop(values: CropFormValues): Promise<CropItem> {
return createCrop({
crop_name: values.cropName.trim(),
});
}

View File

@@ -0,0 +1,28 @@
import type { CropPageState } from "./types";
export type CropPageAction =
| { type: "SET_ROWS"; payload: CropPageState["rows"] }
| { type: "SET_FORM_FIELD"; payload: { key: keyof CropPageState["form"]; value: string } }
| { type: "RESET_FORM" }
| { type: "SET_LOADING"; payload: boolean }
| { type: "SET_SUBMITTING"; payload: boolean }
| { type: "SET_ERROR"; payload: string | null };
export function cropReducer(state: CropPageState, action: CropPageAction): CropPageState {
switch (action.type) {
case "SET_ROWS":
return { ...state, rows: action.payload };
case "SET_FORM_FIELD":
return { ...state, form: { ...state.form, [action.payload.key]: action.payload.value } };
case "RESET_FORM":
return { ...state, form: { cropName: "" } };
case "SET_LOADING":
return { ...state, loading: action.payload };
case "SET_SUBMITTING":
return { ...state, submitting: action.payload };
case "SET_ERROR":
return { ...state, error: action.payload };
default:
return state;
}
}

View File

@@ -0,0 +1,25 @@
import type { CropRecord } from "@/services/dictionaryService";
export type CropItem = CropRecord;
export interface CropFormValues {
cropName: string;
}
export interface CropPageState {
rows: CropItem[];
form: CropFormValues;
loading: boolean;
submitting: boolean;
error: string | null;
}
export const initialCropState: CropPageState = {
rows: [],
form: {
cropName: "",
},
loading: true,
submitting: false,
error: null,
};

View File

@@ -0,0 +1,43 @@
"use client";
import { Leaf } from "lucide-react";
import { BrapiEntityPage } from "@/components/brapi/BrapiEntityPage";
import { createCrop, deleteCrop, listCrops, updateCrop } from "@/services/dictionaryService";
const loadCropRows = async () => listCrops() as unknown as Record<string, unknown>[];
const createCropRow = async (payload: Record<string, unknown>) => createCrop({
crop_name: String(payload.crop_name ?? ""),
}) as unknown as Record<string, unknown>;
const updateCropRow = async (id: string, payload: Record<string, unknown>) => updateCrop(id, {
crop_name: String(payload.crop_name ?? ""),
}) as unknown as Record<string, unknown>;
const deleteCropRow = async (id: string) => {
await deleteCrop(id);
};
export default function CropDictionaryPage() {
return (
<BrapiEntityPage
icon={Leaf}
iconBg="bg-gradient-to-br from-amber-500 to-orange-600"
title="Crop 作物"
description="多作物平台入口,所有业务对象均归属于特定作物"
addLabel="新增作物"
columns={[
{ key: "crop_name", label: "作物名称" },
]}
fields={[
{ key: "crop_name", label: "作物名称 (Crop Name)", type: "text", required: true, placeholder: "如 Maize、Rice、Wheat" },
]}
data={[]}
stats={[{ label: "后端 crop 表", value: "API", className: "bg-amber-50 text-amber-700 dark:bg-amber-400/10 dark:text-amber-200" }]}
loadData={loadCropRows}
createRecord={createCropRow}
updateRecord={updateCropRow}
deleteRecord={deleteCropRow}
/>
);
}

View File

@@ -0,0 +1,161 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { ArrowLeft, List, Pencil } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { fetchListDetail, fetchPersonOptions } from "../api";
import { ListItemPanel } from "../components/ListItemPanel";
import { ListMetaDialog } from "../components/ListMetaDialog";
import { listTypeLabel, type ListRecord, type SelectOption } from "../types";
function formatTimestamp(value: string | null | undefined) {
if (!value) return "—";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString();
}
export default function ListDetailPage() {
const params = useParams<{ listDbId: string }>();
const listDbId = decodeURIComponent(params.listDbId ?? "");
const [record, setRecord] = useState<ListRecord | null>(null);
const [personOptions, setPersonOptions] = useState<SelectOption[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [editOpen, setEditOpen] = useState(false);
const loadDetail = useCallback(async () => {
if (!listDbId) return;
setLoading(true);
setError(null);
try {
const [people, detail] = await Promise.all([fetchPersonOptions(), fetchListDetail(listDbId)]);
setPersonOptions(people);
setRecord(detail);
} catch (event) {
setError(event instanceof Error ? event.message : "加载失败");
setRecord(null);
} finally {
setLoading(false);
}
}, [listDbId]);
useEffect(() => {
let mounted = true;
loadDetail().catch(() => undefined);
return () => {
mounted = false;
};
}, [loadDetail]);
const handleItemsChange = useCallback((items: string[]) => {
setRecord((current) => (current ? { ...current, data: items, listSize: items.length } : current));
}, []);
if (!listDbId) {
return <div className="p-6 text-sm text-destructive"> ID</div>;
}
return (
<div className="flex min-h-full flex-col">
<div className="mb-5 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="flex items-start gap-3">
<Button variant="outline" size="icon" className="shrink-0" asChild>
<Link href="/basic-dictionary/base/list" aria-label="返回列表">
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<div className="flex items-center gap-3">
<div className="rounded-xl bg-gradient-to-br from-violet-500 to-purple-600 p-2.5">
<List className="h-5 w-5 text-white" />
</div>
<div>
<h2 className="text-lg font-semibold text-slate-900 dark:text-slate-50">
{record?.listName || "List 详情"}
</h2>
<p className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">
list_item
</p>
</div>
</div>
</div>
{record ? (
<Button variant="outline" className="gap-2" onClick={() => setEditOpen(true)}>
<Pencil className="h-4 w-4" />
</Button>
) : null}
</div>
{error ? (
<div className="mb-4 rounded-xl border border-destructive/20 bg-destructive/10 px-4 py-3 text-sm text-destructive">
{error}
</div>
) : null}
<div className="mb-5 rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
{loading ? (
<div className="grid gap-3 md:grid-cols-2">
{Array.from({ length: 6 }).map((_, index) => (
<Skeleton key={index} className="h-5 w-full" />
))}
</div>
) : record ? (
<dl className="grid gap-3 text-sm md:grid-cols-2">
<div>
<dt className="text-xs text-slate-500 dark:text-slate-400"> ID</dt>
<dd className="mt-0.5 break-all font-medium text-slate-800 dark:text-slate-100">{record.id}</dd>
</div>
<div>
<dt className="text-xs text-slate-500 dark:text-slate-400"></dt>
<dd className="mt-0.5 text-slate-800 dark:text-slate-100">{listTypeLabel(record.listType)}</dd>
</div>
<div>
<dt className="text-xs text-slate-500 dark:text-slate-400">Owner</dt>
<dd className="mt-0.5 text-slate-800 dark:text-slate-100">{record.listOwnerName || record.listOwnerPersonDbId || "—"}</dd>
</div>
<div>
<dt className="text-xs text-slate-500 dark:text-slate-400"></dt>
<dd className="mt-0.5 text-slate-800 dark:text-slate-100">{record.listSource || "—"}</dd>
</div>
<div>
<dt className="text-xs text-slate-500 dark:text-slate-400"></dt>
<dd className="mt-0.5 text-slate-800 dark:text-slate-100">{formatTimestamp(record.dateCreated)}</dd>
</div>
<div>
<dt className="text-xs text-slate-500 dark:text-slate-400"></dt>
<dd className="mt-0.5 text-slate-800 dark:text-slate-100">{formatTimestamp(record.dateModified)}</dd>
</div>
<div className="md:col-span-2">
<dt className="text-xs text-slate-500 dark:text-slate-400"></dt>
<dd className="mt-0.5 whitespace-pre-wrap text-slate-800 dark:text-slate-100">{record.listDescription || "—"}</dd>
</div>
</dl>
) : (
<p className="text-sm text-slate-400"></p>
)}
</div>
{record ? (
<ListItemPanel
listDbId={listDbId}
items={record.data ?? []}
onItemsChange={handleItemsChange}
/>
) : null}
{record ? (
<ListMetaDialog
open={editOpen}
record={record}
personOptions={personOptions}
onOpenChange={setEditOpen}
onSaved={setRecord}
/>
) : null}
</div>
);
}

View File

@@ -0,0 +1,205 @@
import { getAuthToken } from "@/utils/token";
import type { ListDetails, ListNewRequest, ListSummary } from "@/lib/api/types.gen";
import { NONE_SELECT_VALUE, type ListRecord, type ListType, type SelectOption } from "./types";
interface BrapiPagination {
currentPage: number;
pageSize: number;
totalCount: number;
totalPages: number;
}
interface BrapiListResponse<T> {
metadata: {
pagination: BrapiPagination;
status: Array<Record<string, unknown>>;
datafiles: Array<Record<string, unknown>>;
};
result: {
data: T[];
};
}
interface BrapiSingleResponse<T> {
metadata: {
pagination: BrapiPagination;
status: Array<Record<string, unknown>>;
datafiles: Array<Record<string, unknown>>;
};
result: T;
}
interface PersonResponse {
personDbId: string;
firstName?: string | null;
lastName?: string | null;
emailAddress?: string | null;
}
type ListPayload = Partial<Record<
| "listName"
| "list_name"
| "listType"
| "list_type"
| "listDescription"
| "list_description"
| "listSource"
| "list_source"
| "listOwnerName"
| "list_owner_name"
| "listOwnerPersonDbId"
| "list_owner_person_id"
| "data",
unknown
>>;
const apiBase = () => {
if (typeof window !== "undefined") return "";
return process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000";
};
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const token = getAuthToken();
const response = await fetch(`${apiBase()}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(init?.headers || {}),
},
});
if (!response.ok) {
const detail = await response.text();
throw new Error(detail || `请求失败:${response.status}`);
}
return response.json() as Promise<T>;
}
const optionalText = (value: unknown) => {
const normalized = String(value ?? "").trim();
if (!normalized || normalized === NONE_SELECT_VALUE) return undefined;
return normalized;
};
const requiredText = (value: unknown, message: string) => {
const normalized = optionalText(value);
if (!normalized) throw new Error(message);
return normalized;
};
const requiredListType = (value: unknown): ListType => {
const normalized = optionalText(value);
if (!normalized) throw new Error("请选择列表类型");
return normalized as ListType;
};
export const mapListRecord = (list: ListSummary | ListDetails): ListRecord => ({
...list,
id: list.listDbId || "",
list_name: list.listName ?? null,
list_description: list.listDescription ?? null,
list_source: list.listSource ?? null,
list_owner_name: list.listOwnerName ?? null,
list_owner_person_id: list.listOwnerPersonDbId ?? null,
list_type: list.listType ?? null,
date_created: list.dateCreated ?? null,
date_modified: list.dateModified ?? null,
data: "data" in list && list.data ? list.data : [],
});
const toRequestBody = (payload: ListPayload, data?: string[]): ListNewRequest => ({
listName: requiredText(payload.listName ?? payload.list_name, "请填写列表名称"),
listType: requiredListType(payload.listType ?? payload.list_type),
listDescription: optionalText(payload.listDescription ?? payload.list_description),
listSource: optionalText(payload.listSource ?? payload.list_source),
listOwnerName: optionalText(payload.listOwnerName ?? payload.list_owner_name),
listOwnerPersonDbId: optionalText(payload.listOwnerPersonDbId ?? payload.list_owner_person_id),
...(data !== undefined ? { data } : {}),
});
export async function fetchListRows(): Promise<ListRecord[]> {
const response = await request<BrapiListResponse<ListSummary>>("/brapi/v2/lists?page=0&pageSize=1000");
return response.result.data.map(mapListRecord);
}
export async function fetchListDetail(listDbId: string): Promise<ListRecord> {
const response = await request<BrapiSingleResponse<ListDetails>>(`/brapi/v2/lists/${encodeURIComponent(listDbId)}`);
return mapListRecord(response.result);
}
export async function fetchPersonOptions(): Promise<SelectOption[]> {
const response = await request<BrapiListResponse<PersonResponse>>("/brapi/v2/people?page=0&pageSize=1000");
return response.result.data.map((person) => {
const name = [person.firstName, person.lastName].filter(Boolean).join(" ").trim();
const label = name
? `${name}${person.emailAddress ? ` / ${person.emailAddress}` : ""}`
: person.personDbId;
return { value: person.personDbId, label };
});
}
export async function createListRow(payload: ListPayload): Promise<ListRecord> {
const response = await request<BrapiListResponse<ListSummary>>("/brapi/v2/lists", {
method: "POST",
body: JSON.stringify([toRequestBody(payload)]),
});
return mapListRecord(response.result.data[0]);
}
export async function updateListRow(listDbId: string, payload: ListPayload, data?: string[]): Promise<ListRecord> {
const response = await request<BrapiSingleResponse<ListDetails>>(`/brapi/v2/lists/${encodeURIComponent(listDbId)}`, {
method: "PUT",
body: JSON.stringify(toRequestBody(payload, data)),
});
return mapListRecord(response.result);
}
export async function appendListItems(listDbId: string, items: string[]): Promise<ListRecord> {
const response = await request<BrapiSingleResponse<ListDetails>>(`/brapi/v2/lists/${encodeURIComponent(listDbId)}/items`, {
method: "POST",
body: JSON.stringify(items),
});
return mapListRecord(response.result);
}
export async function replaceListItems(listDbId: string, items: string[]): Promise<ListRecord> {
const detail = await fetchListDetail(listDbId);
return updateListRow(
listDbId,
{
listName: detail.listName,
listType: detail.listType,
listDescription: detail.listDescription,
listSource: detail.listSource,
listOwnerName: detail.listOwnerName,
listOwnerPersonDbId: detail.listOwnerPersonDbId,
},
items,
);
}
export function normalizeNewItems(existing: string[], incoming: string[]): string[] {
const seen = new Set(existing.map((item) => item.trim()).filter(Boolean));
const duplicates: string[] = [];
const added: string[] = [];
for (const raw of incoming) {
const item = raw.trim();
if (!item) continue;
if (seen.has(item)) {
duplicates.push(item);
continue;
}
seen.add(item);
added.push(item);
}
if (duplicates.length > 0) {
throw new Error(`以下列表项已存在:${duplicates.join("、")}`);
}
if (added.length === 0) {
throw new Error("请至少填写一个有效的列表项");
}
return added;
}

View File

@@ -0,0 +1,234 @@
"use client";
import { useCallback, useState } from "react";
import { ArrowDown, ArrowUp, Plus, Trash2, Upload } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogBody,
DialogContent,
DialogFooter,
} from "@/components/common/shadcn-enhanced";
import { appendListItems, normalizeNewItems, replaceListItems } from "../api";
type ListItemPanelProps = {
listDbId: string;
items: string[];
onItemsChange: (items: string[]) => void;
};
export function ListItemPanel({ listDbId, items, onItemsChange }: ListItemPanelProps) {
const [addOpen, setAddOpen] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const [newItem, setNewItem] = useState("");
const [importText, setImportText] = useState("");
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
const runAction = useCallback(async (action: () => Promise<{ data?: string[] }>) => {
setSaving(true);
setError(null);
try {
const result = await action();
onItemsChange(result.data ?? []);
} catch (event) {
setError(event instanceof Error ? event.message : "操作失败");
} finally {
setSaving(false);
}
}, [onItemsChange]);
const handleAddItem = async () => {
const added = normalizeNewItems(items, [newItem]);
await runAction(async () => appendListItems(listDbId, added));
setNewItem("");
setAddOpen(false);
};
const handleImport = async () => {
const lines = importText.split(/\r?\n/);
const added = normalizeNewItems(items, lines);
await runAction(async () => appendListItems(listDbId, added));
setImportText("");
setImportOpen(false);
};
const handleDelete = async () => {
if (deleteIndex === null) return;
const nextItems = items.filter((_, index) => index !== deleteIndex);
await runAction(async () => replaceListItems(listDbId, nextItems));
setDeleteIndex(null);
};
const moveItem = async (index: number, direction: -1 | 1) => {
const target = index + direction;
if (target < 0 || target >= items.length) return;
const nextItems = [...items];
[nextItems[index], nextItems[target]] = [nextItems[target], nextItems[index]];
await runAction(async () => replaceListItems(listDbId, nextItems));
};
return (
<div className="rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950">
<div className="flex flex-col gap-3 border-b border-slate-200 px-4 py-3 sm:flex-row sm:items-center sm:justify-between dark:border-slate-800">
<div>
<h3 className="text-sm font-semibold text-slate-900 dark:text-slate-50"> (list_item)</h3>
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400"> PUT </p>
</div>
<div className="flex flex-wrap gap-2">
<Button size="sm" className="gap-1.5" onClick={() => setAddOpen(true)} disabled={saving}>
<Plus className="h-3.5 w-3.5" />
</Button>
<Button size="sm" variant="outline" className="gap-1.5" onClick={() => setImportOpen(true)} disabled={saving}>
<Upload className="h-3.5 w-3.5" />
</Button>
</div>
</div>
{error ? (
<div className="mx-4 mt-3 rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</div>
) : null}
<Table>
<TableHeader>
<TableRow className="border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900">
<TableHead className="w-12 text-xs">#</TableHead>
<TableHead className="text-xs"> (item)</TableHead>
<TableHead className="w-36 text-right text-xs"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.length === 0 ? (
<TableRow>
<TableCell colSpan={3} className="py-12 text-center text-sm text-slate-400">
</TableCell>
</TableRow>
) : (
items.map((item, index) => (
<TableRow key={`${item}-${index}`} className="border-slate-100 dark:border-slate-800">
<TableCell className="text-xs text-slate-400">{index + 1}</TableCell>
<TableCell className="break-all text-sm text-slate-700 dark:text-slate-200">{item}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1">
<Button
size="icon"
variant="ghost"
className="h-7 w-7"
disabled={saving || index === 0}
onClick={() => moveItem(index, -1)}
>
<ArrowUp className="h-3.5 w-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-7 w-7"
disabled={saving || index === items.length - 1}
onClick={() => moveItem(index, 1)}
>
<ArrowDown className="h-3.5 w-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 text-slate-400 hover:text-red-500"
disabled={saving}
onClick={() => setDeleteIndex(index)}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
<Dialog open={addOpen} onOpenChange={setAddOpen}>
<DialogContent title="新增列表项" defaultWidth={520} defaultHeight={320} minHeight={280}>
<DialogBody>
<p className="mb-3 text-sm text-muted-foreground"> ID </p>
<Label htmlFor="new-list-item" className="mb-1.5 block text-sm"></Label>
<Input
id="new-list-item"
value={newItem}
onChange={(event) => setNewItem(event.target.value)}
placeholder="如 germplasm ID、study ID 或备注文本"
/>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={() => setAddOpen(false)}></Button>
<Button onClick={handleAddItem} disabled={saving || !newItem.trim()}>
{saving ? "保存中..." : "确认添加"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={importOpen} onOpenChange={setImportOpen}>
<DialogContent title="批量导入列表项" defaultWidth={560} defaultHeight={480} minHeight={360}>
<DialogBody>
<p className="mb-3 text-sm text-muted-foreground"></p>
<Textarea
value={importText}
onChange={(event) => setImportText(event.target.value)}
rows={8}
placeholder={"GERM-001\nGERM-002\nstudy-2026-spring"}
className="resize-none font-mono text-sm"
/>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={() => setImportOpen(false)}></Button>
<Button onClick={handleImport} disabled={saving || !importText.trim()}>
{saving ? "导入中..." : "确认导入"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog open={deleteIndex !== null} onOpenChange={(open) => !open && setDeleteIndex(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{" "}
<span className="font-medium text-slate-700 dark:text-slate-200">
{deleteIndex !== null ? items[deleteIndex] : ""}
</span>
{" "}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={saving}></AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} disabled={saving} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
{saving ? "删除中..." : "确认删除"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@@ -0,0 +1,165 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogBody,
DialogContent,
DialogFooter,
} from "@/components/common/shadcn-enhanced";
import { updateListRow } from "../api";
import { LIST_TYPE_OPTIONS, NONE_SELECT_VALUE, type ListRecord, type SelectOption } from "../types";
type ListMetaDialogProps = {
open: boolean;
record: ListRecord;
personOptions: SelectOption[];
onOpenChange: (open: boolean) => void;
onSaved: (record: ListRecord) => void;
};
export function ListMetaDialog({ open, record, personOptions, onOpenChange, onSaved }: ListMetaDialogProps) {
const [form, setForm] = useState({
listName: "",
listType: "",
listDescription: "",
listSource: "",
listOwnerName: "",
listOwnerPersonDbId: NONE_SELECT_VALUE,
});
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
setForm({
listName: record.listName ?? "",
listType: record.listType ?? "",
listDescription: record.listDescription ?? "",
listSource: record.listSource ?? "",
listOwnerName: record.listOwnerName ?? "",
listOwnerPersonDbId: record.listOwnerPersonDbId || NONE_SELECT_VALUE,
});
setError(null);
}, [open, record]);
const updateField = (key: keyof typeof form, value: string) => {
setForm((current) => ({ ...current, [key]: value }));
};
const handleSave = async () => {
setSaving(true);
setError(null);
try {
const updated = await updateListRow(record.id, form, record.data ?? []);
onSaved(updated);
onOpenChange(false);
} catch (event) {
setError(event instanceof Error ? event.message : "保存失败");
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent title="编辑列表基本信息" defaultWidth={720} defaultHeight={560} minHeight={420}>
<DialogBody>
<p className="mb-3 text-sm text-muted-foreground"> list list_item</p>
{error ? (
<div className="mb-3 rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</div>
) : null}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="md:col-span-2">
<Label htmlFor="edit-listName" className="mb-1.5 block text-sm">
<span className="ml-0.5 text-red-500">*</span>
</Label>
<Input
id="edit-listName"
value={form.listName}
onChange={(event) => updateField("listName", event.target.value)}
/>
</div>
<div>
<Label htmlFor="edit-listType" className="mb-1.5 block text-sm">
<span className="ml-0.5 text-red-500">*</span>
</Label>
<Select value={form.listType} onValueChange={(value) => updateField("listType", value)}>
<SelectTrigger id="edit-listType">
<SelectValue placeholder="请选择列表类型" />
</SelectTrigger>
<SelectContent position="item-aligned">
{LIST_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="edit-listSource" className="mb-1.5 block text-sm"></Label>
<Input
id="edit-listSource"
value={form.listSource}
onChange={(event) => updateField("listSource", event.target.value)}
/>
</div>
<div>
<Label htmlFor="edit-listOwnerName" className="mb-1.5 block text-sm">Owner </Label>
<Input
id="edit-listOwnerName"
value={form.listOwnerName}
onChange={(event) => updateField("listOwnerName", event.target.value)}
/>
</div>
<div>
<Label htmlFor="edit-listOwnerPersonDbId" className="mb-1.5 block text-sm">Owner </Label>
<Select
value={form.listOwnerPersonDbId}
onValueChange={(value) => updateField("listOwnerPersonDbId", value)}
>
<SelectTrigger id="edit-listOwnerPersonDbId">
<SelectValue placeholder="不绑定人员" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value={NONE_SELECT_VALUE}></SelectItem>
{personOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="md:col-span-2">
<Label htmlFor="edit-listDescription" className="mb-1.5 block text-sm"></Label>
<Textarea
id="edit-listDescription"
value={form.listDescription}
onChange={(event) => updateField("listDescription", event.target.value)}
rows={3}
className="resize-none"
/>
</div>
</div>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
</Button>
<Button onClick={handleSave} disabled={saving}>
{saving ? "保存中..." : "保存"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,24 @@
import type { BrapiFormField } from "@/components/brapi/BrapiEntityPage";
import { LIST_TYPE_OPTIONS, NONE_SELECT_VALUE, type SelectOption } from "../types";
export function buildListFormFields(personOptions: SelectOption[]): BrapiFormField[] {
return [
{ key: "listName", label: "列表名称", type: "text", required: true, placeholder: "如 2026 核心种质清单" },
{
key: "listType",
label: "列表类型",
type: "select",
required: true,
options: LIST_TYPE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
},
{ key: "listDescription", label: "列表描述", type: "textarea", colSpan: 2, placeholder: "用途、范围或备注" },
{ key: "listSource", label: "列表来源", type: "text", placeholder: "如 田间调查、导入批次" },
{ key: "listOwnerName", label: "Owner 名称", type: "text", placeholder: "可选,选择人员后可自动带出" },
{
key: "listOwnerPersonDbId",
label: "Owner 人员",
type: "select",
options: [{ value: NONE_SELECT_VALUE, label: "不绑定人员" }, ...personOptions],
},
];
}

View File

@@ -0,0 +1,85 @@
"use client";
import Link from "next/link";
import { useCallback, useMemo, useState } from "react";
import { List } from "lucide-react";
import { BrapiEntityPage } from "@/components/brapi/BrapiEntityPage";
import { Button } from "@/components/ui/button";
import {
createListRow,
fetchListDetail,
fetchListRows,
fetchPersonOptions,
updateListRow,
} from "./api";
import { buildListFormFields } from "./components/listFormFields";
import { listTypeLabel, type SelectOption } from "./types";
export default function ListDictionaryPage() {
const [personOptions, setPersonOptions] = useState<SelectOption[]>([]);
const loadRows = useCallback(async () => {
const [people, rows] = await Promise.all([fetchPersonOptions(), fetchListRows()]);
setPersonOptions(people);
return rows as unknown as Record<string, unknown>[];
}, []);
const fields = useMemo(() => buildListFormFields(personOptions), [personOptions]);
const createRecord = useCallback(
async (payload: Record<string, unknown>) => createListRow(payload) as unknown as Record<string, unknown>,
[],
);
const updateRecord = useCallback(
async (id: string, payload: Record<string, unknown>) => updateListRow(id, payload) as unknown as Record<string, unknown>,
[],
);
const fetchRecord = useCallback(
async (id: string) => fetchListDetail(id) as unknown as Record<string, unknown>,
[],
);
return (
<BrapiEntityPage
icon={List}
iconBg="bg-gradient-to-br from-violet-500 to-purple-600"
title="List 通用列表"
description="维护 BrAPI 通用分组列表,可在详情页管理 list_item 明细"
addLabel="新增列表"
useEnhancedDialog
columns={[
{ key: "listName", label: "列表名称" },
{
key: "listType",
label: "类型",
render: (value) => listTypeLabel(value),
},
{ key: "listSize", label: "项数" },
{ key: "listOwnerName", label: "Owner" },
{ key: "listSource", label: "来源" },
{
key: "id",
label: "明细",
render: (_value, row) => {
const id = String(row.id ?? row.listDbId ?? "");
if (!id) return <span className="text-slate-300"></span>;
return (
<Button variant="link" className="h-auto p-0 text-violet-600 dark:text-violet-300" asChild>
<Link href={`/basic-dictionary/base/list/${encodeURIComponent(id)}`}></Link>
</Button>
);
},
},
]}
fields={fields}
data={[]}
stats={[{ label: "/brapi/v2/lists", value: "BrAPI", className: "bg-violet-50 text-violet-700 dark:bg-violet-400/10 dark:text-violet-200" }]}
loadData={loadRows}
fetchRecord={fetchRecord}
createRecord={createRecord}
updateRecord={updateRecord}
/>
);
}

Some files were not shown because too many files have changed in this diff Show More