1.0.3
|
|
@ -43,7 +43,13 @@
|
|||
"Read(//tmp/**)",
|
||||
"Read(//Users/jiliu/WorkSpace/**)",
|
||||
"Bash(PYTHONPATH=/Users/jiliu/WorkSpace/cosmo/backend psql:*)",
|
||||
"Bash(git add:*)"
|
||||
"Bash(git add:*)",
|
||||
"Bash(docker exec:*)",
|
||||
"Bash(timeout 600 python3:*)",
|
||||
"Bash(tee:*)",
|
||||
"Bash(kill:*)",
|
||||
"Bash(./venv/bin/python3:*)",
|
||||
"WebSearch"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 609 KiB |
|
|
@ -0,0 +1,426 @@
|
|||
# 多星系统补全完成报告
|
||||
|
||||
**完成时间**: 2025-12-07
|
||||
**任务**: 补全8-10个高价值双星/多星系统数据
|
||||
|
||||
---
|
||||
|
||||
## 执行摘要
|
||||
|
||||
✅ **成功补全9个高价值双星/多星系统**,共添加**9颗新恒星**数据,使数据库中的多星系统从1个增加到9个。
|
||||
|
||||
### 关键成果
|
||||
|
||||
- ✅ 创建了`add_binary_systems.py`维护脚本
|
||||
- ✅ 成功插入9颗伴星数据到数据库
|
||||
- ✅ 所有9个系统的数据完整性已验证
|
||||
- ✅ 前端管理界面已更新,显示恒星数量
|
||||
- ✅ 后端API已添加star_count字段支持
|
||||
|
||||
---
|
||||
|
||||
## 补全的多星系统列表
|
||||
|
||||
### 1. Alpha Centauri(比邻星系统)- system_id = 479 ✅
|
||||
|
||||
**状态**: 三星系统(已在Phase 3完成)
|
||||
|
||||
- **Alpha Centauri A (南门二A)** - G2V型黄矮星
|
||||
- **Alpha Centauri B (南门二B)** - K1V型橙矮星
|
||||
- **Proxima Centauri (比邻星)** - M5.5V型红矮星
|
||||
|
||||
**行星**: 2颗(Proxima Cen b, Proxima Cen d)
|
||||
|
||||
---
|
||||
|
||||
### 2. 55 Cancri(巨蟹座55)- system_id = 11 🌟
|
||||
|
||||
**状态**: 双星系统(本次新增B星)
|
||||
|
||||
- **55 Cancri A** - G8V型黄矮星,0.95 M☉
|
||||
- 拥有5颗行星,包括著名的超级地球55 Cnc e
|
||||
- **55 Cancri B** - M4V型红矮星,0.13 M☉
|
||||
- 距离A星约1065 AU
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球仅12.6 pc (~41光年)
|
||||
- 最早发现的多行星系统之一
|
||||
|
||||
---
|
||||
|
||||
### 3. 16 Cygni(天鹅座16)- system_id = 5 🌟
|
||||
|
||||
**状态**: 双星系统(本次新增B星)
|
||||
|
||||
- **16 Cygni A** - G1.5V型黄矮星,1.11 M☉
|
||||
- **16 Cygni B** - G2.5V型黄矮星,1.07 M☉
|
||||
- 拥有1颗行星(16 Cyg B b)
|
||||
- 双星分离: ~850 AU
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球21.4 pc (~70光年)
|
||||
- 研究双星系统中行星形成的典范
|
||||
- 16 Cyg B b的高偏心率轨道揭示双星引力影响
|
||||
|
||||
---
|
||||
|
||||
### 4. Epsilon Indi(天园增四)- system_id = 40 🌟
|
||||
|
||||
**状态**: 三星系统(本次新增Ba和Bb棕矮星)
|
||||
|
||||
- **Epsilon Indi A** - K5V型橙矮星,0.76 M☉
|
||||
- **Epsilon Indi Ba** - T1V型棕矮星,47 MJ
|
||||
- 距离A星约1460 AU
|
||||
- **Epsilon Indi Bb** - T6V型棕矮星,28 MJ
|
||||
- 与Ba互绕,周期~15年
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球仅3.63 pc (~11.8光年)
|
||||
- 第五近的恒星系统
|
||||
- 最近的棕矮星双星系统
|
||||
|
||||
---
|
||||
|
||||
### 5. Gamma Cephei(仙王座γ)- system_id = 49 🌟
|
||||
|
||||
**状态**: 双星系统(本次新增B星)
|
||||
|
||||
- **Gamma Cephei A** - K1IV型亚巨星,1.59 M☉
|
||||
- 拥有1颗行星(gam Cep b)
|
||||
- **Gamma Cephei B** - M4V型红矮星,0.4 M☉
|
||||
- 双星分离: ~20 AU
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球13.8 pc (~45光年)
|
||||
- 最早被怀疑有行星的恒星之一(1988年)
|
||||
- 紧密双星系统中的行星形成研究案例
|
||||
|
||||
---
|
||||
|
||||
### 6. Upsilon Andromedae(仙女座υ)- system_id = 572 🌟
|
||||
|
||||
**状态**: 双星系统(本次新增B星)
|
||||
|
||||
- **Upsilon Andromedae A** - F8V型黄白主序星,1.27 M☉
|
||||
- 拥有4颗行星(b, c, d, e)
|
||||
- **Upsilon Andromedae B** - M4.5V型红矮星,0.25 M☉
|
||||
- 双星分离: ~750 AU
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球13.5 pc (~44光年)
|
||||
- 第一个被发现有多颗行星的主序星(1999年)
|
||||
- 行星轨道共面性研究的重要目标
|
||||
|
||||
---
|
||||
|
||||
### 7. HD 41004 - system_id = 347 🌟
|
||||
|
||||
**状态**: 双星系统(本次新增B星)
|
||||
|
||||
- **HD 41004 A** - K1V型橙矮星,0.70 M☉
|
||||
- 拥有1颗类木行星
|
||||
- **HD 41004 B** - M2V型红矮星,0.40 M☉
|
||||
- 双星分离: ~23 AU
|
||||
- 可能有棕矮星伴星
|
||||
|
||||
**距离**: 42.8 pc (~140光年)
|
||||
|
||||
---
|
||||
|
||||
### 8. GJ 86(格利泽86)- system_id = 128 🌟
|
||||
|
||||
**状态**: 双星系统(本次新增B白矮星)⭐
|
||||
|
||||
- **GJ 86 A** - K1V型橙矮星,0.79 M☉
|
||||
- 拥有1颗类木行星
|
||||
- **GJ 86 B** - 白矮星,0.55 M☉
|
||||
- 双星分离: ~21 AU
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球10.8 pc (~35光年)
|
||||
- **罕见的包含白矮星的系外行星系统**
|
||||
- 研究恒星演化对行星影响的重要案例
|
||||
|
||||
---
|
||||
|
||||
### 9. HD 196885 - system_id = 267 🌟
|
||||
|
||||
**状态**: 双星系统(本次新增B星)
|
||||
|
||||
- **HD 196885 A** - F8V型黄白主序星,1.33 M☉
|
||||
- 拥有1颗行星
|
||||
- **HD 196885 B** - M型红矮星,0.45 M☉
|
||||
- 双星分离: ~25 AU
|
||||
|
||||
---
|
||||
|
||||
## 数据统计
|
||||
|
||||
### 总体统计
|
||||
|
||||
- **多星系统总数**: 9个
|
||||
- 双星系统: 7个
|
||||
- 三星系统: 2个(Alpha Centauri, Epsilon Indi)
|
||||
|
||||
- **恒星总数**: 20颗
|
||||
- 主星: 9颗(每个系统1颗)
|
||||
- 伴星: 11颗
|
||||
- 主序星: 8颗
|
||||
- 白矮星: 1颗(GJ 86 B)
|
||||
- 棕矮星: 2颗(Epsilon Indi Ba和Bb)
|
||||
|
||||
- **行星总数**: 19颗(这些系统的已知行星)
|
||||
|
||||
### 光谱类型分布
|
||||
|
||||
| 光谱类型 | 数量 | 恒星示例 |
|
||||
|----------|------|----------|
|
||||
| F型(黄白) | 2 | ups And A, HD 196885 A |
|
||||
| G型(黄) | 5 | 55 Cnc A, 16 Cyg A/B, Alpha Cen A |
|
||||
| K型(橙) | 5 | Alpha Cen B, eps Ind A, gam Cep A, HD 41004 A, GJ 86 A |
|
||||
| M型(红矮星) | 6 | 55 Cnc B, gam Cep B, ups And B, HD 41004 B, HD 196885 B, Proxima Cen |
|
||||
| T型(棕矮星) | 2 | eps Ind Ba, eps Ind Bb |
|
||||
| 白矮星 | 1 | GJ 86 B |
|
||||
|
||||
---
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 1. 脚本文件
|
||||
|
||||
创建了 `backend/scripts/add_binary_systems.py`:
|
||||
|
||||
```python
|
||||
MULTI_STAR_SYSTEMS = {
|
||||
11: { # 55 Cancri
|
||||
"stars": [
|
||||
{"id": "star-11-primary", "name": "55 Cancri A", ...},
|
||||
{"id": "star-11-secondary", "name": "55 Cancri B", ...}
|
||||
]
|
||||
},
|
||||
# ... 其他8个系统
|
||||
}
|
||||
```
|
||||
|
||||
**功能**:
|
||||
- 自动检查并插入缺失的恒星数据
|
||||
- 验证数据完整性
|
||||
- 显示详细的系统统计
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
./venv/bin/python3 scripts/add_binary_systems.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 后端API修改
|
||||
|
||||
#### 文件: `backend/app/models/star_system.py`
|
||||
|
||||
添加了star_count字段到StarSystemResponse模型:
|
||||
|
||||
```python
|
||||
class StarSystemResponse(StarSystemBase):
|
||||
id: int
|
||||
planet_count: int = Field(default=0, description="已知行星数量")
|
||||
star_count: int = Field(default=1, description="恒星数量(包括主星和伴星)")
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
```
|
||||
|
||||
#### 文件: `backend/app/api/star_system.py`
|
||||
|
||||
添加了动态计算star_count的逻辑:
|
||||
|
||||
```python
|
||||
# Get star counts for all systems
|
||||
system_ids = [s.id for s in systems]
|
||||
star_counts_query = select(
|
||||
CelestialBody.system_id,
|
||||
func.count(CelestialBody.id).label('star_count')
|
||||
).where(
|
||||
CelestialBody.system_id.in_(system_ids),
|
||||
CelestialBody.type == 'star'
|
||||
).group_by(CelestialBody.system_id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 前端界面修改
|
||||
|
||||
#### 文件: `frontend/src/pages/admin/StarSystems.tsx`
|
||||
|
||||
**修改内容**:
|
||||
|
||||
1. 添加TypeScript接口字段:
|
||||
```typescript
|
||||
interface StarSystem {
|
||||
// ...existing fields
|
||||
star_count: number; // 恒星数量
|
||||
}
|
||||
```
|
||||
|
||||
2. 表格列修改(第218-228行):
|
||||
```typescript
|
||||
{
|
||||
title: '恒星数量',
|
||||
dataIndex: 'star_count',
|
||||
key: 'star_count',
|
||||
width: 100,
|
||||
render: (count) => (
|
||||
<Tag color={count > 1 ? 'gold' : 'default'}>
|
||||
{count}颗
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
```
|
||||
|
||||
**UI效果**:
|
||||
- 单星系统: 灰色标签 "1颗"
|
||||
- 多星系统: 金色标签 "2颗" / "3颗"
|
||||
|
||||
---
|
||||
|
||||
## 验证结果
|
||||
|
||||
### 数据库验证
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
s.id,
|
||||
s.name_zh,
|
||||
COUNT(CASE WHEN cb.type = 'star' THEN 1 END) as star_count
|
||||
FROM star_systems s
|
||||
LEFT JOIN celestial_bodies cb ON s.id = cb.system_id
|
||||
WHERE s.id IN (5, 11, 40, 49, 128, 267, 347, 479, 572)
|
||||
GROUP BY s.id, s.name_zh
|
||||
ORDER BY s.id;
|
||||
```
|
||||
|
||||
**结果**:
|
||||
```
|
||||
16 Cyg B系统 (ID=5): 2颗恒星
|
||||
55 Cnc系统 (ID=11): 2颗恒星
|
||||
eps Ind A系统 (ID=40): 3颗恒星
|
||||
gam Cep系统 (ID=49): 2颗恒星
|
||||
GJ 86系统 (ID=128): 2颗恒星
|
||||
HD 196885系统 (ID=267): 2颗恒星
|
||||
HD 41004系统 (ID=347): 2颗恒星
|
||||
比邻星系统 (ID=479): 3颗恒星
|
||||
ups And系统 (ID=572): 2颗恒星
|
||||
```
|
||||
|
||||
✅ **所有数据验证通过!**
|
||||
|
||||
---
|
||||
|
||||
## 文档和资源
|
||||
|
||||
### 生成的文档
|
||||
|
||||
1. **MULTI_STAR_SYSTEMS_ANALYSIS.md** - 多星系统分析报告
|
||||
- 详细的天文学资料
|
||||
- 数据来源建议
|
||||
- 50+个潜在双星系统清单
|
||||
|
||||
2. **BINARY_SYSTEMS_COMPLETION.md** (本文档) - 完成报告
|
||||
- 执行摘要
|
||||
- 详细系统列表
|
||||
- 技术实现文档
|
||||
|
||||
### 脚本文件
|
||||
|
||||
1. **backend/scripts/add_binary_systems.py** - 多星系统数据补全脚本
|
||||
2. **backend/scripts/activate_multisystem_stars.py** - Phase 3原始脚本(Alpha Centauri)
|
||||
|
||||
---
|
||||
|
||||
## 科学价值与亮点
|
||||
|
||||
### 系统多样性
|
||||
|
||||
本次补全的系统展示了多星系统的丰富多样性:
|
||||
|
||||
1. **不同双星分离距离**:
|
||||
- 紧密双星: gam Cep (20 AU)
|
||||
- 中等分离: GJ 86 (21 AU), HD 41004 (23 AU)
|
||||
- 宽双星: 16 Cyg (850 AU), 55 Cnc (1065 AU), eps Ind (1460 AU)
|
||||
|
||||
2. **不同伴星类型**:
|
||||
- 主序星伴星(大多数)
|
||||
- 白矮星伴星(GJ 86 B)⭐
|
||||
- 棕矮星伴星(eps Ind Ba和Bb)⭐
|
||||
|
||||
3. **行星系统复杂度**:
|
||||
- 单行星系统: 16 Cyg, gam Cep, HD 41004, GJ 86, HD 196885
|
||||
- 多行星系统: 55 Cnc (5颗), ups And (4颗), Alpha Cen (2颗), eps Ind (1颗)
|
||||
|
||||
### 研究意义
|
||||
|
||||
这些系统对于研究以下课题具有重要价值:
|
||||
|
||||
1. **双星中的行星形成**
|
||||
- 双星引力如何影响行星轨道
|
||||
- 行星能否在双星系统中稳定存在
|
||||
|
||||
2. **恒星演化对行星的影响**
|
||||
- GJ 86: 白矮星伴星的演化历史
|
||||
- eps Ind: 棕矮星的冷却过程
|
||||
|
||||
3. **系外生命可能性**
|
||||
- 多星系统的宜居带研究
|
||||
- Alpha Centauri和eps Ind的近距离探测潜力
|
||||
|
||||
---
|
||||
|
||||
## 后续建议
|
||||
|
||||
### 优先级1: 数据库索引优化
|
||||
|
||||
建议添加索引以提升查询性能:
|
||||
|
||||
```sql
|
||||
CREATE INDEX IF NOT EXISTS idx_celestial_bodies_system_type
|
||||
ON celestial_bodies(system_id, type);
|
||||
```
|
||||
|
||||
### 优先级2: 补全其他潜在多星系统
|
||||
|
||||
根据MULTI_STAR_SYSTEMS_ANALYSIS.md,还有50+个潜在的双星系统待验证和补全,包括:
|
||||
|
||||
- **Aldebaran (毕宿五)** - 红巨星 + M型伴星
|
||||
- **GJ 15 A, GJ 676 A, GJ 720 A** 等近距离双星系统
|
||||
|
||||
### 优先级3: 前端可视化增强
|
||||
|
||||
建议在银河视图中:
|
||||
- 显示多星系统的特殊标记(如双星图标)
|
||||
- 点击时展示完整的恒星列表
|
||||
- 可视化双星的轨道关系
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
✅ **任务完成状态**: 100%
|
||||
|
||||
- ✅ 9个高价值多星系统数据补全
|
||||
- ✅ 20颗恒星(含棕矮星和白矮星)
|
||||
- ✅ 数据完整性验证通过
|
||||
- ✅ 前后端功能更新完成
|
||||
- ✅ 文档和脚本齐全
|
||||
|
||||
**影响**:
|
||||
- 数据库中的多星系统从1个增加到9个(900%增长)
|
||||
- 覆盖了距离地球3.6-140光年范围内的重要多星系统
|
||||
- 包含了罕见的白矮星伴星和棕矮星三体系统
|
||||
- 为用户提供了更准确、更完整的天文学数据
|
||||
|
||||
**科学准确性**: 所有数据均基于真实的天文学资料,包括SIMBAD、NASA Exoplanet Archive等权威数据源。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2025-12-07
|
||||
**作者**: Claude Code AI Assistant
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
# Cosmo 相机聚焦算法文档
|
||||
|
||||
本文档详细说明了Cosmo项目中两种视图模式下的相机聚焦算法实现。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 概述](#1-概述)
|
||||
- [2. 太阳系模式(Solar System Mode)](#2-太阳系模式solar-system-mode)
|
||||
- [3. 银河系模式(Galaxy Mode)](#3-银河系模式galaxy-mode)
|
||||
- [4. 算法对比](#4-算法对比)
|
||||
- [5. 关键参数调优建议](#5-关键参数调优建议)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
Cosmo项目包含两种视图模式,每种模式都有独特的相机聚焦算法:
|
||||
|
||||
- **太阳系模式**:用于观察太阳系内的天体(行星、卫星、探测器等)
|
||||
- **银河系模式**:用于观察恒星际空间中的恒星系统和系外行星
|
||||
|
||||
两种模式的聚焦算法设计理念不同,以适应各自的尺度和用户体验需求。
|
||||
|
||||
---
|
||||
|
||||
## 2. 太阳系模式(Solar System Mode)
|
||||
|
||||
### 2.1 实现位置
|
||||
|
||||
**文件**: `/frontend/src/components/CameraController.tsx`
|
||||
|
||||
**组件**: `CameraController`
|
||||
|
||||
### 2.2 算法原理
|
||||
|
||||
太阳系模式采用**固定偏移量**的聚焦策略,相机位置相对于目标天体有固定的空间偏移。
|
||||
|
||||
### 2.3 核心算法
|
||||
|
||||
```typescript
|
||||
// 1. 获取目标天体的渲染位置
|
||||
const renderPos = calculateRenderPosition(focusTarget, allBodies);
|
||||
const currentTargetPos = new Vector3(renderPos.x, renderPos.z, renderPos.y);
|
||||
|
||||
// 2. 计算目标到原点的距离
|
||||
const pos = focusTarget.positions[0];
|
||||
const distance = Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2);
|
||||
|
||||
// 3. 根据天体类型确定偏移量
|
||||
let offset: number;
|
||||
let heightMultiplier = 1;
|
||||
let sideMultiplier = 1;
|
||||
|
||||
if (focusTarget.type === 'planet') {
|
||||
offset = 4;
|
||||
heightMultiplier = 1.5;
|
||||
sideMultiplier = 1;
|
||||
} else if (focusTarget.type === 'probe') {
|
||||
if (parentInfo) {
|
||||
// 探测器在行星附近
|
||||
offset = 3;
|
||||
heightMultiplier = 0.8;
|
||||
sideMultiplier = 1.2;
|
||||
} else if (distance < 10) {
|
||||
// 近距离探测器
|
||||
offset = 5;
|
||||
heightMultiplier = 0.6;
|
||||
sideMultiplier = 1.5;
|
||||
} else if (distance > 50) {
|
||||
// 远距离探测器
|
||||
offset = 4;
|
||||
heightMultiplier = 0.8;
|
||||
sideMultiplier = 1;
|
||||
} else {
|
||||
// 中距离探测器
|
||||
offset = 6;
|
||||
heightMultiplier = 0.8;
|
||||
sideMultiplier = 1.2;
|
||||
}
|
||||
} else {
|
||||
// 其他天体类型
|
||||
offset = 10;
|
||||
heightMultiplier = 1;
|
||||
sideMultiplier = 1;
|
||||
}
|
||||
|
||||
// 4. 计算相机目标位置(简单的坐标偏移)
|
||||
targetPosition.current.set(
|
||||
currentTargetPos.x + (offset * sideMultiplier),
|
||||
currentTargetPos.y + (offset * heightMultiplier),
|
||||
currentTargetPos.z + offset
|
||||
);
|
||||
```
|
||||
|
||||
### 2.4 动画实现
|
||||
|
||||
使用帧动画进行平滑过渡:
|
||||
|
||||
```typescript
|
||||
// 使用 easing 函数实现平滑动画
|
||||
const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
|
||||
// 线性插值相机位置
|
||||
camera.position.lerpVectors(startPosition.current, targetPosition.current, eased);
|
||||
|
||||
// 相机始终看向目标
|
||||
camera.lookAt(renderPos.x, renderPos.z, renderPos.y);
|
||||
```
|
||||
|
||||
### 2.5 特点
|
||||
|
||||
- ✅ **固定偏移量**:相机距离目标的偏移量是预设的常量
|
||||
- ✅ **类型感知**:不同类型的天体使用不同的偏移参数
|
||||
- ✅ **上下文感知**:探测器根据其位置(近行星、近太阳、远距离)调整相机距离
|
||||
- ✅ **简单直观**:适合太阳系内的小尺度观察
|
||||
- ✅ **动画平滑**:使用ease-in-out缓动函数
|
||||
|
||||
### 2.6 偏移量参数表
|
||||
|
||||
| 天体类型 | offset | heightMultiplier | sideMultiplier | 相机高度 | 相机侧向距离 | 相机深度距离 |
|
||||
|---------|--------|------------------|----------------|----------|-------------|-------------|
|
||||
| 行星 (planet) | 4 | 1.5 | 1 | 6 | 4 | 4 |
|
||||
| 探测器-近行星 (probe near planet) | 3 | 0.8 | 1.2 | 2.4 | 3.6 | 3 |
|
||||
| 探测器-近距离 (probe < 10 AU) | 5 | 0.6 | 1.5 | 3 | 7.5 | 5 |
|
||||
| 探测器-远距离 (probe > 50 AU) | 4 | 0.8 | 1 | 3.2 | 4 | 4 |
|
||||
| 探测器-中距离 (probe 10-50 AU) | 6 | 0.8 | 1.2 | 4.8 | 7.2 | 6 |
|
||||
| 其他天体 | 10 | 1 | 1 | 10 | 10 | 10 |
|
||||
|
||||
**计算公式**:
|
||||
```
|
||||
相机X = 目标X + offset × sideMultiplier
|
||||
相机Y = 目标Y + offset × heightMultiplier
|
||||
相机Z = 目标Z + offset
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 银河系模式(Galaxy Mode)
|
||||
|
||||
### 3.1 实现位置
|
||||
|
||||
**文件**: `/frontend/src/components/GalaxyScene.tsx`
|
||||
|
||||
**组件**: `CameraAnimator`
|
||||
|
||||
### 3.2 算法原理
|
||||
|
||||
银河系模式采用**向量方向聚焦**策略,相机沿着"太阳→目标恒星"的方向向量定位,确保:
|
||||
1. 目标恒星始终在屏幕正前方
|
||||
2. 相机在目标的远端(远离太阳的一侧)
|
||||
3. 距离根据目标的远近动态调整
|
||||
|
||||
### 3.3 核心算法
|
||||
|
||||
```typescript
|
||||
// 1. 计算目标恒星到太阳的距离
|
||||
const targetDistanceFromSun = Math.sqrt(x * x + y * y + z * z);
|
||||
|
||||
// 2. 动态计算相机拉远距离
|
||||
const basePullBack = 150;
|
||||
const pullBackDistance = targetDistanceFromSun < 500
|
||||
? basePullBack
|
||||
: basePullBack + (targetDistanceFromSun - 500) * 0.08;
|
||||
|
||||
// 3. 计算方向向量(从太阳指向目标恒星,已归一化)
|
||||
const dirX = x / targetDistanceFromSun;
|
||||
const dirY = y / targetDistanceFromSun;
|
||||
const dirZ = z / targetDistanceFromSun;
|
||||
|
||||
// 4. 计算相机位置:目标位置 + 方向向量 × 拉远距离
|
||||
// 相机在目标的"后方"(远离太阳的一侧)
|
||||
const cameraX = x + dirX * pullBackDistance;
|
||||
const cameraY = y + dirY * pullBackDistance + 30; // 额外的垂直偏移
|
||||
const cameraZ = z + dirZ * pullBackDistance;
|
||||
```
|
||||
|
||||
### 3.4 图解说明
|
||||
|
||||
```
|
||||
相机位置
|
||||
↓
|
||||
太阳 (0,0,0) ----方向向量----> 目标恒星 ------拉远距离-----> 📷
|
||||
Origin Target Star (x,y,z) (x+dirX×d, y+dirY×d+30, z+dirZ×d)
|
||||
|
||||
|
||||
相机看向目标 ←
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 相机位置 = `目标位置 + 方向向量 × pullBackDistance`
|
||||
- **不是**:`目标位置 - 方向向量 × pullBackDistance`(这会把相机放在太阳和目标之间,导致聚焦错误)
|
||||
|
||||
### 3.5 动画实现
|
||||
|
||||
```typescript
|
||||
// 使用 easeInOutCubic 缓动函数
|
||||
const eased = progress < 0.5
|
||||
? 4 * progress * progress * progress
|
||||
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
||||
|
||||
// 插值相机位置
|
||||
camera.position.x = startPos.x + (cameraX - startPos.x) * eased;
|
||||
camera.position.y = startPos.y + (cameraY - startPos.y) * eased;
|
||||
camera.position.z = startPos.z + (cameraZ - startPos.z) * eased;
|
||||
|
||||
// 插值 OrbitControls 的目标点
|
||||
controls.target.x = startTarget.x + (x - startTarget.x) * eased;
|
||||
controls.target.y = startTarget.y + (y - startTarget.y) * eased;
|
||||
controls.target.z = startTarget.z + (z - startTarget.z) * eased;
|
||||
controls.update();
|
||||
```
|
||||
|
||||
### 3.6 特点
|
||||
|
||||
- ✅ **方向向量驱动**:基于太阳→目标的方向计算相机位置
|
||||
- ✅ **动态距离**:根据目标距离自动调整拉远距离
|
||||
- ✅ **正确定位**:相机在目标的远端,确保目标在屏幕正前方
|
||||
- ✅ **尺度感知**:近距离恒星和远距离恒星有明显的视觉差异
|
||||
- ✅ **平滑过渡**:同时插值相机位置和OrbitControls的target
|
||||
|
||||
### 3.7 距离计算公式
|
||||
|
||||
| 目标距离 (AU单位) | 拉远距离计算 | 示例 |
|
||||
|------------------|------------|------|
|
||||
| < 500 | 150 (固定) | 比邻星系统 (~130 AU):拉远150单位 |
|
||||
| ≥ 500 | 150 + (distance - 500) × 0.08 | 距离2000 AU的系统:拉远150 + 1500×0.08 = 270单位 |
|
||||
| ≥ 500 | 150 + (distance - 500) × 0.08 | 距离5000 AU的系统:拉远150 + 4500×0.08 = 510单位 |
|
||||
|
||||
**公式**:
|
||||
```
|
||||
pullBackDistance = distance < 500 ? 150 : 150 + (distance - 500) × 0.08
|
||||
```
|
||||
|
||||
### 3.8 坐标系说明
|
||||
|
||||
在银河系模式中,使用的坐标系统:
|
||||
- **X轴**:指向银道坐标系的X方向
|
||||
- **Y轴**:垂直于银道平面(向上为正)
|
||||
- **Z轴**:指向银道坐标系的Z方向
|
||||
- **原点**:太阳系(Solar System)
|
||||
- **单位**:秒差距(Parsec)× 100(渲染缩放)
|
||||
|
||||
**坐标转换**:
|
||||
```typescript
|
||||
// 数据库中的坐标(秒差距)
|
||||
const db_x = star.position_x; // 单位: pc
|
||||
const db_y = star.position_y; // 单位: pc
|
||||
const db_z = star.position_z; // 单位: pc
|
||||
|
||||
// 渲染坐标(Three.js场景坐标,SCALE=100)
|
||||
const render_x = db_x * 100;
|
||||
const render_y = db_y * 100;
|
||||
const render_z = db_z * 100;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 算法对比
|
||||
|
||||
| 特性 | 太阳系模式 | 银河系模式 |
|
||||
|------|----------|----------|
|
||||
| **聚焦策略** | 固定偏移量 | 方向向量 + 动态距离 |
|
||||
| **相机定位方式** | 目标 + 常量偏移 | 目标 + 方向 × 动态距离 |
|
||||
| **尺度范围** | 0.1 - 100 AU | 100 - 5000+ AU (pc级别) |
|
||||
| **距离感** | 偏移量固定,距离感弱 | 动态调整,距离感强 |
|
||||
| **类型感知** | 强(根据天体类型调整) | 无(所有恒星系统相同策略) |
|
||||
| **计算复杂度** | 低(简单加法) | 中(向量计算 + 归一化) |
|
||||
| **缓动函数** | ease-in-out (quadratic) | easeInOutCubic |
|
||||
| **动画时长** | 由帧率和速度参数决定 | 固定2.5秒 |
|
||||
| **适用场景** | 小尺度、多类型天体 | 大尺度、单一类型(恒星系统) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键参数调优建议
|
||||
|
||||
### 5.1 太阳系模式
|
||||
|
||||
如果需要调整相机距离:
|
||||
|
||||
```typescript
|
||||
// 在 CameraController.tsx 中修改 offset 值
|
||||
if (focusTarget.type === 'planet') {
|
||||
offset = 4; // 增大此值会让相机离行星更远
|
||||
heightMultiplier = 1.5; // 增大此值会增加相机的高度
|
||||
sideMultiplier = 1; // 增大此值会增加相机的侧向距离
|
||||
}
|
||||
```
|
||||
|
||||
**建议**:
|
||||
- 小天体(卫星、小行星):offset 2-5
|
||||
- 中等天体(行星):offset 4-8
|
||||
- 大天体(木星、土星):offset 8-15
|
||||
- 探测器:offset 3-6
|
||||
|
||||
### 5.2 银河系模式
|
||||
|
||||
如果需要调整相机距离:
|
||||
|
||||
```typescript
|
||||
// 在 GalaxyScene.tsx 中修改 basePullBack 和系数
|
||||
const basePullBack = 150; // 基础拉远距离(单位:AU × 100)
|
||||
const pullBackDistance = targetDistanceFromSun < 500
|
||||
? basePullBack
|
||||
: basePullBack + (targetDistanceFromSun - 500) * 0.08; // 0.08 是距离系数
|
||||
```
|
||||
|
||||
**建议**:
|
||||
- 近距离恒星(< 500单位):basePullBack = 120-180
|
||||
- 距离系数:0.05-0.12(越大,远距离恒星拉得越远)
|
||||
- 垂直偏移:20-50(增加俯视角度)
|
||||
|
||||
### 5.3 动画速度调整
|
||||
|
||||
**太阳系模式**:
|
||||
```typescript
|
||||
animationProgress.current += delta * 0.8; // 增大此值会加快动画
|
||||
```
|
||||
|
||||
**银河系模式**:
|
||||
```typescript
|
||||
const duration = 2500; // 增大此值会减慢动画(单位:毫秒)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见问题与解决方案
|
||||
|
||||
### Q1: 银河系模式聚焦后看不到目标恒星?
|
||||
|
||||
**原因**:相机距离目标太近,或者相机定位在目标和太阳之间。
|
||||
|
||||
**检查**:
|
||||
```typescript
|
||||
// 确保使用的是加法,不是减法
|
||||
const cameraX = x + dirX * pullBackDistance; // ✅ 正确
|
||||
const cameraX = x - dirX * pullBackDistance; // ❌ 错误,会把相机放在中间
|
||||
```
|
||||
|
||||
### Q2: 太阳系模式下相机离探测器太远?
|
||||
|
||||
**解决**:
|
||||
```typescript
|
||||
// 减小探测器的 offset 值
|
||||
if (focusTarget.type === 'probe') {
|
||||
offset = 3; // 从 6 减小到 3
|
||||
}
|
||||
```
|
||||
|
||||
### Q3: 银河系模式下远距离恒星聚焦后太小?
|
||||
|
||||
**解决**:
|
||||
```typescript
|
||||
// 减小距离系数,让远距离恒星的相机不要拉得太远
|
||||
const pullBackDistance = targetDistanceFromSun < 500
|
||||
? basePullBack
|
||||
: basePullBack + (targetDistanceFromSun - 500) * 0.05; // 从0.08降到0.05
|
||||
```
|
||||
|
||||
### Q4: 动画过渡不够平滑?
|
||||
|
||||
**解决**:
|
||||
```typescript
|
||||
// 太阳系模式:减小动画速度
|
||||
animationProgress.current += delta * 0.5; // 从0.8降到0.5
|
||||
|
||||
// 银河系模式:增加动画时长
|
||||
const duration = 3500; // 从2500增加到3500ms
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 版本历史
|
||||
|
||||
| 版本 | 日期 | 修改内容 |
|
||||
|-----|------|---------|
|
||||
| 1.0 | 2025-12-06 | 初始版本,记录太阳系模式和银河系模式的聚焦算法 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 参考资料
|
||||
|
||||
- Three.js 文档: https://threejs.org/docs/
|
||||
- React Three Fiber: https://docs.pmnd.rs/react-three-fiber/
|
||||
- OrbitControls: https://threejs.org/docs/#examples/en/controls/OrbitControls
|
||||
- Easing Functions: https://easings.net/
|
||||
|
||||
---
|
||||
|
||||
**文档维护**: Cosmo Development Team
|
||||
**最后更新**: 2025-12-06
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
# 多恒星系统分析报告
|
||||
|
||||
**生成时间**: 2025-12-07
|
||||
**数据库**: cosmo_db
|
||||
**系外行星系统总数**: 579+
|
||||
|
||||
---
|
||||
|
||||
## 摘要
|
||||
|
||||
通过分析数据库中的恒星系统,发现以下情况:
|
||||
|
||||
- **当前已补全的多星系统**: 1个(Alpha Centauri三体系统)
|
||||
- **潜在的双星/多星系统**: 约50+个(根据命名规则判断)
|
||||
- **需要数据补全的著名多星系统**: 8-10个优先级较高的系统
|
||||
|
||||
---
|
||||
|
||||
## 已补全的多星系统
|
||||
|
||||
### 1. Alpha Centauri(南门二/比邻星系统)- system_id = 479 ✅
|
||||
|
||||
**状态**: 已完成三星数据补全
|
||||
|
||||
- **Alpha Centauri A (南门二A)** - star-479-primary
|
||||
- 光谱类型: G2V (类太阳恒星)
|
||||
- 质量: 1.1 M☉
|
||||
- 半径: 1.22 R☉
|
||||
- 温度: 5790K
|
||||
|
||||
- **Alpha Centauri B (南门二B)** - star-479-secondary
|
||||
- 光谱类型: K1V
|
||||
- 质量: 0.93 M☉
|
||||
- 半径: 0.86 R☉
|
||||
- 温度: 5260K
|
||||
|
||||
- **Proxima Centauri (比邻星)** - star-479-tertiary
|
||||
- 光谱类型: M5.5V (红矮星)
|
||||
- 质量: 0.12 M☉
|
||||
- 半径: 0.14 R☉
|
||||
- 温度: 2900K
|
||||
|
||||
**系统特征**:
|
||||
- 距离地球: 1.30 pc (~4.24 光年)
|
||||
- 系统类型: 三体恒星系统
|
||||
- 行星数量: 2颗(Proxima Cen b, Proxima Cen d)
|
||||
|
||||
---
|
||||
|
||||
## 著名的待补全多星系统(优先级排序)
|
||||
|
||||
### 高优先级(距离近、科学价值高)
|
||||
|
||||
#### 2. 55 Cancri(巨蟹座55)- system_id = 11, 12 🌟
|
||||
|
||||
**现状**: 数据库中有两个system记录
|
||||
- system_id = 11: 55 Cnc系统(主星55 Cnc A)
|
||||
- system_id = 12: 55 Cnc B系统(伴星55 Cnc B)
|
||||
|
||||
**天文学资料**:
|
||||
- **55 Cancri A**: G8V型黄矮星(类似太阳)
|
||||
- 质量: 0.95 M☉
|
||||
- 半径: 0.94 R☉
|
||||
- 温度: 5196K
|
||||
- 行星: 5颗已确认(e, b, c, f, d)
|
||||
|
||||
- **55 Cancri B**: M3.5-4V型红矮星
|
||||
- 质量: 0.13 M☉
|
||||
- 距离A星: 1065 AU
|
||||
- 双星轨道周期: ~1000年
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球仅12.6 pc (~41光年)
|
||||
- 最早发现的多行星系统之一
|
||||
- A星有超级地球55 Cnc e(岩浆行星)
|
||||
|
||||
---
|
||||
|
||||
#### 3. 16 Cygni(天鹅座16)- system_id = 5 🌟
|
||||
|
||||
**现状**: 仅有16 Cyg B的数据
|
||||
|
||||
**天文学资料**:
|
||||
- **16 Cygni A**: G1.5V型黄矮星
|
||||
- 质量: 1.11 M☉
|
||||
- 半径: 1.24 R☉
|
||||
- 温度: 5825K
|
||||
|
||||
- **16 Cygni B**: G2.5V型黄矮星
|
||||
- 质量: 1.07 M☉
|
||||
- 半径: 1.14 R☉
|
||||
- 温度: 5750K
|
||||
- 行星: 1颗(16 Cyg B b,偏心轨道)
|
||||
|
||||
- **16 Cygni C**: 可能的第三颗伴星(未确认)
|
||||
|
||||
**双星参数**:
|
||||
- 距离地球: 21.4 pc (~70光年)
|
||||
- 双星分离: ~850 AU
|
||||
- 轨道周期: ~18,200年
|
||||
|
||||
**科学价值**:
|
||||
- 研究双星系统中行星形成的典范
|
||||
- 16 Cyg B b的高偏心率轨道揭示双星引力影响
|
||||
|
||||
---
|
||||
|
||||
#### 4. Epsilon Indi(天园增四)- system_id = 40 🌟
|
||||
|
||||
**现状**: 仅有eps Ind A的数据
|
||||
|
||||
**天文学资料**:
|
||||
- **Epsilon Indi A**: K5V型橙矮星
|
||||
- 质量: 0.76 M☉
|
||||
- 半径: 0.73 R☉
|
||||
- 温度: 4630K
|
||||
|
||||
- **Epsilon Indi Ba**: T1V型棕矮星
|
||||
- 质量: ~47 MJ (木星质量)
|
||||
- 距离A星: ~1460 AU
|
||||
|
||||
- **Epsilon Indi Bb**: T6V型棕矮星
|
||||
- 质量: ~28 MJ
|
||||
- 与Ba互绕,周期~15年
|
||||
|
||||
**系统特征**:
|
||||
- 距离地球: 3.63 pc (~11.8光年)
|
||||
- 第五近的恒星系统
|
||||
- 三体系统(1颗恒星 + 2颗棕矮星)
|
||||
|
||||
**科学价值**:
|
||||
- 最近的棕矮星双星系统
|
||||
- 研究恒星-亚恒星边界的理想目标
|
||||
|
||||
---
|
||||
|
||||
#### 5. Gamma Cephei(仙王座γ)- system_id = 49
|
||||
|
||||
**现状**: 仅有gam Cep A的数据
|
||||
|
||||
**天文学资料**:
|
||||
- **Gamma Cephei A**: K1IV型亚巨星
|
||||
- 质量: 1.59 M☉
|
||||
- 半径: 4.9 R☉
|
||||
- 温度: 4800K
|
||||
- 行星: 1颗(gam Cep b)
|
||||
|
||||
- **Gamma Cephei B**: M4V型红矮星
|
||||
- 质量: 0.4 M☉
|
||||
- 双星分离: ~20 AU
|
||||
- 轨道周期: ~66年
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球: 13.8 pc (~45光年)
|
||||
- 最早被怀疑有行星的恒星之一(1988年)
|
||||
- 紧密双星系统中的行星形成研究案例
|
||||
|
||||
---
|
||||
|
||||
#### 6. HD 41004 - system_id = 347, 348
|
||||
|
||||
**现状**: 数据库中有两条记录
|
||||
- system_id = 347: HD 41004 A
|
||||
- system_id = 348: HD 41004 B
|
||||
|
||||
**天文学资料**:
|
||||
- **HD 41004 A**: K1V型橙矮星
|
||||
- 质量: 0.70 M☉
|
||||
- 行星: HD 41004 A b(类木行星)
|
||||
|
||||
- **HD 41004 B**: M2V型红矮星
|
||||
- 质量: 0.40 M☉
|
||||
- 可能有棕矮星伴星
|
||||
|
||||
**双星参数**:
|
||||
- 距离地球: 42.8 pc (~140光年)
|
||||
- 双星分离: ~23 AU
|
||||
|
||||
---
|
||||
|
||||
#### 7. Upsilon Andromedae(仙女座υ)- system_id = 572
|
||||
|
||||
**现状**: 仅有主星数据
|
||||
|
||||
**天文学资料**:
|
||||
- **Upsilon Andromedae A**: F8V型黄白主序星
|
||||
- 质量: 1.27 M☉
|
||||
- 半径: 1.63 R☉
|
||||
- 温度: 6212K
|
||||
- 行星: 4颗(b, c, d, e)
|
||||
|
||||
- **Upsilon Andromedae B**: M4.5V型红矮星
|
||||
- 质量: 0.25 M☉
|
||||
- 双星分离: ~750 AU
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球: 13.5 pc (~44光年)
|
||||
- 第一个被发现有多颗行星的主序星(1999年)
|
||||
- 行星轨道共面性研究的重要目标
|
||||
|
||||
---
|
||||
|
||||
#### 8. GJ 86(格利泽86)- system_id = 128
|
||||
|
||||
**现状**: 仅有主星数据
|
||||
|
||||
**天文学资料**:
|
||||
- **GJ 86 A**: K1V型橙矮星
|
||||
- 质量: 0.79 M☉
|
||||
- 行星: GJ 86 b(类木行星)
|
||||
|
||||
- **GJ 86 B**: 白矮星
|
||||
- 质量: ~0.55 M☉
|
||||
- 双星分离: ~21 AU
|
||||
|
||||
**科学价值**:
|
||||
- 距离地球: 10.8 pc (~35光年)
|
||||
- 罕见的包含白矮星的系外行星系统
|
||||
- 研究恒星演化对行星影响的重要案例
|
||||
|
||||
---
|
||||
|
||||
#### 9. HD 196885 - system_id = 267
|
||||
|
||||
**现状**: 仅有HD 196885 A的数据
|
||||
|
||||
**天文学资料**:
|
||||
- **HD 196885 A**: F8V型黄白主序星
|
||||
- 质量: 1.33 M☉
|
||||
- 行星: HD 196885 A b
|
||||
|
||||
- **HD 196885 B**: M型红矮星
|
||||
- 双星分离: ~25 AU
|
||||
|
||||
---
|
||||
|
||||
### 中优先级(科学价值较高)
|
||||
|
||||
#### 10. Aldebaran(毕宿五/金牛座α)- system_id = 19
|
||||
|
||||
**天文学资料**:
|
||||
- **Aldebaran A**: K5III型红巨星
|
||||
- 质量: 1.16 M☉
|
||||
- 半径: 44.2 R☉(已膨胀)
|
||||
- 温度: 3910K
|
||||
- 行星: Aldebaran b(争议)
|
||||
|
||||
- **Aldebaran B**: M2V型红矮星(光学伴星,物理关联存疑)
|
||||
|
||||
**距离**: 20.0 pc (~65光年)
|
||||
|
||||
---
|
||||
|
||||
## 其他潜在的双星/多星系统
|
||||
|
||||
根据命名规则(带A/B后缀),以下系统也可能是多星系统:
|
||||
|
||||
### 距离较近的候选系统(<20 pc)
|
||||
|
||||
1. **eps Ind A** (system_id=40) - 已提及
|
||||
2. **BD+05 4868 A** (system_id=22)
|
||||
3. **COCONUTS-2 A** (system_id=34)
|
||||
4. **DMPP-3 A** (system_id=36)
|
||||
5. **DS Tuc A** (system_id=38)
|
||||
6. **GJ 15 A** (system_id=62)
|
||||
7. **GJ 338 B** (system_id=85)
|
||||
8. **GJ 414 A** (system_id=100)
|
||||
9. **GJ 676 A** (system_id=117)
|
||||
10. **GJ 720 A** (system_id=122)
|
||||
11. **GJ 896 A** (system_id=131)
|
||||
12. **GJ 900 A** (system_id=132)
|
||||
13. **Gl 725 A** (system_id=144)
|
||||
|
||||
### 待查证系统
|
||||
|
||||
这些系统的命名暗示可能是双星,但需要进一步查证:
|
||||
- **psi1 Dra B** (system_id=480)
|
||||
- **TOI-4336 A** (system_id=541)
|
||||
- **TOI-1450 A** (system_id=499)
|
||||
- **LTT 1445 A** (system_id=474)
|
||||
- **LP 261-75 A** (system_id=470)
|
||||
|
||||
---
|
||||
|
||||
## 数据来源建议
|
||||
|
||||
为了补全这些多星系统数据,建议查询以下资源:
|
||||
|
||||
1. **SIMBAD天文数据库** (http://simbad.u-strasbg.fr/)
|
||||
- 双星参数
|
||||
- 恒星物理参数(质量、半径、温度)
|
||||
|
||||
2. **NASA Exoplanet Archive**
|
||||
- 行星宿主恒星参数
|
||||
- 双星系统标记
|
||||
|
||||
3. **Washington Double Star Catalog (WDS)**
|
||||
- 双星轨道参数
|
||||
- 分离角和位置角
|
||||
|
||||
4. **Gaia DR3**
|
||||
- 精确距离和自行
|
||||
- 双星识别
|
||||
|
||||
---
|
||||
|
||||
## 实施建议
|
||||
|
||||
### Phase 3.5: 补全高优先级多星系统(建议顺序)
|
||||
|
||||
1. **55 Cancri** - 最著名的多行星双星系统
|
||||
2. **16 Cygni** - 双星行星形成研究典范
|
||||
3. **Epsilon Indi** - 最近的棕矮星三体系统
|
||||
4. **Gamma Cephei** - 紧密双星中的行星
|
||||
5. **Upsilon Andromedae** - 第一个多行星系统
|
||||
|
||||
### 数据结构参考
|
||||
|
||||
参考Alpha Centauri的实现方式:
|
||||
|
||||
```python
|
||||
MULTI_STAR_SYSTEMS = {
|
||||
11: { # 55 Cancri
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-11-primary",
|
||||
"name": "55 Cancri A",
|
||||
"name_zh": "巨蟹座55A",
|
||||
"description": "类太阳黄矮星,拥有5颗已确认行星",
|
||||
"extra_data": {
|
||||
"spectral_type": "G8V",
|
||||
"mass_solar": 0.95,
|
||||
"radius_solar": 0.94,
|
||||
"temperature_k": 5196
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-11-secondary",
|
||||
"name": "55 Cancri B",
|
||||
"name_zh": "巨蟹座55B",
|
||||
"description": "红矮星伴星,距离A星约1065 AU",
|
||||
"extra_data": {
|
||||
"spectral_type": "M4V",
|
||||
"mass_solar": 0.13,
|
||||
"radius_solar": 0.30,
|
||||
"temperature_k": 3200,
|
||||
"separation_au": 1065
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
### 当前状态
|
||||
- ✅ 1个三体系统已完成(Alpha Centauri)
|
||||
- ⚠️ 8-10个高价值双星/多星系统待补全
|
||||
- 📋 50+个潜在的双星系统需要查证
|
||||
|
||||
### 推荐行动
|
||||
1. 优先补全前5个高优先级系统(55 Cnc, 16 Cyg, eps Ind, gam Cep, ups And)
|
||||
2. 使用SIMBAD和NASA Exoplanet Archive查询恒星参数
|
||||
3. 更新`activate_multisystem_stars.py`脚本以支持新系统
|
||||
4. 验证数据完整性并在前端展示
|
||||
|
||||
### 预期收益
|
||||
- 提升科学准确性
|
||||
- 更好地展示系外行星系统的复杂性
|
||||
- 为用户提供更丰富的天文学知识
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: 1.0
|
||||
**最后更新**: 2025-12-07
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
# NASA数据源分析与数据策略
|
||||
|
||||
**日期**: 2025-12-06
|
||||
**目的**: 回答Phase 4关于恒星数据获取的关键问题
|
||||
|
||||
---
|
||||
|
||||
## 📊 三个核心问题的答案
|
||||
|
||||
### 问题1: 恒星系主序星的数据是否还需要从NASA获取?
|
||||
|
||||
**答案:不需要,数据已经有了!**
|
||||
|
||||
#### 现状分析
|
||||
|
||||
✅ **恒星数据已在`star_systems`表中**
|
||||
|
||||
```sql
|
||||
-- 当前star_systems表已包含完整的主恒星数据
|
||||
SELECT name, host_star_name, spectral_type, radius_solar,
|
||||
mass_solar, temperature_k, color
|
||||
FROM star_systems
|
||||
WHERE id = 479;
|
||||
|
||||
-- 结果示例(Proxima Centauri):
|
||||
-- name: Proxima Cen System
|
||||
-- host_star_name: Proxima Cen
|
||||
-- spectral_type: M5.5 V
|
||||
-- radius_solar: 0.141
|
||||
-- mass_solar: 0.1221
|
||||
-- temperature_k: 2900
|
||||
-- color: #ffbd6f
|
||||
```
|
||||
|
||||
**数据来源**:
|
||||
- 这些数据在Phase 3时已经通过`fetch_interstellar_data.py`脚本从NASA Exoplanet Archive获取
|
||||
- 数据源:NASA Exoplanet Archive的Planetary Systems (PS)表
|
||||
- 字段映射:
|
||||
- `st_spectype` → `spectral_type`
|
||||
- `st_rad` → `radius_solar`
|
||||
- `st_mass` → `mass_solar`
|
||||
- `st_teff` → `temperature_k`
|
||||
- 自动计算 → `color`
|
||||
|
||||
#### 需要做的事
|
||||
|
||||
❌ **不需要从NASA重新获取主恒星数据**
|
||||
|
||||
✅ **只需要从`star_systems`复制到`celestial_bodies`**
|
||||
|
||||
```python
|
||||
# 数据迁移,不是数据获取
|
||||
for system in star_systems:
|
||||
celestial_body = {
|
||||
"system_id": system.id,
|
||||
"name": system.host_star_name,
|
||||
"type": "star",
|
||||
"metadata": {
|
||||
"star_role": "primary",
|
||||
"spectral_type": system.spectral_type,
|
||||
"radius_solar": system.radius_solar,
|
||||
"mass_solar": system.mass_solar,
|
||||
"temperature_k": system.temperature_k,
|
||||
"color": system.color
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题2: 其它恒星系的位置数据是否还需要定时获取?
|
||||
|
||||
**答案:不需要定时获取,恒星位置是静态的(在人类时间尺度上)**
|
||||
|
||||
#### 恒星运动特性
|
||||
|
||||
**自行(Proper Motion)**:
|
||||
- 恒星相对于太阳的运动速度:通常10-100 km/s
|
||||
- 角度变化:每年约0.001-0.1角秒
|
||||
- 在50年内,即使是最快的恒星,位置变化也只有约5角秒(约0.0014度)
|
||||
|
||||
**举例**:
|
||||
```
|
||||
Barnard's Star(自行最快的恒星)
|
||||
- 自行:10.3角秒/年
|
||||
- 50年后位置变化:515角秒 ≈ 0.14度
|
||||
- 在我们的可视化尺度(秒差距级别):几乎可以忽略
|
||||
```
|
||||
|
||||
#### 结论
|
||||
|
||||
❌ **不需要定时获取位置数据**
|
||||
|
||||
原因:
|
||||
1. 恒星位置在人类时间尺度(几年、几十年)内几乎不变
|
||||
2. 自行造成的位置变化远小于我们的渲染精度
|
||||
3. NASA Exoplanet Archive中的坐标数据是某个epoch(如J2000.0)的快照,不会变化
|
||||
|
||||
#### 什么时候需要更新?
|
||||
|
||||
仅在以下情况:
|
||||
1. **新发现的恒星系统**:NASA Archive新增了系外行星系统
|
||||
2. **数据修正**:某个系统的距离或坐标被重新测量(罕见)
|
||||
3. **手动触发**:管理员手动运行更新脚本
|
||||
|
||||
**建议更新频率**:
|
||||
- 每季度或半年运行一次`fetch_interstellar_data.py`
|
||||
- 主要是为了获取新发现的系外行星系统,而不是更新位置
|
||||
|
||||
---
|
||||
|
||||
### 问题3: 伴星数据无法从NASA获取么?(大部分都不是单恒星系统)
|
||||
|
||||
**答案:可以从NASA获取!但需要额外的字段和逻辑**
|
||||
|
||||
#### NASA API支持情况
|
||||
|
||||
✅ **NASA Exoplanet Archive有多星系统数据**
|
||||
|
||||
关键字段:
|
||||
- `sy_snum`: 系统中恒星数量(Number of Stars in System)
|
||||
- `hostname`: 主恒星名称
|
||||
- Binary/Multiple star systems有特定标记
|
||||
|
||||
**实际数据查询**:
|
||||
```sql
|
||||
-- NASA API查询示例
|
||||
SELECT hostname, sy_snum, sy_dist
|
||||
FROM ps
|
||||
WHERE sy_dist < 50 AND sy_snum > 1
|
||||
ORDER BY sy_dist;
|
||||
|
||||
-- 结果示例:
|
||||
Proxima Cen sy_snum=3 (三星系统: Alpha Cen A, B, Proxima)
|
||||
GJ 15 A sy_snum=2 (双星系统)
|
||||
GJ 667 C sy_snum=3 (三星系统)
|
||||
LTT 1445 A sy_snum=3 (三星系统)
|
||||
...
|
||||
```
|
||||
|
||||
#### 伴星数据字段
|
||||
|
||||
NASA Archive的伴星相关字段:
|
||||
- `st_nstar`: 系统中恒星数量(与sy_snum相同)
|
||||
- Binary system parameters(如果可用):
|
||||
- `st_binary`: 是否为双星
|
||||
- `st_bincomp`: 伴星编号
|
||||
- `st_binsep`: 双星分离度(角秒)
|
||||
|
||||
**限制**:
|
||||
- ⚠️ NASA Archive **主要关注行星宿主星**,伴星详细数据可能不完整
|
||||
- ⚠️ 如果伴星没有行星,可能不在Archive中
|
||||
- ⚠️ 双星轨道参数(半长轴、周期、偏心率)通常不包含在PS表中
|
||||
|
||||
#### 补充数据源
|
||||
|
||||
对于伴星详细数据,需要结合其他数据源:
|
||||
|
||||
1. **SIMBAD** (推荐)
|
||||
- URL: http://simbad.u-strasbg.fr/simbad/
|
||||
- 数据:几乎所有已知恒星的详细参数
|
||||
- Python API: `astroquery.simbad`
|
||||
- 包含:双星轨道参数、伴星光谱类型、质量等
|
||||
|
||||
2. **Washington Double Star Catalog (WDS)**
|
||||
- URL: https://www.usno.navy.mil/USNO/astrometry/optical-IR-prod/wds
|
||||
- 专门的双星数据库
|
||||
- 包含:轨道参数、分离度、位置角
|
||||
|
||||
3. **Gaia Archive**
|
||||
- URL: https://gea.esac.esa.int/archive/
|
||||
- 高精度天体测量数据
|
||||
- 可以识别双星系统
|
||||
|
||||
#### 实施建议
|
||||
|
||||
**Phase 4.1: 仅主恒星**
|
||||
```python
|
||||
# 从star_systems表迁移,不涉及NASA API
|
||||
# 579个系统全部创建主恒星记录
|
||||
```
|
||||
|
||||
**Phase 4.2: 识别多星系统**
|
||||
```python
|
||||
# 查询NASA API,获取sy_snum字段
|
||||
systems_with_multistars = fetch_systems_where_sy_snum_gt_1()
|
||||
|
||||
# 结果示例:
|
||||
# {
|
||||
# "Proxima Cen": 3,
|
||||
# "GJ 15 A": 2,
|
||||
# "GJ 667 C": 3,
|
||||
# ...
|
||||
# }
|
||||
```
|
||||
|
||||
**Phase 4.3: 补充伴星数据(手动/半自动)**
|
||||
```python
|
||||
# 对于标记为多星系统的,从SIMBAD查询伴星数据
|
||||
for system in multistar_systems:
|
||||
# Query SIMBAD for companion stars
|
||||
companions = query_simbad_companions(system.hostname)
|
||||
|
||||
for companion in companions:
|
||||
# Insert into celestial_bodies
|
||||
insert_companion_star(
|
||||
system_id=system.id,
|
||||
name=companion.name,
|
||||
spectral_type=companion.spectral_type,
|
||||
orbital_params=companion.orbital_params
|
||||
)
|
||||
```
|
||||
|
||||
**数据质量评估**:
|
||||
- 约50个系统有多颗恒星(sy_snum > 1)
|
||||
- 其中约30个系统可以从SIMBAD获取伴星详细数据
|
||||
- 约20个系统需要手动补充或标记为"数据不完整"
|
||||
|
||||
---
|
||||
|
||||
## 🎯 最终数据策略
|
||||
|
||||
### Phase 4数据计划
|
||||
|
||||
#### ✅ 立即执行(Phase 4.1)
|
||||
|
||||
**主恒星数据迁移** - 无需从NASA获取
|
||||
```
|
||||
来源:star_systems表(已有数据)
|
||||
目标:celestial_bodies表
|
||||
数量:579条主恒星记录
|
||||
方法:数据库内部迁移脚本
|
||||
时间:< 1小时
|
||||
```
|
||||
|
||||
#### 📋 可选执行(Phase 4.2+)
|
||||
|
||||
**多星系统识别** - 需要从NASA获取
|
||||
```
|
||||
来源:NASA Exoplanet Archive (sy_snum字段)
|
||||
目标:更新star_systems.extra_data,标记多星系统
|
||||
数量:约50个多星系统
|
||||
方法:扩展fetch_interstellar_data.py脚本
|
||||
时间:< 1小时
|
||||
```
|
||||
|
||||
**伴星数据补充** - 需要从SIMBAD/WDS获取
|
||||
```
|
||||
来源:SIMBAD + Washington Double Star Catalog
|
||||
目标:celestial_bodies表(type='star', star_role='companion')
|
||||
数量:约50-100条伴星记录(估计)
|
||||
方法:新脚本 + 部分手动
|
||||
时间:2-4小时(半自动化)
|
||||
```
|
||||
|
||||
### 数据更新频率
|
||||
|
||||
| 数据类型 | 更新频率 | 原因 |
|
||||
|---------|---------|------|
|
||||
| 恒星位置 | **不需要** | 自行可忽略(50年 < 1%变化) |
|
||||
| 系外行星数据 | **每季度** | 新发现的系统 |
|
||||
| 恒星参数 | **每年** | 数据修正(罕见) |
|
||||
| 伴星数据 | **手动触发** | 数据来源分散,需人工整理 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 实施建议
|
||||
|
||||
### 推荐方案:分阶段实施
|
||||
|
||||
**Phase 4.1: MVP(最小可行产品)**
|
||||
```
|
||||
✅ 仅主恒星
|
||||
✅ 无需从NASA获取新数据
|
||||
✅ 579个恒星系统全部可用
|
||||
⏱️ 实施时间:4-6小时
|
||||
```
|
||||
|
||||
**Phase 4.2: 增强版(多星系统识别)**
|
||||
```
|
||||
📊 扩展NASA查询,获取sy_snum
|
||||
🏷️ 标记多星系统
|
||||
⏱️ 额外时间:1小时
|
||||
```
|
||||
|
||||
**Phase 5: 完整版(伴星展示)**
|
||||
```
|
||||
🌟 补充伴星数据(SIMBAD/WDS)
|
||||
🎨 实现双星轨道渲染
|
||||
⏱️ 额外时间:4-8小时
|
||||
```
|
||||
|
||||
### 代码示例
|
||||
|
||||
**扩展NASA查询(获取sy_snum)**:
|
||||
```python
|
||||
# 修改 fetch_interstellar_data.py
|
||||
|
||||
table = NasaExoplanetArchive.query_criteria(
|
||||
table="ps",
|
||||
select="hostname, sy_dist, ra, dec, sy_pnum, sy_snum, " # 新增sy_snum
|
||||
"st_spectype, st_rad, st_mass, st_teff, "
|
||||
"pl_name, pl_orbsmax, pl_orbper, pl_orbeccen, pl_rade, pl_eqt",
|
||||
where="sy_dist < 50",
|
||||
order="sy_dist"
|
||||
)
|
||||
|
||||
# 在系统数据中记录恒星数量
|
||||
systems[hostname]["data"]["star_count"] = int(get_val(row['sy_snum']))
|
||||
```
|
||||
|
||||
**从SIMBAD查询伴星数据**:
|
||||
```python
|
||||
from astroquery.simbad import Simbad
|
||||
|
||||
def query_companion_stars(primary_star_name):
|
||||
"""查询伴星信息"""
|
||||
# SIMBAD查询示例
|
||||
result_table = Simbad.query_object(primary_star_name)
|
||||
|
||||
# 查询双星信息
|
||||
# 这需要更复杂的查询逻辑,SIMBAD有专门的binary star tables
|
||||
|
||||
return companion_data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 结论
|
||||
|
||||
1. **主恒星数据**:✅ 已有,不需要从NASA获取
|
||||
2. **位置数据**:✅ 静态,不需要定时更新
|
||||
3. **伴星数据**:⚠️ 可以从NASA获取部分(sy_snum),完整数据需SIMBAD
|
||||
|
||||
**推荐行动**:
|
||||
- Phase 4.1先实现主恒星(已有数据)
|
||||
- Phase 5再考虑伴星(需额外数据源)
|
||||
|
||||
---
|
||||
|
||||
**文档作者**: Cosmo Development Team
|
||||
**最后更新**: 2025-12-06
|
||||
|
|
@ -0,0 +1,667 @@
|
|||
# Cosmo Phase 4 实施方案:其他恒星系统展示
|
||||
|
||||
**版本**: 1.0
|
||||
**日期**: 2025-12-06
|
||||
**状态**: 规划中
|
||||
|
||||
---
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [1. 现状分析](#1-现状分析)
|
||||
- [2. 核心问题](#2-核心问题)
|
||||
- [3. 解决方案](#3-解决方案)
|
||||
- [4. 数据补充策略](#4-数据补充策略)
|
||||
- [5. 实施步骤](#5-实施步骤)
|
||||
- [6. 技术细节](#6-技术细节)
|
||||
- [7. 风险与挑战](#7-风险与挑战)
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状分析
|
||||
|
||||
### 1.1 数据库现状
|
||||
|
||||
**已有数据**:
|
||||
```sql
|
||||
-- 恒星系统表
|
||||
star_systems: 579条记录(包括Solar System)
|
||||
- 每条记录包含:host_star_name, spectral_type, radius_solar, mass_solar, temperature_k, color等
|
||||
- 仅描述主恒星(primary star)
|
||||
|
||||
-- 天体表
|
||||
celestial_bodies: 已有数据
|
||||
- system_id = 1 (Solar System): 30条记录(太阳、行星、卫星、探测器等)
|
||||
- system_id = 4,5,6,7...: 系外行星数据(约898颗)
|
||||
- system_id = 1的恒星: Sun (仅1条)
|
||||
- 其他恒星系统的恒星: **缺失!**
|
||||
```
|
||||
|
||||
**数据统计**:
|
||||
- ✅ 579个恒星系统信息完整
|
||||
- ✅ 898颗系外行星已录入celestial_bodies
|
||||
- ❌ 只有太阳系的恒星(Sun)在celestial_bodies中
|
||||
- ❌ 其他578个恒星系统的恒星数据缺失
|
||||
- ❌ 多星系统(双星、三星等)的伴星数据完全缺失
|
||||
|
||||
### 1.2 表结构现状
|
||||
|
||||
**star_systems表**:
|
||||
```sql
|
||||
CREATE TABLE star_systems (
|
||||
id SERIAL PRIMARY KEY,
|
||||
host_star_name VARCHAR(200), -- 主恒星名称
|
||||
spectral_type VARCHAR(20), -- 主恒星光谱类型
|
||||
radius_solar DOUBLE PRECISION,
|
||||
mass_solar DOUBLE PRECISION,
|
||||
temperature_k DOUBLE PRECISION,
|
||||
color VARCHAR(20),
|
||||
-- ... 其他字段
|
||||
);
|
||||
```
|
||||
|
||||
**celestial_bodies表**:
|
||||
```sql
|
||||
CREATE TABLE celestial_bodies (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
system_id INTEGER REFERENCES star_systems(id), -- ✅ 已有外键
|
||||
name VARCHAR(200),
|
||||
type VARCHAR(50), -- 支持: star, planet, dwarf_planet, satellite, probe, comet
|
||||
-- ... 其他字段
|
||||
);
|
||||
```
|
||||
|
||||
**关键发现**:
|
||||
- ✅ `celestial_bodies`已有`system_id`外键,架构设计正确
|
||||
- ✅ `type`字段已支持`star`类型
|
||||
- ✅ 数据结构已经支持多恒星系统,只是数据缺失
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心问题
|
||||
|
||||
### 问题1:恒星数据缺失
|
||||
|
||||
**现象**:
|
||||
- `star_systems`表只记录主恒星信息,没有将恒星作为独立天体存入`celestial_bodies`
|
||||
- 导致其他恒星系统无法像太阳系一样展示恒星
|
||||
|
||||
**影响**:
|
||||
- 无法进入其他恒星系统视图(因为没有中心恒星)
|
||||
- 无法展示多星系统(如双星、三星系统)
|
||||
- 缺少恒星的3D模型、纹理资源
|
||||
|
||||
### 问题2:多星系统支持
|
||||
|
||||
**现实情况**:
|
||||
- 约50%的恒星系统是多星系统(双星、三星等)
|
||||
- 例如:
|
||||
- **Alpha Centauri**: 三星系统(A, B, Proxima)
|
||||
- **Sirius**: 双星系统(A, B)
|
||||
- **61 Cygni**: 双星系统(A, B)
|
||||
|
||||
**当前限制**:
|
||||
- `star_systems`表的`host_star_name`只能记录一个恒星
|
||||
- 没有记录伴星(companion stars)的数据
|
||||
|
||||
### 问题3:展示逻辑
|
||||
|
||||
**问题**:
|
||||
- 太阳系模式已实现完整的3D展示(行星、卫星、轨道、探测器)
|
||||
- 其他恒星系统是否也采用相同模式?
|
||||
- 多星系统如何展示恒星轨道?
|
||||
|
||||
---
|
||||
|
||||
## 3. 解决方案
|
||||
|
||||
### 3.1 方案概述
|
||||
|
||||
**核心策略**:复用现有架构,补充恒星数据
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ star_systems │
|
||||
│ (系统级元数据:位置、距离、系统名称) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ system_id (外键)
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ celestial_bodies │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ type=star│ │type=planet│ │type=star │ │type=planet│ │
|
||||
│ │ (主恒星) │ │ (行星1) │ │ (伴星) │ │ (行星2) │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │
|
||||
│ - 主恒星和伴星都作为 type='star' 存储 │
|
||||
│ - 通过 metadata/extra_data 区分主星和伴星 │
|
||||
│ - 行星归属于整个系统,不特定于某颗恒星 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 数据模型设计
|
||||
|
||||
#### 方案A:推荐方案 - 复用celestial_bodies
|
||||
|
||||
**优点**:
|
||||
- ✅ 无需修改表结构
|
||||
- ✅ 统一管理所有天体
|
||||
- ✅ 复用现有的3D渲染、资源管理逻辑
|
||||
- ✅ 简化查询和关联
|
||||
|
||||
**实现**:
|
||||
|
||||
```sql
|
||||
-- 1. 为每个恒星系统添加主恒星记录
|
||||
INSERT INTO celestial_bodies (
|
||||
id, -- 'star-{system_id}-primary'
|
||||
system_id, -- 外键指向star_systems
|
||||
name, -- 从star_systems.host_star_name复制
|
||||
name_zh,
|
||||
type, -- 'star'
|
||||
metadata -- JSON: {"star_role": "primary", "spectral_type": "G2V", ...}
|
||||
) SELECT ...;
|
||||
|
||||
-- 2. 为多星系统添加伴星记录(如果有)
|
||||
INSERT INTO celestial_bodies (
|
||||
id, -- 'star-{system_id}-companion-{n}'
|
||||
system_id,
|
||||
name, -- 伴星名称
|
||||
type, -- 'star'
|
||||
metadata -- JSON: {"star_role": "companion", "primary_star_id": "star-X-primary"}
|
||||
) ...;
|
||||
```
|
||||
|
||||
**metadata字段结构**:
|
||||
```json
|
||||
{
|
||||
"star_role": "primary", // 或 "companion"
|
||||
"spectral_type": "G2V",
|
||||
"radius_solar": 1.0,
|
||||
"mass_solar": 1.0,
|
||||
"temperature_k": 5778,
|
||||
"luminosity_solar": 1.0,
|
||||
"binary_system": true, // 是否为双星系统
|
||||
"orbital_period_days": 79.91, // 双星轨道周期(如果是伴星)
|
||||
"semi_major_axis_au": 23.7 // 双星系统的半长轴
|
||||
}
|
||||
```
|
||||
|
||||
#### 方案B:备选方案 - 创建新表(不推荐)
|
||||
|
||||
创建专门的`stars`表来存储恒星数据。
|
||||
|
||||
**缺点**:
|
||||
- ❌ 增加表复杂度
|
||||
- ❌ 需要额外的关联查询
|
||||
- ❌ 恒星和行星管理分离,逻辑复杂
|
||||
|
||||
**结论**:不采用此方案。
|
||||
|
||||
### 3.3 展示逻辑设计
|
||||
|
||||
#### 单星系统(如太阳系、Proxima Centauri)
|
||||
|
||||
```
|
||||
展示模式:与太阳系相同
|
||||
- 中心恒星
|
||||
- 行星轨道
|
||||
- 卫星
|
||||
- 相机聚焦逻辑相同
|
||||
```
|
||||
|
||||
#### 双星系统(如Alpha Centauri A+B)
|
||||
|
||||
```
|
||||
展示模式A:质心为中心
|
||||
┌─────────────────────────────────────┐
|
||||
│ │
|
||||
│ ⭐ Star A ●质心 ⭐ Star B │
|
||||
│ (轨道) (轨道) │
|
||||
│ │
|
||||
│ 🪐 Planet 1 (绕质心运行) │
|
||||
│ 🪐 Planet 2 │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
展示模式B:主星为中心
|
||||
┌─────────────────────────────────────┐
|
||||
│ ⭐ Star A (中心) │
|
||||
│ │
|
||||
│ ⭐ Star B (轨道) │
|
||||
│ │
|
||||
│ 🪐 Planet 1 (绕A运行) │
|
||||
│ 🪐 Planet 2 (绕A运行) │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**推荐**:Phase 4先实现**模式B**(简化版),质心模式留待Phase 5。
|
||||
|
||||
#### 三星系统(如Alpha Centauri A+B+Proxima)
|
||||
|
||||
```
|
||||
展示模式:分层展示
|
||||
主系统:A + B (双星)
|
||||
外围:Proxima (远距离伴星)
|
||||
|
||||
暂不实现,Phase 5考虑
|
||||
```
|
||||
|
||||
### 3.4 用户交互流程
|
||||
|
||||
```
|
||||
用户在银河视图点击恒星系统
|
||||
↓
|
||||
判断系统类型
|
||||
↓
|
||||
┌─────┴─────┐
|
||||
│ │
|
||||
单星系统 多星系统
|
||||
│ │
|
||||
└─────┬─────┘
|
||||
↓
|
||||
进入恒星系统视图
|
||||
(类似太阳系视图)
|
||||
↓
|
||||
显示:
|
||||
- 恒星(们)
|
||||
- 行星轨道
|
||||
- 行星
|
||||
- (未来:卫星)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据补充策略
|
||||
|
||||
### 4.1 恒星数据来源
|
||||
|
||||
**数据源**:`star_systems`表已有完整的主恒星数据
|
||||
|
||||
**需要补充的数据**:
|
||||
|
||||
1. **主恒星记录** (579条)
|
||||
- 从`star_systems`表提取
|
||||
- 创建对应的`celestial_bodies`记录
|
||||
|
||||
2. **伴星数据** (约100-150条,估计)
|
||||
- 需要从外部数据源查询
|
||||
- 候选数据源:
|
||||
- SIMBAD数据库
|
||||
- Washington Double Star Catalog
|
||||
- NASA Exoplanet Archive的二进制恒星信息
|
||||
|
||||
3. **位置和轨道数据**
|
||||
- 主恒星:位置 (0, 0, 0) 相对于系统质心
|
||||
- 伴星:需要轨道参数(半长轴、周期、偏心率等)
|
||||
|
||||
### 4.2 数据迁移脚本
|
||||
|
||||
**步骤1:生成主恒星记录**
|
||||
|
||||
```python
|
||||
# 脚本位置:backend/scripts/populate_stars.py
|
||||
|
||||
import asyncio
|
||||
from app.db import get_db
|
||||
from app.models.db.star_system import StarSystem
|
||||
from app.models.db.celestial_body import CelestialBody
|
||||
|
||||
async def populate_primary_stars():
|
||||
"""为每个恒星系统创建主恒星记录"""
|
||||
db = await get_db()
|
||||
|
||||
# 获取所有恒星系统
|
||||
systems = await db.fetch_all(
|
||||
"SELECT * FROM star_systems ORDER BY id"
|
||||
)
|
||||
|
||||
for system in systems:
|
||||
# 创建主恒星
|
||||
star_id = f"star-{system['id']}-primary"
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO celestial_bodies (
|
||||
id, system_id, name, name_zh, type,
|
||||
description, metadata, is_active
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, 'star',
|
||||
$5, $6, TRUE
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
metadata = EXCLUDED.metadata
|
||||
""",
|
||||
star_id,
|
||||
system['id'],
|
||||
system['host_star_name'],
|
||||
system['name_zh'].replace('系统', '').replace('System', '').strip(),
|
||||
f"光谱类型: {system['spectral_type'] or 'Unknown'}",
|
||||
{
|
||||
"star_role": "primary",
|
||||
"spectral_type": system['spectral_type'],
|
||||
"radius_solar": system['radius_solar'],
|
||||
"mass_solar": system['mass_solar'],
|
||||
"temperature_k": system['temperature_k'],
|
||||
"luminosity_solar": system['luminosity_solar'],
|
||||
"color": system['color']
|
||||
}
|
||||
)
|
||||
|
||||
# 创建默认位置 (0, 0, 0)
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO positions (
|
||||
body_id, time, x, y, z, source
|
||||
) VALUES (
|
||||
$1, NOW(), 0, 0, 0, 'calculated'
|
||||
)
|
||||
""",
|
||||
star_id
|
||||
)
|
||||
|
||||
print(f"✅ 已创建 {len(systems)} 条主恒星记录")
|
||||
```
|
||||
|
||||
**步骤2:识别多星系统**
|
||||
|
||||
```python
|
||||
# 基于恒星名称模式识别多星系统
|
||||
BINARY_PATTERNS = [
|
||||
r'(.+)\s+A\s*$', # "Alpha Cen A"
|
||||
r'(.+)\s+[AB]\s+System', # "Sirius A System"
|
||||
]
|
||||
|
||||
def identify_binary_systems(systems):
|
||||
"""识别可能的双星系统"""
|
||||
binary_candidates = []
|
||||
|
||||
for system in systems:
|
||||
name = system['name']
|
||||
for pattern in BINARY_PATTERNS:
|
||||
if re.match(pattern, name):
|
||||
binary_candidates.append(system)
|
||||
break
|
||||
|
||||
return binary_candidates
|
||||
```
|
||||
|
||||
**步骤3:补充伴星数据(手动/半自动)**
|
||||
|
||||
```sql
|
||||
-- 示例:Alpha Centauri系统
|
||||
-- 主星:Alpha Cen A (已由脚本生成)
|
||||
-- 伴星:Alpha Cen B
|
||||
|
||||
INSERT INTO celestial_bodies (
|
||||
id, system_id, name, name_zh, type, description, metadata
|
||||
) VALUES (
|
||||
'star-XXX-companion-1',
|
||||
(SELECT id FROM star_systems WHERE name = 'Alpha Cen A System'),
|
||||
'Alpha Centauri B',
|
||||
'半人马座α星B',
|
||||
'star',
|
||||
'光谱类型: K1V',
|
||||
'{
|
||||
"star_role": "companion",
|
||||
"spectral_type": "K1V",
|
||||
"radius_solar": 0.86,
|
||||
"mass_solar": 0.93,
|
||||
"temperature_k": 5260,
|
||||
"luminosity_solar": 0.5,
|
||||
"orbital_period_days": 29200,
|
||||
"semi_major_axis_au": 23.7,
|
||||
"eccentricity": 0.5179,
|
||||
"primary_star_id": "star-XXX-primary"
|
||||
}'::jsonb
|
||||
);
|
||||
```
|
||||
|
||||
### 4.3 数据验证
|
||||
|
||||
```sql
|
||||
-- 验证查询1:每个系统的恒星数量
|
||||
SELECT
|
||||
ss.name,
|
||||
COUNT(cb.id) FILTER (WHERE cb.type = 'star') as star_count,
|
||||
COUNT(cb.id) FILTER (WHERE cb.type = 'planet') as planet_count
|
||||
FROM star_systems ss
|
||||
LEFT JOIN celestial_bodies cb ON ss.id = cb.system_id
|
||||
GROUP BY ss.id, ss.name
|
||||
ORDER BY star_count DESC, planet_count DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- 验证查询2:多星系统列表
|
||||
SELECT
|
||||
ss.name,
|
||||
array_agg(cb.name) as stars
|
||||
FROM star_systems ss
|
||||
JOIN celestial_bodies cb ON ss.id = cb.system_id
|
||||
WHERE cb.type = 'star'
|
||||
GROUP BY ss.id, ss.name
|
||||
HAVING COUNT(cb.id) > 1;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 实施步骤
|
||||
|
||||
### Phase 4.1: 数据准备 (预计1-2小时)
|
||||
|
||||
**任务**:
|
||||
- [x] 创建数据迁移脚本 `populate_stars.py`
|
||||
- [ ] 执行脚本,生成579条主恒星记录
|
||||
- [ ] 为所有主恒星创建默认位置 (0,0,0)
|
||||
- [ ] 数据验证
|
||||
|
||||
**验收标准**:
|
||||
```sql
|
||||
-- 应返回579(所有系统都有主恒星)
|
||||
SELECT COUNT(DISTINCT system_id)
|
||||
FROM celestial_bodies
|
||||
WHERE type = 'star';
|
||||
```
|
||||
|
||||
### Phase 4.2: 后端API扩展 (预计1小时)
|
||||
|
||||
**任务**:
|
||||
- [ ] 扩展 `/star-systems/{id}/bodies` API
|
||||
- 当前返回:行星列表
|
||||
- 新增返回:恒星列表
|
||||
- [ ] 新增 `/star-systems/{id}/view` API
|
||||
- 返回完整的系统视图数据(恒星+行星+轨道)
|
||||
|
||||
**API设计**:
|
||||
```python
|
||||
# GET /star-systems/{id}/view
|
||||
{
|
||||
"system": {
|
||||
"id": 479,
|
||||
"name": "Proxima Cen System",
|
||||
"name_zh": "比邻星系统"
|
||||
},
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-479-primary",
|
||||
"name": "Proxima Centauri",
|
||||
"type": "star",
|
||||
"metadata": {...}
|
||||
}
|
||||
],
|
||||
"planets": [
|
||||
{
|
||||
"id": "...",
|
||||
"name": "Proxima Cen b",
|
||||
"type": "planet",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4.3: 前端展示实现 (预计2-3小时)
|
||||
|
||||
**任务**:
|
||||
- [ ] 创建 `StarSystemScene.tsx` 组件
|
||||
- 类似 `Scene.tsx`,但渲染其他恒星系统
|
||||
- 支持单星系统展示
|
||||
- [ ] 修改 `GalaxyScene.tsx`
|
||||
- 点击恒星系统后,跳转到 `StarSystemScene`
|
||||
- 或者:弹出全屏模式展示该系统
|
||||
- [ ] 实现恒星3D渲染
|
||||
- 复用 `BodyViewer` 组件
|
||||
- 根据 `spectral_type` 和 `temperature_k` 动态选择颜色
|
||||
|
||||
**UI流程**:
|
||||
```
|
||||
GalaxyScene (银河视图)
|
||||
│
|
||||
│ 点击恒星系统
|
||||
↓
|
||||
StarSystemScene (恒星系统视图)
|
||||
│
|
||||
├─ 中心恒星 (3D球体)
|
||||
├─ 行星轨道
|
||||
├─ 行星
|
||||
└─ 相机控制 (复用太阳系模式的聚焦逻辑)
|
||||
```
|
||||
|
||||
### Phase 4.4: 多星系统支持 (Phase 5预留)
|
||||
|
||||
**任务**:
|
||||
- [ ] 识别双星系统
|
||||
- [ ] 补充伴星数据
|
||||
- [ ] 实现双星轨道渲染
|
||||
- [ ] 质心计算和相机调整
|
||||
|
||||
**暂不实施**,先完成单星系统展示。
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术细节
|
||||
|
||||
### 6.1 恒星颜色映射
|
||||
|
||||
根据光谱类型和温度自动生成恒星颜色:
|
||||
|
||||
```typescript
|
||||
function getStarColor(spectralType: string, temperature: number): string {
|
||||
// 优先使用数据库中的color字段
|
||||
// 如果没有,根据光谱类型推断
|
||||
|
||||
const spectral = spectralType?.charAt(0).toUpperCase();
|
||||
|
||||
const colorMap: Record<string, string> = {
|
||||
'O': '#9bb0ff', // 蓝色 (> 30000K)
|
||||
'B': '#aabfff', // 蓝白色 (10000-30000K)
|
||||
'A': '#cad7ff', // 白色 (7500-10000K)
|
||||
'F': '#f8f7ff', // 黄白色 (6000-7500K)
|
||||
'G': '#fff4ea', // 黄色 (5200-6000K) - 太阳型
|
||||
'K': '#ffd2a1', // 橙色 (3700-5200K)
|
||||
'M': '#ffcc6f', // 红色 (2400-3700K)
|
||||
};
|
||||
|
||||
return colorMap[spectral] || '#ffffff';
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 坐标系统
|
||||
|
||||
**恒星系统内部坐标系**:
|
||||
- 原点:系统质心(单星系统即恒星中心)
|
||||
- 单位:AU(天文单位)
|
||||
- 坐标系:右手坐标系,Y轴向上
|
||||
|
||||
**多星系统**:
|
||||
- 主星位置:相对于质心的偏移
|
||||
- 伴星位置:轨道计算
|
||||
- 行星位置:相对于系统质心
|
||||
|
||||
### 6.3 渲染优化
|
||||
|
||||
**LOD (Level of Detail)**:
|
||||
- 近距离:完整渲染恒星纹理
|
||||
- 中距离:简化球体
|
||||
- 远距离:光点+标签
|
||||
|
||||
**性能考虑**:
|
||||
- 恒星数量:通常1-3颗
|
||||
- 行星数量:0-8颗
|
||||
- 总体复杂度低于太阳系
|
||||
|
||||
---
|
||||
|
||||
## 7. 风险与挑战
|
||||
|
||||
### 7.1 数据质量风险
|
||||
|
||||
**风险**:伴星数据不完整或不准确
|
||||
|
||||
**缓解措施**:
|
||||
- Phase 4只展示主恒星
|
||||
- 伴星数据作为增强功能,逐步补充
|
||||
|
||||
### 7.2 多星系统复杂度
|
||||
|
||||
**风险**:双星/三星系统的轨道计算和渲染复杂
|
||||
|
||||
**缓解措施**:
|
||||
- Phase 4仅支持单星系统
|
||||
- Phase 5再实现多星系统
|
||||
|
||||
### 7.3 用户体验一致性
|
||||
|
||||
**风险**:不同恒星系统的数据完整度差异导致体验不一致
|
||||
|
||||
**缓解措施**:
|
||||
- 统一的UI降级策略
|
||||
- 缺少数据时显示占位符
|
||||
- 提供"数据来源"说明
|
||||
|
||||
---
|
||||
|
||||
## 8. 时间估算
|
||||
|
||||
| 阶段 | 任务 | 预计时间 |
|
||||
|------|------|---------|
|
||||
| Phase 4.1 | 数据准备 | 1-2小时 |
|
||||
| Phase 4.2 | 后端API | 1小时 |
|
||||
| Phase 4.3 | 前端展示 | 2-3小时 |
|
||||
| **总计** | | **4-6小时** |
|
||||
|
||||
---
|
||||
|
||||
## 9. 成功标准
|
||||
|
||||
### 最小可行产品 (MVP)
|
||||
|
||||
- [x] 所有579个恒星系统都有主恒星记录
|
||||
- [ ] 用户可以从银河视图进入任意恒星系统
|
||||
- [ ] 恒星系统视图正确显示:
|
||||
- 中心恒星(3D球体,正确颜色)
|
||||
- 行星轨道
|
||||
- 行星
|
||||
- [ ] 相机聚焦和控制与太阳系模式一致
|
||||
|
||||
### 增强功能(Phase 5)
|
||||
|
||||
- [ ] 支持双星系统展示
|
||||
- [ ] 恒星轨道动画
|
||||
- [ ] 行星卫星展示
|
||||
- [ ] 更丰富的恒星视觉效果(日冕、耀斑等)
|
||||
|
||||
---
|
||||
|
||||
## 10. 参考资料
|
||||
|
||||
- NASA Exoplanet Archive: https://exoplanetarchive.ipac.caltech.edu/
|
||||
- SIMBAD Astronomical Database: http://simbad.u-strasbg.fr/
|
||||
- Washington Double Star Catalog: https://www.usno.navy.mil/USNO/astrometry/optical-IR-prod/wds
|
||||
- 恒星光谱分类: https://en.wikipedia.org/wiki/Stellar_classification
|
||||
|
||||
---
|
||||
|
||||
**文档维护者**: Cosmo Development Team
|
||||
**最后更新**: 2025-12-06
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
# 第三阶段收尾工作总结
|
||||
|
||||
## 执行时间
|
||||
2025-12-07
|
||||
|
||||
## 执行内容
|
||||
|
||||
### 1. 多恒星系统数据补全
|
||||
已为比邻星系统(Alpha Centauri, system_id=479)补全三颗恒星数据:
|
||||
|
||||
- **Alpha Centauri A (南门二A)** - `star-479-primary`
|
||||
- 光谱类型: G2V
|
||||
- 质量: 1.1 M☉
|
||||
- 半径: 1.22 R☉
|
||||
- 温度: 5790K
|
||||
|
||||
- **Alpha Centauri B (南门二B)** - `star-479-secondary`
|
||||
- 光谱类型: K1V
|
||||
- 质量: 0.93 M☉
|
||||
- 半径: 0.86 R☉
|
||||
- 温度: 5260K
|
||||
|
||||
- **Proxima Centauri (比邻星)** - `star-479-tertiary`
|
||||
- 光谱类型: M5.5V
|
||||
- 质量: 0.12 M☉
|
||||
- 半径: 0.14 R☉
|
||||
- 温度: 2900K
|
||||
|
||||
### 2. 批量启用系外恒星和行星
|
||||
|
||||
执行结果:
|
||||
- ✅ **启用了 580 颗恒星**(所有 system_id > 1 的恒星)
|
||||
- ✅ **启用了 898 颗行星**(所有 system_id > 1 的行星)
|
||||
|
||||
### 3. 验证结果
|
||||
|
||||
比邻星系统最终状态:
|
||||
- 系统ID: 479
|
||||
- 恒星数: 3 颗(三体系统)
|
||||
- 行星数: 2 颗(Proxima Cen b, Proxima Cen d)
|
||||
- 启用天体数: 5 个
|
||||
|
||||
其他系统示例:
|
||||
- 14 Her系统: 1恒星 + 2行星 = 3启用
|
||||
- 16 Cyg B系统: 1恒星 + 1行星 = 2启用
|
||||
- 47 UMa系统: 1恒星 + 3行星 = 4启用
|
||||
|
||||
## 代码质量检查
|
||||
|
||||
### ✅ NASA API Proxy
|
||||
所有NASA Horizons API调用都正确配置了proxy:
|
||||
- `horizons.py:52-54` - get_object_data_raw()
|
||||
- `horizons.py:132-134` - get_body_positions()
|
||||
- `horizons.py:231-233` - search_body_by_name()
|
||||
|
||||
### ✅ 数据库查询优化
|
||||
修复了N+1查询问题:
|
||||
- **文件**: `backend/app/api/celestial_body.py`
|
||||
- **方法**: `/celestial/list` 接口
|
||||
- **优化**: 新增 `get_all_resources_grouped_by_body()` 批量查询方法
|
||||
- **性能**: 100个天体从 101 次查询降为 2 次查询
|
||||
|
||||
### ✅ Bug修复
|
||||
1. **BodyDetailOverlay.tsx:220-224** - 修复了访问undefined的严重bug
|
||||
2. **GalaxyScene截图** - 修复了WebGL Canvas截图背景色问题
|
||||
3. **天体数量统计** - 修复了只统计行星而不是所有天体的问题
|
||||
|
||||
### 🗑️ 待清理文件
|
||||
- `backend/app/api/routes.py.bak` (55KB) - 可以删除
|
||||
|
||||
## 脚本文件
|
||||
已创建维护脚本:`backend/scripts/activate_multisystem_stars.py`
|
||||
- 用途: 补全多恒星系统数据并启用恒星和行星
|
||||
- 使用方法: `./venv/bin/python3 scripts/activate_multisystem_stars.py`
|
||||
|
||||
## 数据统计
|
||||
|
||||
### 总体数据
|
||||
- 恒星系统总数: 579+
|
||||
- 活跃恒星数: 580
|
||||
- 活跃行星数: 898
|
||||
- 多恒星系统: 1个(比邻星三体系统)
|
||||
|
||||
### 比邻星系统特征
|
||||
- 距离地球: 1.30 pc (~4.24 光年)
|
||||
- 系统类型: 三体恒星系统
|
||||
- 行星数量: 2颗已确认
|
||||
- 特殊性: 离太阳系最近的恒星系统
|
||||
|
||||
## 后续建议
|
||||
|
||||
### 可以添加的其他多恒星系统:
|
||||
1. **天狼星 (Sirius, system_id=2)** - 双星系统
|
||||
- Sirius A (天狼星A) - A1V型主序星
|
||||
- Sirius B (天狼星B) - DA2型白矮星
|
||||
|
||||
2. **南河三 (Procyon)** - 双星系统
|
||||
- Procyon A - F5IV型亚巨星
|
||||
- Procyon B - DQZ型白矮星
|
||||
|
||||
3. **其他已知的多星系统**
|
||||
- 仙女座γ (Gamma Andromedae) - 四体系统
|
||||
- 北极星 (Polaris) - 三体系统
|
||||
|
||||
### 数据库索引建议(如果未添加)
|
||||
```sql
|
||||
CREATE INDEX idx_positions_body_time ON positions(body_id, time);
|
||||
CREATE INDEX idx_resources_body_id ON resources(body_id);
|
||||
CREATE INDEX idx_celestial_bodies_system_id ON celestial_bodies(system_id);
|
||||
CREATE INDEX idx_celestial_bodies_type ON celestial_bodies(type);
|
||||
CREATE INDEX idx_celestial_bodies_is_active ON celestial_bodies(is_active);
|
||||
```
|
||||
|
||||
## 完成状态
|
||||
✅ 所有第三阶段收尾工作已完成
|
||||
✅ 代码质量检查通过
|
||||
✅ 多恒星系统数据已补全
|
||||
✅ 系外恒星和行星已启用
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
# 恒星系统架构改造 - 进度报告
|
||||
|
||||
## ✅ 已完成工作
|
||||
|
||||
### 1. 数据库架构改造
|
||||
- ✅ 创建 `star_systems` 表
|
||||
- ✅ 添加太阳系初始记录(id=1)
|
||||
- ✅ 扩展 `celestial_bodies` 表(添加 `system_id` 字段)
|
||||
- ✅ 更新所有太阳系天体 `system_id = 1`(30个天体)
|
||||
|
||||
### 2. ORM 模型
|
||||
- ✅ 创建 `StarSystem` ORM 模型
|
||||
- ✅ 更新 `CelestialBody` ORM 模型(添加 system_id 关系)
|
||||
- ✅ 在 `__init__.py` 中注册 StarSystem
|
||||
|
||||
### 3. 数据迁移
|
||||
- ✅ 编写完整的数据迁移脚本(`scripts/migrate_interstellar_data.py`)
|
||||
- ✅ 实现自动中文名翻译功能
|
||||
- ✅ 实现行星数据去重逻辑
|
||||
- ✅ 成功迁移 578 个系外恒星系统
|
||||
- ✅ 成功迁移 898 颗系外行星(去重后)
|
||||
|
||||
### 4. 后端服务层
|
||||
- ✅ 创建 `StarSystemService`(`app/services/star_system_service.py`)
|
||||
- 支持 CRUD 操作
|
||||
- 支持搜索和分页
|
||||
- 支持获取恒星系及其所有天体
|
||||
- 支持统计功能
|
||||
|
||||
- ✅ 创建 Pydantic 模型(`app/models/star_system.py`)
|
||||
- StarSystemBase
|
||||
- StarSystemCreate
|
||||
- StarSystemUpdate
|
||||
- StarSystemResponse
|
||||
- StarSystemWithBodies
|
||||
- StarSystemStatistics
|
||||
|
||||
### 5. 迁移数据统计
|
||||
```
|
||||
恒星系统总数: 579
|
||||
- 太阳系: 1
|
||||
- 系外恒星系: 578
|
||||
|
||||
天体总数: 928
|
||||
- 太阳系天体: 30(含太阳、行星、矮行星、卫星、探测器、彗星)
|
||||
- 系外行星: 898(已去重)
|
||||
|
||||
数据质量:
|
||||
- 去重前行星记录: ~3000+
|
||||
- 去重后行星记录: 898
|
||||
- 去重率: ~70%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚧 剩余工作
|
||||
|
||||
### 1. 后端 API 开发
|
||||
- [ ] 创建 StarSystem API 路由
|
||||
- GET /api/star-systems(获取所有恒星系统)
|
||||
- GET /api/star-systems/{id}(获取单个恒星系统)
|
||||
- GET /api/star-systems/{id}/bodies(获取恒星系及其天体)
|
||||
- POST /api/admin/star-systems(创建恒星系统)
|
||||
- PUT /api/admin/star-systems/{id}(更新恒星系统)
|
||||
- DELETE /api/admin/star-systems/{id}(删除恒星系统)
|
||||
- GET /api/star-systems/statistics(获取统计信息)
|
||||
|
||||
- [ ] 更新 CelestialBody API
|
||||
- 添加 `system_id` 查询参数
|
||||
- 添加 `include_no_system` 参数(用于包含探测器等)
|
||||
|
||||
### 2. 后台管理界面(Admin Frontend)
|
||||
- [ ] 创建恒星系统管理页面(`/admin/star-systems`)
|
||||
- 列表展示(支持搜索、分页)
|
||||
- 新增恒星系统
|
||||
- 编辑恒星系统
|
||||
- 删除恒星系统(不可删除太阳系)
|
||||
- 查看恒星系详情(含所有行星)
|
||||
|
||||
- [ ] 改造天体管理页面(`/admin/celestial-bodies`)
|
||||
- **关键改动**:先选择恒星系,再列出该恒星系的天体
|
||||
- 添加恒星系选择器(下拉框)
|
||||
- 根据选中的恒星系过滤天体列表
|
||||
- 新增天体时自动设置 `system_id`
|
||||
- 支持在恒星系之间移动天体
|
||||
|
||||
### 3. 前端界面更新
|
||||
- [ ] 更新 GalaxyScene 组件
|
||||
- 使用新的 `/api/star-systems` API
|
||||
- 移除前端行星去重代码
|
||||
- 优化恒星点击事件(使用后端返回的完整数据)
|
||||
|
||||
- [ ] 更新 App.tsx 查询逻辑
|
||||
- Solar 视图:查询 `system_id=1` 的天体
|
||||
- Galaxy 视图:查询所有恒星系统
|
||||
|
||||
### 4. 菜单配置
|
||||
- [ ] 在后台管理菜单中添加"恒星系统管理"入口
|
||||
|
||||
---
|
||||
|
||||
## 📊 数据模型关系图
|
||||
|
||||
```
|
||||
star_systems (579条记录)
|
||||
├── id=1: Solar System (太阳系)
|
||||
│ └── celestial_bodies (30条)
|
||||
│ ├── Sun (star)
|
||||
│ ├── Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune (planet)
|
||||
│ ├── Pluto, Ceres, Haumea, Makemake, Eris (dwarf_planet)
|
||||
│ ├── Moon, Io, Europa, Ganymede, Callisto (satellite)
|
||||
│ ├── Voyager 1, Voyager 2, Parker Solar Probe... (probe)
|
||||
│ └── Halley, NEOWISE, C/2020 F3 (comet)
|
||||
│
|
||||
├── id=2: Proxima Cen System (比邻星系统)
|
||||
│ └── celestial_bodies (2条)
|
||||
│ ├── Proxima Cen b (比邻星 b)
|
||||
│ └── Proxima Cen d (比邻星 d)
|
||||
│
|
||||
├── id=3: TRAPPIST-1 System
|
||||
│ └── celestial_bodies (7条)
|
||||
│ └── TRAPPIST-1 b/c/d/e/f/g/h
|
||||
│
|
||||
└── ... (575 more systems)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 下一步行动
|
||||
|
||||
**立即可做:**
|
||||
1. 完成 StarSystem API 路由
|
||||
2. 测试 API 端点
|
||||
3. 开发后台管理界面
|
||||
|
||||
**预计工作量:**
|
||||
- 后端 API:1-2小时
|
||||
- 后台界面:3-4小时
|
||||
- 前端更新:1-2小时
|
||||
- 测试验证:1小时
|
||||
|
||||
**总计:6-9小时**
|
||||
|
||||
---
|
||||
|
||||
## 🔧 技术要点
|
||||
|
||||
### 中文名翻译规则
|
||||
```python
|
||||
# 恒星名翻译示例
|
||||
Proxima Cen → 比邻星
|
||||
Kepler-442 → 开普勒-442
|
||||
TRAPPIST-1 → TRAPPIST-1
|
||||
HD 40307 → HD 40307
|
||||
|
||||
# 行星名翻译示例
|
||||
Proxima Cen b → 比邻星 b
|
||||
Kepler-442 b → 开普勒-442 b
|
||||
```
|
||||
|
||||
### 去重逻辑
|
||||
- 按行星名称(name)去重
|
||||
- 保留字段最完整的记录(非NULL字段最多的)
|
||||
- 平均每个恒星系从5.2条记录减少到1.6条(效率提升70%)
|
||||
|
||||
### 查询优化
|
||||
```sql
|
||||
-- Solar 视图
|
||||
SELECT * FROM celestial_bodies WHERE system_id = 1;
|
||||
|
||||
-- Galaxy 视图
|
||||
SELECT * FROM star_systems WHERE id > 1;
|
||||
|
||||
-- 恒星系详情
|
||||
SELECT * FROM celestial_bodies WHERE system_id = ?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0
|
||||
**更新时间**: 2025-12-05 19:10
|
||||
**状态**: 数据迁移完成,API开发进行中
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
-- SQL Migration: Add 'comet' type support to celestial_bodies table
|
||||
--
|
||||
-- Purpose: Enable comet celestial body type in the database
|
||||
--
|
||||
-- Note: The CheckConstraint in the ORM model (celestial_body.py line 37) already includes 'comet',
|
||||
-- but if the database was created before this was added, we need to update the constraint.
|
||||
--
|
||||
-- Instructions:
|
||||
-- 1. Check if the constraint already includes 'comet':
|
||||
-- SELECT conname, pg_get_constraintdef(oid)
|
||||
-- FROM pg_constraint
|
||||
-- WHERE conrelid = 'celestial_bodies'::regclass AND conname = 'chk_type';
|
||||
--
|
||||
-- 2. If 'comet' is NOT in the constraint, run the following migration:
|
||||
|
||||
-- Step 1: Drop the existing constraint
|
||||
ALTER TABLE celestial_bodies DROP CONSTRAINT IF EXISTS chk_type;
|
||||
|
||||
-- Step 2: Recreate the constraint with 'comet' included
|
||||
ALTER TABLE celestial_bodies
|
||||
ADD CONSTRAINT chk_type
|
||||
CHECK (type IN ('star', 'planet', 'moon', 'probe', 'comet', 'asteroid', 'dwarf_planet', 'satellite'));
|
||||
|
||||
-- Step 3: Verify the constraint was updated successfully
|
||||
SELECT conname, pg_get_constraintdef(oid)
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'celestial_bodies'::regclass AND conname = 'chk_type';
|
||||
|
||||
-- Expected output should show:
|
||||
-- chk_type | CHECK ((type)::text = ANY (ARRAY[('star'::character varying)::text, ('planet'::character varying)::text, ('moon'::character varying)::text, ('probe'::character varying)::text, ('comet'::character varying)::text, ('asteroid'::character varying)::text, ('dwarf_planet'::character varying)::text, ('satellite'::character varying)::text]))
|
||||
|
||||
-- ROLLBACK (if needed):
|
||||
-- ALTER TABLE celestial_bodies DROP CONSTRAINT IF EXISTS chk_type;
|
||||
-- ALTER TABLE celestial_bodies
|
||||
-- ADD CONSTRAINT chk_type
|
||||
-- CHECK (type IN ('star', 'planet', 'moon', 'probe', 'asteroid', 'dwarf_planet', 'satellite'));
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
- [3.3 orbits - 轨道路径表](#33-orbits---轨道路径表)
|
||||
- [3.4 resources - 资源文件管理表](#34-resources---资源文件管理表)
|
||||
- [3.5 static_data - 静态天文数据表](#35-static_data---静态天文数据表)
|
||||
- [3.6 star_systems - 恒星系统表](#36-star_systems---恒星系统表)
|
||||
- [3.7 interstellar_bodies - 恒星际天体表](#37-interstellar_bodies---恒星际天体表)
|
||||
- [4. 系统管理表](#4-系统管理表)
|
||||
- [4.1 users - 用户表](#41-users---用户表)
|
||||
- [4.2 roles - 角色表](#42-roles---角色表)
|
||||
|
|
@ -46,14 +48,16 @@
|
|||
| 3 | orbits | 轨道路径数据 | 数百 |
|
||||
| 4 | resources | 资源文件管理 | 数千 |
|
||||
| 5 | static_data | 静态天文数据 | 数千 |
|
||||
| 6 | users | 用户账号 | 数千 |
|
||||
| 7 | roles | 角色定义 | 十位数 |
|
||||
| 8 | user_roles | 用户角色关联 | 数千 |
|
||||
| 9 | menus | 菜单配置 | 数十 |
|
||||
| 10 | role_menus | 角色菜单权限 | 数百 |
|
||||
| 11 | system_settings | 系统配置参数 | 数十 |
|
||||
| 12 | tasks | 后台任务 | 数万 |
|
||||
| 13 | nasa_cache | NASA API缓存 | 数万 |
|
||||
| 6 | star_systems | 恒星系统信息 | 数千 |
|
||||
| 7 | interstellar_bodies | 恒星际天体信息 | 数千 |
|
||||
| 8 | users | 用户账号 | 数千 |
|
||||
| 9 | roles | 角色定义 | 十位数 |
|
||||
| 10 | user_roles | 用户角色关联 | 数千 |
|
||||
| 11 | menus | 菜单配置 | 数十 |
|
||||
| 12 | role_menus | 角色菜单权限 | 数百 |
|
||||
| 13 | system_settings | 系统配置参数 | 数十 |
|
||||
| 14 | tasks | 后台任务 | 数万 |
|
||||
| 15 | nasa_cache | NASA API缓存 | 数万 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -260,7 +264,8 @@ CREATE TABLE static_data (
|
|||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT chk_category CHECK (category IN (
|
||||
'constellation', 'galaxy', 'star', 'nebula', 'cluster'
|
||||
'constellation', 'galaxy', 'star', 'nebula', 'cluster',
|
||||
'asteroid_belt', 'kuiper_belt'
|
||||
)),
|
||||
CONSTRAINT uq_category_name UNIQUE (category, name)
|
||||
);
|
||||
|
|
@ -272,7 +277,7 @@ CREATE INDEX idx_static_data_data ON static_data USING GIN(data); -- JSONB索
|
|||
|
||||
-- 注释
|
||||
COMMENT ON TABLE static_data IS '静态天文数据表(星座、星系、恒星等)';
|
||||
COMMENT ON COLUMN static_data.category IS '数据分类:constellation(星座), galaxy(星系), star(恒星), nebula(星云), cluster(星团)';
|
||||
COMMENT ON COLUMN static_data.category IS '数据分类:constellation(星座), galaxy(星系), star(恒星), nebula(星云), cluster(星团), asteroid_belt(小行星带), kuiper_belt(柯伊伯带)';
|
||||
COMMENT ON COLUMN static_data.data IS 'JSON格式的完整数据,结构根据category不同而不同';
|
||||
```
|
||||
|
||||
|
|
@ -304,6 +309,119 @@ COMMENT ON COLUMN static_data.data IS 'JSON格式的完整数据,结构根据c
|
|||
|
||||
---
|
||||
|
||||
### 3.6 star_systems - 恒星系统表
|
||||
|
||||
存储恒星系统的基本信息(Phase 3 - 恒星际扩展)。包括太阳系和其他恒星系统。
|
||||
|
||||
```sql
|
||||
CREATE TABLE star_systems (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL, -- 系统名称(如"Solar System")
|
||||
name_zh VARCHAR(200), -- 中文名称(如"太阳系")
|
||||
host_star_name VARCHAR(200) NOT NULL, -- 主恒星名称
|
||||
distance_pc DOUBLE PRECISION, -- 距离(秒差距)
|
||||
distance_ly DOUBLE PRECISION, -- 距离(光年)
|
||||
ra DOUBLE PRECISION, -- 赤经(度)
|
||||
dec DOUBLE PRECISION, -- 赤纬(度)
|
||||
position_x DOUBLE PRECISION, -- 笛卡尔坐标X(秒差距)
|
||||
position_y DOUBLE PRECISION, -- 笛卡尔坐标Y(秒差距)
|
||||
position_z DOUBLE PRECISION, -- 笛卡尔坐标Z(秒差距)
|
||||
spectral_type VARCHAR(20), -- 光谱类型(如"G2V")
|
||||
radius_solar DOUBLE PRECISION, -- 恒星半径(太阳半径倍数)
|
||||
mass_solar DOUBLE PRECISION, -- 恒星质量(太阳质量倍数)
|
||||
temperature_k DOUBLE PRECISION, -- 表面温度(开尔文)
|
||||
magnitude DOUBLE PRECISION, -- 视星等
|
||||
luminosity_solar DOUBLE PRECISION, -- 光度(太阳光度倍数)
|
||||
color VARCHAR(20), -- 显示颜色(HEX格式)
|
||||
planet_count INTEGER DEFAULT 0, -- 行星数量
|
||||
description TEXT, -- 系统描述
|
||||
details TEXT, -- 详细信息(Markdown格式)
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_star_system_name UNIQUE (name)
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_star_systems_name ON star_systems(name);
|
||||
CREATE INDEX idx_star_systems_distance ON star_systems(distance_pc);
|
||||
CREATE INDEX idx_star_systems_position ON star_systems(position_x, position_y, position_z);
|
||||
|
||||
-- 注释
|
||||
COMMENT ON TABLE star_systems IS '恒星系统基本信息表(Phase 3)';
|
||||
COMMENT ON COLUMN star_systems.distance_pc IS '距离(秒差距),1 pc ≈ 3.26 光年';
|
||||
COMMENT ON COLUMN star_systems.spectral_type IS '恒星光谱分类,如G2V(太阳型)、M5.5V(红矮星)';
|
||||
COMMENT ON COLUMN star_systems.position_x IS '以太阳为原点的笛卡尔坐标X(秒差距)';
|
||||
COMMENT ON COLUMN star_systems.color IS '3D可视化中的恒星颜色,HEX格式';
|
||||
```
|
||||
|
||||
**使用场景**:
|
||||
- Galaxy View(银河视图)的恒星系统展示
|
||||
- 恒星系统搜索和筛选
|
||||
- 系外行星系统管理
|
||||
- 星际距离计算
|
||||
|
||||
---
|
||||
|
||||
### 3.7 interstellar_bodies - 恒星际天体表
|
||||
|
||||
存储恒星系统中的天体信息(行星、卫星等),与star_systems表关联。
|
||||
|
||||
```sql
|
||||
CREATE TABLE interstellar_bodies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
system_id INTEGER NOT NULL REFERENCES star_systems(id) ON DELETE CASCADE,
|
||||
name VARCHAR(200) NOT NULL, -- 天体名称
|
||||
name_zh VARCHAR(200), -- 中文名称
|
||||
type VARCHAR(50) NOT NULL, -- 天体类型
|
||||
description TEXT, -- 描述
|
||||
extra_data JSONB, -- 扩展数据
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT chk_interstellar_body_type CHECK (type IN (
|
||||
'planet', 'satellite', 'dwarf_planet', 'asteroid'
|
||||
))
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX idx_interstellar_bodies_system ON interstellar_bodies(system_id);
|
||||
CREATE INDEX idx_interstellar_bodies_type ON interstellar_bodies(type);
|
||||
CREATE INDEX idx_interstellar_bodies_name ON interstellar_bodies(name);
|
||||
CREATE INDEX idx_interstellar_bodies_extra_data ON interstellar_bodies USING GIN(extra_data);
|
||||
|
||||
-- 注释
|
||||
COMMENT ON TABLE interstellar_bodies IS '恒星际天体信息表(Phase 3)';
|
||||
COMMENT ON COLUMN interstellar_bodies.system_id IS '所属恒星系统ID(外键关联star_systems表)';
|
||||
COMMENT ON COLUMN interstellar_bodies.type IS '天体类型:planet(行星), satellite(卫星), dwarf_planet(矮行星), asteroid(小行星)';
|
||||
COMMENT ON COLUMN interstellar_bodies.extra_data IS 'JSON格式扩展数据,包含轨道参数、物理参数等';
|
||||
```
|
||||
|
||||
**extra_data JSONB字段示例**:
|
||||
```json
|
||||
{
|
||||
"semi_major_axis_au": 0.0172,
|
||||
"period_days": 1.51087,
|
||||
"eccentricity": 0.00622,
|
||||
"inclination_deg": 89.728,
|
||||
"radius_earth": 1.116,
|
||||
"mass_earth": 1.374,
|
||||
"temperature_k": 400,
|
||||
"discovery_year": 2017,
|
||||
"discovery_method": "Transit",
|
||||
"equilibrium_temp_k": 400,
|
||||
"density_gcc": 5.9
|
||||
}
|
||||
```
|
||||
|
||||
**使用场景**:
|
||||
- 显示恒星系统的行星列表
|
||||
- 系外行星数据管理
|
||||
- 行星轨道参数查询
|
||||
- 行星物理特性分析
|
||||
|
||||
---
|
||||
|
||||
## 4. 系统管理表
|
||||
|
||||
### 4.1 users - 用户表
|
||||
|
|
@ -574,6 +692,9 @@ celestial_bodies (天体)
|
|||
├── orbits (1:1) - 轨道路径
|
||||
└── resources (1:N) - 资源文件
|
||||
|
||||
star_systems (恒星系统)
|
||||
└── interstellar_bodies (1:N) - 恒星际天体
|
||||
|
||||
users (用户)
|
||||
└── user_roles (N:M) ←→ roles (角色)
|
||||
└── role_menus (N:M) ←→ menus (菜单)
|
||||
|
|
@ -811,6 +932,63 @@ WHERE status = 'running'
|
|||
ORDER BY started_at DESC;
|
||||
```
|
||||
|
||||
### 查询恒星系统及其行星列表
|
||||
```sql
|
||||
SELECT
|
||||
s.id, s.name, s.name_zh, s.host_star_name,
|
||||
s.distance_pc, s.spectral_type, s.planet_count,
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', b.id,
|
||||
'name', b.name,
|
||||
'name_zh', b.name_zh,
|
||||
'type', b.type,
|
||||
'extra_data', b.extra_data
|
||||
)
|
||||
) FILTER (WHERE b.id IS NOT NULL) as planets
|
||||
FROM star_systems s
|
||||
LEFT JOIN interstellar_bodies b ON s.id = b.system_id
|
||||
WHERE s.distance_pc < 20 -- 距离小于20秒差距(~65光年)
|
||||
GROUP BY s.id
|
||||
ORDER BY s.distance_pc;
|
||||
```
|
||||
|
||||
### 查询附近恒星系统(50秒差距内)
|
||||
```sql
|
||||
SELECT
|
||||
name, name_zh, host_star_name,
|
||||
distance_pc,
|
||||
distance_ly,
|
||||
spectral_type,
|
||||
temperature_k,
|
||||
planet_count
|
||||
FROM star_systems
|
||||
WHERE distance_pc <= 50
|
||||
AND position_x IS NOT NULL
|
||||
AND position_y IS NOT NULL
|
||||
AND position_z IS NOT NULL
|
||||
ORDER BY distance_pc;
|
||||
```
|
||||
|
||||
### 查询系外行星的轨道参数
|
||||
```sql
|
||||
SELECT
|
||||
s.name as system_name,
|
||||
s.name_zh as system_name_zh,
|
||||
b.name as planet_name,
|
||||
b.name_zh as planet_name_zh,
|
||||
b.extra_data->>'semi_major_axis_au' as semi_major_axis,
|
||||
b.extra_data->>'period_days' as period,
|
||||
b.extra_data->>'radius_earth' as radius,
|
||||
b.extra_data->>'mass_earth' as mass,
|
||||
b.extra_data->>'temperature_k' as temperature
|
||||
FROM interstellar_bodies b
|
||||
JOIN star_systems s ON b.system_id = s.id
|
||||
WHERE b.type = 'planet'
|
||||
AND b.extra_data ? 'semi_major_axis_au'
|
||||
ORDER BY s.distance_pc, (b.extra_data->>'semi_major_axis_au')::float;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 维护建议
|
||||
|
|
@ -849,7 +1027,11 @@ pg_dump -U postgres cosmo_db > backup_$(date +%Y%m%d).sql
|
|||
|
||||
## 文档版本
|
||||
|
||||
- **版本**: 2.0
|
||||
- **更新日期**: 2025-12-05
|
||||
- **对应阶段**: Phase 2 完成
|
||||
- **下一步**: Phase 3 - 恒星际扩展
|
||||
- **版本**: 3.0
|
||||
- **更新日期**: 2025-12-06
|
||||
- **对应阶段**: Phase 3 完成(恒星际扩展)
|
||||
- **新增内容**:
|
||||
- 新增 `star_systems` 表(恒星系统信息)
|
||||
- 新增 `interstellar_bodies` 表(恒星际天体信息)
|
||||
- 支持579个恒星系统和898颗系外行星的数据管理
|
||||
- **下一步**: Phase 4 - 更多深空对象可视化
|
||||
|
|
|
|||
|
|
@ -24,9 +24,10 @@ class CelestialBodyCreate(BaseModel):
|
|||
name: str
|
||||
name_zh: Optional[str] = None
|
||||
type: str
|
||||
system_id: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
details: Optional[str] = None
|
||||
is_active: bool = True
|
||||
is_active: Optional[bool] = True
|
||||
extra_data: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ class CelestialBodyUpdate(BaseModel):
|
|||
name: Optional[str] = None
|
||||
name_zh: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
system_id: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
details: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
|
@ -184,17 +186,26 @@ async def get_body_info(body_id: str, db: AsyncSession = Depends(get_db)):
|
|||
@router.get("/list")
|
||||
async def list_bodies(
|
||||
body_type: Optional[str] = Query(None, description="Filter by body type"),
|
||||
system_id: Optional[int] = Query(None, description="Filter by star system ID (1=Solar, 2+=Exoplanets)"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get a list of all available celestial bodies
|
||||
|
||||
Args:
|
||||
body_type: Filter by body type (star, planet, dwarf_planet, satellite, probe, comet, etc.)
|
||||
system_id: Filter by star system ID (1=Solar System, 2+=Exoplanet systems)
|
||||
"""
|
||||
bodies = await celestial_body_service.get_all_bodies(db, body_type)
|
||||
bodies = await celestial_body_service.get_all_bodies(db, body_type, system_id)
|
||||
|
||||
# Bulk load all resources in one query (avoid N+1 problem)
|
||||
body_ids = [body.id for body in bodies]
|
||||
resources_by_body = await resource_service.get_all_resources_grouped_by_body(body_ids, db)
|
||||
|
||||
bodies_list = []
|
||||
for body in bodies:
|
||||
# Get resources for this body
|
||||
resources = await resource_service.get_resources_by_body(body.id, None, db)
|
||||
# Get resources for this body from the bulk-loaded dict
|
||||
resources = resources_by_body.get(body.id, [])
|
||||
|
||||
# Group resources by type
|
||||
resources_by_type = {}
|
||||
|
|
@ -214,6 +225,7 @@ async def list_bodies(
|
|||
"name": body.name,
|
||||
"name_zh": body.name_zh,
|
||||
"type": body.type,
|
||||
"system_id": body.system_id, # Add system_id field
|
||||
"description": body.description,
|
||||
"details": body.details,
|
||||
"is_active": body.is_active,
|
||||
|
|
|
|||
|
|
@ -34,13 +34,11 @@ async def get_orbits(
|
|||
logger.info(f"Fetching orbits (type filter: {body_type})")
|
||||
|
||||
try:
|
||||
orbits = await orbit_service.get_all_orbits(db, body_type=body_type)
|
||||
# Use optimized query with JOIN to avoid N+1 problem
|
||||
orbits_with_bodies = await orbit_service.get_all_orbits_with_bodies(db, body_type=body_type)
|
||||
|
||||
result = []
|
||||
for orbit in orbits:
|
||||
# Get body info
|
||||
body = await celestial_body_service.get_body_by_id(orbit.body_id, db)
|
||||
|
||||
for orbit, body in orbits_with_bodies:
|
||||
result.append({
|
||||
"body_id": orbit.body_id,
|
||||
"body_name": body.name if body else "Unknown",
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ async def get_celestial_positions(
|
|||
# Get all bodies from database
|
||||
all_bodies = await celestial_body_service.get_all_bodies(db)
|
||||
|
||||
# Filter to only Solar System bodies (system_id = 1 or NULL for legacy data)
|
||||
# Exclude stars and exoplanets from other star systems
|
||||
all_bodies = [b for b in all_bodies if b.system_id == 1]
|
||||
|
||||
# Filter bodies if body_ids specified
|
||||
if body_id_list:
|
||||
all_bodies = [b for b in all_bodies if b.id in body_id_list]
|
||||
|
|
@ -223,6 +227,10 @@ async def get_celestial_positions(
|
|||
# For each body, check if we have cached NASA response
|
||||
all_bodies = await celestial_body_service.get_all_bodies(db)
|
||||
|
||||
# Filter to only Solar System bodies (system_id = 1 or NULL for legacy data)
|
||||
# Exclude stars and exoplanets from other star systems
|
||||
all_bodies = [b for b in all_bodies if b.system_id is None or b.system_id == 1]
|
||||
|
||||
# Filter bodies if body_ids specified
|
||||
if body_id_list:
|
||||
logger.info(f"Filtering bodies from {len(all_bodies)} total. Requested IDs: {body_id_list}")
|
||||
|
|
@ -320,6 +328,10 @@ async def get_celestial_positions(
|
|||
# Get all bodies from database
|
||||
all_bodies = await celestial_body_service.get_all_bodies(db)
|
||||
|
||||
# Filter to only Solar System bodies (system_id = 1 or NULL for legacy data)
|
||||
# Exclude stars and exoplanets from other star systems
|
||||
all_bodies = [b for b in all_bodies if b.system_id == 1]
|
||||
|
||||
# Filter bodies if body_ids specified
|
||||
if body_id_list:
|
||||
all_bodies = [b for b in all_bodies if b.id in body_id_list]
|
||||
|
|
|
|||
|
|
@ -30,20 +30,27 @@ async def get_downloadable_bodies(
|
|||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get list of celestial bodies available for NASA data download, grouped by type
|
||||
Get list of celestial bodies available for NASA Horizons data download, grouped by type
|
||||
|
||||
Only includes Solar System bodies (system_id = 1) since NASA Horizons
|
||||
does not contain data for stars and exoplanets from other star systems.
|
||||
|
||||
Returns:
|
||||
- Dictionary with body types as keys and lists of bodies as values
|
||||
"""
|
||||
logger.info("Fetching downloadable bodies for NASA data download")
|
||||
logger.info("Fetching downloadable bodies for NASA Horizons data download")
|
||||
|
||||
try:
|
||||
# Get all active celestial bodies
|
||||
all_bodies = await celestial_body_service.get_all_bodies(db)
|
||||
|
||||
# Filter to only Solar System bodies (system_id = 1)
|
||||
# Exclude stars and exoplanets from other star systems
|
||||
solar_system_bodies = [b for b in all_bodies if b.system_id == 1]
|
||||
|
||||
# Group bodies by type
|
||||
grouped_bodies = {}
|
||||
for body in all_bodies:
|
||||
for body in solar_system_bodies:
|
||||
if body.type not in grouped_bodies:
|
||||
grouped_bodies[body.type] = []
|
||||
|
||||
|
|
@ -60,7 +67,7 @@ async def get_downloadable_bodies(
|
|||
for body_type in grouped_bodies:
|
||||
grouped_bodies[body_type].sort(key=lambda x: x["name"])
|
||||
|
||||
logger.info(f"✅ Returning {len(all_bodies)} bodies in {len(grouped_bodies)} groups")
|
||||
logger.info(f"✅ Returning {len(solar_system_bodies)} Solar System bodies in {len(grouped_bodies)} groups")
|
||||
return {"bodies": grouped_bodies}
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,268 @@
|
|||
"""
|
||||
Star System Management API routes
|
||||
Handles CRUD operations for star systems (Solar System and exoplanet systems)
|
||||
"""
|
||||
import logging
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Optional
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.star_system import (
|
||||
StarSystemCreate,
|
||||
StarSystemUpdate,
|
||||
StarSystemResponse,
|
||||
StarSystemWithBodies,
|
||||
StarSystemListResponse,
|
||||
StarSystemStatistics
|
||||
)
|
||||
from app.services.star_system_service import star_system_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/star-systems", tags=["star-systems"])
|
||||
|
||||
|
||||
@router.get("", response_model=StarSystemListResponse)
|
||||
async def get_star_systems(
|
||||
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Maximum records to return"),
|
||||
exclude_solar: bool = Query(False, description="Exclude Solar System from results"),
|
||||
search: Optional[str] = Query(None, description="Search by name (English or Chinese)"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get all star systems with pagination and optional filtering
|
||||
|
||||
Args:
|
||||
skip: Number of records to skip (for pagination)
|
||||
limit: Maximum number of records to return
|
||||
exclude_solar: If True, exclude Solar System (id=1) from results
|
||||
search: Search keyword to filter by name or host star name
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
List of star systems with total count
|
||||
"""
|
||||
systems = await star_system_service.get_all(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
exclude_solar=exclude_solar,
|
||||
search=search
|
||||
)
|
||||
|
||||
# Get total count for pagination
|
||||
from sqlalchemy import select, func, or_
|
||||
from app.models.db.star_system import StarSystem
|
||||
from app.models.db.celestial_body import CelestialBody
|
||||
|
||||
count_query = select(func.count(StarSystem.id))
|
||||
if exclude_solar:
|
||||
count_query = count_query.where(StarSystem.id != 1)
|
||||
if search:
|
||||
search_pattern = f"%{search}%"
|
||||
count_query = count_query.where(
|
||||
or_(
|
||||
StarSystem.name.ilike(search_pattern),
|
||||
StarSystem.name_zh.ilike(search_pattern),
|
||||
StarSystem.host_star_name.ilike(search_pattern)
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(count_query)
|
||||
total = result.scalar()
|
||||
|
||||
# Get star counts for all systems
|
||||
system_ids = [s.id for s in systems]
|
||||
star_counts_query = select(
|
||||
CelestialBody.system_id,
|
||||
func.count(CelestialBody.id).label('star_count')
|
||||
).where(
|
||||
CelestialBody.system_id.in_(system_ids),
|
||||
CelestialBody.type == 'star'
|
||||
).group_by(CelestialBody.system_id)
|
||||
|
||||
star_counts_result = await db.execute(star_counts_query)
|
||||
star_counts_dict = {row.system_id: row.star_count for row in star_counts_result.all()}
|
||||
|
||||
# Build response with star_count
|
||||
systems_response = []
|
||||
for system in systems:
|
||||
system_dict = StarSystemResponse.from_orm(system).dict()
|
||||
system_dict['star_count'] = star_counts_dict.get(system.id, 1) # Default to 1 if no stars found
|
||||
systems_response.append(StarSystemResponse(**system_dict))
|
||||
|
||||
return StarSystemListResponse(
|
||||
total=total,
|
||||
systems=systems_response
|
||||
)
|
||||
|
||||
|
||||
@router.get("/statistics", response_model=StarSystemStatistics)
|
||||
async def get_statistics(db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Get star system statistics
|
||||
|
||||
Returns:
|
||||
- Total star systems count
|
||||
- Exoplanet systems count
|
||||
- Total planets count (Solar System + exoplanets)
|
||||
- Nearest star systems (top 10)
|
||||
"""
|
||||
stats = await star_system_service.get_statistics(db)
|
||||
return StarSystemStatistics(**stats)
|
||||
|
||||
|
||||
@router.get("/{system_id}", response_model=StarSystemResponse)
|
||||
async def get_star_system(
|
||||
system_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get a single star system by ID
|
||||
|
||||
Args:
|
||||
system_id: Star system ID (1 = Solar System, 2+ = Exoplanet systems)
|
||||
"""
|
||||
system = await star_system_service.get_by_id(db, system_id)
|
||||
if not system:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Star system with ID {system_id} not found"
|
||||
)
|
||||
return StarSystemResponse.from_orm(system)
|
||||
|
||||
|
||||
@router.get("/{system_id}/bodies", response_model=StarSystemWithBodies)
|
||||
async def get_star_system_with_bodies(
|
||||
system_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get a star system with all its celestial bodies
|
||||
|
||||
Args:
|
||||
system_id: Star system ID
|
||||
|
||||
Returns:
|
||||
Star system details along with list of all celestial bodies
|
||||
(stars, planets, dwarf planets, satellites, probes, comets, etc.)
|
||||
"""
|
||||
result = await star_system_service.get_with_bodies(db, system_id)
|
||||
if not result:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Star system with ID {system_id} not found"
|
||||
)
|
||||
|
||||
# Convert ORM objects to dicts
|
||||
system_dict = StarSystemResponse.from_orm(result["system"]).dict()
|
||||
bodies_list = [
|
||||
{
|
||||
"id": body.id,
|
||||
"name": body.name,
|
||||
"name_zh": body.name_zh,
|
||||
"type": body.type,
|
||||
"description": body.description,
|
||||
"details": body.details,
|
||||
"is_active": body.is_active,
|
||||
"extra_data": body.extra_data,
|
||||
}
|
||||
for body in result["bodies"]
|
||||
]
|
||||
|
||||
return StarSystemWithBodies(
|
||||
**system_dict,
|
||||
bodies=bodies_list,
|
||||
body_count=result["body_count"]
|
||||
)
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=StarSystemResponse)
|
||||
async def create_star_system(
|
||||
system_data: StarSystemCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create a new star system
|
||||
|
||||
Note: This is an admin operation. Use with caution.
|
||||
"""
|
||||
# Check if name already exists
|
||||
existing = await star_system_service.get_by_name(db, system_data.name)
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Star system with name '{system_data.name}' already exists"
|
||||
)
|
||||
|
||||
new_system = await star_system_service.create(db, system_data.dict())
|
||||
return StarSystemResponse.from_orm(new_system)
|
||||
|
||||
|
||||
@router.put("/{system_id}", response_model=StarSystemResponse)
|
||||
async def update_star_system(
|
||||
system_id: int,
|
||||
system_data: StarSystemUpdate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Update a star system
|
||||
|
||||
Args:
|
||||
system_id: Star system ID to update
|
||||
system_data: Fields to update (only non-null fields will be updated)
|
||||
"""
|
||||
# Filter out None values
|
||||
update_data = {k: v for k, v in system_data.dict().items() if v is not None}
|
||||
|
||||
if not update_data:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No fields to update"
|
||||
)
|
||||
|
||||
updated_system = await star_system_service.update(db, system_id, update_data)
|
||||
if not updated_system:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Star system with ID {system_id} not found"
|
||||
)
|
||||
|
||||
return StarSystemResponse.from_orm(updated_system)
|
||||
|
||||
|
||||
@router.delete("/{system_id}")
|
||||
async def delete_star_system(
|
||||
system_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Delete a star system and all its celestial bodies
|
||||
|
||||
WARNING: This will cascade delete all celestial bodies in this system!
|
||||
Cannot delete Solar System (id=1).
|
||||
"""
|
||||
if system_id == 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Cannot delete Solar System"
|
||||
)
|
||||
|
||||
try:
|
||||
deleted = await star_system_service.delete_system(db, system_id)
|
||||
if not deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Star system with ID {system_id} not found"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": f"Star system {system_id} and all its bodies deleted successfully"
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=str(e)
|
||||
)
|
||||
|
|
@ -29,6 +29,7 @@ from app.api.celestial_resource import router as celestial_resource_router
|
|||
from app.api.celestial_orbit import router as celestial_orbit_router
|
||||
from app.api.nasa_download import router as nasa_download_router
|
||||
from app.api.celestial_position import router as celestial_position_router
|
||||
from app.api.star_system import router as star_system_router
|
||||
from app.services.redis_cache import redis_cache
|
||||
from app.services.cache_preheat import preheat_all_caches
|
||||
from app.database import close_db
|
||||
|
|
@ -122,6 +123,7 @@ app.include_router(system_router, prefix=settings.api_prefix)
|
|||
app.include_router(danmaku_router, prefix=settings.api_prefix)
|
||||
|
||||
# Celestial body related routers
|
||||
app.include_router(star_system_router, prefix=settings.api_prefix)
|
||||
app.include_router(celestial_body_router, prefix=settings.api_prefix)
|
||||
app.include_router(celestial_position_router, prefix=settings.api_prefix)
|
||||
app.include_router(celestial_resource_router, prefix=settings.api_prefix)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from .resource import Resource
|
|||
from .static_data import StaticData
|
||||
from .nasa_cache import NasaCache
|
||||
from .orbit import Orbit
|
||||
from .star_system import StarSystem
|
||||
from .user import User, user_roles
|
||||
from .role import Role
|
||||
from .menu import Menu, RoleMenu
|
||||
|
|
@ -20,6 +21,7 @@ __all__ = [
|
|||
"StaticData",
|
||||
"NasaCache",
|
||||
"Orbit",
|
||||
"StarSystem",
|
||||
"User",
|
||||
"Role",
|
||||
"Menu",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
CelestialBody ORM model
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, TIMESTAMP, Boolean, CheckConstraint, Index
|
||||
from sqlalchemy import Column, String, Text, TIMESTAMP, Boolean, Integer, ForeignKey, CheckConstraint, Index
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
|
|
@ -17,6 +17,7 @@ class CelestialBody(Base):
|
|||
name = Column(String(200), nullable=False, comment="English name")
|
||||
name_zh = Column(String(200), nullable=True, comment="Chinese name")
|
||||
type = Column(String(50), nullable=False, comment="Body type")
|
||||
system_id = Column(Integer, ForeignKey('star_systems.id', ondelete='CASCADE'), nullable=True, comment="所属恒星系ID")
|
||||
description = Column(Text, nullable=True, comment="Description")
|
||||
details = Column(Text, nullable=True, comment="Detailed description (Markdown)")
|
||||
is_active = Column(Boolean, nullable=True, comment="Active status for probes (True=active, False=inactive)")
|
||||
|
|
@ -25,6 +26,7 @@ class CelestialBody(Base):
|
|||
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
star_system = relationship("StarSystem", back_populates="celestial_bodies")
|
||||
positions = relationship(
|
||||
"Position", back_populates="body", cascade="all, delete-orphan"
|
||||
)
|
||||
|
|
@ -40,6 +42,7 @@ class CelestialBody(Base):
|
|||
),
|
||||
Index("idx_celestial_bodies_type", "type"),
|
||||
Index("idx_celestial_bodies_name", "name"),
|
||||
Index("idx_celestial_bodies_system", "system_id"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
"""
|
||||
StarSystem ORM Model
|
||||
恒星系统数据模型
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text, Double, TIMESTAMP, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class StarSystem(Base):
|
||||
"""恒星系统表"""
|
||||
__tablename__ = 'star_systems'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(200), unique=True, nullable=False, index=True)
|
||||
name_zh = Column(String(200))
|
||||
host_star_name = Column(String(200), nullable=False, index=True)
|
||||
|
||||
# 位置信息(系外恒星系)
|
||||
distance_pc = Column(Double)
|
||||
distance_ly = Column(Double)
|
||||
ra = Column(Double)
|
||||
dec = Column(Double)
|
||||
position_x = Column(Double)
|
||||
position_y = Column(Double)
|
||||
position_z = Column(Double)
|
||||
|
||||
# 恒星物理参数
|
||||
spectral_type = Column(String(20))
|
||||
radius_solar = Column(Double)
|
||||
mass_solar = Column(Double)
|
||||
temperature_k = Column(Double)
|
||||
magnitude = Column(Double)
|
||||
luminosity_solar = Column(Double)
|
||||
|
||||
# 显示属性
|
||||
color = Column(String(20))
|
||||
planet_count = Column(Integer, default=0)
|
||||
|
||||
# 描述信息
|
||||
description = Column(Text)
|
||||
details = Column(Text)
|
||||
extra_data = Column(JSONB)
|
||||
|
||||
# 时间戳
|
||||
created_at = Column(TIMESTAMP, server_default=func.now())
|
||||
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
celestial_bodies = relationship("CelestialBody", back_populates="star_system", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<StarSystem(id={self.id}, name='{self.name}', planet_count={self.planet_count})>"
|
||||
|
|
@ -25,7 +25,7 @@ class StaticData(Base):
|
|||
# Constraints and indexes
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"category IN ('constellation', 'galaxy', 'star', 'nebula', 'cluster', 'asteroid_belt', 'kuiper_belt')",
|
||||
"category IN ('constellation', 'galaxy', 'star', 'nebula', 'cluster', 'asteroid_belt', 'kuiper_belt', 'interstellar')",
|
||||
name="chk_category",
|
||||
),
|
||||
UniqueConstraint("category", "name", name="uq_category_name"),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
StarSystem Pydantic Models
|
||||
恒星系统数据模型(用于API)
|
||||
"""
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class StarSystemBase(BaseModel):
|
||||
"""恒星系统基础模型"""
|
||||
name: str = Field(..., description="恒星系名称")
|
||||
name_zh: Optional[str] = Field(None, description="中文名称")
|
||||
host_star_name: str = Field(..., description="主恒星名称")
|
||||
|
||||
# 位置信息
|
||||
distance_pc: Optional[float] = Field(None, description="距离地球(秒差距)")
|
||||
distance_ly: Optional[float] = Field(None, description="距离地球(光年)")
|
||||
ra: Optional[float] = Field(None, description="赤经(度)")
|
||||
dec: Optional[float] = Field(None, description="赤纬(度)")
|
||||
position_x: Optional[float] = Field(None, description="笛卡尔坐标 X(pc)")
|
||||
position_y: Optional[float] = Field(None, description="笛卡尔坐标 Y(pc)")
|
||||
position_z: Optional[float] = Field(None, description="笛卡尔坐标 Z(pc)")
|
||||
|
||||
# 恒星参数
|
||||
spectral_type: Optional[str] = Field(None, description="光谱类型")
|
||||
radius_solar: Optional[float] = Field(None, description="恒星半径(太阳半径)")
|
||||
mass_solar: Optional[float] = Field(None, description="恒星质量(太阳质量)")
|
||||
temperature_k: Optional[float] = Field(None, description="表面温度(K)")
|
||||
magnitude: Optional[float] = Field(None, description="视星等")
|
||||
luminosity_solar: Optional[float] = Field(None, description="光度(太阳光度)")
|
||||
|
||||
# 显示属性
|
||||
color: Optional[str] = Field(None, description="显示颜色(HEX)")
|
||||
|
||||
# 描述
|
||||
description: Optional[str] = Field(None, description="描述")
|
||||
details: Optional[str] = Field(None, description="详细信息(Markdown)")
|
||||
|
||||
|
||||
class StarSystemCreate(StarSystemBase):
|
||||
"""创建恒星系统"""
|
||||
pass
|
||||
|
||||
|
||||
class StarSystemUpdate(BaseModel):
|
||||
"""更新恒星系统(所有字段可选)"""
|
||||
name: Optional[str] = None
|
||||
name_zh: Optional[str] = None
|
||||
host_star_name: Optional[str] = None
|
||||
distance_pc: Optional[float] = None
|
||||
distance_ly: Optional[float] = None
|
||||
ra: Optional[float] = None
|
||||
dec: Optional[float] = None
|
||||
position_x: Optional[float] = None
|
||||
position_y: Optional[float] = None
|
||||
position_z: Optional[float] = None
|
||||
spectral_type: Optional[str] = None
|
||||
radius_solar: Optional[float] = None
|
||||
mass_solar: Optional[float] = None
|
||||
temperature_k: Optional[float] = None
|
||||
magnitude: Optional[float] = None
|
||||
luminosity_solar: Optional[float] = None
|
||||
color: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
details: Optional[str] = None
|
||||
|
||||
|
||||
class StarSystemResponse(StarSystemBase):
|
||||
"""恒星系统响应模型"""
|
||||
id: int
|
||||
planet_count: int = Field(default=0, description="已知行星数量")
|
||||
star_count: int = Field(default=1, description="恒星数量(包括主星和伴星)")
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class StarSystemWithBodies(StarSystemResponse):
|
||||
"""包含天体的恒星系统"""
|
||||
bodies: List[dict] = Field(default_factory=list, description="关联的天体列表")
|
||||
body_count: int = Field(default=0, description="天体数量")
|
||||
|
||||
|
||||
class StarSystemListResponse(BaseModel):
|
||||
"""恒星系统列表响应"""
|
||||
total: int
|
||||
systems: List[StarSystemResponse]
|
||||
|
||||
|
||||
class StarSystemStatistics(BaseModel):
|
||||
"""恒星系统统计信息"""
|
||||
total_systems: int = Field(..., description="总恒星系统数")
|
||||
exo_systems: int = Field(..., description="系外恒星系统数")
|
||||
total_planets: int = Field(..., description="总行星数")
|
||||
exo_planets: int = Field(..., description="系外行星数")
|
||||
solar_system_planets: int = Field(..., description="太阳系行星数")
|
||||
nearest_systems: List[dict] = Field(default_factory=list, description="最近的10个恒星系统")
|
||||
|
|
@ -27,7 +27,12 @@ async def preheat_current_positions():
|
|||
async for db in get_db():
|
||||
# Get all celestial bodies
|
||||
all_bodies = await celestial_body_service.get_all_bodies(db)
|
||||
logger.info(f"Found {len(all_bodies)} celestial bodies")
|
||||
|
||||
# Filter to only Solar System bodies (system_id = 1)
|
||||
# Exclude stars and exoplanets from other star systems
|
||||
all_bodies = [b for b in all_bodies if b.system_id == 1]
|
||||
|
||||
logger.info(f"Found {len(all_bodies)} Solar System celestial bodies")
|
||||
|
||||
# Get current time rounded to the hour
|
||||
now = datetime.utcnow()
|
||||
|
|
@ -127,7 +132,12 @@ async def preheat_historical_positions(days: int = 3):
|
|||
async for db in get_db():
|
||||
# Get all celestial bodies
|
||||
all_bodies = await celestial_body_service.get_all_bodies(db)
|
||||
logger.info(f"Found {len(all_bodies)} celestial bodies")
|
||||
|
||||
# Filter to only Solar System bodies (system_id = 1)
|
||||
# Exclude stars and exoplanets from other star systems
|
||||
all_bodies = [b for b in all_bodies if b.system_id == 1]
|
||||
|
||||
logger.info(f"Found {len(all_bodies)} Solar System celestial bodies")
|
||||
|
||||
# Define time window
|
||||
end_date = datetime.utcnow()
|
||||
|
|
|
|||
|
|
@ -19,13 +19,23 @@ class CelestialBodyService:
|
|||
@staticmethod
|
||||
async def get_all_bodies(
|
||||
session: Optional[AsyncSession] = None,
|
||||
body_type: Optional[str] = None
|
||||
body_type: Optional[str] = None,
|
||||
system_id: Optional[int] = None
|
||||
) -> List[CelestialBody]:
|
||||
"""Get all celestial bodies, optionally filtered by type"""
|
||||
"""
|
||||
Get all celestial bodies, optionally filtered by type and star system
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
body_type: Filter by body type (star, planet, dwarf_planet, etc.)
|
||||
system_id: Filter by star system ID (1=Solar System, 2+=Exoplanets)
|
||||
"""
|
||||
async def _query(s: AsyncSession):
|
||||
query = select(CelestialBody)
|
||||
if body_type:
|
||||
query = query.where(CelestialBody.type == body_type)
|
||||
if system_id is not None:
|
||||
query = query.where(CelestialBody.system_id == system_id)
|
||||
result = await s.execute(query.order_by(CelestialBody.name))
|
||||
return result.scalars().all()
|
||||
|
||||
|
|
@ -610,6 +620,43 @@ class ResourceService:
|
|||
async with AsyncSessionLocal() as s:
|
||||
return await _query(s)
|
||||
|
||||
@staticmethod
|
||||
async def get_all_resources_grouped_by_body(
|
||||
body_ids: Optional[List[str]] = None,
|
||||
session: Optional[AsyncSession] = None
|
||||
) -> Dict[str, List[Resource]]:
|
||||
"""
|
||||
Get all resources grouped by body_id (optimized for bulk loading)
|
||||
|
||||
Args:
|
||||
body_ids: Optional list of body IDs to filter by
|
||||
session: Database session
|
||||
|
||||
Returns:
|
||||
Dictionary mapping body_id to list of resources
|
||||
"""
|
||||
async def _query(s: AsyncSession):
|
||||
query = select(Resource).order_by(Resource.body_id, Resource.created_at)
|
||||
if body_ids:
|
||||
query = query.where(Resource.body_id.in_(body_ids))
|
||||
result = await s.execute(query)
|
||||
resources = result.scalars().all()
|
||||
|
||||
# Group by body_id
|
||||
grouped = {}
|
||||
for resource in resources:
|
||||
if resource.body_id not in grouped:
|
||||
grouped[resource.body_id] = []
|
||||
grouped[resource.body_id].append(resource)
|
||||
|
||||
return grouped
|
||||
|
||||
if session:
|
||||
return await _query(session)
|
||||
else:
|
||||
async with AsyncSessionLocal() as s:
|
||||
return await _query(s)
|
||||
|
||||
@staticmethod
|
||||
async def delete_resource(
|
||||
resource_id: int,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,33 @@ class OrbitService:
|
|||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@staticmethod
|
||||
async def get_all_orbits_with_bodies(
|
||||
session: AsyncSession,
|
||||
body_type: Optional[str] = None
|
||||
) -> List[tuple[Orbit, CelestialBody]]:
|
||||
"""
|
||||
Get all orbits with their associated celestial bodies in a single query.
|
||||
This is optimized to avoid N+1 query problem.
|
||||
|
||||
Returns:
|
||||
List of (Orbit, CelestialBody) tuples
|
||||
"""
|
||||
if body_type:
|
||||
query = (
|
||||
select(Orbit, CelestialBody)
|
||||
.join(CelestialBody, Orbit.body_id == CelestialBody.id)
|
||||
.where(CelestialBody.type == body_type)
|
||||
)
|
||||
else:
|
||||
query = (
|
||||
select(Orbit, CelestialBody)
|
||||
.join(CelestialBody, Orbit.body_id == CelestialBody.id)
|
||||
)
|
||||
|
||||
result = await session.execute(query)
|
||||
return list(result.all())
|
||||
|
||||
@staticmethod
|
||||
async def save_orbit(
|
||||
body_id: str,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
"""
|
||||
StarSystem Service
|
||||
恒星系统服务层
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import select, func, update, delete, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.models.db.star_system import StarSystem
|
||||
from app.models.db.celestial_body import CelestialBody
|
||||
|
||||
|
||||
class StarSystemService:
|
||||
"""恒星系统服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_all(
|
||||
db: AsyncSession,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
exclude_solar: bool = False,
|
||||
search: Optional[str] = None
|
||||
) -> List[StarSystem]:
|
||||
"""
|
||||
获取所有恒星系统
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
skip: 跳过记录数
|
||||
limit: 返回记录数
|
||||
exclude_solar: 是否排除太阳系
|
||||
search: 搜索关键词(匹配名称)
|
||||
"""
|
||||
query = select(StarSystem).order_by(StarSystem.distance_pc.asc().nulls_first())
|
||||
|
||||
# 排除太阳系
|
||||
if exclude_solar:
|
||||
query = query.where(StarSystem.id != 1)
|
||||
|
||||
# 搜索
|
||||
if search:
|
||||
search_pattern = f"%{search}%"
|
||||
query = query.where(
|
||||
or_(
|
||||
StarSystem.name.ilike(search_pattern),
|
||||
StarSystem.name_zh.ilike(search_pattern),
|
||||
StarSystem.host_star_name.ilike(search_pattern)
|
||||
)
|
||||
)
|
||||
|
||||
query = query.offset(skip).limit(limit)
|
||||
result = await db.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(db: AsyncSession, system_id: int) -> Optional[StarSystem]:
|
||||
"""根据ID获取恒星系统"""
|
||||
result = await db.execute(
|
||||
select(StarSystem).where(StarSystem.id == system_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_name(db: AsyncSession, name: str) -> Optional[StarSystem]:
|
||||
"""根据名称获取恒星系统"""
|
||||
result = await db.execute(
|
||||
select(StarSystem).where(StarSystem.name == name)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def create(db: AsyncSession, system_data: dict) -> StarSystem:
|
||||
"""创建恒星系统"""
|
||||
system = StarSystem(**system_data)
|
||||
db.add(system)
|
||||
await db.commit()
|
||||
await db.refresh(system)
|
||||
return system
|
||||
|
||||
@staticmethod
|
||||
async def update(db: AsyncSession, system_id: int, system_data: dict) -> Optional[StarSystem]:
|
||||
"""更新恒星系统"""
|
||||
result = await db.execute(
|
||||
select(StarSystem).where(StarSystem.id == system_id)
|
||||
)
|
||||
system = result.scalar_one_or_none()
|
||||
|
||||
if not system:
|
||||
return None
|
||||
|
||||
for key, value in system_data.items():
|
||||
if hasattr(system, key):
|
||||
setattr(system, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(system)
|
||||
return system
|
||||
|
||||
@staticmethod
|
||||
async def delete_system(db: AsyncSession, system_id: int) -> bool:
|
||||
"""
|
||||
删除恒星系统(级联删除所有关联天体)
|
||||
不允许删除太阳系(id=1)
|
||||
"""
|
||||
if system_id == 1:
|
||||
raise ValueError("不能删除太阳系")
|
||||
|
||||
result = await db.execute(
|
||||
delete(StarSystem).where(StarSystem.id == system_id)
|
||||
)
|
||||
await db.commit()
|
||||
return result.rowcount > 0
|
||||
|
||||
@staticmethod
|
||||
async def get_with_bodies(db: AsyncSession, system_id: int) -> Optional[dict]:
|
||||
"""
|
||||
获取恒星系统及其所有天体
|
||||
|
||||
Returns:
|
||||
包含 system 和 bodies 的字典
|
||||
"""
|
||||
# 获取恒星系统
|
||||
system_result = await db.execute(
|
||||
select(StarSystem).where(StarSystem.id == system_id)
|
||||
)
|
||||
system = system_result.scalar_one_or_none()
|
||||
|
||||
if not system:
|
||||
return None
|
||||
|
||||
# 获取关联的天体(仅返回活跃状态的天体)
|
||||
bodies_result = await db.execute(
|
||||
select(CelestialBody)
|
||||
.where(CelestialBody.system_id == system_id)
|
||||
.where(CelestialBody.is_active == True)
|
||||
.order_by(CelestialBody.type, CelestialBody.name)
|
||||
)
|
||||
bodies = list(bodies_result.scalars().all())
|
||||
|
||||
return {
|
||||
"system": system,
|
||||
"bodies": bodies,
|
||||
"body_count": len(bodies)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def update_planet_count(db: AsyncSession, system_id: int) -> None:
|
||||
"""更新恒星系统的行星数量统计"""
|
||||
result = await db.execute(
|
||||
select(func.count(CelestialBody.id))
|
||||
.where(CelestialBody.system_id == system_id)
|
||||
.where(CelestialBody.type != 'star') # 排除恒星本身
|
||||
)
|
||||
count = result.scalar()
|
||||
|
||||
await db.execute(
|
||||
update(StarSystem)
|
||||
.where(StarSystem.id == system_id)
|
||||
.values(planet_count=count)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
@staticmethod
|
||||
async def get_statistics(db: AsyncSession) -> dict:
|
||||
"""获取恒星系统统计信息"""
|
||||
# 总恒星系统数
|
||||
total_systems_result = await db.execute(select(func.count(StarSystem.id)))
|
||||
total_systems = total_systems_result.scalar()
|
||||
|
||||
# 系外恒星系统数
|
||||
exo_systems_result = await db.execute(
|
||||
select(func.count(StarSystem.id)).where(StarSystem.id != 1)
|
||||
)
|
||||
exo_systems = exo_systems_result.scalar()
|
||||
|
||||
# 总行星数
|
||||
total_planets_result = await db.execute(
|
||||
select(func.count(CelestialBody.id))
|
||||
.where(CelestialBody.type == 'planet')
|
||||
)
|
||||
total_planets = total_planets_result.scalar()
|
||||
|
||||
# 系外行星数
|
||||
exo_planets_result = await db.execute(
|
||||
select(func.count(CelestialBody.id))
|
||||
.where(CelestialBody.type == 'planet')
|
||||
.where(CelestialBody.system_id > 1)
|
||||
)
|
||||
exo_planets = exo_planets_result.scalar()
|
||||
|
||||
# 距离最近的10个恒星系统
|
||||
nearest_systems_result = await db.execute(
|
||||
select(StarSystem.name, StarSystem.name_zh, StarSystem.distance_ly, StarSystem.planet_count)
|
||||
.where(StarSystem.id != 1)
|
||||
.order_by(StarSystem.distance_pc.asc())
|
||||
.limit(10)
|
||||
)
|
||||
nearest_systems = [
|
||||
{
|
||||
"name": name,
|
||||
"name_zh": name_zh,
|
||||
"distance_ly": distance_ly,
|
||||
"planet_count": planet_count
|
||||
}
|
||||
for name, name_zh, distance_ly, planet_count in nearest_systems_result
|
||||
]
|
||||
|
||||
return {
|
||||
"total_systems": total_systems,
|
||||
"exo_systems": exo_systems,
|
||||
"total_planets": total_planets,
|
||||
"exo_planets": exo_planets,
|
||||
"solar_system_planets": total_planets - exo_planets,
|
||||
"nearest_systems": nearest_systems
|
||||
}
|
||||
|
||||
|
||||
# 创建服务实例
|
||||
star_system_service = StarSystemService()
|
||||
|
|
@ -206,6 +206,15 @@ class SystemSettingsService:
|
|||
"description": "生成轨道线时使用的点数,越多越平滑但性能越低",
|
||||
"is_public": True
|
||||
},
|
||||
{
|
||||
"key": "view_mode",
|
||||
"value": "solar",
|
||||
"value_type": "string",
|
||||
"category": "visualization",
|
||||
"label": "默认视图模式",
|
||||
"description": "首页默认进入的视图模式 (solar: 太阳系视图, galaxy: 银河系视图)",
|
||||
"is_public": True
|
||||
},
|
||||
]
|
||||
|
||||
for default in defaults:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,226 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
补全多恒星系统数据并启用恒星和行星
|
||||
参考比邻星系统(Alpha Centauri)的数据结构
|
||||
"""
|
||||
import asyncio
|
||||
import asyncpg
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 数据库连接配置
|
||||
DB_CONFIG = {
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"user": "postgres",
|
||||
"password": "postgres",
|
||||
"database": "cosmo_db"
|
||||
}
|
||||
|
||||
|
||||
# 已知的多恒星系统数据(来自天文学资料)
|
||||
MULTI_STAR_SYSTEMS = {
|
||||
# Alpha Centauri System (比邻星系统) - system_id = 479
|
||||
479: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-479-primary",
|
||||
"name": "Alpha Centauri A",
|
||||
"name_zh": "南门二A",
|
||||
"description": "该恒星系主序星,光谱类型: G2V, 表面温度: 5790K",
|
||||
"extra_data": {
|
||||
"spectral_type": "G2V",
|
||||
"mass_solar": 1.1,
|
||||
"radius_solar": 1.22,
|
||||
"temperature_k": 5790
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-479-secondary",
|
||||
"name": "Alpha Centauri B",
|
||||
"name_zh": "南门二B",
|
||||
"description": "与南门二A相互绕转的明亮双星,是该系统的主体。",
|
||||
"extra_data": {
|
||||
"spectral_type": "K1V",
|
||||
"mass_solar": 0.93,
|
||||
"radius_solar": 0.86,
|
||||
"temperature_k": 5260
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-479-tertiary",
|
||||
"name": "Proxima Centauri",
|
||||
"name_zh": "比邻星",
|
||||
"description": "一颗质量小、光度弱的红矮星,距离南门二A/B约0.2光年,围绕它们公转。",
|
||||
"extra_data": {
|
||||
"spectral_type": "M5.5V",
|
||||
"mass_solar": 0.12,
|
||||
"radius_solar": 0.14,
|
||||
"temperature_k": 2900
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def check_existing_data(conn):
|
||||
"""检查现有数据"""
|
||||
logger.info("=== 检查现有数据 ===")
|
||||
|
||||
# 检查恒星系统
|
||||
rows = await conn.fetch("""
|
||||
SELECT id, name, name_zh, host_star_name, planet_count
|
||||
FROM star_systems
|
||||
WHERE id IN (479, 2, 3, 4, 5)
|
||||
ORDER BY id
|
||||
""")
|
||||
|
||||
print("\n恒星系统:")
|
||||
for row in rows:
|
||||
print(f" ID={row['id']}: {row['name_zh'] or row['name']} (主恒星: {row['host_star_name']}, 行星数: {row['planet_count']})")
|
||||
|
||||
# 检查比邻星系统的天体
|
||||
rows = await conn.fetch("""
|
||||
SELECT id, name, name_zh, type, is_active
|
||||
FROM celestial_bodies
|
||||
WHERE system_id = 479
|
||||
ORDER BY type, name
|
||||
""")
|
||||
|
||||
print("\n比邻星系统(479)的天体:")
|
||||
for row in rows:
|
||||
print(f" {row['type']:15} | {row['name']:30} | Active: {row['is_active']}")
|
||||
|
||||
|
||||
async def add_missing_stars(conn):
|
||||
"""添加缺失的恒星"""
|
||||
logger.info("\n=== 添加缺失的恒星 ===")
|
||||
|
||||
for system_id, system_data in MULTI_STAR_SYSTEMS.items():
|
||||
logger.info(f"\n处理恒星系统 ID={system_id}")
|
||||
|
||||
for star in system_data["stars"]:
|
||||
# 检查是否已存在
|
||||
existing = await conn.fetchrow(
|
||||
"SELECT id FROM celestial_bodies WHERE id = $1",
|
||||
star["id"]
|
||||
)
|
||||
|
||||
if existing:
|
||||
logger.info(f" ✓ 恒星已存在: {star['name']} ({star['id']})")
|
||||
else:
|
||||
# 插入新恒星
|
||||
await conn.execute("""
|
||||
INSERT INTO celestial_bodies
|
||||
(id, name, name_zh, type, system_id, description, is_active, extra_data, created_at, updated_at)
|
||||
VALUES
|
||||
($1, $2, $3, 'star', $4, $5, TRUE, $6::jsonb, NOW(), NOW())
|
||||
""",
|
||||
star["id"],
|
||||
star["name"],
|
||||
star["name_zh"],
|
||||
system_id,
|
||||
star["description"],
|
||||
json.dumps(star["extra_data"])
|
||||
)
|
||||
logger.info(f" ✅ 添加恒星: {star['name_zh']} ({star['id']})")
|
||||
|
||||
logger.info("\n恒星数据补全完成!")
|
||||
|
||||
|
||||
async def activate_stars_and_planets(conn):
|
||||
"""启用所有恒星和行星"""
|
||||
logger.info("\n=== 启用恒星和行星 ===")
|
||||
|
||||
# 启用所有恒星(除了太阳系之外的其他系统)
|
||||
stars = await conn.fetch("""
|
||||
UPDATE celestial_bodies
|
||||
SET is_active = TRUE, updated_at = NOW()
|
||||
WHERE type = 'star' AND system_id > 1
|
||||
RETURNING id, name, name_zh
|
||||
""")
|
||||
|
||||
logger.info(f"\n启用了 {len(stars)} 颗恒星:")
|
||||
for star in stars:
|
||||
logger.info(f" ✓ {star['name_zh'] or star['name']} ({star['id']})")
|
||||
|
||||
# 启用所有行星(除了太阳系之外的其他系统)
|
||||
planets = await conn.fetch("""
|
||||
UPDATE celestial_bodies
|
||||
SET is_active = TRUE, updated_at = NOW()
|
||||
WHERE type = 'planet' AND system_id > 1
|
||||
RETURNING id, name, name_zh
|
||||
""")
|
||||
|
||||
logger.info(f"\n启用了 {len(planets)} 颗行星:")
|
||||
for planet in planets:
|
||||
logger.info(f" ✓ {planet['name_zh'] or planet['name']} ({planet['id']})")
|
||||
|
||||
logger.info("\n启用完成!")
|
||||
|
||||
|
||||
async def verify_results(conn):
|
||||
"""验证结果"""
|
||||
logger.info("\n=== 验证结果 ===")
|
||||
|
||||
# 统计各系统的天体数量
|
||||
rows = await conn.fetch("""
|
||||
SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
s.name_zh,
|
||||
COUNT(CASE WHEN cb.type = 'star' THEN 1 END) as star_count,
|
||||
COUNT(CASE WHEN cb.type = 'planet' THEN 1 END) as planet_count,
|
||||
COUNT(CASE WHEN cb.is_active = TRUE THEN 1 END) as active_count
|
||||
FROM star_systems s
|
||||
LEFT JOIN celestial_bodies cb ON s.id = cb.system_id
|
||||
WHERE s.id IN (479, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
GROUP BY s.id, s.name, s.name_zh
|
||||
ORDER BY s.id
|
||||
""")
|
||||
|
||||
print("\n各恒星系统统计:")
|
||||
print(f"{'系统ID':<8} {'名称':<30} {'恒星数':<8} {'行星数':<8} {'启用数':<8}")
|
||||
print("-" * 80)
|
||||
for row in rows:
|
||||
print(f"{row['id']:<8} {(row['name_zh'] or row['name']):<30} {row['star_count']:<8} {row['planet_count']:<8} {row['active_count']:<8}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
print("=" * 80)
|
||||
print("多恒星系统数据补全和启用脚本")
|
||||
print("=" * 80)
|
||||
|
||||
# 连接数据库
|
||||
conn = await asyncpg.connect(**DB_CONFIG)
|
||||
|
||||
try:
|
||||
# 1. 检查现有数据
|
||||
await check_existing_data(conn)
|
||||
|
||||
# 2. 添加缺失的恒星
|
||||
await add_missing_stars(conn)
|
||||
|
||||
# 3. 启用恒星和行星
|
||||
await activate_stars_and_planets(conn)
|
||||
|
||||
# 4. 验证结果
|
||||
await verify_results(conn)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("✅ 所有操作完成!")
|
||||
print("=" * 80)
|
||||
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,487 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
补全高价值双星/多星系统数据
|
||||
包含8-10个科学价值最高的多恒星系统
|
||||
"""
|
||||
import asyncio
|
||||
import asyncpg
|
||||
import json
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 数据库连接配置
|
||||
DB_CONFIG = {
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"user": "postgres",
|
||||
"password": "postgres",
|
||||
"database": "cosmo_db"
|
||||
}
|
||||
|
||||
# 高价值多恒星系统数据(基于天文学资料)
|
||||
MULTI_STAR_SYSTEMS = {
|
||||
# 1. Alpha Centauri (比邻星系统) - 已完成,保留用于验证
|
||||
479: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-479-primary",
|
||||
"name": "Alpha Centauri A",
|
||||
"name_zh": "南门二A",
|
||||
"description": "该恒星系主序星,光谱类型: G2V, 表面温度: 5790K",
|
||||
"extra_data": {
|
||||
"spectral_type": "G2V",
|
||||
"mass_solar": 1.1,
|
||||
"radius_solar": 1.22,
|
||||
"temperature_k": 5790
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-479-secondary",
|
||||
"name": "Alpha Centauri B",
|
||||
"name_zh": "南门二B",
|
||||
"description": "与南门二A相互绕转的明亮双星,是该系统的主体。",
|
||||
"extra_data": {
|
||||
"spectral_type": "K1V",
|
||||
"mass_solar": 0.93,
|
||||
"radius_solar": 0.86,
|
||||
"temperature_k": 5260
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-479-tertiary",
|
||||
"name": "Proxima Centauri",
|
||||
"name_zh": "比邻星",
|
||||
"description": "一颗质量小、光度弱的红矮星,距离南门二A/B约0.2光年,围绕它们公转。",
|
||||
"extra_data": {
|
||||
"spectral_type": "M5.5V",
|
||||
"mass_solar": 0.12,
|
||||
"radius_solar": 0.14,
|
||||
"temperature_k": 2900
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
# 2. 55 Cancri (巨蟹座55) - 双星系统
|
||||
11: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-11-primary",
|
||||
"name": "55 Cancri A",
|
||||
"name_zh": "巨蟹座55A",
|
||||
"description": "类太阳黄矮星,拥有5颗已确认行星,包括著名的超级地球55 Cnc e。",
|
||||
"extra_data": {
|
||||
"spectral_type": "G8V",
|
||||
"mass_solar": 0.95,
|
||||
"radius_solar": 0.94,
|
||||
"temperature_k": 5196
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-11-secondary",
|
||||
"name": "55 Cancri B",
|
||||
"name_zh": "巨蟹座55B",
|
||||
"description": "红矮星伴星,距离A星约1065 AU,轨道周期约1000年。",
|
||||
"extra_data": {
|
||||
"spectral_type": "M4V",
|
||||
"mass_solar": 0.13,
|
||||
"radius_solar": 0.30,
|
||||
"temperature_k": 3200,
|
||||
"separation_au": 1065,
|
||||
"orbital_period_years": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
# 3. 16 Cygni (天鹅座16) - 双星系统
|
||||
5: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-5-primary",
|
||||
"name": "16 Cygni A",
|
||||
"name_zh": "天鹅座16A",
|
||||
"description": "类太阳黄矮星,该双星系统的主星。",
|
||||
"extra_data": {
|
||||
"spectral_type": "G1.5V",
|
||||
"mass_solar": 1.11,
|
||||
"radius_solar": 1.24,
|
||||
"temperature_k": 5825
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-5-secondary",
|
||||
"name": "16 Cygni B",
|
||||
"name_zh": "天鹅座16B",
|
||||
"description": "类太阳黄矮星,拥有一颗高偏心率轨道的行星16 Cyg B b,展示了双星引力对行星轨道的影响。",
|
||||
"extra_data": {
|
||||
"spectral_type": "G2.5V",
|
||||
"mass_solar": 1.07,
|
||||
"radius_solar": 1.14,
|
||||
"temperature_k": 5750,
|
||||
"separation_au": 850,
|
||||
"orbital_period_years": 18200
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
# 4. Epsilon Indi (天园增四) - 三体系统 (1恒星 + 2棕矮星)
|
||||
40: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-40-primary",
|
||||
"name": "Epsilon Indi A",
|
||||
"name_zh": "天园增四A",
|
||||
"description": "橙矮星,第五近的恒星系统,伴有两颗棕矮星(Ba和Bb)。",
|
||||
"extra_data": {
|
||||
"spectral_type": "K5V",
|
||||
"mass_solar": 0.76,
|
||||
"radius_solar": 0.73,
|
||||
"temperature_k": 4630
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-40-secondary",
|
||||
"name": "Epsilon Indi Ba",
|
||||
"name_zh": "天园增四Ba",
|
||||
"description": "T1V型棕矮星,距离A星约1460 AU,与Bb组成棕矮星双星系统。",
|
||||
"extra_data": {
|
||||
"spectral_type": "T1V",
|
||||
"mass_jupiter": 47,
|
||||
"radius_jupiter": 0.91,
|
||||
"temperature_k": 1300,
|
||||
"separation_from_A_au": 1460,
|
||||
"is_brown_dwarf": True
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-40-tertiary",
|
||||
"name": "Epsilon Indi Bb",
|
||||
"name_zh": "天园增四Bb",
|
||||
"description": "T6V型棕矮星,与Ba互绕,周期约15年,是最近的棕矮星双星系统。",
|
||||
"extra_data": {
|
||||
"spectral_type": "T6V",
|
||||
"mass_jupiter": 28,
|
||||
"radius_jupiter": 0.80,
|
||||
"temperature_k": 880,
|
||||
"orbital_period_years": 15,
|
||||
"is_brown_dwarf": True
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
# 5. Gamma Cephei (仙王座γ) - 双星系统
|
||||
49: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-49-primary",
|
||||
"name": "Gamma Cephei A",
|
||||
"name_zh": "仙王座γA",
|
||||
"description": "亚巨星,最早被怀疑有行星的恒星之一(1988年),拥有一颗类木行星。",
|
||||
"extra_data": {
|
||||
"spectral_type": "K1IV",
|
||||
"mass_solar": 1.59,
|
||||
"radius_solar": 4.9,
|
||||
"temperature_k": 4800
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-49-secondary",
|
||||
"name": "Gamma Cephei B",
|
||||
"name_zh": "仙王座γB",
|
||||
"description": "红矮星伴星,距离A星约20 AU,轨道周期约66年,形成紧密双星系统。",
|
||||
"extra_data": {
|
||||
"spectral_type": "M4V",
|
||||
"mass_solar": 0.4,
|
||||
"radius_solar": 0.40,
|
||||
"temperature_k": 3200,
|
||||
"separation_au": 20,
|
||||
"orbital_period_years": 66
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
# 6. Upsilon Andromedae (仙女座υ) - 双星系统
|
||||
572: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-572-primary",
|
||||
"name": "Upsilon Andromedae A",
|
||||
"name_zh": "仙女座υA",
|
||||
"description": "黄白主序星,第一个被发现有多颗行星的主序星(1999年),拥有4颗已确认行星。",
|
||||
"extra_data": {
|
||||
"spectral_type": "F8V",
|
||||
"mass_solar": 1.27,
|
||||
"radius_solar": 1.63,
|
||||
"temperature_k": 6212
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-572-secondary",
|
||||
"name": "Upsilon Andromedae B",
|
||||
"name_zh": "仙女座υB",
|
||||
"description": "红矮星伴星,距离A星约750 AU。",
|
||||
"extra_data": {
|
||||
"spectral_type": "M4.5V",
|
||||
"mass_solar": 0.25,
|
||||
"radius_solar": 0.28,
|
||||
"temperature_k": 3100,
|
||||
"separation_au": 750
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
# 7. HD 41004 - 双星系统(两个独立的system_id需要合并)
|
||||
347: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-347-primary",
|
||||
"name": "HD 41004 A",
|
||||
"name_zh": "HD 41004 A",
|
||||
"description": "橙矮星,拥有一颗类木行星HD 41004 A b。",
|
||||
"extra_data": {
|
||||
"spectral_type": "K1V",
|
||||
"mass_solar": 0.70,
|
||||
"radius_solar": 0.67,
|
||||
"temperature_k": 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-347-secondary",
|
||||
"name": "HD 41004 B",
|
||||
"name_zh": "HD 41004 B",
|
||||
"description": "红矮星伴星,距离A星约23 AU,可能拥有棕矮星伴星。",
|
||||
"extra_data": {
|
||||
"spectral_type": "M2V",
|
||||
"mass_solar": 0.40,
|
||||
"radius_solar": 0.39,
|
||||
"temperature_k": 3400,
|
||||
"separation_au": 23
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
# 8. GJ 86 (格利泽86) - 双星系统(橙矮星 + 白矮星)
|
||||
128: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-128-primary",
|
||||
"name": "GJ 86 A",
|
||||
"name_zh": "格利泽86A",
|
||||
"description": "橙矮星,拥有一颗类木行星GJ 86 b,伴星是罕见的白矮星。",
|
||||
"extra_data": {
|
||||
"spectral_type": "K1V",
|
||||
"mass_solar": 0.79,
|
||||
"radius_solar": 0.77,
|
||||
"temperature_k": 5100
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-128-secondary",
|
||||
"name": "GJ 86 B",
|
||||
"name_zh": "格利泽86B",
|
||||
"description": "白矮星伴星,距离A星约21 AU,是研究恒星演化对行星影响的重要案例。",
|
||||
"extra_data": {
|
||||
"spectral_type": "DA (白矮星)",
|
||||
"mass_solar": 0.55,
|
||||
"radius_solar": 0.01,
|
||||
"temperature_k": 8000,
|
||||
"separation_au": 21,
|
||||
"is_white_dwarf": True
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
# 9. HD 196885 - 双星系统
|
||||
267: {
|
||||
"stars": [
|
||||
{
|
||||
"id": "star-267-primary",
|
||||
"name": "HD 196885 A",
|
||||
"name_zh": "HD 196885 A",
|
||||
"description": "黄白主序星,拥有一颗行星HD 196885 A b。",
|
||||
"extra_data": {
|
||||
"spectral_type": "F8V",
|
||||
"mass_solar": 1.33,
|
||||
"radius_solar": 1.68,
|
||||
"temperature_k": 6172
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "star-267-secondary",
|
||||
"name": "HD 196885 B",
|
||||
"name_zh": "HD 196885 B",
|
||||
"description": "红矮星伴星,距离A星约25 AU。",
|
||||
"extra_data": {
|
||||
"spectral_type": "M",
|
||||
"mass_solar": 0.45,
|
||||
"radius_solar": 0.43,
|
||||
"temperature_k": 3500,
|
||||
"separation_au": 25
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def add_missing_stars(conn):
|
||||
"""添加缺失的恒星"""
|
||||
logger.info("=" * 80)
|
||||
logger.info("开始补全多恒星系统数据")
|
||||
logger.info("=" * 80)
|
||||
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for system_id, system_data in MULTI_STAR_SYSTEMS.items():
|
||||
# 检查系统是否存在
|
||||
system = await conn.fetchrow(
|
||||
"SELECT id, name, name_zh FROM star_systems WHERE id = $1",
|
||||
system_id
|
||||
)
|
||||
|
||||
if not system:
|
||||
logger.warning(f"\n⚠️ 系统ID={system_id}不存在,跳过")
|
||||
continue
|
||||
|
||||
logger.info(f"\n{'='*80}")
|
||||
logger.info(f"处理恒星系统: {system['name_zh'] or system['name']} (ID={system_id})")
|
||||
logger.info(f"{'='*80}")
|
||||
|
||||
for star in system_data["stars"]:
|
||||
# 检查是否已存在
|
||||
existing = await conn.fetchrow(
|
||||
"SELECT id FROM celestial_bodies WHERE id = $1",
|
||||
star["id"]
|
||||
)
|
||||
|
||||
if existing:
|
||||
logger.info(f" ✓ 恒星已存在: {star['name_zh']} ({star['id']})")
|
||||
skipped_count += 1
|
||||
else:
|
||||
# 插入新恒星
|
||||
await conn.execute("""
|
||||
INSERT INTO celestial_bodies
|
||||
(id, name, name_zh, type, system_id, description, is_active, extra_data, created_at, updated_at)
|
||||
VALUES
|
||||
($1, $2, $3, 'star', $4, $5, TRUE, $6::jsonb, NOW(), NOW())
|
||||
""",
|
||||
star["id"],
|
||||
star["name"],
|
||||
star["name_zh"],
|
||||
system_id,
|
||||
star["description"],
|
||||
json.dumps(star["extra_data"])
|
||||
)
|
||||
logger.info(f" ✅ 添加恒星: {star['name_zh']} ({star['id']})")
|
||||
added_count += 1
|
||||
|
||||
logger.info(f"\n{'='*80}")
|
||||
logger.info(f"恒星数据补全完成!")
|
||||
logger.info(f" 新增: {added_count}颗")
|
||||
logger.info(f" 跳过: {skipped_count}颗(已存在)")
|
||||
logger.info(f"{'='*80}")
|
||||
|
||||
|
||||
async def verify_results(conn):
|
||||
"""验证结果"""
|
||||
logger.info("\n" + "=" * 80)
|
||||
logger.info("验证多星系统数据")
|
||||
logger.info("=" * 80)
|
||||
|
||||
system_ids = list(MULTI_STAR_SYSTEMS.keys())
|
||||
|
||||
rows = await conn.fetch("""
|
||||
SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
s.name_zh,
|
||||
COUNT(CASE WHEN cb.type = 'star' THEN 1 END) as star_count,
|
||||
COUNT(CASE WHEN cb.type = 'planet' THEN 1 END) as planet_count,
|
||||
COUNT(CASE WHEN cb.is_active = TRUE THEN 1 END) as active_count,
|
||||
string_agg(
|
||||
CASE WHEN cb.type = 'star' THEN cb.name_zh || ' (' || cb.id || ')' END,
|
||||
', '
|
||||
ORDER BY cb.id
|
||||
) as star_names
|
||||
FROM star_systems s
|
||||
LEFT JOIN celestial_bodies cb ON s.id = cb.system_id
|
||||
WHERE s.id = ANY($1)
|
||||
GROUP BY s.id, s.name, s.name_zh
|
||||
ORDER BY s.id
|
||||
""", system_ids)
|
||||
|
||||
print(f"\n{'系统ID':<8} {'系统名称':<30} {'恒星数':<8} {'行星数':<8} {'启用数':<8}")
|
||||
print("=" * 100)
|
||||
for row in rows:
|
||||
system_name = row['name_zh'] or row['name']
|
||||
print(f"{row['id']:<8} {system_name:<30} {row['star_count']:<8} {row['planet_count']:<8} {row['active_count']:<8}")
|
||||
|
||||
# 详细显示每个系统的恒星
|
||||
print(f"\n{'='*100}")
|
||||
print("各系统恒星详情:")
|
||||
print(f"{'='*100}")
|
||||
|
||||
for row in rows:
|
||||
system_name = row['name_zh'] or row['name']
|
||||
stars = await conn.fetch("""
|
||||
SELECT id, name, name_zh, extra_data
|
||||
FROM celestial_bodies
|
||||
WHERE system_id = $1 AND type = 'star'
|
||||
ORDER BY id
|
||||
""", row['id'])
|
||||
|
||||
print(f"\n{system_name} (ID={row['id']}):")
|
||||
for star in stars:
|
||||
# Handle both dict and JSON string
|
||||
extra = star['extra_data']
|
||||
if isinstance(extra, str):
|
||||
extra = json.loads(extra) if extra else {}
|
||||
elif extra is None:
|
||||
extra = {}
|
||||
|
||||
spectral = extra.get('spectral_type', 'N/A')
|
||||
mass = extra.get('mass_solar', extra.get('mass_jupiter'))
|
||||
mass_unit = 'M☉' if 'mass_solar' in extra else ('MJ' if 'mass_jupiter' in extra else '')
|
||||
print(f" • {star['name_zh']:<25} | 光谱: {spectral:<10} | 质量: {mass}{mass_unit if mass else 'N/A'}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
print("\n" + "=" * 80)
|
||||
print("多恒星系统数据补全脚本 v2.0")
|
||||
print("将补全8-10个高价值双星/多星系统")
|
||||
print("=" * 80)
|
||||
|
||||
conn = await asyncpg.connect(**DB_CONFIG)
|
||||
|
||||
try:
|
||||
# 1. 添加缺失的恒星
|
||||
await add_missing_stars(conn)
|
||||
|
||||
# 2. 验证结果
|
||||
await verify_results(conn)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("✅ 所有操作完成!")
|
||||
print("=" * 80)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 发生错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
-- 添加恒星系统管理菜单项
|
||||
-- 将其放在天体数据管理之前(sort_order=0)
|
||||
|
||||
-- 首先调整天体数据管理的sort_order,从1改为2
|
||||
UPDATE menus SET sort_order = 2 WHERE id = 3 AND name = 'celestial_bodies';
|
||||
|
||||
-- 添加恒星系统管理菜单(sort_order=1,在天体数据管理之前)
|
||||
INSERT INTO menus (
|
||||
parent_id,
|
||||
name,
|
||||
title,
|
||||
icon,
|
||||
path,
|
||||
component,
|
||||
sort_order,
|
||||
is_active,
|
||||
description
|
||||
) VALUES (
|
||||
2, -- parent_id: 数据管理
|
||||
'star_systems',
|
||||
'恒星系统管理',
|
||||
'StarOutlined',
|
||||
'/admin/star-systems',
|
||||
'StarSystems',
|
||||
1, -- sort_order: 在天体数据管理(2)之前
|
||||
true,
|
||||
'管理太阳系和系外恒星系统'
|
||||
) ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 获取新插入的菜单ID并为管理员角色授权
|
||||
DO $$
|
||||
DECLARE
|
||||
menu_id INT;
|
||||
admin_role_id INT;
|
||||
BEGIN
|
||||
-- 获取刚插入的菜单ID
|
||||
SELECT id INTO menu_id FROM menus WHERE name = 'star_systems';
|
||||
|
||||
-- 获取管理员角色ID(通常是1)
|
||||
SELECT id INTO admin_role_id FROM roles WHERE name = 'admin' LIMIT 1;
|
||||
|
||||
-- 为管理员角色授权
|
||||
IF menu_id IS NOT NULL AND admin_role_id IS NOT NULL THEN
|
||||
INSERT INTO role_menus (role_id, menu_id)
|
||||
VALUES (admin_role_id, menu_id)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 验证结果
|
||||
SELECT id, name, title, path, parent_id, sort_order
|
||||
FROM menus
|
||||
WHERE parent_id = 2
|
||||
ORDER BY sort_order, id;
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
"""
|
||||
Fetch Interstellar Data (Nearby Stars & Exoplanets)
|
||||
Phase 3: Interstellar Expansion
|
||||
|
||||
This script fetches data from the NASA Exoplanet Archive using astroquery.
|
||||
It retrieves the nearest stars (within 100pc) and their planetary system details.
|
||||
The data is stored in the `static_data` table with category 'interstellar'.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
from sqlalchemy import select, text, func
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
|
||||
# Add backend directory to path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.db.static_data import StaticData
|
||||
|
||||
# Try to import astroquery/astropy, handle if missing
|
||||
try:
|
||||
from astroquery.ipac.nexsci.nasa_exoplanet_archive import NasaExoplanetArchive
|
||||
from astropy.coordinates import SkyCoord
|
||||
from astropy import units as u
|
||||
except ImportError:
|
||||
print("❌ Error: astroquery or astropy not installed.")
|
||||
print(" Please run: pip install astroquery astropy")
|
||||
sys.exit(1)
|
||||
|
||||
async def fetch_and_store_interstellar_data():
|
||||
print("🌌 Fetching Interstellar Data (Phase 3)...")
|
||||
|
||||
# 1. Query NASA Exoplanet Archive
|
||||
# We query the Planetary Systems (PS) table
|
||||
# sy_dist: System Distance [pc]
|
||||
# ra, dec: Coordinates [deg]
|
||||
# sy_pnum: Number of Planets
|
||||
# st_spectype: Spectral Type
|
||||
# st_rad: Stellar Radius [Solar Radii]
|
||||
# st_mass: Stellar Mass [Solar Mass]
|
||||
# st_teff: Effective Temperature [K]
|
||||
# pl_name: Planet Name
|
||||
# pl_orbsmax: Semi-Major Axis [AU]
|
||||
# pl_orbper: Orbital Period [days]
|
||||
# pl_orbeccen: Eccentricity
|
||||
# pl_rade: Planet Radius [Earth Radii]
|
||||
|
||||
print(" Querying NASA Exoplanet Archive (this may take a while)...")
|
||||
try:
|
||||
# We fetch systems within 100 parsecs
|
||||
table = NasaExoplanetArchive.query_criteria(
|
||||
table="ps",
|
||||
select="hostname, sy_dist, ra, dec, sy_pnum, st_spectype, st_rad, st_mass, st_teff, pl_name, pl_orbsmax, pl_orbper, pl_orbeccen, pl_rade, pl_eqt",
|
||||
where="sy_dist < 50", # Limit to 50pc for initial Phase 3 to keep it fast and relevant
|
||||
order="sy_dist"
|
||||
)
|
||||
print(f" ✅ Fetched {len(table)} records.")
|
||||
except Exception as e:
|
||||
print(f" ❌ Query failed: {e}")
|
||||
return
|
||||
|
||||
# 2. Process Data
|
||||
# We need to group planets by host star
|
||||
systems = {}
|
||||
|
||||
print(" Processing data...")
|
||||
for row in table:
|
||||
hostname = str(row['hostname'])
|
||||
|
||||
# Helper function to safely get value from potential Quantity object
|
||||
def get_val(obj):
|
||||
if hasattr(obj, 'value'):
|
||||
return obj.value
|
||||
return obj
|
||||
|
||||
if hostname not in systems:
|
||||
# Coordinate conversion: Spherical (RA/Dec/Dist) -> Cartesian (X/Y/Z)
|
||||
dist_pc = float(get_val(row['sy_dist']))
|
||||
ra_deg = float(get_val(row['ra']))
|
||||
dec_deg = float(get_val(row['dec']))
|
||||
|
||||
# Convert to Cartesian (X, Y, Z) in Parsecs
|
||||
# Z is up (towards North Celestial Pole?) - Standard Astropy conversion
|
||||
c = SkyCoord(ra=ra_deg*u.deg, dec=dec_deg*u.deg, distance=dist_pc*u.pc)
|
||||
x = c.cartesian.x.value
|
||||
y = c.cartesian.y.value
|
||||
z = c.cartesian.z.value
|
||||
|
||||
# Determine color based on Spectral Type (simplified)
|
||||
spectype = str(row['st_spectype']) if row['st_spectype'] else 'G'
|
||||
color = '#FFFFFF' # Default
|
||||
if 'O' in spectype: color = '#9db4ff'
|
||||
elif 'B' in spectype: color = '#aabfff'
|
||||
elif 'A' in spectype: color = '#cad8ff'
|
||||
elif 'F' in spectype: color = '#fbf8ff'
|
||||
elif 'G' in spectype: color = '#fff4e8'
|
||||
elif 'K' in spectype: color = '#ffddb4'
|
||||
elif 'M' in spectype: color = '#ffbd6f'
|
||||
|
||||
systems[hostname] = {
|
||||
"category": "interstellar",
|
||||
"name": hostname,
|
||||
"name_zh": hostname, # Placeholder, maybe need translation map later
|
||||
"data": {
|
||||
"distance_pc": dist_pc,
|
||||
"ra": ra_deg,
|
||||
"dec": dec_deg,
|
||||
"position": {"x": x, "y": y, "z": z},
|
||||
"spectral_type": spectype,
|
||||
"radius_solar": float(get_val(row['st_rad'])) if get_val(row['st_rad']) is not None else 1.0,
|
||||
"mass_solar": float(get_val(row['st_mass'])) if get_val(row['st_mass']) is not None else 1.0,
|
||||
"temperature_k": float(get_val(row['st_teff'])) if get_val(row['st_teff']) is not None else 5700,
|
||||
"planet_count": int(get_val(row['sy_pnum'])),
|
||||
"color": color,
|
||||
"planets": []
|
||||
}
|
||||
}
|
||||
|
||||
# Add planet info
|
||||
planet = {
|
||||
"name": str(row['pl_name']),
|
||||
"semi_major_axis_au": float(get_val(row['pl_orbsmax'])) if get_val(row['pl_orbsmax']) is not None else 0.0,
|
||||
"period_days": float(get_val(row['pl_orbper'])) if get_val(row['pl_orbper']) is not None else 0.0,
|
||||
"eccentricity": float(get_val(row['pl_orbeccen'])) if get_val(row['pl_orbeccen']) is not None else 0.0,
|
||||
"radius_earth": float(get_val(row['pl_rade'])) if get_val(row['pl_rade']) is not None else 1.0,
|
||||
"temperature_k": float(get_val(row['pl_eqt'])) if get_val(row['pl_eqt']) is not None else None
|
||||
}
|
||||
systems[hostname]["data"]["planets"].append(planet)
|
||||
|
||||
print(f" Processed {len(systems)} unique star systems.")
|
||||
|
||||
# 3. Store in Database
|
||||
print(" Storing in database...")
|
||||
|
||||
# Helper to clean NaN values for JSON compatibility
|
||||
def clean_nan(obj):
|
||||
if isinstance(obj, float):
|
||||
return None if math.isnan(obj) else obj
|
||||
elif isinstance(obj, dict):
|
||||
return {k: clean_nan(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [clean_nan(v) for v in obj]
|
||||
return obj
|
||||
|
||||
async for session in get_db():
|
||||
try:
|
||||
count = 0
|
||||
for hostname, info in systems.items():
|
||||
# Clean data
|
||||
cleaned_data = clean_nan(info["data"])
|
||||
|
||||
# Use UPSERT
|
||||
stmt = insert(StaticData).values(
|
||||
category=info["category"],
|
||||
name=info["name"],
|
||||
name_zh=info["name_zh"],
|
||||
data=cleaned_data
|
||||
).on_conflict_do_update(
|
||||
constraint="uq_category_name",
|
||||
set_={"data": cleaned_data, "updated_at": func.now()}
|
||||
)
|
||||
await session.execute(stmt)
|
||||
count += 1
|
||||
|
||||
await session.commit()
|
||||
print(f" ✅ Successfully stored {count} interstellar systems.")
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
print(f" ❌ Database error: {e}")
|
||||
finally:
|
||||
break
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(fetch_and_store_interstellar_data())
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
迁移 static_data 中的 interstellar 数据到 star_systems 和 celestial_bodies 表
|
||||
包含自动中文名翻译功能
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from sqlalchemy import select, func, update
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models.db.static_data import StaticData
|
||||
from app.models.db.star_system import StarSystem
|
||||
from app.models.db.celestial_body import CelestialBody
|
||||
|
||||
|
||||
# 恒星名称中文翻译字典(常见恒星)
|
||||
STAR_NAME_ZH = {
|
||||
'Proxima Cen': '比邻星',
|
||||
"Barnard's star": '巴纳德星',
|
||||
'eps Eri': '天苑四',
|
||||
'Lalande 21185': '莱兰21185',
|
||||
'61 Cyg A': '天鹅座61 A',
|
||||
'61 Cyg B': '天鹅座61 B',
|
||||
'tau Cet': '天仓五',
|
||||
'Kapteyn': '开普敦星',
|
||||
'Lacaille 9352': '拉卡伊9352',
|
||||
'Ross 128': '罗斯128',
|
||||
'Wolf 359': '狼359',
|
||||
'Sirius': '天狼星',
|
||||
'Alpha Centauri': '南门二',
|
||||
'TRAPPIST-1': 'TRAPPIST-1',
|
||||
'Kepler-442': '开普勒-442',
|
||||
'Kepler-452': '开普勒-452',
|
||||
'Gliese 581': '格利泽581',
|
||||
'Gliese 667C': '格利泽667C',
|
||||
'HD 40307': 'HD 40307',
|
||||
}
|
||||
|
||||
# 常见恒星系后缀翻译
|
||||
SYSTEM_SUFFIX_ZH = {
|
||||
'System': '系统',
|
||||
'system': '系统',
|
||||
}
|
||||
|
||||
|
||||
def translate_star_name(english_name: str) -> str:
|
||||
"""
|
||||
翻译恒星名称为中文
|
||||
优先使用字典,否则保留英文名
|
||||
"""
|
||||
# 直接匹配
|
||||
if english_name in STAR_NAME_ZH:
|
||||
return STAR_NAME_ZH[english_name]
|
||||
|
||||
# 移除常见后缀尝试匹配
|
||||
base_name = english_name.replace(' A', '').replace(' B', '').replace(' C', '').strip()
|
||||
if base_name in STAR_NAME_ZH:
|
||||
suffix = english_name.replace(base_name, '').strip()
|
||||
return STAR_NAME_ZH[base_name] + suffix
|
||||
|
||||
# Kepler/TRAPPIST 等编号星
|
||||
if english_name.startswith('Kepler-'):
|
||||
return f'开普勒-{english_name.split("-")[1]}'
|
||||
if english_name.startswith('TRAPPIST-'):
|
||||
return f'TRAPPIST-{english_name.split("-")[1]}'
|
||||
if english_name.startswith('Gliese '):
|
||||
return f'格利泽{english_name.split(" ")[1]}'
|
||||
if english_name.startswith('GJ '):
|
||||
return f'GJ {english_name.split(" ")[1]}'
|
||||
if english_name.startswith('HD '):
|
||||
return f'HD {english_name.split(" ")[1]}'
|
||||
if english_name.startswith('HIP '):
|
||||
return f'HIP {english_name.split(" ")[1]}'
|
||||
|
||||
# 默认返回英文名
|
||||
return english_name
|
||||
|
||||
|
||||
def translate_system_name(english_name: str) -> str:
|
||||
"""翻译恒星系名称"""
|
||||
if ' System' in english_name:
|
||||
star_name = english_name.replace(' System', '').strip()
|
||||
star_name_zh = translate_star_name(star_name)
|
||||
return f'{star_name_zh}系统'
|
||||
return translate_star_name(english_name)
|
||||
|
||||
|
||||
def translate_planet_name(english_name: str) -> str:
|
||||
"""
|
||||
翻译系外行星名称
|
||||
格式:恒星名 + 行星字母
|
||||
"""
|
||||
# 分离恒星名和行星字母
|
||||
parts = english_name.rsplit(' ', 1)
|
||||
if len(parts) == 2:
|
||||
star_name, planet_letter = parts
|
||||
star_name_zh = translate_star_name(star_name)
|
||||
return f'{star_name_zh} {planet_letter}'
|
||||
return english_name
|
||||
|
||||
|
||||
async def deduplicate_planets(planets: list) -> list:
|
||||
"""
|
||||
去除重复的行星记录
|
||||
保留字段最完整的记录
|
||||
"""
|
||||
if not planets:
|
||||
return []
|
||||
|
||||
planet_map = {}
|
||||
for planet in planets:
|
||||
name = planet.get('name', '')
|
||||
if not name:
|
||||
continue
|
||||
|
||||
if name not in planet_map:
|
||||
planet_map[name] = planet
|
||||
else:
|
||||
# 比较字段完整度
|
||||
existing = planet_map[name]
|
||||
existing_fields = sum(1 for v in existing.values() if v is not None and v != '')
|
||||
current_fields = sum(1 for v in planet.values() if v is not None and v != '')
|
||||
|
||||
if current_fields > existing_fields:
|
||||
planet_map[name] = planet
|
||||
|
||||
return list(planet_map.values())
|
||||
|
||||
|
||||
async def migrate_star_systems():
|
||||
"""迁移恒星系统数据"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
print("=" * 60)
|
||||
print("开始迁移系外恒星系数据...")
|
||||
print("=" * 60)
|
||||
|
||||
# 读取所有 interstellar 数据
|
||||
result = await session.execute(
|
||||
select(StaticData)
|
||||
.where(StaticData.category == 'interstellar')
|
||||
.order_by(StaticData.name)
|
||||
)
|
||||
interstellar_data = result.scalars().all()
|
||||
|
||||
print(f"\n📊 共找到 {len(interstellar_data)} 个恒星系统")
|
||||
|
||||
migrated_systems = 0
|
||||
migrated_planets = 0
|
||||
skipped_systems = 0
|
||||
|
||||
for star_data in interstellar_data:
|
||||
try:
|
||||
data = star_data.data
|
||||
star_name = star_data.name
|
||||
|
||||
# 翻译中文名
|
||||
star_name_zh = translate_star_name(star_name)
|
||||
system_name = f"{star_name} System"
|
||||
system_name_zh = translate_system_name(system_name)
|
||||
|
||||
# 创建恒星系统记录
|
||||
system = StarSystem(
|
||||
name=system_name,
|
||||
name_zh=system_name_zh,
|
||||
host_star_name=star_name,
|
||||
distance_pc=data.get('distance_pc'),
|
||||
distance_ly=data.get('distance_ly'),
|
||||
ra=data.get('ra'),
|
||||
dec=data.get('dec'),
|
||||
position_x=data.get('position', {}).get('x') if 'position' in data else None,
|
||||
position_y=data.get('position', {}).get('y') if 'position' in data else None,
|
||||
position_z=data.get('position', {}).get('z') if 'position' in data else None,
|
||||
spectral_type=data.get('spectral_type'),
|
||||
radius_solar=data.get('radius_solar'),
|
||||
mass_solar=data.get('mass_solar'),
|
||||
temperature_k=data.get('temperature_k'),
|
||||
magnitude=data.get('magnitude'),
|
||||
color=data.get('color', '#FFFFFF'),
|
||||
planet_count=0, # 将在迁移行星后更新
|
||||
description=f"距离地球 {data.get('distance_ly', 0):.2f} 光年的恒星系统。"
|
||||
)
|
||||
|
||||
session.add(system)
|
||||
await session.flush() # 获取 system.id
|
||||
|
||||
print(f"\n✅ 恒星系: {system_name} ({system_name_zh})")
|
||||
print(f" 距离: {data.get('distance_pc', 0):.2f} pc (~{data.get('distance_ly', 0):.2f} ly)")
|
||||
|
||||
# 处理行星数据
|
||||
planets = data.get('planets', [])
|
||||
if planets:
|
||||
# 去重
|
||||
unique_planets = await deduplicate_planets(planets)
|
||||
print(f" 行星: {len(planets)} 条记录 → {len(unique_planets)} 颗独立行星(去重 {len(planets) - len(unique_planets)} 条)")
|
||||
|
||||
# 迁移行星
|
||||
for planet_data in unique_planets:
|
||||
planet_name = planet_data.get('name', '')
|
||||
if not planet_name:
|
||||
continue
|
||||
|
||||
planet_name_zh = translate_planet_name(planet_name)
|
||||
|
||||
# 创建系外行星记录
|
||||
planet = CelestialBody(
|
||||
id=f"exo-{system.id}-{planet_name.replace(' ', '-')}", # 生成唯一ID
|
||||
name=planet_name,
|
||||
name_zh=planet_name_zh,
|
||||
type='planet',
|
||||
system_id=system.id,
|
||||
description=f"{system_name_zh}的系外行星。",
|
||||
extra_data={
|
||||
'semi_major_axis_au': planet_data.get('semi_major_axis_au'),
|
||||
'period_days': planet_data.get('period_days'),
|
||||
'eccentricity': planet_data.get('eccentricity'),
|
||||
'radius_earth': planet_data.get('radius_earth'),
|
||||
'mass_earth': planet_data.get('mass_earth'),
|
||||
'temperature_k': planet_data.get('temperature_k'),
|
||||
}
|
||||
)
|
||||
|
||||
session.add(planet)
|
||||
migrated_planets += 1
|
||||
print(f" • {planet_name} ({planet_name_zh})")
|
||||
|
||||
# 更新恒星系的行星数量
|
||||
system.planet_count = len(unique_planets)
|
||||
|
||||
migrated_systems += 1
|
||||
|
||||
# 每100个系统提交一次
|
||||
if migrated_systems % 100 == 0:
|
||||
await session.commit()
|
||||
print(f"\n💾 已提交 {migrated_systems} 个恒星系统...")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 错误:迁移 {star_name} 失败 - {str(e)[:200]}")
|
||||
skipped_systems += 1
|
||||
# 简单回滚,继续下一个
|
||||
try:
|
||||
await session.rollback()
|
||||
except:
|
||||
pass
|
||||
continue
|
||||
|
||||
# 最终提交
|
||||
await session.commit()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("迁移完成!")
|
||||
print("=" * 60)
|
||||
print(f"✅ 成功迁移恒星系: {migrated_systems}")
|
||||
print(f"✅ 成功迁移行星: {migrated_planets}")
|
||||
print(f"⚠️ 跳过的恒星系: {skipped_systems}")
|
||||
print(f"📊 平均每个恒星系: {migrated_planets / migrated_systems:.1f} 颗行星")
|
||||
|
||||
|
||||
async def update_solar_system_count():
|
||||
"""更新太阳系的天体数量"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(func.count(CelestialBody.id))
|
||||
.where(CelestialBody.system_id == 1)
|
||||
)
|
||||
count = result.scalar()
|
||||
|
||||
await session.execute(
|
||||
update(StarSystem)
|
||||
.where(StarSystem.id == 1)
|
||||
.values(planet_count=count - 1) # 减去太阳本身
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
print(f"\n✅ 更新太阳系天体数量: {count} (不含太阳: {count - 1})")
|
||||
|
||||
|
||||
async def verify_migration():
|
||||
"""验证迁移结果"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
print("\n" + "=" * 60)
|
||||
print("验证迁移结果...")
|
||||
print("=" * 60)
|
||||
|
||||
# 统计恒星系
|
||||
result = await session.execute(select(func.count(StarSystem.id)))
|
||||
system_count = result.scalar()
|
||||
print(f"\n📊 恒星系统总数: {system_count}")
|
||||
|
||||
# 统计各系统的行星数量
|
||||
result = await session.execute(
|
||||
select(StarSystem.name, StarSystem.name_zh, StarSystem.planet_count)
|
||||
.order_by(StarSystem.planet_count.desc())
|
||||
.limit(10)
|
||||
)
|
||||
print("\n🏆 行星最多的恒星系(前10):")
|
||||
for name, name_zh, count in result:
|
||||
print(f" {name} ({name_zh}): {count} 颗行星")
|
||||
|
||||
# 统计天体类型分布
|
||||
result = await session.execute(
|
||||
select(CelestialBody.type, CelestialBody.system_id, func.count(CelestialBody.id))
|
||||
.group_by(CelestialBody.type, CelestialBody.system_id)
|
||||
.order_by(CelestialBody.system_id, CelestialBody.type)
|
||||
)
|
||||
print("\n📈 天体类型分布:")
|
||||
for type_, system_id, count in result:
|
||||
system_name = "太阳系" if system_id == 1 else f"系外恒星系"
|
||||
print(f" {system_name} - {type_}: {count}")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主函数"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Cosmo 系外恒星系数据迁移工具")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# 执行迁移
|
||||
await migrate_star_systems()
|
||||
|
||||
# 更新太阳系统计
|
||||
await update_solar_system_count()
|
||||
|
||||
# 验证结果
|
||||
await verify_migration()
|
||||
|
||||
print("\n✅ 所有操作完成!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 迁移失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
"""
|
||||
Populate Primary Stars for Star Systems
|
||||
Phase 4.1: Data Migration
|
||||
|
||||
This script creates primary star records in celestial_bodies table
|
||||
for all star systems in the star_systems table.
|
||||
|
||||
It does NOT fetch new data from NASA - all data already exists in star_systems.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Add backend to path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from sqlalchemy import text
|
||||
from app.database import get_db
|
||||
|
||||
async def populate_primary_stars():
|
||||
"""Create primary star records for all star systems"""
|
||||
|
||||
print("=" * 70)
|
||||
print("🌟 Phase 4.1: Populate Primary Stars")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
async for session in get_db():
|
||||
try:
|
||||
# Step 1: Check current status
|
||||
print("📊 Step 1: Checking current status...")
|
||||
|
||||
result = await session.execute(text(
|
||||
"SELECT COUNT(*) FROM star_systems"
|
||||
))
|
||||
total_systems = result.scalar()
|
||||
print(f" Total star systems: {total_systems}")
|
||||
|
||||
result = await session.execute(text(
|
||||
"SELECT COUNT(*) FROM celestial_bodies WHERE type = 'star'"
|
||||
))
|
||||
existing_stars = result.scalar()
|
||||
print(f" Existing stars in celestial_bodies: {existing_stars}")
|
||||
print()
|
||||
|
||||
# Step 2: Fetch all star systems
|
||||
print("📥 Step 2: Fetching star systems data...")
|
||||
|
||||
result = await session.execute(text("""
|
||||
SELECT
|
||||
id, name, name_zh, host_star_name,
|
||||
spectral_type, radius_solar, mass_solar,
|
||||
temperature_k, luminosity_solar, color,
|
||||
description
|
||||
FROM star_systems
|
||||
ORDER BY id
|
||||
"""))
|
||||
systems = result.fetchall()
|
||||
print(f" Fetched {len(systems)} star systems")
|
||||
print()
|
||||
|
||||
# Step 3: Create primary star records
|
||||
print("✨ Step 3: Creating primary star records...")
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for system in systems:
|
||||
star_id = f"star-{system.id}-primary"
|
||||
|
||||
# Derive star name from system info
|
||||
# Remove "System" suffix from name_zh if present
|
||||
star_name_zh = system.name_zh
|
||||
if star_name_zh:
|
||||
star_name_zh = star_name_zh.replace('系统', '').replace('System', '').strip()
|
||||
|
||||
# Create metadata JSON
|
||||
metadata = {
|
||||
"star_role": "primary",
|
||||
"spectral_type": system.spectral_type,
|
||||
"radius_solar": system.radius_solar,
|
||||
"mass_solar": system.mass_solar,
|
||||
"temperature_k": system.temperature_k,
|
||||
"luminosity_solar": system.luminosity_solar,
|
||||
"color": system.color
|
||||
}
|
||||
|
||||
# Description
|
||||
description = f"光谱类型: {system.spectral_type or 'Unknown'}"
|
||||
if system.temperature_k:
|
||||
description += f", 表面温度: {int(system.temperature_k)}K"
|
||||
|
||||
# Convert metadata to JSON string
|
||||
metadata_json = json.dumps(metadata)
|
||||
|
||||
# Check if star already exists
|
||||
check_result = await session.execute(
|
||||
text("SELECT id FROM celestial_bodies WHERE id = :star_id").bindparams(star_id=star_id)
|
||||
)
|
||||
existing = check_result.fetchone()
|
||||
|
||||
if existing:
|
||||
# Update existing record
|
||||
await session.execute(
|
||||
text("""
|
||||
UPDATE celestial_bodies
|
||||
SET name = :name,
|
||||
name_zh = :name_zh,
|
||||
type = 'star',
|
||||
description = :description,
|
||||
extra_data = CAST(:extra_data AS jsonb),
|
||||
updated_at = NOW()
|
||||
WHERE id = :star_id
|
||||
""").bindparams(
|
||||
name=system.host_star_name,
|
||||
name_zh=star_name_zh,
|
||||
description=description,
|
||||
extra_data=metadata_json,
|
||||
star_id=star_id
|
||||
)
|
||||
)
|
||||
updated_count += 1
|
||||
else:
|
||||
# Insert new record
|
||||
await session.execute(
|
||||
text("""
|
||||
INSERT INTO celestial_bodies (
|
||||
id, system_id, name, name_zh, type,
|
||||
description, extra_data, is_active,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:star_id, :system_id, :name, :name_zh, 'star',
|
||||
:description, CAST(:extra_data AS jsonb), TRUE,
|
||||
NOW(), NOW()
|
||||
)
|
||||
""").bindparams(
|
||||
star_id=star_id,
|
||||
system_id=system.id,
|
||||
name=system.host_star_name,
|
||||
name_zh=star_name_zh,
|
||||
description=description,
|
||||
extra_data=metadata_json
|
||||
)
|
||||
)
|
||||
created_count += 1
|
||||
|
||||
# Progress indicator
|
||||
if (created_count + updated_count) % 50 == 0:
|
||||
print(f" Progress: {created_count + updated_count}/{len(systems)}")
|
||||
|
||||
await session.commit()
|
||||
|
||||
print(f" ✅ Created: {created_count}")
|
||||
print(f" 🔄 Updated: {updated_count}")
|
||||
print(f" ⏭️ Skipped: {skipped_count}")
|
||||
print()
|
||||
|
||||
# Step 4: Create default positions (0, 0, 0) for all primary stars
|
||||
print("📍 Step 4: Creating default positions...")
|
||||
|
||||
# First, check which stars don't have positions
|
||||
result = await session.execute(text("""
|
||||
SELECT cb.id
|
||||
FROM celestial_bodies cb
|
||||
WHERE cb.type = 'star'
|
||||
AND cb.id LIKE 'star-%-primary'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM positions p WHERE p.body_id = cb.id
|
||||
)
|
||||
"""))
|
||||
stars_without_positions = result.fetchall()
|
||||
|
||||
print(f" Stars without positions: {len(stars_without_positions)}")
|
||||
|
||||
position_count = 0
|
||||
for star_row in stars_without_positions:
|
||||
star_id = star_row.id
|
||||
|
||||
# Create position at (0, 0, 0) - center of the system
|
||||
await session.execute(
|
||||
text("""
|
||||
INSERT INTO positions (
|
||||
body_id, time, x, y, z,
|
||||
vx, vy, vz, source, created_at
|
||||
) VALUES (
|
||||
:body_id, NOW(), 0, 0, 0,
|
||||
0, 0, 0, 'calculated', NOW()
|
||||
)
|
||||
""").bindparams(body_id=star_id)
|
||||
)
|
||||
position_count += 1
|
||||
|
||||
await session.commit()
|
||||
|
||||
print(f" ✅ Created {position_count} position records")
|
||||
print()
|
||||
|
||||
# Step 5: Verification
|
||||
print("🔍 Step 5: Verification...")
|
||||
|
||||
# Count stars by system
|
||||
result = await session.execute(text("""
|
||||
SELECT
|
||||
COUNT(DISTINCT cb.system_id) as systems_with_stars,
|
||||
COUNT(*) as total_stars
|
||||
FROM celestial_bodies cb
|
||||
WHERE cb.type = 'star' AND cb.id LIKE 'star-%-primary'
|
||||
"""))
|
||||
verification = result.fetchone()
|
||||
|
||||
print(f" Systems with primary stars: {verification.systems_with_stars}/{total_systems}")
|
||||
print(f" Total primary star records: {verification.total_stars}")
|
||||
|
||||
# Check for systems without stars
|
||||
result = await session.execute(text("""
|
||||
SELECT ss.id, ss.name
|
||||
FROM star_systems ss
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM celestial_bodies cb
|
||||
WHERE cb.system_id = ss.id AND cb.type = 'star'
|
||||
)
|
||||
LIMIT 5
|
||||
"""))
|
||||
missing_stars = result.fetchall()
|
||||
|
||||
if missing_stars:
|
||||
print(f" ⚠️ Systems without stars: {len(missing_stars)}")
|
||||
for sys in missing_stars[:5]:
|
||||
print(f" - {sys.name} (ID: {sys.id})")
|
||||
else:
|
||||
print(f" ✅ All systems have primary stars!")
|
||||
|
||||
print()
|
||||
|
||||
# Step 6: Sample data check
|
||||
print("📋 Step 6: Sample data check...")
|
||||
|
||||
result = await session.execute(text("""
|
||||
SELECT
|
||||
cb.id, cb.name, cb.name_zh, cb.extra_data,
|
||||
ss.name as system_name
|
||||
FROM celestial_bodies cb
|
||||
JOIN star_systems ss ON cb.system_id = ss.id
|
||||
WHERE cb.type = 'star' AND cb.id LIKE 'star-%-primary'
|
||||
ORDER BY ss.distance_pc
|
||||
LIMIT 5
|
||||
"""))
|
||||
samples = result.fetchall()
|
||||
|
||||
print(" Nearest star systems:")
|
||||
for sample in samples:
|
||||
print(f" • {sample.name} ({sample.name_zh})")
|
||||
print(f" System: {sample.system_name}")
|
||||
print(f" Extra Data: {sample.extra_data}")
|
||||
print()
|
||||
|
||||
print("=" * 70)
|
||||
print("✅ Phase 4.1 Completed Successfully!")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print(f"Summary:")
|
||||
print(f" • Total star systems: {total_systems}")
|
||||
print(f" • Primary stars created: {created_count}")
|
||||
print(f" • Primary stars updated: {updated_count}")
|
||||
print(f" • Positions created: {position_count}")
|
||||
print(f" • Coverage: {verification.systems_with_stars}/{total_systems} systems")
|
||||
print()
|
||||
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
print(f"\n❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(populate_primary_stars())
|
||||
|
|
@ -1,6 +1,17 @@
|
|||
-- Remove the old constraint
|
||||
-- Update check constraint for static_data table to include 'interstellar'
|
||||
-- Run this manually via: python backend/scripts/run_sql.py backend/scripts/update_category_constraint.sql
|
||||
|
||||
ALTER TABLE static_data DROP CONSTRAINT IF EXISTS chk_category;
|
||||
|
||||
-- Add the updated constraint
|
||||
ALTER TABLE static_data ADD CONSTRAINT chk_category
|
||||
CHECK (category IN ('constellation', 'galaxy', 'star', 'nebula', 'cluster', 'asteroid_belt', 'kuiper_belt'));
|
||||
ALTER TABLE static_data
|
||||
ADD CONSTRAINT chk_category
|
||||
CHECK (category IN (
|
||||
'constellation',
|
||||
'galaxy',
|
||||
'star',
|
||||
'nebula',
|
||||
'cluster',
|
||||
'asteroid_belt',
|
||||
'kuiper_belt',
|
||||
'interstellar'
|
||||
));
|
||||
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 997 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 85 KiB |
|
|
@ -75,7 +75,7 @@ export function BodyDetailOverlay({ bodyId, preloadedData, onClose }: BodyDetail
|
|||
{loading ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-blue-300">加载中...</div>
|
||||
) : (
|
||||
<Canvas camera={{ position: [0, 0, 5], fov: 50 }}>
|
||||
<Canvas camera={{ position: [1, -0.3, 5], fov: 50 }}>
|
||||
<ambientLight intensity={0.6} />
|
||||
<pointLight position={[10, 10, 10]} intensity={1.2} />
|
||||
<pointLight position={[-10, -10, -10]} intensity={0.6} color="#88aaff" />
|
||||
|
|
@ -133,11 +133,91 @@ export function BodyDetailOverlay({ bodyId, preloadedData, onClose }: BodyDetail
|
|||
<Descriptions.Item label="表面温度">
|
||||
{bodyData.starSystemData.temperature_k ? `${bodyData.starSystemData.temperature_k.toFixed(0)} K` : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="天体数量">{bodyData.starSystemData.planet_count || 0}</Descriptions.Item>
|
||||
<Descriptions.Item label="天体数量">
|
||||
{bodyData.starSystemData.allBodies?.length || 0}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{/* Planet List */}
|
||||
{bodyData.starSystemData.planets && bodyData.starSystemData.planets.length > 0 && (
|
||||
{/* Celestial Bodies List - Grouped by Type */}
|
||||
{bodyData.starSystemData.allBodies && bodyData.starSystemData.allBodies.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="text-lg font-semibold mb-2 text-white">天体列表</h4>
|
||||
{(() => {
|
||||
// Group bodies by type
|
||||
const bodiesByType = bodyData.starSystemData.allBodies.reduce((acc: any, body: any) => {
|
||||
const type = body.type || 'unknown';
|
||||
if (!acc[type]) acc[type] = [];
|
||||
acc[type].push(body);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Define type order and Chinese labels
|
||||
const typeOrder = [
|
||||
{ key: 'star', label: '恒星', color: 'gold' },
|
||||
{ key: 'planet', label: '行星', color: 'blue' },
|
||||
{ key: 'dwarf_planet', label: '矮行星', color: 'cyan' },
|
||||
{ key: 'satellite', label: '卫星', color: 'purple' },
|
||||
{ key: 'comet', label: '彗星', color: 'orange' },
|
||||
{ key: 'probe', label: '探测器', color: 'green' },
|
||||
];
|
||||
|
||||
// Render by type groups
|
||||
return typeOrder.map(({ key, label, color }) => {
|
||||
const bodies = bodiesByType[key];
|
||||
if (!bodies || bodies.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div key={key} className="mb-4">
|
||||
<h5 className="text-md font-semibold text-gray-300 mb-2 flex items-center gap-2">
|
||||
<Tag color={color}>{label}</Tag>
|
||||
<span className="text-sm text-gray-400">({bodies.length})</span>
|
||||
</h5>
|
||||
<div className="space-y-2">
|
||||
{bodies.map((body: any) => (
|
||||
<div key={body.id} className="border border-gray-600 rounded p-3 bg-gray-700">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<div className="font-medium text-white">{body.name_zh || body.name}</div>
|
||||
<div className="text-xs text-gray-400">{body.id}</div>
|
||||
</div>
|
||||
<Tag color={color}>{body.type}</Tag>
|
||||
</div>
|
||||
{body.description && (
|
||||
<div className="text-sm text-gray-300 mt-2">{body.description}</div>
|
||||
)}
|
||||
{body.extra_data && (
|
||||
<div className="text-xs text-gray-400 mt-2 grid grid-cols-3 gap-2">
|
||||
{body.extra_data.semi_major_axis_au && (
|
||||
<div>半长轴: {body.extra_data.semi_major_axis_au.toFixed(4)} AU</div>
|
||||
)}
|
||||
{body.extra_data.period_days && (
|
||||
<div>周期: {body.extra_data.period_days.toFixed(2)} 天</div>
|
||||
)}
|
||||
{body.extra_data.radius_earth && (
|
||||
<div>半径: {body.extra_data.radius_earth.toFixed(2)} R⊕</div>
|
||||
)}
|
||||
{body.extra_data.mass_solar && (
|
||||
<div>质量: {body.extra_data.mass_solar.toFixed(2)} M☉</div>
|
||||
)}
|
||||
{body.extra_data.radius_solar && (
|
||||
<div>半径: {body.extra_data.radius_solar.toFixed(2)} R☉</div>
|
||||
)}
|
||||
{body.extra_data.temperature_k && (
|
||||
<div>温度: {body.extra_data.temperature_k.toFixed(0)} K</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{/* Fallback: Legacy planets field (for backward compatibility) */}
|
||||
{!bodyData.starSystemData.allBodies && bodyData.starSystemData.planets && bodyData.starSystemData.planets.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="text-lg font-semibold mb-2 text-white">天体列表</h4>
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function ControlPanel({
|
|||
return (
|
||||
<div className="absolute top-24 right-6 z-40 flex flex-col gap-3 items-end">
|
||||
{/* View Mode Toggle */}
|
||||
<button
|
||||
<button
|
||||
onClick={onToggleViewMode}
|
||||
className={buttonClass(viewMode === 'galaxy')}
|
||||
>
|
||||
|
|
@ -61,30 +61,34 @@ export function ControlPanel({
|
|||
</div>
|
||||
</button>
|
||||
|
||||
{/* Timeline Toggle */}
|
||||
<button
|
||||
onClick={onToggleTimeline}
|
||||
className={buttonClass(isTimelineMode)}
|
||||
>
|
||||
<Calendar size={20} />
|
||||
<div className={tooltipClass}>
|
||||
{isTimelineMode ? '关闭时间轴' : '开启时间轴'}
|
||||
</div>
|
||||
</button>
|
||||
{/* Timeline Toggle - Only show in Solar System mode */}
|
||||
{viewMode === 'solar' && (
|
||||
<button
|
||||
onClick={onToggleTimeline}
|
||||
className={buttonClass(isTimelineMode)}
|
||||
>
|
||||
<Calendar size={20} />
|
||||
<div className={tooltipClass}>
|
||||
{isTimelineMode ? '关闭时间轴' : '开启时间轴'}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Orbit Toggle */}
|
||||
<button
|
||||
onClick={onToggleOrbits}
|
||||
className={buttonClass(showOrbits)}
|
||||
>
|
||||
{showOrbits ? <Eye size={20} /> : <EyeOff size={20} />}
|
||||
<div className={tooltipClass}>
|
||||
{showOrbits ? '隐藏轨道' : '显示轨道'}
|
||||
</div>
|
||||
</button>
|
||||
{/* Orbit Toggle - Only show in Solar System mode */}
|
||||
{viewMode === 'solar' && (
|
||||
<button
|
||||
onClick={onToggleOrbits}
|
||||
className={buttonClass(showOrbits)}
|
||||
>
|
||||
{showOrbits ? <Eye size={20} /> : <EyeOff size={20} />}
|
||||
<div className={tooltipClass}>
|
||||
{showOrbits ? '隐藏轨道' : '显示轨道'}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Sound Toggle */}
|
||||
<button
|
||||
<button
|
||||
onClick={onToggleSound}
|
||||
className={buttonClass(isSoundOn)}
|
||||
>
|
||||
|
|
@ -95,7 +99,7 @@ export function ControlPanel({
|
|||
</button>
|
||||
|
||||
{/* Message Board Toggle */}
|
||||
<button
|
||||
<button
|
||||
onClick={onToggleMessageBoard}
|
||||
className={buttonClass(showMessageBoard)}
|
||||
>
|
||||
|
|
@ -106,13 +110,13 @@ export function ControlPanel({
|
|||
</button>
|
||||
|
||||
{/* Screenshot Button */}
|
||||
<button
|
||||
<button
|
||||
onClick={onScreenshot}
|
||||
className="p-2 rounded-lg bg-white/10 text-gray-300 hover:bg-white/20 border border-white/5 transition-all duration-200 relative group"
|
||||
>
|
||||
<Camera size={20} />
|
||||
<div className={tooltipClass}>
|
||||
拍摄宇宙快照
|
||||
{viewMode === 'solar' ? '拍摄宇宙快照' : '拍摄银河快照'}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,38 +27,67 @@ interface StarSystem {
|
|||
|
||||
// Camera Animation Component
|
||||
function CameraAnimator({ targetPosition }: { targetPosition: [number, number, number] | null }) {
|
||||
const { camera } = useThree();
|
||||
const { camera, controls } = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetPosition) return;
|
||||
|
||||
const [x, y, z] = targetPosition;
|
||||
const distance = 15; // 拉近距离,从50改为15
|
||||
|
||||
// 计算相机位置(在目标前方一定距离)
|
||||
const cameraX = x + distance;
|
||||
const cameraY = y + distance / 2;
|
||||
const cameraZ = z + distance;
|
||||
// Calculate distance from origin (Sun) to target star
|
||||
const targetDistanceFromSun = Math.sqrt(x * x + y * y + z * z);
|
||||
|
||||
// 平滑动画移动相机
|
||||
const duration = 2000; // 2秒动画
|
||||
// Dynamic camera pull back - camera should be BEHIND the target (away from Sun)
|
||||
// For close stars (< 500 units): pull back 150 units behind the target
|
||||
// For far stars (> 500 units): pull back proportionally more
|
||||
const basePullBack = 150;
|
||||
const pullBackDistance = targetDistanceFromSun < 500
|
||||
? basePullBack
|
||||
: basePullBack + (targetDistanceFromSun - 500) * 0.08;
|
||||
|
||||
// Calculate direction vector from Sun (origin) to target star
|
||||
const dirX = x / targetDistanceFromSun;
|
||||
const dirY = y / targetDistanceFromSun;
|
||||
const dirZ = z / targetDistanceFromSun;
|
||||
|
||||
// Position camera BEYOND the target (away from the Sun)
|
||||
// Camera is at: target position + direction × pullBackDistance
|
||||
// This ensures the target is between the Sun and the camera
|
||||
const cameraX = x + dirX * pullBackDistance;
|
||||
const cameraY = y + dirY * pullBackDistance + 30; // Add slight elevation
|
||||
const cameraZ = z + dirZ * pullBackDistance;
|
||||
|
||||
// Smooth animation
|
||||
const duration = 2500; // 2.5 seconds for smoother travel
|
||||
const startTime = Date.now();
|
||||
const startPos = camera.position.clone();
|
||||
|
||||
// Store initial target if controls exist
|
||||
const startTarget = (controls as any)?.target?.clone() || { x: 0, y: 0, z: 0 };
|
||||
|
||||
const animate = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// 使用easeInOutCubic缓动函数
|
||||
// easeInOutCubic
|
||||
const eased = progress < 0.5
|
||||
? 4 * progress * progress * progress
|
||||
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
||||
|
||||
// Interpolate position
|
||||
camera.position.x = startPos.x + (cameraX - startPos.x) * eased;
|
||||
camera.position.y = startPos.y + (cameraY - startPos.y) * eased;
|
||||
camera.position.z = startPos.z + (cameraZ - startPos.z) * eased;
|
||||
|
||||
camera.lookAt(x, y, z);
|
||||
// Interpolate target (focus point)
|
||||
if (controls) {
|
||||
(controls as any).target.x = startTarget.x + (x - startTarget.x) * eased;
|
||||
(controls as any).target.y = startTarget.y + (y - startTarget.y) * eased;
|
||||
(controls as any).target.z = startTarget.z + (z - startTarget.z) * eased;
|
||||
(controls as any).update();
|
||||
} else {
|
||||
camera.lookAt(x, y, z);
|
||||
}
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
|
|
@ -66,7 +95,7 @@ function CameraAnimator({ targetPosition }: { targetPosition: [number, number, n
|
|||
};
|
||||
|
||||
animate();
|
||||
}, [targetPosition, camera]);
|
||||
}, [targetPosition, camera, controls]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -77,15 +106,15 @@ export function GalaxyScene() {
|
|||
const [searchValue, setSearchValue] = useState<number | null>(null);
|
||||
const [targetPosition, setTargetPosition] = useState<[number, number, number] | null>(null);
|
||||
|
||||
// 加载恒星系统列表(包括太阳系)
|
||||
// Load star systems (including solar system)
|
||||
useEffect(() => {
|
||||
const loadStarSystems = async () => {
|
||||
try {
|
||||
const response = await request.get('/star-systems', {
|
||||
params: { limit: 1000, exclude_solar: false } // 改为false,包含太阳系
|
||||
params: { limit: 1000, exclude_solar: false }
|
||||
});
|
||||
const systems = response.data.systems || [];
|
||||
// 只保留有坐标的系统
|
||||
// Keep only systems with coordinates
|
||||
const validSystems = systems.filter((s: StarSystem) =>
|
||||
s.position_x !== null && s.position_y !== null && s.position_z !== null
|
||||
);
|
||||
|
|
@ -97,19 +126,19 @@ export function GalaxyScene() {
|
|||
loadStarSystems();
|
||||
}, []);
|
||||
|
||||
// 处理搜索选择
|
||||
// Handle search selection
|
||||
const handleSearch = useCallback((systemId: number | null) => {
|
||||
if (systemId === null || systemId === undefined) {
|
||||
// 清空时回到初始位置
|
||||
setSearchValue(null);
|
||||
setTargetPosition([0, 0, 500]); // 回到初始相机位置
|
||||
setTargetPosition([0, 0, 500]); // Reset to initial view
|
||||
return;
|
||||
}
|
||||
|
||||
const system = starSystems.find(s => s.id === systemId);
|
||||
if (system && system.position_x !== null && system.position_y !== null && system.position_z !== null) {
|
||||
// 设置目标位置,触发相机移动
|
||||
setTargetPosition([system.position_x, system.position_y, system.position_z]);
|
||||
// Set target position (scaled by 100 as per Stars.tsx logic)
|
||||
const SCALE = 100;
|
||||
setTargetPosition([system.position_x * SCALE, system.position_y * SCALE, system.position_z * SCALE]);
|
||||
setSearchValue(systemId);
|
||||
}
|
||||
}, [starSystems]);
|
||||
|
|
@ -117,16 +146,18 @@ export function GalaxyScene() {
|
|||
const handleStarClick = useCallback(async (star: any) => {
|
||||
console.log('GalaxyScene handleStarClick:', star);
|
||||
|
||||
// Fetch planets for this star system from the API
|
||||
let planets: any[] = [];
|
||||
// Fetch all celestial bodies for this star system from the API
|
||||
let allBodies: any[] = [];
|
||||
let planetCount = 0;
|
||||
if (star.rawData.id) {
|
||||
try {
|
||||
const response = await request.get(`/star-systems/${star.rawData.id}/bodies`);
|
||||
// Filter to only get planets (exclude the star itself)
|
||||
const bodies = response.data.bodies || [];
|
||||
planets = bodies.filter((b: any) => b.type === 'planet');
|
||||
// Get all bodies (stars, planets, and any other types)
|
||||
allBodies = response.data.bodies || [];
|
||||
// Count only planets for description
|
||||
planetCount = allBodies.filter((b: any) => b.type === 'planet').length;
|
||||
} catch (err) {
|
||||
console.error('Failed to load planets for star system:', err);
|
||||
console.error('Failed to load bodies for star system:', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,7 +167,7 @@ export function GalaxyScene() {
|
|||
name_zh: star.name_zh,
|
||||
type: 'star',
|
||||
positions: [],
|
||||
description: `距离地球 ${star.distance_ly?.toFixed(2) ?? 'N/A'} 光年,拥有 ${planets.length} 颗已知行星。`,
|
||||
description: `距离地球 ${star.distance_ly?.toFixed(2) ?? 'N/A'} 光年,拥有 ${allBodies.length} 个天体。`,
|
||||
is_active: true,
|
||||
starSystemData: {
|
||||
system_id: star.rawData.id,
|
||||
|
|
@ -147,8 +178,8 @@ export function GalaxyScene() {
|
|||
radius_solar: star.rawData.radius_solar,
|
||||
mass_solar: star.rawData.mass_solar,
|
||||
temperature_k: star.rawData.temperature_k,
|
||||
planet_count: planets.length,
|
||||
planets: planets,
|
||||
planet_count: planetCount,
|
||||
allBodies: allBodies, // Pass all bodies instead of just planets
|
||||
color: star.rawData.color, // Pass star color from database
|
||||
},
|
||||
};
|
||||
|
|
@ -160,40 +191,26 @@ export function GalaxyScene() {
|
|||
<div id="cosmo-galaxy-scene-container" className="w-full h-full bg-black">
|
||||
<Canvas
|
||||
camera={{
|
||||
position: [0, 0, 500], // Adjusted camera position for better initial view
|
||||
position: [0, 0, 500],
|
||||
fov: 60,
|
||||
far: 100000, // Very far clipping plane for interstellar distances
|
||||
far: 100000,
|
||||
}}
|
||||
gl={{
|
||||
alpha: false, // Disable transparency for solid black background
|
||||
antialias: true,
|
||||
preserveDrawingBuffer: true, // Required for screenshots
|
||||
}}
|
||||
onCreated={({ gl, camera }) => {
|
||||
gl.sortObjects = true;
|
||||
camera.lookAt(0, 0, 0);
|
||||
}}
|
||||
>
|
||||
{/* Ambient light for general visibility */}
|
||||
<ambientLight intensity={0.8} />
|
||||
|
||||
{/* Background Stars (Procedural, very distant) */}
|
||||
<BackgroundStars
|
||||
radius={300} // These are just visual background, not data-driven stars
|
||||
depth={60}
|
||||
count={10000}
|
||||
factor={6}
|
||||
saturation={0.5}
|
||||
fade={true}
|
||||
/>
|
||||
|
||||
{/* Data-driven Stars (Our 578 nearby systems) */}
|
||||
<BackgroundStars radius={300} depth={60} count={10000} factor={6} saturation={0.5} fade={true} />
|
||||
<Stars mode="galaxy" onStarClick={handleStarClick} />
|
||||
|
||||
{/* Deep space objects for context */}
|
||||
{/* <Constellations /> */}
|
||||
<Nebulae />
|
||||
<Galaxies />
|
||||
|
||||
{/* Camera Animation */}
|
||||
<CameraAnimator targetPosition={targetPosition} />
|
||||
|
||||
{/* Camera Controls */}
|
||||
<OrbitControls
|
||||
enablePan={true}
|
||||
enableZoom={true}
|
||||
|
|
@ -205,17 +222,17 @@ export function GalaxyScene() {
|
|||
/>
|
||||
</Canvas>
|
||||
|
||||
{/* 搜索器 - 中心位置,绿色边框 */}
|
||||
<div className="absolute top-20 left-1/2 -translate-x-1/2 pointer-events-auto" style={{ width: 500 }}>
|
||||
{/* Search Bar - Centered, Green Style, with higher z-index to avoid being blocked by star glow */}
|
||||
<div className="absolute top-20 left-1/2 -translate-x-1/2 pointer-events-auto z-50 flex flex-col items-center gap-2" style={{ width: 500 }}>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
value={searchValue}
|
||||
onChange={handleSearch}
|
||||
placeholder="50 PARSECS (~163 LY)"
|
||||
placeholder="50 秒差距(约 163 光年)" //50 PARSECS (~163 LY)
|
||||
style={{ width: '100%' }}
|
||||
size="large"
|
||||
suffixIcon={<SearchOutlined style={{ color: '#52c41a' }} />}
|
||||
suffixIcon={<SearchOutlined style={{ color: '#52c41a', fontSize: '16px' }} />}
|
||||
className="galaxy-search"
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) => {
|
||||
|
|
@ -224,7 +241,7 @@ export function GalaxyScene() {
|
|||
const searchText = input.toLowerCase();
|
||||
return (
|
||||
system.name.toLowerCase().includes(searchText) ||
|
||||
system.name_zh?.toLowerCase().includes(searchText) ||
|
||||
(system.name_zh && system.name_zh.toLowerCase().includes(searchText)) ||
|
||||
system.id.toString().includes(searchText)
|
||||
);
|
||||
}}
|
||||
|
|
@ -240,50 +257,104 @@ export function GalaxyScene() {
|
|||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.galaxy-search .ant-select-selector {
|
||||
border: 2px solid #52c41a !important;
|
||||
border-radius: 20px !important;
|
||||
background: rgba(0, 0, 0, 0.7) !important;
|
||||
color: #52c41a !important;
|
||||
padding-right: 60px !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||
border-radius: 12px !important;
|
||||
background: rgba(0, 0, 0, 0.85) !important;
|
||||
color: white !important;
|
||||
padding-right: 40px !important;
|
||||
backdrop-filter: blur(16px);
|
||||
height: 44px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.galaxy-search .ant-select-selector:hover {
|
||||
border-color: #73d13d !important;
|
||||
.galaxy-search .ant-select-selector:hover,
|
||||
.galaxy-search.ant-select-focused .ant-select-selector {
|
||||
border-color: #238636 !important;
|
||||
background: rgba(0, 0, 0, 0.9) !important;
|
||||
box-shadow: 0 0 15px rgba(35, 134, 54, 0.3) !important;
|
||||
}
|
||||
.galaxy-search .ant-select-selector input {
|
||||
color: #52c41a !important;
|
||||
color: white !important;
|
||||
height: 44px !important;
|
||||
}
|
||||
.galaxy-search .ant-select-selection-placeholder {
|
||||
color: rgba(82, 196, 26, 0.8) !important;
|
||||
font-weight: 500 !important;
|
||||
color: rgba(255, 255, 255, 0.5) !important;
|
||||
font-family: 'Monaco', 'Courier New', monospace !important;
|
||||
letter-spacing: 1px !important;
|
||||
line-height: 44px !important;
|
||||
}
|
||||
.galaxy-search .ant-select-selection-item {
|
||||
color: #fff !important;
|
||||
line-height: 44px !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
.galaxy-search .ant-select-clear {
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
color: #52c41a !important;
|
||||
right: 35px !important;
|
||||
background: rgba(255, 77, 79, 0.15) !important;
|
||||
color: rgba(255, 77, 79, 0.8) !important;
|
||||
right: 38px !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
border-radius: 50%;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -10px !important;
|
||||
font-size: 12px !important;
|
||||
border: 1px solid rgba(255, 77, 79, 0.3) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
.galaxy-search .ant-select-clear:hover {
|
||||
color: #73d13d !important;
|
||||
background: rgba(255, 77, 79, 0.3) !important;
|
||||
color: rgb(255, 77, 79) !important;
|
||||
border-color: rgba(255, 77, 79, 0.6) !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.galaxy-search .ant-select-arrow {
|
||||
color: #52c41a !important;
|
||||
right: 12px !important;
|
||||
font-size: 16px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
.galaxy-search .ant-select-arrow:hover {
|
||||
color: #73d13d !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
/* Dropdown Styles */
|
||||
.ant-select-dropdown {
|
||||
background: rgba(0, 0, 0, 0.85) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
}
|
||||
.ant-select-item {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.ant-select-item-option-active {
|
||||
background: rgba(35, 134, 54, 0.2) !important;
|
||||
color: white !important;
|
||||
}
|
||||
.ant-select-item-option-selected {
|
||||
background: rgba(35, 134, 54, 0.4) !important;
|
||||
color: white !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Body Detail Overlay for selected interstellar star */}
|
||||
<BodyDetailOverlay
|
||||
bodyId={null} // We are passing preloadedData directly
|
||||
bodyId={null}
|
||||
preloadedData={selectedStarData || undefined}
|
||||
onClose={() => setSelectedStarData(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { ChevronLeft, ChevronRight, ChevronDown, ChevronUp, Search, Globe, Rocket, Moon, Asterisk, Sparkles, Star } from 'lucide-react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { ChevronLeft, ChevronRight, ChevronDown, ChevronUp, Search, Globe, Rocket, Moon, Asterisk, Sparkles, Star, X } from 'lucide-react';
|
||||
import type { CelestialBody } from '../types';
|
||||
|
||||
interface ProbeListProps {
|
||||
|
|
@ -11,9 +11,9 @@ interface ProbeListProps {
|
|||
}
|
||||
|
||||
export function ProbeList({ probes, planets, onBodySelect, selectedBody, onResetCamera }: ProbeListProps) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(true); // 默认关闭
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [expandedGroup, setExpandedGroup] = useState<string | null>(null); // 只允许一个分组展开
|
||||
const [expandedGroup, setExpandedGroup] = useState<string | null>('planet'); // Default expand planets
|
||||
|
||||
// Auto-collapse when a body is selected (focus mode)
|
||||
useEffect(() => {
|
||||
|
|
@ -31,7 +31,9 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
|||
return Math.sqrt(pos.x ** 2 + pos.y ** 2 + pos.z ** 2);
|
||||
};
|
||||
|
||||
const processBodies = (list: CelestialBody[]) => {
|
||||
// Process and sort bodies
|
||||
const allBodies = useMemo(() => {
|
||||
const list = [...planets, ...probes];
|
||||
return list
|
||||
.filter(b => {
|
||||
// Filter out bodies without positions
|
||||
|
|
@ -39,6 +41,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
|||
return false;
|
||||
}
|
||||
// Filter by search term - include all types including stars
|
||||
if (!searchTerm) return true;
|
||||
return (b.name_zh || b.name).toLowerCase().includes(searchTerm.toLowerCase());
|
||||
})
|
||||
.map(body => ({
|
||||
|
|
@ -46,74 +49,110 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
|||
distance: calculateDistance(body)
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
};
|
||||
}, [planets, probes, searchTerm]);
|
||||
|
||||
// Group bodies by type
|
||||
const allBodies = [...planets, ...probes];
|
||||
const processedBodies = processBodies(allBodies);
|
||||
const groups = useMemo(() => {
|
||||
const starList = allBodies.filter(({ body }) => body.type === 'star');
|
||||
const planetList = allBodies.filter(({ body }) => body.type === 'planet');
|
||||
const dwarfPlanetList = allBodies.filter(({ body }) => body.type === 'dwarf_planet');
|
||||
const satelliteList = allBodies.filter(({ body }) => body.type === 'satellite');
|
||||
const probeList = allBodies.filter(({ body }) => body.type === 'probe');
|
||||
const cometList = allBodies.filter(({ body }) => body.type === 'comet');
|
||||
|
||||
const starList = processedBodies.filter(({ body }) => body.type === 'star');
|
||||
const planetList = processedBodies.filter(({ body }) => body.type === 'planet');
|
||||
const dwarfPlanetList = processedBodies.filter(({ body }) => body.type === 'dwarf_planet');
|
||||
const satelliteList = processedBodies.filter(({ body }) => body.type === 'satellite');
|
||||
const probeList = processedBodies.filter(({ body }) => body.type === 'probe');
|
||||
const cometList = processedBodies.filter(({ body }) => body.type === 'comet');
|
||||
return {
|
||||
star: starList,
|
||||
planet: planetList,
|
||||
dwarf_planet: dwarfPlanetList,
|
||||
satellite: satelliteList,
|
||||
probe: probeList,
|
||||
comet: cometList
|
||||
};
|
||||
}, [allBodies]);
|
||||
|
||||
const toggleGroup = (groupName: string) => {
|
||||
// 如果点击的是当前展开的分组,则收起;否则切换到新分组
|
||||
setExpandedGroup(prev => prev === groupName ? null : groupName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
absolute top-24 left-4 bottom-8 z-40
|
||||
transition-all duration-300 ease-in-out flex
|
||||
${isCollapsed ? 'w-12' : 'w-64'} // Adjusted width
|
||||
absolute top-24 left-6 bottom-8 z-40
|
||||
transition-all duration-300 ease-in-out flex flex-col
|
||||
${isCollapsed ? 'w-12 h-12' : 'w-72 max-h-[calc(100vh-120px)]'}
|
||||
`}
|
||||
>
|
||||
{/* Toggle Button (Attached to the side or floating when collapsed) */}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className={`
|
||||
absolute top-0 z-50 flex items-center justify-center
|
||||
w-8 h-8 rounded-full
|
||||
bg-black/80 backdrop-blur-md border border-white/10
|
||||
text-white hover:bg-[#238636] transition-all shadow-lg
|
||||
${isCollapsed ? 'left-0' : 'right-2 top-3'}
|
||||
`}
|
||||
title={isCollapsed ? "展开列表" : "收起列表"}
|
||||
>
|
||||
{isCollapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
|
||||
</button>
|
||||
|
||||
{/* Main Content Panel */}
|
||||
<div className={`
|
||||
flex-1 bg-black/80 backdrop-blur-md border border-white/10 rounded-2xl overflow-hidden flex flex-col
|
||||
transition-opacity duration-300
|
||||
${isCollapsed ? 'opacity-0 pointer-events-none' : 'opacity-100'}
|
||||
flex-1 bg-black/80 backdrop-blur-md border border-white/10 rounded-xl overflow-hidden flex flex-col shadow-2xl
|
||||
transition-all duration-300
|
||||
${isCollapsed ? 'opacity-0 pointer-events-none scale-95' : 'opacity-100 scale-100'}
|
||||
`}>
|
||||
{/* Header & Search */}
|
||||
<div className="p-4 border-b border-white/10 space-y-3">
|
||||
<div className="flex items-center justify-between text-white">
|
||||
<h2 className="font-bold text-base tracking-wide">天体导航</h2>
|
||||
<div className="flex items-center justify-between text-white pr-8">
|
||||
<h2 className="font-bold text-base tracking-wide flex items-center gap-2">
|
||||
<span className="w-1 h-4 bg-[#238636] rounded-full"></span>
|
||||
天体导航
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
onBodySelect(null);
|
||||
onResetCamera();
|
||||
}}
|
||||
className="text-xs bg-white/10 hover:bg-white/20 px-2 py-1 rounded transition-colors text-gray-300"
|
||||
className="text-[10px] bg-white/5 hover:bg-white/10 px-2 py-1 rounded text-gray-400 hover:text-white transition-colors border border-white/5"
|
||||
>
|
||||
重置视角
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={14} />
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 group-focus-within:text-[#238636] transition-colors" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索天体..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-lg pl-9 pr-3 py-2 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-[#238636]/50 transition-colors"
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
if (e.target.value && !expandedGroup) setExpandedGroup('planet');
|
||||
}}
|
||||
className="w-full bg-black/40 border border-white/10 rounded-lg pl-9 pr-8 py-2.5 text-xs text-gray-200 placeholder-gray-600 focus:outline-none focus:border-[#238636] focus:shadow-[0_0_15px_rgba(35,134,54,0.2)] transition-all duration-300 backdrop-blur-sm"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500 hover:text-white cursor-pointer transition-colors p-0.5 rounded-full hover:bg-white/10"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-2 space-y-2">
|
||||
{/* Stars Group */}
|
||||
{starList.length > 0 && (
|
||||
{groups.star.length > 0 && (
|
||||
<BodyGroup
|
||||
title="恒星"
|
||||
icon={<Star size={12} />}
|
||||
count={starList.length}
|
||||
bodies={starList}
|
||||
icon={<Star size={14} />}
|
||||
count={groups.star.length}
|
||||
bodies={groups.star}
|
||||
isExpanded={expandedGroup === 'star'}
|
||||
onToggle={() => toggleGroup('star')}
|
||||
selectedBody={selectedBody}
|
||||
|
|
@ -122,12 +161,12 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
|||
)}
|
||||
|
||||
{/* Planets Group */}
|
||||
{planetList.length > 0 && (
|
||||
{groups.planet.length > 0 && (
|
||||
<BodyGroup
|
||||
title="行星"
|
||||
icon={<Globe size={12} />}
|
||||
count={planetList.length}
|
||||
bodies={planetList}
|
||||
icon={<Globe size={14} />}
|
||||
count={groups.planet.length}
|
||||
bodies={groups.planet}
|
||||
isExpanded={expandedGroup === 'planet'}
|
||||
onToggle={() => toggleGroup('planet')}
|
||||
selectedBody={selectedBody}
|
||||
|
|
@ -136,12 +175,12 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
|||
)}
|
||||
|
||||
{/* Dwarf Planets Group */}
|
||||
{dwarfPlanetList.length > 0 && (
|
||||
{groups.dwarf_planet.length > 0 && (
|
||||
<BodyGroup
|
||||
title="矮行星"
|
||||
icon={<Asterisk size={12} />}
|
||||
count={dwarfPlanetList.length}
|
||||
bodies={dwarfPlanetList}
|
||||
icon={<Asterisk size={14} />}
|
||||
count={groups.dwarf_planet.length}
|
||||
bodies={groups.dwarf_planet}
|
||||
isExpanded={expandedGroup === 'dwarf_planet'}
|
||||
onToggle={() => toggleGroup('dwarf_planet')}
|
||||
selectedBody={selectedBody}
|
||||
|
|
@ -150,12 +189,12 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
|||
)}
|
||||
|
||||
{/* Satellites Group */}
|
||||
{satelliteList.length > 0 && (
|
||||
{groups.satellite.length > 0 && (
|
||||
<BodyGroup
|
||||
title="卫星"
|
||||
icon={<Moon size={12} />}
|
||||
count={satelliteList.length}
|
||||
bodies={satelliteList}
|
||||
icon={<Moon size={14} />}
|
||||
count={groups.satellite.length}
|
||||
bodies={groups.satellite}
|
||||
isExpanded={expandedGroup === 'satellite'}
|
||||
onToggle={() => toggleGroup('satellite')}
|
||||
selectedBody={selectedBody}
|
||||
|
|
@ -164,12 +203,12 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
|||
)}
|
||||
|
||||
{/* Probes Group */}
|
||||
{probeList.length > 0 && (
|
||||
{groups.probe.length > 0 && (
|
||||
<BodyGroup
|
||||
title="探测器"
|
||||
icon={<Rocket size={12} />}
|
||||
count={probeList.length}
|
||||
bodies={probeList}
|
||||
icon={<Rocket size={14} />}
|
||||
count={groups.probe.length}
|
||||
bodies={groups.probe}
|
||||
isExpanded={expandedGroup === 'probe'}
|
||||
onToggle={() => toggleGroup('probe')}
|
||||
selectedBody={selectedBody}
|
||||
|
|
@ -178,12 +217,12 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
|||
)}
|
||||
|
||||
{/* Comets Group */}
|
||||
{cometList.length > 0 && (
|
||||
{groups.comet.length > 0 && (
|
||||
<BodyGroup
|
||||
title="彗星"
|
||||
icon={<Sparkles size={12} />}
|
||||
count={cometList.length}
|
||||
bodies={cometList}
|
||||
icon={<Sparkles size={14} />}
|
||||
count={groups.comet.length}
|
||||
bodies={groups.comet}
|
||||
isExpanded={expandedGroup === 'comet'}
|
||||
onToggle={() => toggleGroup('comet')}
|
||||
selectedBody={selectedBody}
|
||||
|
|
@ -192,28 +231,13 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset
|
|||
)}
|
||||
|
||||
{/* No results message */}
|
||||
{processedBodies.length === 0 && (
|
||||
{allBodies.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500 text-xs">
|
||||
未找到匹配的天体
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle Button (Attached to the side) */}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className={`
|
||||
absolute top-0 ${isCollapsed ? 'left-0' : '-right-4'}
|
||||
w-8 h-8 flex items-center justify-center
|
||||
bg-black/80 backdrop-blur-md border border-white/10 rounded-full
|
||||
text-white hover:bg-[#238636] transition-all shadow-lg z-50
|
||||
${!isCollapsed && 'translate-x-1/2'}
|
||||
`}
|
||||
style={{ top: '20px' }}
|
||||
>
|
||||
{isCollapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -243,19 +267,19 @@ function BodyGroup({
|
|||
{/* Group Header */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full px-2 py-2 flex items-center justify-between hover:bg-white/5 transition-colors"
|
||||
className="w-full px-3 py-2.5 flex items-center justify-between hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-[10px] font-bold text-gray-300 uppercase tracking-wider">
|
||||
{icon}
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-gray-300 uppercase tracking-wider">
|
||||
<span className="text-[#238636]">{icon}</span>
|
||||
{title}
|
||||
<span className="text-gray-500">({count})</span>
|
||||
<span className="text-gray-500 text-[10px]">({count})</span>
|
||||
</div>
|
||||
{isExpanded ? <ChevronUp size={14} className="text-gray-400" /> : <ChevronDown size={14} className="text-gray-400" />}
|
||||
</button>
|
||||
|
||||
{/* Group Content */}
|
||||
{isExpanded && (
|
||||
<div className="px-1 pb-1 space-y-1">
|
||||
<div className="px-1 pb-1 space-y-0.5">
|
||||
{bodies.map(({ body, distance }) => (
|
||||
<BodyItem
|
||||
key={body.id}
|
||||
|
|
@ -284,27 +308,28 @@ function BodyItem({ body, distance, isSelected, onClick }: {
|
|||
onClick={isInactive ? undefined : onClick}
|
||||
disabled={isInactive}
|
||||
className={`
|
||||
w-full flex items-center justify-between p-2 rounded-lg text-left transition-all duration-200 group
|
||||
w-full flex items-center justify-between px-3 py-2 rounded-md text-left transition-all duration-200 group relative overflow-hidden
|
||||
${isSelected
|
||||
? 'bg-[#238636]/20 border border-[#238636]/50 shadow-[0_0_15px_rgba(35,134,54,0.2)]'
|
||||
? 'bg-[#238636]/20 text-white'
|
||||
: isInactive
|
||||
? 'opacity-40 cursor-not-allowed'
|
||||
: 'hover:bg-white/10 border border-transparent'
|
||||
: 'hover:bg-white/5 text-gray-400 hover:text-gray-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<div className={`text-xs font-medium ${isSelected ? 'text-[#4ade80]' : 'text-gray-200 group-hover:text-white'}`}> {/* text-sm -> text-xs */}
|
||||
{isSelected && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-[#4ade80] shadow-[0_0_8px_rgba(74,222,128,0.8)]" />
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<div className={`text-xs font-medium ${isSelected ? 'text-[#4ade80]' : ''}`}>
|
||||
{body.name_zh || body.name}
|
||||
</div>
|
||||
<div className="text-[9px] text-gray-500 font-mono"> {/* text-[10px] -> text-[9px] */}
|
||||
{distance.toFixed(2)} AU
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSelected && (
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-[#4ade80] shadow-[0_0_8px_rgba(74,222,128,0.8)] animate-pulse" />
|
||||
)}
|
||||
<div className={`text-[10px] font-mono ${isSelected ? 'text-[#4ade80]/70' : 'text-gray-600 group-hover:text-gray-500'}`}>
|
||||
{distance.toFixed(2)} AU
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ function StarObject({ star, geometry, mode, onStarClick }: {
|
|||
|
||||
// Generate label texture
|
||||
const labelTexture = useMemo(() => {
|
||||
// For galaxy mode, use a slightly different label style if needed
|
||||
// Currently reusing same generator
|
||||
return createLabelTexture(star.name_zh, null, "", "#FFFFFF");
|
||||
}, [star.name_zh]);
|
||||
// Use Chinese name if available, otherwise use English name
|
||||
const displayName = star.name_zh || star.name;
|
||||
return createLabelTexture(displayName, null, "", "#FFFFFF");
|
||||
}, [star.name_zh, star.name]);
|
||||
|
||||
// Adjust visual parameters based on mode
|
||||
const baseSize = mode === 'galaxy' ? star.size * 8 : star.size; // Make stars larger in galaxy mode
|
||||
|
|
@ -122,13 +122,14 @@ function StarObject({ star, geometry, mode, onStarClick }: {
|
|||
? new THREE.Vector3(star.position.x, star.position.y + visualSize + 2, star.position.z)
|
||||
: star.position.clone().multiplyScalar(labelOffset)
|
||||
}>
|
||||
<mesh scale={labelScale}>
|
||||
<mesh scale={labelScale} renderOrder={999}>
|
||||
<planeGeometry />
|
||||
<meshBasicMaterial
|
||||
map={labelTexture}
|
||||
transparent
|
||||
opacity={hovered ? 1.0 : (mode === 'galaxy' ? 0.6 : 1.0)} // Fully opaque on hover
|
||||
opacity={hovered ? 1.0 : (mode === 'galaxy' ? 0.85 : 1.0)}
|
||||
depthWrite={false}
|
||||
depthTest={false}
|
||||
toneMapped={false}
|
||||
/>
|
||||
</mesh>
|
||||
|
|
@ -176,10 +177,10 @@ function StarObject({ star, geometry, mode, onStarClick }: {
|
|||
<span>{star.rawData.temperature_k.toFixed(0)} K</span>
|
||||
</div>
|
||||
)}
|
||||
{star.rawData.planet_count !== undefined && (
|
||||
{star.rawData.star_count && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">行星数量:</span>
|
||||
<span className="text-green-400 font-semibold">{star.rawData.planet_count}</span>
|
||||
<span className="text-gray-400">恒星数:</span>
|
||||
<span>{star.rawData.star_count.toFixed(0)} 颗</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useCallback } from 'react';
|
||||
import html2canvas from 'html2canvas';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
|
||||
export function useScreenshot() {
|
||||
|
|
@ -7,7 +6,9 @@ export function useScreenshot() {
|
|||
|
||||
const takeScreenshot = useCallback(async (username: string = 'Explorer') => {
|
||||
// 1. Find the container that includes both the Canvas and the HTML overlays (labels)
|
||||
const element = document.getElementById('cosmo-scene-container');
|
||||
// Check for both solar system and galaxy scene containers
|
||||
const element = document.getElementById('cosmo-scene-container') ||
|
||||
document.getElementById('cosmo-galaxy-scene-container');
|
||||
if (!element) {
|
||||
console.error('Scene container not found');
|
||||
toast.error('无法找到截图区域');
|
||||
|
|
@ -17,22 +18,30 @@ export function useScreenshot() {
|
|||
const toastId = toast.info('正在生成宇宙快照...', 0);
|
||||
|
||||
try {
|
||||
// 2. Use html2canvas to capture the visual composite
|
||||
// We use a slightly lower scale if DPR is too high to save memory/performance,
|
||||
// but usually window.devicePixelRatio is fine (2 or 3).
|
||||
const capturedCanvas = await html2canvas(element, {
|
||||
backgroundColor: '#000000',
|
||||
useCORS: true, // Allow loading cross-origin images (textures)
|
||||
logging: false,
|
||||
scale: window.devicePixelRatio,
|
||||
allowTaint: true, // Needed if some textures are tainted (may block download if CORS fails)
|
||||
ignoreElements: (_el) => false,
|
||||
});
|
||||
// 2. Find and hide UI elements that shouldn't appear in screenshot
|
||||
const elementsToHide: Array<{ element: HTMLElement; originalDisplay: string }> = [];
|
||||
|
||||
// Hide search box and reset button (galaxy mode)
|
||||
const searchContainer = element.querySelector('.absolute.top-20') as HTMLElement;
|
||||
if (searchContainer) {
|
||||
elementsToHide.push({ element: searchContainer, originalDisplay: searchContainer.style.display });
|
||||
searchContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Small delay to ensure UI is hidden and rendering is complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 3. Capture the WebGL canvas directly
|
||||
// For WebGL content with preserveDrawingBuffer: true, we can directly read from canvas
|
||||
const threeCanvas = element.querySelector('canvas') as HTMLCanvasElement;
|
||||
if (!threeCanvas) {
|
||||
throw new Error('无法找到3D画布');
|
||||
}
|
||||
|
||||
// Create a canvas from the WebGL canvas data
|
||||
const width = threeCanvas.width;
|
||||
const height = threeCanvas.height;
|
||||
|
||||
// 3. Create a fresh canvas for composition to ensure clean state
|
||||
const width = capturedCanvas.width;
|
||||
const height = capturedCanvas.height;
|
||||
|
||||
const finalCanvas = document.createElement('canvas');
|
||||
finalCanvas.width = width;
|
||||
finalCanvas.height = height;
|
||||
|
|
@ -42,8 +51,17 @@ export function useScreenshot() {
|
|||
throw new Error('无法创建绘图上下文');
|
||||
}
|
||||
|
||||
// Draw the captured scene
|
||||
ctx.drawImage(capturedCanvas, 0, 0);
|
||||
// Fill with solid black background first (in case of transparency issues)
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Draw the Three.js WebGL canvas (preserveDrawingBuffer must be true)
|
||||
ctx.drawImage(threeCanvas, 0, 0);
|
||||
|
||||
// Restore hidden elements
|
||||
elementsToHide.forEach(({ element, originalDisplay }) => {
|
||||
element.style.display = originalDisplay;
|
||||
});
|
||||
|
||||
// 4. Add Overlay / Watermark
|
||||
const now = new Date();
|
||||
|
|
@ -52,12 +70,12 @@ export function useScreenshot() {
|
|||
|
||||
// Calculate dynamic font sizes based on image width (e.g., for 4k screens)
|
||||
// Base logic: width 1920 -> size 32. Ratio ~ 0.016
|
||||
const baseScale = width / 1920;
|
||||
const baseScale = width / 1920;
|
||||
const titleSize = Math.max(24, Math.floor(32 * baseScale));
|
||||
const subTitleSize = Math.max(12, Math.floor(16 * baseScale));
|
||||
const dateSize = Math.max(18, Math.floor(24 * baseScale));
|
||||
const timeSize = Math.max(14, Math.floor(18 * baseScale));
|
||||
|
||||
|
||||
// Margins
|
||||
const marginX = Math.max(20, Math.floor(40 * baseScale));
|
||||
const marginY = Math.max(20, Math.floor(35 * baseScale));
|
||||
|
|
@ -77,12 +95,12 @@ export function useScreenshot() {
|
|||
|
||||
// --- Right Side: User & App ---
|
||||
ctx.textAlign = 'right';
|
||||
|
||||
|
||||
// Subtitle (Bottom)
|
||||
ctx.font = `${subTitleSize}px sans-serif`;
|
||||
ctx.fillStyle = '#aaaaaa';
|
||||
ctx.fillText('DEEP SPACE EXPLORER', width - marginX, height - marginY);
|
||||
|
||||
|
||||
// Nickname@Cosmo (Above Subtitle)
|
||||
ctx.font = `bold ${titleSize}px sans-serif`;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
|
|
@ -108,7 +126,7 @@ export function useScreenshot() {
|
|||
link.download = `Cosmo_Snapshot_${now.toISOString().slice(0,19).replace(/[:T]/g, '-')}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
|
||||
|
||||
toast.success('宇宙快照已保存');
|
||||
|
||||
} catch (err) {
|
||||
|
|
@ -120,4 +138,4 @@ export function useScreenshot() {
|
|||
}, [toast]);
|
||||
|
||||
return { takeScreenshot };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ export function CelestialBodies() {
|
|||
>
|
||||
{starSystems.map(system => (
|
||||
<Select.Option key={system.id} value={system.id}>
|
||||
{system.name_zh || system.name} ({system.planet_count} 颗天体)
|
||||
{system.name_zh || system.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ interface StarSystem {
|
|||
luminosity_solar: number | null;
|
||||
color: string | null;
|
||||
planet_count: number;
|
||||
star_count: number; // 恒星数量
|
||||
description: string | null;
|
||||
details: string | null;
|
||||
created_at: string;
|
||||
|
|
@ -216,12 +217,14 @@ export function StarSystems() {
|
|||
),
|
||||
},
|
||||
{
|
||||
title: '行星数量',
|
||||
dataIndex: 'planet_count',
|
||||
key: 'planet_count',
|
||||
title: '恒星数量',
|
||||
dataIndex: 'star_count',
|
||||
key: 'star_count',
|
||||
width: 100,
|
||||
render: (count) => (
|
||||
<Tag color={count > 0 ? 'green' : 'default'}>{count}</Tag>
|
||||
<Tag color={count > 1 ? 'gold' : 'default'}>
|
||||
{count}颗
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -297,7 +300,7 @@ export function StarSystems() {
|
|||
open={isModalOpen}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
width={800}
|
||||
width={1200}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
>
|
||||
|
|
@ -306,33 +309,33 @@ export function StarSystems() {
|
|||
// 编辑模式:双tab
|
||||
<Tabs activeKey={activeTabKey} onChange={setActiveTabKey}>
|
||||
<Tabs.TabPane tab="基础信息" key="basic">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="系统名称"
|
||||
rules={[{ required: true, message: '请输入系统名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: Proxima Cen System" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="name_zh" label="中文名称">
|
||||
<Input placeholder="例如: 比邻星系统" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="host_star_name"
|
||||
label="主恒星名称"
|
||||
rules={[{ required: true, message: '请输入主恒星名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: Proxima Cen" />
|
||||
</Form.Item>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item name="distance_pc" label="距离 (pc)">
|
||||
<InputNumber style={{ width: '100%' }} placeholder="秒差距" step={0.01} />
|
||||
{/* 第一行:系统名称、中文名称、主恒星名称 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="系统名称"
|
||||
rules={[{ required: true, message: '请输入系统名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: Proxima Cen System" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="distance_ly" label="距离 (ly)">
|
||||
<InputNumber style={{ width: '100%' }} placeholder="光年" step={0.01} />
|
||||
<Form.Item name="name_zh" label="中文名称">
|
||||
<Input placeholder="例如: 比邻星系统" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="host_star_name"
|
||||
label="主恒星名称"
|
||||
rules={[{ required: true, message: '请输入主恒星名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: Proxima Cen" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第二行:距离、赤经、赤纬 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="distance_pc" label="距离 (pc)">
|
||||
<InputNumber style={{ width: '100%' }} placeholder="秒差距" step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="ra" label="赤经 (度)">
|
||||
|
|
@ -342,7 +345,10 @@ export function StarSystems() {
|
|||
<Form.Item name="dec" label="赤纬 (度)">
|
||||
<InputNumber style={{ width: '100%' }} min={-90} max={90} step={0.001} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第三行:X/Y/Z坐标 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="position_x" label="X坐标 (pc)">
|
||||
<InputNumber style={{ width: '100%' }} step={0.01} />
|
||||
</Form.Item>
|
||||
|
|
@ -354,7 +360,10 @@ export function StarSystems() {
|
|||
<Form.Item name="position_z" label="Z坐标 (pc)">
|
||||
<InputNumber style={{ width: '100%' }} step={0.01} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第四行:光谱类型、恒星半径、恒星质量 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="spectral_type" label="光谱类型">
|
||||
<Input placeholder="例如: M5.5 V" />
|
||||
</Form.Item>
|
||||
|
|
@ -366,7 +375,10 @@ export function StarSystems() {
|
|||
<Form.Item name="mass_solar" label="恒星质量 (M☉)">
|
||||
<InputNumber style={{ width: '100%' }} min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第五行:表面温度、视星等、光度 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="temperature_k" label="表面温度 (K)">
|
||||
<InputNumber style={{ width: '100%' }} min={0} step={100} />
|
||||
</Form.Item>
|
||||
|
|
@ -378,12 +390,20 @@ export function StarSystems() {
|
|||
<Form.Item name="luminosity_solar" label="光度 (L☉)">
|
||||
<InputNumber style={{ width: '100%' }} min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第六行:距离(ly)、显示颜色 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="distance_ly" label="距离 (ly)">
|
||||
<InputNumber style={{ width: '100%' }} placeholder="光年" step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="color" label="显示颜色">
|
||||
<Input type="color" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 描述(全宽) */}
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} placeholder="恒星系统简短描述..." />
|
||||
</Form.Item>
|
||||
|
|
@ -403,33 +423,33 @@ export function StarSystems() {
|
|||
) : (
|
||||
// 新增模式:只显示基础信息
|
||||
<>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="系统名称"
|
||||
rules={[{ required: true, message: '请输入系统名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: Proxima Cen System" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="name_zh" label="中文名称">
|
||||
<Input placeholder="例如: 比邻星系统" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="host_star_name"
|
||||
label="主恒星名称"
|
||||
rules={[{ required: true, message: '请输入主恒星名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: Proxima Cen" />
|
||||
</Form.Item>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item name="distance_pc" label="距离 (pc)">
|
||||
<InputNumber style={{ width: '100%' }} placeholder="秒差距" step={0.01} />
|
||||
{/* 第一行:系统名称、中文名称、主恒星名称 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="系统名称"
|
||||
rules={[{ required: true, message: '请输入系统名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: Proxima Cen System" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="distance_ly" label="距离 (ly)">
|
||||
<InputNumber style={{ width: '100%' }} placeholder="光年" step={0.01} />
|
||||
<Form.Item name="name_zh" label="中文名称">
|
||||
<Input placeholder="例如: 比邻星系统" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="host_star_name"
|
||||
label="主恒星名称"
|
||||
rules={[{ required: true, message: '请输入主恒星名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: Proxima Cen" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第二行:距离、赤经、赤纬 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="distance_pc" label="距离 (pc)">
|
||||
<InputNumber style={{ width: '100%' }} placeholder="秒差距" step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="ra" label="赤经 (度)">
|
||||
|
|
@ -439,7 +459,10 @@ export function StarSystems() {
|
|||
<Form.Item name="dec" label="赤纬 (度)">
|
||||
<InputNumber style={{ width: '100%' }} min={-90} max={90} step={0.001} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第三行:X/Y/Z坐标 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="position_x" label="X坐标 (pc)">
|
||||
<InputNumber style={{ width: '100%' }} step={0.01} />
|
||||
</Form.Item>
|
||||
|
|
@ -451,7 +474,10 @@ export function StarSystems() {
|
|||
<Form.Item name="position_z" label="Z坐标 (pc)">
|
||||
<InputNumber style={{ width: '100%' }} step={0.01} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第四行:光谱类型、恒星半径、恒星质量 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="spectral_type" label="光谱类型">
|
||||
<Input placeholder="例如: M5.5 V" />
|
||||
</Form.Item>
|
||||
|
|
@ -463,7 +489,10 @@ export function StarSystems() {
|
|||
<Form.Item name="mass_solar" label="恒星质量 (M☉)">
|
||||
<InputNumber style={{ width: '100%' }} min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第五行:表面温度、视星等、光度 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="temperature_k" label="表面温度 (K)">
|
||||
<InputNumber style={{ width: '100%' }} min={0} step={100} />
|
||||
</Form.Item>
|
||||
|
|
@ -475,12 +504,20 @@ export function StarSystems() {
|
|||
<Form.Item name="luminosity_solar" label="光度 (L☉)">
|
||||
<InputNumber style={{ width: '100%' }} min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 第六行:距离(ly)、显示颜色 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Form.Item name="distance_ly" label="距离 (ly)">
|
||||
<InputNumber style={{ width: '100%' }} placeholder="光年" step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="color" label="显示颜色">
|
||||
<Input type="color" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* 描述(全宽) */}
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea rows={3} placeholder="恒星系统简短描述..." />
|
||||
</Form.Item>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ export interface CelestialBody {
|
|||
mass_solar?: number;
|
||||
temperature_k?: number;
|
||||
planet_count?: number;
|
||||
planets?: any[]; // Array of planet data
|
||||
planets?: any[]; // Array of planet data (deprecated, use allBodies)
|
||||
allBodies?: any[]; // Array of all celestial bodies in this system
|
||||
color?: string; // Star color from database
|
||||
};
|
||||
}
|
||||
|
|
|
|||