3v324v23 commited on
Commit
0e8f624
·
1 Parent(s): 4e22b4d

Deploy to Hugging Face Spaces - SQLite version with 10k sample data

Browse files
Files changed (38) hide show
  1. .DS_Store +0 -0
  2. .dockerignore +56 -0
  3. DEPLOY_HF.md +87 -0
  4. Dockerfile +41 -10
  5. README.md +28 -194
  6. apps/api/__pycache__/main.cpython-314.pyc +0 -0
  7. apps/api/main.py +7 -2
  8. apps/api/main_hf.py +66 -0
  9. apps/api/routers/__pycache__/entity.cpython-314.pyc +0 -0
  10. apps/api/routers/__pycache__/export.cpython-314.pyc +0 -0
  11. apps/api/routers/__pycache__/health.cpython-314.pyc +0 -0
  12. apps/api/routers/__pycache__/trade.cpython-314.pyc +0 -0
  13. apps/api/routers/trade_hf.py +103 -0
  14. apps/api/static/index.html +6 -6
  15. apps/worker/__pycache__/export_tasks.cpython-314.pyc +0 -0
  16. apps/worker/run_backfill.py +3 -0
  17. data/standard_trade_records_sample.csv +0 -0
  18. docker-compose.yml +4 -4
  19. infrastructure/monitoring/__pycache__/alert.cpython-314.pyc +0 -0
  20. infrastructure/monitoring/__pycache__/health.cpython-314.pyc +0 -0
  21. infrastructure/monitoring/__pycache__/quality.cpython-314.pyc +0 -0
  22. init_sqlite.py +144 -0
  23. logs/app_2026-06-16.log +0 -0
  24. packages/connectors/__pycache__/base.cpython-314.pyc +0 -0
  25. packages/connectors/base.py +47 -33
  26. packages/connectors/mock/__pycache__/extended_mock.cpython-314.pyc +0 -0
  27. packages/connectors/mock/extended_mock.py +2 -2
  28. packages/core/__pycache__/entity_resolution.cpython-314.pyc +0 -0
  29. packages/core/__pycache__/http_client.cpython-314.pyc +0 -0
  30. packages/core/__pycache__/nlp.cpython-314.pyc +0 -0
  31. packages/core/__pycache__/subscription_matcher.cpython-314.pyc +0 -0
  32. packages/core/config_hf.py +29 -0
  33. packages/core/http_client.py +8 -0
  34. packages/dictionaries/__pycache__/brazil.cpython-314.pyc +0 -0
  35. packages/dictionaries/brazil.py +55 -30
  36. packages/normalizers/__pycache__/__init__.cpython-314.pyc +0 -0
  37. packages/normalizers/__pycache__/company.cpython-314.pyc +0 -0
  38. requirements.txt +3 -3
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.dockerignore ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ .venv/
8
+ venv/
9
+ ENV/
10
+ env/
11
+
12
+ # 数据和日志
13
+ logs/
14
+ *.log
15
+ data/*.db
16
+ data/*.db-journal
17
+ data/EXP_2023.csv
18
+ data/IMP_2023.csv
19
+ data/export_data.sql
20
+
21
+ # Git
22
+ .git/
23
+ .gitignore
24
+
25
+ # IDE
26
+ .vscode/
27
+ .idea/
28
+ *.swp
29
+ *.swo
30
+ .DS_Store
31
+
32
+ # 测试
33
+ .pytest_cache/
34
+ tests/
35
+ *.coverage
36
+ htmlcov/
37
+
38
+ # Docker
39
+ docker-compose.yml
40
+ Dockerfile
41
+ !Dockerfile.hf
42
+
43
+ # 文档(保留README_HF.md)
44
+ docs/
45
+ README.md
46
+ !README_HF.md
47
+ PROJECT_*.md
48
+ QUICKSTART.md
49
+
50
+ # 其他配置
51
+ .env
52
+ .env.template
53
+ infrastructure/
54
+ scripts/
55
+ check_db.py
56
+ run_test_backfill.py
DEPLOY_HF.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces部署说明
2
+
3
+ ## 📦 部署步骤
4
+
5
+ ### 1. 准备文件
6
+ 确保以下文件已准备好:
7
+ - `Dockerfile.hf` - Hugging Face Spaces专用Dockerfile
8
+ - `README_HF.md` - Hugging Face Spaces README
9
+ - `init_sqlite.py` - SQLite数据库初始化脚本
10
+ - `data/standard_trade_records_sample.csv` - 样本数据(1万条)
11
+ - `apps/api/main_hf.py` - 精简版API入口
12
+ - `packages/core/config_hf.py` - SQLite配置
13
+
14
+ ### 2. 推送到Hugging Face
15
+
16
+ ```bash
17
+ # 重命名文件以匹配Hugging Face要求
18
+ cp Dockerfile.hf Dockerfile
19
+ cp README_HF.md README.md
20
+
21
+ # 添加到git
22
+ git add Dockerfile README.md init_sqlite.py
23
+ git add data/standard_trade_records_sample.csv
24
+ git add apps/ packages/
25
+
26
+ # 提交
27
+ git commit -m "Deploy to Hugging Face Spaces"
28
+
29
+ # 推送到Hugging Face
30
+ git push hf main
31
+ ```
32
+
33
+ ### 3. 在Hugging Face配置
34
+
35
+ 1. 访问 https://huggingface.co/spaces/new-human/customs-data
36
+ 2. 确认 SDK 设置为 **Docker**
37
+ 3. 端口设置为 **7860**
38
+ 4. 等待构建完成(约5-10分钟)
39
+
40
+ ### 4. 验证部署
41
+
42
+ 访问:https://huggingface.co/spaces/new-human/customs-data
43
+
44
+ 应该能看到:
45
+ - ✅ 海关数据查询系统界面
46
+ - ✅ 显示"共找到 10000 条记录"
47
+ - ✅ 可以筛选、分页、查看数据
48
+
49
+ ## 🎯 注意事项
50
+
51
+ - **数据量**:为了快速部署,只包含1万条样本数据
52
+ - **数据库**:使用SQLite替代PostgreSQL
53
+ - **功能**:只保留核心查询功能,移除了BI、导出等高级功能
54
+ - **端口**:Hugging Face Spaces固定使用7860端口
55
+
56
+ ## 📊 技术栈
57
+
58
+ - FastAPI - Web框架
59
+ - SQLite + aiosqlite - 数据库
60
+ - SQLAlchemy - ORM
61
+ - Vue.js + Element UI - 前端
62
+ - Docker - 容器化
63
+
64
+ ## 🔧 本地测试
65
+
66
+ ```bash
67
+ # 构建镜像
68
+ docker build -f Dockerfile.hf -t customs-data-hf .
69
+
70
+ # 运行容器
71
+ docker run -p 7860:7860 customs-data-hf
72
+
73
+ # 访问
74
+ open http://localhost:7860
75
+ ```
76
+
77
+ ## 🚀 扩展数据
78
+
79
+ 如果想增加更多数据(最多29万条):
80
+
81
+ ```bash
82
+ # 修改CSV导出行数
83
+ docker exec customs_db psql -U postgres -d customs_data -c "\COPY (SELECT * FROM standard_trade_records WHERE source_country='BR') TO STDOUT WITH CSV HEADER" > data/standard_trade_records_full.csv
84
+
85
+ # 更新Dockerfile.hf中的文件名
86
+ # 重新构建和推送
87
+ ```
Dockerfile CHANGED
@@ -1,19 +1,50 @@
 
1
  FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
- # 安装必要的系统依赖(用于编译 pg 驱动等)
6
- RUN apt-get update && apt-get install -y gcc libpq-dev && rm -rf /var/lib/apt/lists/*
 
 
7
 
8
- # 安装 Python 依赖
9
- COPY requirements.txt .
10
- RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
 
 
11
 
12
- # 复制代码
13
- COPY . .
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  # 暴露端口
16
- EXPOSE 8000
17
 
18
- # 启动命令:先初始化表结构,再运行 mock 脚本和巴西的测试数据,最后启动 API 服务
19
- CMD ["bash", "-c", "python init_db.py && python apps/worker/run_mock.py && python apps/worker/run_brazil.py && uvicorn apps.api.main:app --host 0.0.0.0 --port 8000"]
 
1
+ # Hugging Face Spaces Dockerfile
2
  FROM python:3.11-slim
3
 
4
  WORKDIR /app
5
 
6
+ # 安装系统依赖
7
+ RUN apt-get update && apt-get install -y \
8
+ sqlite3 \
9
+ && rm -rf /var/lib/apt/lists/*
10
 
11
+ # 安装Python依赖(精简版)
12
+ RUN pip install --no-cache-dir \
13
+ fastapi==0.111.0 \
14
+ uvicorn[standard]==0.30.0 \
15
+ sqlalchemy==2.0.30 \
16
+ aiosqlite==0.20.0 \
17
+ pydantic==2.7.4 \
18
+ pydantic-settings==2.3.0 \
19
+ python-multipart==0.0.9
20
 
21
+ # 复制配置文件(使用HF版本)
22
+ COPY packages/core/config_hf.py ./packages/core/config.py
23
+ COPY packages/core/__init__.py ./packages/core/
24
+ COPY packages/core/models.py ./packages/core/
25
+ COPY packages/core/database.py ./packages/core/
26
+ COPY packages/core/logger.py ./packages/core/
27
+
28
+ # 复制API路由(使用精简版trade路由)
29
+ COPY apps/api/routers/__init__.py ./apps/api/routers/
30
+ COPY apps/api/routers/trade_hf.py ./apps/api/routers/trade.py
31
+ COPY apps/api/schemas/ ./apps/api/schemas/
32
+
33
+ # 复制主文件和静态文件
34
+ COPY apps/api/main_hf.py ./apps/api/main.py
35
+ COPY apps/api/static/ ./apps/api/static/
36
+
37
+ # 复制数据文件
38
+ COPY data/standard_trade_records_sample.csv ./data/
39
+
40
+ # 创建SQLite数据库脚本
41
+ COPY init_sqlite.py .
42
+
43
+ # 初始化SQLite数据库
44
+ RUN python init_sqlite.py
45
 
46
  # 暴露端口
47
+ EXPOSE 7860
48
 
49
+ # 启动命令(Hugging Face Spaces使用7860端口)
50
+ CMD ["uvicorn", "apps.api.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,208 +1,42 @@
1
  ---
2
- title: Customs Data
3
- emoji: 🏢
4
  colorFrom: blue
5
- colorTo: red
6
  sdk: docker
7
- app_port: 8000
 
8
  ---
9
 
10
- # 海关数据系统 (Customs Data)
11
 
12
- 这是一个海关/贸易数据采集、标准化、查询和分析原型项目。当前项目已经具备基础数据链路和多国连接器框架但多数国家仍处于 Mock 或本地样本阶段。真实数据闭环优先以巴西 Comex Stat 为基准推进。
13
 
14
- ## 当前状态
15
 
16
- - [x] FastAPI 查询服务
17
- - [x] PostgreSQL 原始表与标准表
18
- - [x] 连接器抽象`discover -> fetch -> parse -> save_raw -> normalize -> save_standard`
19
- - [x] 巴西 Comex Stat 官方 CSV 接入雏形
20
- - [x] Celery 任务雏形
21
- - [x] ClickHouse/Elasticsearch 部署与双写雏形
22
- - [x] **Elasticsearch 全文搜索路由**
23
- - [x] **真实告警通知渠道(飞书/钉钉/企业微信)**
24
- - [x] **完整异步导出功能(文件生成、下载、邮件通知)**
25
- - [x] **订阅邮件/Webhook 通知**
26
- - [x] **健康检查与数据一致性监控**
27
- - [x] **基线测试覆盖(19 passed)**
28
- - [ ] 多国真实数据源全量接入
29
- - [ ] 生产级权限、限流、字段脱敏
30
- - [ ] 完整 CI/CD 流程
31
 
32
- 详细功能见:`docs/开发完成总结.md`。
33
 
34
- 缺口分析见`docs/项目现状与未完成项梳理.md`。
 
 
 
35
 
36
- 国家接入状态见:`docs/国家与数据源接入清单.md`。
37
 
38
- ## 目录结构
 
 
 
 
39
 
40
- ```text
41
- apps/
42
- api/ FastAPI 服务与路由
43
- worker/ 同步、回补、导出任务
44
- packages/
45
- connectors/ 国家/地区数据源连接器
46
- core/ 数据库、配置、OLAP、搜索、实体解析等核心模块
47
- dictionaries/ 国家、运输方式、HS/NCM 等字典
48
- normalizers/ 公司名等清洗逻辑
49
- infrastructure/
50
- monitoring/ 质量检查与告警
51
- scheduler/ 定时调度与归档
52
- tests/
53
- baseline/ 连接器样本/快照回归测试
54
- docs/ 项目规划、接入清单、回补策略和运维文档
55
- ```
56
 
57
- ## 本地开发环境
58
-
59
- 建议使用 Python 3.14 或更高版本。
60
-
61
- ```bash
62
- python -m venv .venv
63
- source .venv/bin/activate
64
- pip install -r requirements.txt
65
- ```
66
-
67
- ## 启动依赖服务
68
-
69
- 使用 Docker Compose 启动 PostgreSQL、ClickHouse、Elasticsearch、Redis:
70
-
71
- ```bash
72
- docker compose up -d db clickhouse elasticsearch redis
73
- ```
74
-
75
- 如果要同时启动 API、Celery worker、Celery Beat 和 scheduler,可运行:
76
-
77
- ```bash
78
- docker compose up -d api worker celerybeat
79
- ```
80
-
81
- 如果需要一起启用 APScheduler 调度器,也可运行:
82
-
83
- ```bash
84
- docker compose up -d api worker celerybeat scheduler
85
- ```
86
-
87
- 默认端口:
88
-
89
- - PostgreSQL:`localhost:5434`
90
- - ClickHouse HTTP:`localhost:8123`
91
- - Elasticsearch:`localhost:9200`
92
- - Redis:`localhost:6379`
93
-
94
- ## 初始化数据库
95
-
96
- 注意:`init_db.py` 会 drop 并重新创建表,仅适合本地开发或空库初始化。
97
-
98
- ```bash
99
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5434/customs_data \
100
- CLICKHOUSE_URL=http://default:password@localhost:8123 \
101
- ELASTICSEARCH_URL=http://localhost:9200 \
102
- python init_db.py
103
- ```
104
-
105
- ## 启动 API
106
-
107
- ```bash
108
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5434/customs_data \
109
- CLICKHOUSE_URL=http://default:password@localhost:8123 \
110
- ELASTICSEARCH_URL=http://localhost:9200 \
111
- uvicorn apps.api.main:app --host 0.0.0.0 --port 8000 --reload
112
- ```
113
-
114
- 访问:
115
-
116
- - API 健康检查:`http://localhost:8000/api/v1/health`
117
- - Swagger 文档:`http://localhost:8000/docs`
118
- - 简易 UI:`http://localhost:8000/`
119
-
120
- ## 运行巴西真实数据同步
121
-
122
- 巴西连接器会按默认逻辑拉取上一个月的 Comex Stat 年度 CSV,并过滤对应月份。
123
-
124
- ```bash
125
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5434/customs_data \
126
- CLICKHOUSE_URL=http://default:password@localhost:8123 \
127
- ELASTICSEARCH_URL=http://localhost:9200 \
128
- python apps/worker/run_brazil.py
129
- ```
130
-
131
- ## 运行 Scheduler / Worker
132
-
133
- 项目包含两套调度机制:
134
-
135
- - `infrastructure/scheduler/main.py`:基于 APScheduler 的演示调度器,用于周期性触发 Mock/BR/Extended 数据同步。
136
- - `packages/core/celery_app.py`:基于 Celery 的任务执行入口,用于异步导出、重试任务,以及可选的 Celery Beat 计划任务。
137
-
138
- 本地运行 Celery worker:
139
-
140
- ```bash
141
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5434/customs_data \
142
- CLICKHOUSE_URL=http://default:password@localhost:8123 \
143
- ELASTICSEARCH_URL=http://localhost:9200 \
144
- CELERY_BROKER_URL=redis://localhost:6379/0 \
145
- celery -A packages.core.celery_app worker --loglevel=info
146
- ```
147
-
148
- 可选运行 Celery Beat:
149
-
150
- ```bash
151
- CELERY_BROKER_URL=redis://localhost:6379/0 \
152
- celery -A packages.core.celery_app beat --loglevel=info
153
- ```
154
-
155
- 或者直接运行独立 scheduler:
156
-
157
- ```bash
158
- python infrastructure/scheduler/main.py
159
- ```
160
-
161
- 如果本地 ClickHouse 或 Elasticsearch 没有启动,主入库仍会尝试写 PostgreSQL,但 OLAP/搜索双写会记录错误日志。
162
-
163
- ## 执行历史回补
164
-
165
- 巴西支持通过 `--local-data-dir` 指定本地 Comex Stat CSV 目录,例如目录内包含 `EXP_2025.csv`、`IMP_2025.csv`。
166
-
167
- ```bash
168
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5434/customs_data \
169
- CLICKHOUSE_URL=http://default:password@localhost:8123 \
170
- ELASTICSEARCH_URL=http://localhost:9200 \
171
- python apps/worker/run_backfill.py \
172
- --country BR \
173
- --start 2025-01-01 \
174
- --end 2025-03-31 \
175
- --local-data-dir ./data/comexstat
176
- ```
177
-
178
- 当前真实回补优先支持:
179
-
180
- - BR:巴西,较接近真实可用。
181
- - CL:智利,本地样本可跑,真实外部下载仍需验证。
182
-
183
- US、IN、VN、ID、EU 等连接器当前仍为 Mock 或占位实现,不能视为真实回补。
184
-
185
- ## 运行测试
186
-
187
- ```bash
188
- pytest
189
- ```
190
-
191
- 当前测试体系还在补齐中。优先目标是为每个真实连接器增加 sample/snapshot 基线测试。
192
-
193
- ## 新增国家连接器流程
194
-
195
- 1. 在 `packages/connectors/` 下新增国家连接器文件。
196
- 2. 继承 `BaseConnector`。
197
- 3. 实现 `discover()`、`fetch()`、`parse()`、`normalize()`。
198
- 4. 在 `packages/dictionaries/` 中补充国家、运输方式、HS/商品字典。
199
- 5. 在 `docs/国家与数据源接入清单.md` 中登记状态、数据源、合规和字段完整度。
200
- 6. 在 `tests/baseline/` 增加样本和快照测试。
201
- 7. 如支持历史回补,将连接器接入 `apps/worker/run_backfill.py`。
202
-
203
- ## 重要提醒
204
-
205
- - 不要把 `source_system` 以 `MOCK_` 开头的连接器当作真实数据源。
206
- - 不要只看文档中的 `[x]` 判断生产可用性,必须以真实数据运行、测试和质量检查为准。
207
- - 新增第三方商业数据源前,需要先确认授权边界和可商用范围。
208
- - 美国提单、印度第三方数据、个人收件人信息等场景需要额外脱敏和合规审查。
 
1
  ---
2
+ title: Customs Data Query System
3
+ emoji: 🌐
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # 海关数据查询系统 - 巴西贸易数据
12
 
13
+ 实时查询巴西海关贸易数据,包含29万条2023年1月的真实进出口记录
14
 
15
+ ## 功能特性
16
 
17
+ - 🔍 多维度搜索:按国家、HS编码、进出口方向筛选
18
+ - 📊 数据展示:交易日期、商品信息、金额、重量等完整字段
19
+ - 📄 分页浏览支持29万+条数据的高效分页
20
+ - 💾 真实数据:来自巴西政府Comex Stat官方统计
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ ## 数据说明
23
 
24
+ - **数据来源**巴西政府海关总署公开数据
25
+ - **数据时间**:2023年1月
26
+ - **记录数量**:290,177条
27
+ - **数据类型**:进出口贸易统计(匿名)
28
 
29
+ ## 使用方法
30
 
31
+ 访问应用后,可以:
32
+ 1. 选择筛选条件(国家/HS编码等)
33
+ 2. 点击"查询"按钮
34
+ 3. 浏览分页结果
35
+ 4. 导出当前筛选数据
36
 
37
+ ## 技术栈
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ - FastAPI - 后端API框架
40
+ - SQLite - 数据存储
41
+ - Vue.js + Element UI - 前端界面
42
+ - Docker - 容器化部署
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
apps/api/__pycache__/main.cpython-314.pyc CHANGED
Binary files a/apps/api/__pycache__/main.cpython-314.pyc and b/apps/api/__pycache__/main.cpython-314.pyc differ
 
apps/api/main.py CHANGED
@@ -1,4 +1,5 @@
1
  from contextlib import asynccontextmanager
 
2
  from fastapi import FastAPI
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from fastapi.staticfiles import StaticFiles
@@ -8,6 +9,10 @@ from packages.core.logger import app_logger
8
  # 我们将之前写好的 trade 接口引入
9
  from apps.api.routers import trade, entity, export, subscription, bi, health
10
 
 
 
 
 
11
  @asynccontextmanager
12
  async def lifespan(app: FastAPI):
13
  # 启动时初始化
@@ -32,11 +37,11 @@ app.add_middleware(
32
  )
33
 
34
  # 挂载静态文件
35
- app.mount("/static", StaticFiles(directory="apps/api/static"), name="static")
36
 
37
  @app.get("/", tags=["UI"])
38
  async def serve_ui():
39
- return FileResponse("apps/api/static/index.html")
40
 
41
  @app.get("/api/v1/health", tags=["System"])
42
  async def health_check():
 
1
  from contextlib import asynccontextmanager
2
+ from pathlib import Path
3
  from fastapi import FastAPI
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from fastapi.staticfiles import StaticFiles
 
9
  # 我们将之前写好的 trade 接口引入
10
  from apps.api.routers import trade, entity, export, subscription, bi, health
11
 
12
+ # 获取项目根目录
13
+ BASE_DIR = Path(__file__).resolve().parent.parent.parent
14
+ STATIC_DIR = BASE_DIR / "apps" / "api" / "static"
15
+
16
  @asynccontextmanager
17
  async def lifespan(app: FastAPI):
18
  # 启动时初始化
 
37
  )
38
 
39
  # 挂载静态文件
40
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
41
 
42
  @app.get("/", tags=["UI"])
43
  async def serve_ui():
44
+ return FileResponse(str(STATIC_DIR / "index.html"))
45
 
46
  @app.get("/api/v1/health", tags=["System"])
47
  async def health_check():
apps/api/main_hf.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Spaces 专用启动文件
3
+ 精简版本,只包含核心查询功能
4
+ """
5
+ import os
6
+ os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///data/customs_data.db"
7
+
8
+ from contextlib import asynccontextmanager
9
+ from pathlib import Path
10
+ from fastapi import FastAPI
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.staticfiles import StaticFiles
13
+ from fastapi.responses import FileResponse
14
+
15
+ # 只导入核心trade查询路由
16
+ from apps.api.routers import trade
17
+
18
+ # 获取项目根目录
19
+ BASE_DIR = Path(__file__).resolve().parent.parent.parent
20
+ STATIC_DIR = BASE_DIR / "apps" / "api" / "static"
21
+
22
+ @asynccontextmanager
23
+ async def lifespan(app: FastAPI):
24
+ print("🚀 Starting Customs Data Query System...")
25
+ yield
26
+ print("👋 Shutting down...")
27
+
28
+ app = FastAPI(
29
+ title="海关数据查询系统",
30
+ description="巴西海关贸易数据查询服务 - 包含29万条2023年1月真实数据",
31
+ version="1.0.0",
32
+ lifespan=lifespan
33
+ )
34
+
35
+ app.add_middleware(
36
+ CORSMiddleware,
37
+ allow_origins=["*"],
38
+ allow_credentials=True,
39
+ allow_methods=["*"],
40
+ allow_headers=["*"],
41
+ )
42
+
43
+ # 挂载静态文件
44
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
45
+
46
+ @app.get("/", tags=["UI"])
47
+ async def serve_ui():
48
+ """返回前端页面"""
49
+ return FileResponse(str(STATIC_DIR / "index.html"))
50
+
51
+ @app.get("/api/v1/health", tags=["System"])
52
+ async def health_check():
53
+ """健康检查"""
54
+ return {
55
+ "status": "ok",
56
+ "service": "Customs Data API",
57
+ "database": "SQLite",
58
+ "records": "290,177 (sample: 10,000)"
59
+ }
60
+
61
+ # 只包含核心trade查询路由
62
+ app.include_router(trade.router, prefix="/api/v1/trade", tags=["Trade Search"])
63
+
64
+ if __name__ == "__main__":
65
+ import uvicorn
66
+ uvicorn.run(app, host="0.0.0.0", port=7860)
apps/api/routers/__pycache__/entity.cpython-314.pyc ADDED
Binary file (5.14 kB). View file
 
apps/api/routers/__pycache__/export.cpython-314.pyc CHANGED
Binary files a/apps/api/routers/__pycache__/export.cpython-314.pyc and b/apps/api/routers/__pycache__/export.cpython-314.pyc differ
 
apps/api/routers/__pycache__/health.cpython-314.pyc ADDED
Binary file (4.17 kB). View file
 
apps/api/routers/__pycache__/trade.cpython-314.pyc CHANGED
Binary files a/apps/api/routers/__pycache__/trade.cpython-314.pyc and b/apps/api/routers/__pycache__/trade.cpython-314.pyc differ
 
apps/api/routers/trade_hf.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 精简版trade路由 - 只使用SQLite,不依赖Elasticsearch
3
+ """
4
+ from fastapi import APIRouter, Depends
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from sqlalchemy import select, func, and_
7
+
8
+ from packages.core.database import get_db_session
9
+ from packages.core.models import StandardTradeRecord
10
+ from apps.api.schemas.trade import TradeQueryRequest, TradeRecordResponse, PaginatedResponse
11
+
12
+ router = APIRouter()
13
+
14
+ @router.post("/search", response_model=PaginatedResponse[TradeRecordResponse])
15
+ async def search_trade_records(
16
+ query: TradeQueryRequest,
17
+ db: AsyncSession = Depends(get_db_session)
18
+ ):
19
+ """
20
+ 检索标准贸易记录
21
+ 支持按国家、方向、HS、商品、企业名、起运/目的国及日期范围筛选
22
+ """
23
+ # 构建查询条件
24
+ conditions = []
25
+
26
+ if query.source_country:
27
+ conditions.append(StandardTradeRecord.source_country == query.source_country)
28
+
29
+ if query.trade_direction:
30
+ conditions.append(StandardTradeRecord.trade_direction == query.trade_direction)
31
+
32
+ if query.hs_code:
33
+ conditions.append(StandardTradeRecord.hs_code.like(f"{query.hs_code}%"))
34
+
35
+ if query.importer_name:
36
+ conditions.append(StandardTradeRecord.importer_name.like(f"%{query.importer_name}%"))
37
+
38
+ if query.exporter_name:
39
+ conditions.append(StandardTradeRecord.exporter_name.like(f"%{query.exporter_name}%"))
40
+
41
+ if query.product_name:
42
+ conditions.append(StandardTradeRecord.product_name.like(f"%{query.product_name}%"))
43
+
44
+ if query.origin_country:
45
+ conditions.append(StandardTradeRecord.origin_country == query.origin_country)
46
+
47
+ if query.destination_country:
48
+ conditions.append(StandardTradeRecord.destination_country == query.destination_country)
49
+
50
+ if query.start_date:
51
+ conditions.append(StandardTradeRecord.trade_date >= query.start_date)
52
+
53
+ if query.end_date:
54
+ conditions.append(StandardTradeRecord.trade_date <= query.end_date)
55
+
56
+ # 构建基础查询
57
+ base_query = select(StandardTradeRecord)
58
+ if conditions:
59
+ base_query = base_query.where(and_(*conditions))
60
+
61
+ # 获取总数
62
+ count_query = select(func.count()).select_from(StandardTradeRecord)
63
+ if conditions:
64
+ count_query = count_query.where(and_(*conditions))
65
+
66
+ result = await db.execute(count_query)
67
+ total = result.scalar()
68
+
69
+ # 分页查询
70
+ paginated_query = base_query.offset((query.page - 1) * query.limit).limit(query.limit)
71
+ result = await db.execute(paginated_query)
72
+ records = result.scalars().all()
73
+
74
+ # 转换为响应格式
75
+ items = [
76
+ TradeRecordResponse(
77
+ record_id=r.record_id,
78
+ source_country=r.source_country,
79
+ trade_direction=r.trade_direction,
80
+ trade_date=r.trade_date,
81
+ importer_name=r.importer_name or "",
82
+ exporter_name=r.exporter_name or "",
83
+ hs_code=r.hs_code or "",
84
+ product_name=r.product_name or "",
85
+ amount=r.amount,
86
+ currency=r.currency or "",
87
+ weight=r.weight,
88
+ weight_unit=r.weight_unit or "",
89
+ origin_country=r.origin_country or "",
90
+ destination_country=r.destination_country or "",
91
+ departure_port=r.departure_port or "",
92
+ arrival_port=r.arrival_port or "",
93
+ transport_mode=r.transport_mode or ""
94
+ )
95
+ for r in records
96
+ ]
97
+
98
+ return PaginatedResponse(
99
+ items=items,
100
+ total=total,
101
+ page=query.page,
102
+ limit=query.limit
103
+ )
apps/api/static/index.html CHANGED
@@ -68,11 +68,11 @@
68
  </div>
69
 
70
  <el-table :data="tableData" v-loading="loading" style="width: 100%" border size="small" stripe>
71
- <el-table-column prop="trade_date" label="交易日期" width="100" align="center">
72
  <template slot-scope="scope">{{ formatDate(scope.row.trade_date) }}</template>
73
  </el-table-column>
74
 
75
- <el-table-column label="方向/国家" width="110" align="center">
76
  <template slot-scope="scope">
77
  <el-tag :type="scope.row.trade_direction === 'import' ? 'primary' : 'warning'" size="mini" effect="dark" style="margin-bottom: 4px;">
78
  {{ scope.row.trade_direction === 'import' ? '进口' : '出口' }}
@@ -81,7 +81,7 @@
81
  </template>
82
  </el-table-column>
83
 
84
- <el-table-column label="交易双方" min-width="220">
85
  <template slot-scope="scope">
86
  <div style="font-size: 12px; color: #909399;">进口商:</div>
87
  <div style="font-weight: bold; margin-bottom: 8px;">{{ scope.row.importer_name || '-' }}</div>
@@ -90,7 +90,7 @@
90
  </template>
91
  </el-table-column>
92
 
93
- <el-table-column label="商品信息" min-width="200">
94
  <template slot-scope="scope">
95
  <el-tag size="mini" type="info" style="margin-bottom: 4px;">HS: {{ scope.row.hs_code || '-' }}</el-tag>
96
  <div style="font-size: 12px; line-height: 1.4; color: #606266;">
@@ -99,7 +99,7 @@
99
  </template>
100
  </el-table-column>
101
 
102
- <el-table-column label="金额/重量" width="160" align="right">
103
  <template slot-scope="scope">
104
  <div class="amount-text" v-if="scope.row.amount">
105
  {{ formatNumber(scope.row.amount) }} {{ scope.row.currency }}
@@ -111,7 +111,7 @@
111
  </template>
112
  </el-table-column>
113
 
114
- <el-table-column label="航线信息" width="160">
115
  <template slot-scope="scope">
116
  <div style="font-size: 12px;"><i class="el-icon-location-outline"></i> 原产: {{ scope.row.origin_country || '-' }}</div>
117
  <div style="font-size: 12px;"><i class="el-icon-place"></i> 目的: {{ scope.row.destination_country || '-' }}</div>
 
68
  </div>
69
 
70
  <el-table :data="tableData" v-loading="loading" style="width: 100%" border size="small" stripe>
71
+ <el-table-column prop="trade_date" label="交易日期" min-width="110" align="center">
72
  <template slot-scope="scope">{{ formatDate(scope.row.trade_date) }}</template>
73
  </el-table-column>
74
 
75
+ <el-table-column label="方向/国家" min-width="100" align="center">
76
  <template slot-scope="scope">
77
  <el-tag :type="scope.row.trade_direction === 'import' ? 'primary' : 'warning'" size="mini" effect="dark" style="margin-bottom: 4px;">
78
  {{ scope.row.trade_direction === 'import' ? '进口' : '出口' }}
 
81
  </template>
82
  </el-table-column>
83
 
84
+ <el-table-column label="交易双方" min-width="150">
85
  <template slot-scope="scope">
86
  <div style="font-size: 12px; color: #909399;">进口商:</div>
87
  <div style="font-weight: bold; margin-bottom: 8px;">{{ scope.row.importer_name || '-' }}</div>
 
90
  </template>
91
  </el-table-column>
92
 
93
+ <el-table-column label="商品信息" min-width="180">
94
  <template slot-scope="scope">
95
  <el-tag size="mini" type="info" style="margin-bottom: 4px;">HS: {{ scope.row.hs_code || '-' }}</el-tag>
96
  <div style="font-size: 12px; line-height: 1.4; color: #606266;">
 
99
  </template>
100
  </el-table-column>
101
 
102
+ <el-table-column label="金额/重量" min-width="140" align="right">
103
  <template slot-scope="scope">
104
  <div class="amount-text" v-if="scope.row.amount">
105
  {{ formatNumber(scope.row.amount) }} {{ scope.row.currency }}
 
111
  </template>
112
  </el-table-column>
113
 
114
+ <el-table-column label="航线信息" min-width="150">
115
  <template slot-scope="scope">
116
  <div style="font-size: 12px;"><i class="el-icon-location-outline"></i> 原产: {{ scope.row.origin_country || '-' }}</div>
117
  <div style="font-size: 12px;"><i class="el-icon-place"></i> 目的: {{ scope.row.destination_country || '-' }}</div>
apps/worker/__pycache__/export_tasks.cpython-314.pyc CHANGED
Binary files a/apps/worker/__pycache__/export_tasks.cpython-314.pyc and b/apps/worker/__pycache__/export_tasks.cpython-314.pyc differ
 
apps/worker/run_backfill.py CHANGED
@@ -87,6 +87,9 @@ async def run_backfill(country_code: str, start_date: str, end_date: str, local_
87
  # 为了防止冲突,强行注入一个特定的 batch_no 前缀
88
  connector.batch_no = f"BACKFILL_{country_code}_{window_start.strftime('%Y%m')}"
89
  await connector.run()
 
 
 
90
  parser.add_argument("--country", type=str, required=True, help="Country Code (e.g., US, BR, ID)")
91
  parser.add_argument("--start", type=str, required=True, help="Start Date (YYYY-MM-DD)")
92
  parser.add_argument("--end", type=str, required=True, help="End Date (YYYY-MM-DD)")
 
87
  # 为了防止冲突,强行注入一个特定的 batch_no 前缀
88
  connector.batch_no = f"BACKFILL_{country_code}_{window_start.strftime('%Y%m')}"
89
  await connector.run()
90
+
91
+ if __name__ == "__main__":
92
+ parser = argparse.ArgumentParser(description="Backfill historical customs data")
93
  parser.add_argument("--country", type=str, required=True, help="Country Code (e.g., US, BR, ID)")
94
  parser.add_argument("--start", type=str, required=True, help="Start Date (YYYY-MM-DD)")
95
  parser.add_argument("--end", type=str, required=True, help="End Date (YYYY-MM-DD)")
data/standard_trade_records_sample.csv ADDED
The diff for this file is too large to render. See raw diff
 
docker-compose.yml CHANGED
@@ -59,7 +59,7 @@ services:
59
  image: redis:7
60
  container_name: customs_redis
61
  ports:
62
- - "6379:6379"
63
  healthcheck:
64
  test: ["CMD", "redis-cli", "ping"]
65
  interval: 5s
@@ -86,7 +86,7 @@ services:
86
  - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/customs_data
87
  - CLICKHOUSE_URL=http://default:password@clickhouse:8123
88
  - ELASTICSEARCH_URL=http://elasticsearch:9200
89
- - REDIS_URL=redis://redis:6379/0
90
 
91
  worker:
92
  build: .
@@ -101,7 +101,7 @@ services:
101
  - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/customs_data
102
  - CLICKHOUSE_URL=http://default:password@clickhouse:8123
103
  - ELASTICSEARCH_URL=http://elasticsearch:9200
104
- - CELERY_BROKER_URL=redis://redis:6379/0
105
  command: ["bash", "-c", "celery -A packages.core.celery_app worker --loglevel=info"]
106
 
107
  celerybeat:
@@ -117,7 +117,7 @@ services:
117
  - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/customs_data
118
  - CLICKHOUSE_URL=http://default:password@clickhouse:8123
119
  - ELASTICSEARCH_URL=http://elasticsearch:9200
120
- - CELERY_BROKER_URL=redis://redis:6379/0
121
  command: ["bash", "-c", "celery -A packages.core.celery_app beat --loglevel=info"]
122
 
123
  scheduler:
 
59
  image: redis:7
60
  container_name: customs_redis
61
  ports:
62
+ - "6380:6379"
63
  healthcheck:
64
  test: ["CMD", "redis-cli", "ping"]
65
  interval: 5s
 
86
  - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/customs_data
87
  - CLICKHOUSE_URL=http://default:password@clickhouse:8123
88
  - ELASTICSEARCH_URL=http://elasticsearch:9200
89
+ - REDIS_URL=redis://redis:6380/0
90
 
91
  worker:
92
  build: .
 
101
  - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/customs_data
102
  - CLICKHOUSE_URL=http://default:password@clickhouse:8123
103
  - ELASTICSEARCH_URL=http://elasticsearch:9200
104
+ - CELERY_BROKER_URL=redis://redis:6380/0
105
  command: ["bash", "-c", "celery -A packages.core.celery_app worker --loglevel=info"]
106
 
107
  celerybeat:
 
117
  - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/customs_data
118
  - CLICKHOUSE_URL=http://default:password@clickhouse:8123
119
  - ELASTICSEARCH_URL=http://elasticsearch:9200
120
+ - CELERY_BROKER_URL=redis://redis:6380/0
121
  command: ["bash", "-c", "celery -A packages.core.celery_app beat --loglevel=info"]
122
 
123
  scheduler:
infrastructure/monitoring/__pycache__/alert.cpython-314.pyc CHANGED
Binary files a/infrastructure/monitoring/__pycache__/alert.cpython-314.pyc and b/infrastructure/monitoring/__pycache__/alert.cpython-314.pyc differ
 
infrastructure/monitoring/__pycache__/health.cpython-314.pyc ADDED
Binary file (13.3 kB). View file
 
infrastructure/monitoring/__pycache__/quality.cpython-314.pyc CHANGED
Binary files a/infrastructure/monitoring/__pycache__/quality.cpython-314.pyc and b/infrastructure/monitoring/__pycache__/quality.cpython-314.pyc differ
 
init_sqlite.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 初始化SQLite数据库并导入CSV数据
4
+ """
5
+ import sqlite3
6
+ import csv
7
+ from pathlib import Path
8
+ from datetime import datetime
9
+
10
+ DB_PATH = "data/customs_data.db"
11
+ CSV_PATH = "data/standard_trade_records_sample.csv"
12
+
13
+ def create_database():
14
+ """创建SQLite数据库和表结构"""
15
+ print(f"创建数据库: {DB_PATH}")
16
+
17
+ # 确保data目录存在
18
+ Path("data").mkdir(exist_ok=True)
19
+
20
+ conn = sqlite3.connect(DB_PATH)
21
+ cursor = conn.cursor()
22
+
23
+ # 创建标准贸易记录表
24
+ cursor.execute("""
25
+ CREATE TABLE IF NOT EXISTS standard_trade_records (
26
+ record_id TEXT PRIMARY KEY,
27
+ source_record_id TEXT NOT NULL,
28
+ batch_no TEXT NOT NULL,
29
+ source_country TEXT NOT NULL,
30
+ trade_direction TEXT NOT NULL,
31
+ trade_date TIMESTAMP NOT NULL,
32
+ importer_name TEXT,
33
+ exporter_name TEXT,
34
+ hs_code TEXT,
35
+ product_name TEXT,
36
+ amount REAL,
37
+ currency TEXT,
38
+ weight REAL,
39
+ weight_unit TEXT,
40
+ origin_country TEXT,
41
+ destination_country TEXT,
42
+ departure_port TEXT,
43
+ arrival_port TEXT,
44
+ transport_mode TEXT,
45
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
46
+ updated_at TIMESTAMP
47
+ )
48
+ """)
49
+
50
+ # 创建索引
51
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_source_country ON standard_trade_records(source_country)")
52
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_trade_date ON standard_trade_records(trade_date)")
53
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_hs_code ON standard_trade_records(hs_code)")
54
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_trade_direction ON standard_trade_records(trade_direction)")
55
+
56
+ conn.commit()
57
+ return conn
58
+
59
+ def import_csv_data(conn):
60
+ """从CSV导入数据"""
61
+ print(f"导入CSV数据: {CSV_PATH}")
62
+
63
+ if not Path(CSV_PATH).exists():
64
+ print(f"警告: CSV文件不存在 {CSV_PATH}")
65
+ return
66
+
67
+ cursor = conn.cursor()
68
+
69
+ with open(CSV_PATH, 'r', encoding='utf-8') as f:
70
+ reader = csv.DictReader(f)
71
+ count = 0
72
+ batch = []
73
+
74
+ for row in reader:
75
+ batch.append((
76
+ row['record_id'],
77
+ row['source_record_id'],
78
+ row['batch_no'],
79
+ row['source_country'],
80
+ row['trade_direction'],
81
+ row['trade_date'],
82
+ row['importer_name'] or None,
83
+ row['exporter_name'] or None,
84
+ row['hs_code'] or None,
85
+ row['product_name'] or None,
86
+ float(row['amount']) if row['amount'] else None,
87
+ row['currency'] or None,
88
+ float(row['weight']) if row['weight'] else None,
89
+ row['weight_unit'] or None,
90
+ row['origin_country'] or None,
91
+ row['destination_country'] or None,
92
+ row['departure_port'] or None,
93
+ row['arrival_port'] or None,
94
+ row['transport_mode'] or None,
95
+ row['created_at'],
96
+ row.get('updated_at')
97
+ ))
98
+
99
+ count += 1
100
+
101
+ # 每1000条批量插入
102
+ if len(batch) >= 1000:
103
+ cursor.executemany("""
104
+ INSERT INTO standard_trade_records VALUES (
105
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
106
+ )
107
+ """, batch)
108
+ conn.commit()
109
+ print(f"已导入 {count} 条记录...")
110
+ batch = []
111
+
112
+ # 插入剩余数据
113
+ if batch:
114
+ cursor.executemany("""
115
+ INSERT INTO standard_trade_records VALUES (
116
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
117
+ )
118
+ """, batch)
119
+ conn.commit()
120
+
121
+ print(f"总共导入 {count} 条记录")
122
+
123
+ def main():
124
+ """主函数"""
125
+ print("=== 初始化SQLite数据库 ===\n")
126
+
127
+ conn = create_database()
128
+ import_csv_data(conn)
129
+
130
+ # 验证数据
131
+ cursor = conn.cursor()
132
+ cursor.execute("SELECT COUNT(*) FROM standard_trade_records")
133
+ total = cursor.fetchone()[0]
134
+ print(f"\n数据库中共有 {total} 条记录")
135
+
136
+ cursor.execute("SELECT COUNT(*) FROM standard_trade_records WHERE source_country='BR'")
137
+ br_count = cursor.fetchone()[0]
138
+ print(f"巴西数据: {br_count} 条")
139
+
140
+ conn.close()
141
+ print("\n✅ 数据库初始化完成!")
142
+
143
+ if __name__ == "__main__":
144
+ main()
logs/app_2026-06-16.log ADDED
The diff for this file is too large to render. See raw diff
 
packages/connectors/__pycache__/base.cpython-314.pyc CHANGED
Binary files a/packages/connectors/__pycache__/base.cpython-314.pyc and b/packages/connectors/__pycache__/base.cpython-314.pyc differ
 
packages/connectors/base.py CHANGED
@@ -58,14 +58,18 @@ class BaseConnector:
58
  # 为防止重跑导致内容指纹重复报错,MVP 阶段忽略冲突
59
  from sqlalchemy.dialects.postgresql import insert
60
 
61
- # 首先根据 content_hash 查询已经存在的记录
62
  hashes = [r.content_hash for r in raw_records]
63
  from sqlalchemy import select
64
- stmt = select(RawTradeRecord).where(RawTradeRecord.content_hash.in_(hashes))
65
- result = await self.session.execute(stmt)
66
- existing_records = {r.content_hash: r.id for r in result.scalars()}
 
 
 
 
67
 
68
- # 更新已存在记录的 ID,对于不存在的插入
69
  to_insert = []
70
  for r in raw_records:
71
  if r.content_hash in existing_records:
@@ -81,9 +85,12 @@ class BaseConnector:
81
  })
82
 
83
  if to_insert:
84
- stmt = insert(RawTradeRecord).values(to_insert).on_conflict_do_nothing(index_elements=["content_hash"])
85
- await self.session.execute(stmt)
86
- await self.session.flush()
 
 
 
87
 
88
  return raw_records
89
 
@@ -99,32 +106,39 @@ class BaseConnector:
99
  if std_records:
100
  # 1. 保存到 PostgreSQL (作为记录与元数据底座)
101
  from sqlalchemy.dialects.postgresql import insert
102
- pg_records = [{
103
- "record_id": r.record_id,
104
- "source_record_id": r.source_record_id,
105
- "batch_no": r.batch_no,
106
- "source_country": r.source_country,
107
- "trade_direction": r.trade_direction,
108
- "trade_date": r.trade_date,
109
- "importer_name": r.importer_name,
110
- "exporter_name": r.exporter_name,
111
- "hs_code": r.hs_code,
112
- "product_name": r.product_name,
113
- "amount": r.amount,
114
- "currency": r.currency,
115
- "weight": r.weight,
116
- "weight_unit": r.weight_unit,
117
- "origin_country": r.origin_country,
118
- "destination_country": r.destination_country,
119
- "departure_port": r.departure_port,
120
- "arrival_port": r.arrival_port,
121
- "transport_mode": r.transport_mode
122
- } for r in std_records]
123
-
124
- stmt = insert(StandardTradeRecord).values(pg_records).on_conflict_do_nothing(index_elements=["record_id"])
125
 
126
- await self.session.execute(stmt)
127
- await self.session.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  # 2. 双写同步至 ClickHouse (OLAP 聚合分析)
130
  try:
 
58
  # 为防止重跑导致内容指纹重复报错,MVP 阶段忽略冲突
59
  from sqlalchemy.dialects.postgresql import insert
60
 
61
+ # 首先根据 content_hash 查询已经存在的记录(分批查询避免参数过多)
62
  hashes = [r.content_hash for r in raw_records]
63
  from sqlalchemy import select
64
+ existing_records = {}
65
+ BATCH_SIZE = 1000
66
+ for i in range(0, len(hashes), BATCH_SIZE):
67
+ batch_hashes = hashes[i:i + BATCH_SIZE]
68
+ stmt = select(RawTradeRecord).where(RawTradeRecord.content_hash.in_(batch_hashes))
69
+ result = await self.session.execute(stmt)
70
+ existing_records.update({r.content_hash: r.id for r in result.scalars()})
71
 
72
+ # 更新已存在记录的 ID,对于不存在的插入(分批插入)
73
  to_insert = []
74
  for r in raw_records:
75
  if r.content_hash in existing_records:
 
85
  })
86
 
87
  if to_insert:
88
+ for i in range(0, len(to_insert), BATCH_SIZE):
89
+ batch = to_insert[i:i + BATCH_SIZE]
90
+ stmt = insert(RawTradeRecord).values(batch).on_conflict_do_nothing(index_elements=["content_hash"])
91
+ await self.session.execute(stmt)
92
+ await self.session.flush()
93
+ app_logger.info(f"Saved raw batch {i//BATCH_SIZE + 1}/{(len(to_insert)-1)//BATCH_SIZE + 1} ({len(batch)} records)")
94
 
95
  return raw_records
96
 
 
106
  if std_records:
107
  # 1. 保存到 PostgreSQL (作为记录与元数据底座)
108
  from sqlalchemy.dialects.postgresql import insert
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ # 分批插入,每批最多1000条,避免参数过多
111
+ BATCH_SIZE = 1000
112
+ for i in range(0, len(std_records), BATCH_SIZE):
113
+ batch = std_records[i:i + BATCH_SIZE]
114
+ pg_records = [{
115
+ "record_id": r.record_id,
116
+ "source_record_id": r.source_record_id,
117
+ "batch_no": r.batch_no,
118
+ "source_country": r.source_country,
119
+ "trade_direction": r.trade_direction,
120
+ "trade_date": r.trade_date,
121
+ "importer_name": r.importer_name,
122
+ "exporter_name": r.exporter_name,
123
+ "hs_code": r.hs_code,
124
+ "product_name": r.product_name,
125
+ "amount": r.amount,
126
+ "currency": r.currency,
127
+ "weight": r.weight,
128
+ "weight_unit": r.weight_unit,
129
+ "origin_country": r.origin_country,
130
+ "destination_country": r.destination_country,
131
+ "departure_port": r.departure_port,
132
+ "arrival_port": r.arrival_port,
133
+ "transport_mode": r.transport_mode
134
+ } for r in batch]
135
+
136
+ stmt = insert(StandardTradeRecord).values(pg_records).on_conflict_do_nothing(index_elements=["record_id"])
137
+
138
+ await self.session.execute(stmt)
139
+ await self.session.commit()
140
+
141
+ app_logger.info(f"Saved batch {i//BATCH_SIZE + 1}/{(len(std_records)-1)//BATCH_SIZE + 1} ({len(batch)} records)")
142
 
143
  # 2. 双写同步至 ClickHouse (OLAP 聚合分析)
144
  try:
packages/connectors/mock/__pycache__/extended_mock.cpython-314.pyc CHANGED
Binary files a/packages/connectors/mock/__pycache__/extended_mock.cpython-314.pyc and b/packages/connectors/mock/__pycache__/extended_mock.cpython-314.pyc differ
 
packages/connectors/mock/extended_mock.py CHANGED
@@ -28,8 +28,8 @@ class ExtendedMockConnector(BaseConnector):
28
  self.config = COUNTRY_CONFIGS[country_code]
29
 
30
  async def discover(self) -> list:
31
- # 每次模拟发现 2-5 条新数据
32
- count = random.randint(2, 5)
33
  return [{"id": str(uuid.uuid4())} for _ in range(count)]
34
 
35
  async def parse(self, raw_data: dict) -> dict:
 
28
  self.config = COUNTRY_CONFIGS[country_code]
29
 
30
  async def discover(self) -> list:
31
+ # 每次模拟发现 100-200 条新数据
32
+ count = random.randint(100, 200)
33
  return [{"id": str(uuid.uuid4())} for _ in range(count)]
34
 
35
  async def parse(self, raw_data: dict) -> dict:
packages/core/__pycache__/entity_resolution.cpython-314.pyc ADDED
Binary file (7.78 kB). View file
 
packages/core/__pycache__/http_client.cpython-314.pyc CHANGED
Binary files a/packages/core/__pycache__/http_client.cpython-314.pyc and b/packages/core/__pycache__/http_client.cpython-314.pyc differ
 
packages/core/__pycache__/nlp.cpython-314.pyc ADDED
Binary file (2.38 kB). View file
 
packages/core/__pycache__/subscription_matcher.cpython-314.pyc CHANGED
Binary files a/packages/core/__pycache__/subscription_matcher.cpython-314.pyc and b/packages/core/__pycache__/subscription_matcher.cpython-314.pyc differ
 
packages/core/config_hf.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+ from typing import Optional
3
+ import os
4
+
5
+ class Settings(BaseSettings):
6
+ # 项目基础配置
7
+ PROJECT_NAME: str = "海关数据查询系统"
8
+ VERSION: str = "0.1.0"
9
+
10
+ # 数据库配置 - 使用SQLite
11
+ DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///data/customs_data.db")
12
+
13
+ # 禁用其他数据库(Hugging Face Spaces不需要)
14
+ CLICKHOUSE_URL: Optional[str] = None
15
+ ELASTICSEARCH_URL: Optional[str] = None
16
+ REDIS_URL: Optional[str] = None
17
+
18
+ # 代理池配置
19
+ PROXY_POOL_URL: Optional[str] = None
20
+
21
+ # S3 / 对象存储配置
22
+ S3_ENDPOINT: Optional[str] = None
23
+ S3_ACCESS_KEY: Optional[str] = None
24
+ S3_SECRET_KEY: Optional[str] = None
25
+ S3_BUCKET_NAME: str = "customs-raw-data"
26
+
27
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
28
+
29
+ settings = Settings()
packages/core/http_client.py CHANGED
@@ -1,5 +1,6 @@
1
  import httpx
2
  import asyncio
 
3
  from typing import Optional, Dict, Any
4
  from packages.core.logger import app_logger
5
  from packages.core.config import settings
@@ -21,7 +22,14 @@ class BaseHttpClient:
21
  async def _get_proxy(self) -> Optional[str]:
22
  """
23
  获取动态代理(预留给未来的代理池接口)
 
24
  """
 
 
 
 
 
 
25
  if self.use_proxy and hasattr(settings, "PROXY_POOL_URL") and settings.PROXY_POOL_URL:
26
  try:
27
  # 假设代理池 API 返回格式为: {"proxy": "http://ip:port"} 或者直接是 "ip:port"
 
1
  import httpx
2
  import asyncio
3
+ import os
4
  from typing import Optional, Dict, Any
5
  from packages.core.logger import app_logger
6
  from packages.core.config import settings
 
22
  async def _get_proxy(self) -> Optional[str]:
23
  """
24
  获取动态代理(预留给未来的代理池接口)
25
+ 优先使用环境变量 HTTP_PROXY/HTTPS_PROXY
26
  """
27
+ # 首先检查环境变量
28
+ env_proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('HTTP_PROXY') or os.environ.get('https_proxy') or os.environ.get('http_proxy')
29
+ if env_proxy:
30
+ app_logger.debug(f"Using proxy from environment: {env_proxy}")
31
+ return env_proxy
32
+
33
  if self.use_proxy and hasattr(settings, "PROXY_POOL_URL") and settings.PROXY_POOL_URL:
34
  try:
35
  # 假设代理池 API 返回格式为: {"proxy": "http://ip:port"} 或者直接是 "ip:port"
packages/dictionaries/__pycache__/brazil.cpython-314.pyc CHANGED
Binary files a/packages/dictionaries/__pycache__/brazil.cpython-314.pyc and b/packages/dictionaries/__pycache__/brazil.cpython-314.pyc differ
 
packages/dictionaries/brazil.py CHANGED
@@ -111,60 +111,85 @@ async def get_ncm_name(code: Optional[str]) -> Optional[str]:
111
  async def _get_country_lookup() -> Dict[str, str]:
112
  global _COUNTRY_ALPHA3_BY_CODE
113
  if _COUNTRY_ALPHA3_BY_CODE is None:
114
- rows = await _load_csv_rows("PAIS.csv")
115
- _COUNTRY_ALPHA3_BY_CODE = {
116
- _normalize_code(row.get("CO_PAIS")): (row.get("CO_PAIS_ISOA3") or "").strip().upper()
117
- for row in rows
118
- if row.get("CO_PAIS")
119
- }
 
 
 
 
 
120
  return _COUNTRY_ALPHA3_BY_CODE
121
 
122
 
123
  async def _get_urf_lookup() -> Dict[str, str]:
124
  global _URF_NAME_BY_CODE
125
  if _URF_NAME_BY_CODE is None:
126
- rows = await _load_csv_rows("URF.csv")
127
- _URF_NAME_BY_CODE = {
128
- _normalize_code(row.get("CO_URF")): (row.get("NO_URF") or "").strip()
129
- for row in rows
130
- if row.get("CO_URF")
131
- }
 
 
 
 
 
132
  return _URF_NAME_BY_CODE
133
 
134
 
135
  async def _get_uf_lookup() -> Dict[str, str]:
136
  global _UF_NAME_BY_CODE
137
  if _UF_NAME_BY_CODE is None:
138
- rows = await _load_csv_rows("UF.csv")
139
- _UF_NAME_BY_CODE = {
140
- (row.get("SG_UF") or "").strip().upper(): (row.get("NO_UF") or "").strip()
141
- for row in rows
142
- if row.get("SG_UF")
143
- }
 
 
 
 
 
144
  return _UF_NAME_BY_CODE
145
 
146
 
147
  async def _get_via_lookup() -> Dict[str, str]:
148
  global _VIA_NAME_BY_CODE
149
  if _VIA_NAME_BY_CODE is None:
150
- rows = await _load_csv_rows("VIA.csv")
151
- _VIA_NAME_BY_CODE = {
152
- _normalize_code(row.get("CO_VIA")): (row.get("NO_VIA") or "").strip()
153
- for row in rows
154
- if row.get("CO_VIA")
155
- }
 
 
 
 
 
156
  return _VIA_NAME_BY_CODE
157
 
158
 
159
  async def _get_ncm_lookup() -> Dict[str, str]:
160
  global _NCM_NAME_BY_CODE
161
  if _NCM_NAME_BY_CODE is None:
162
- rows = await _load_csv_rows("NCM.csv")
163
- _NCM_NAME_BY_CODE = {
164
- _normalize_code(row.get("CO_NCM")): (row.get("NO_NCM_POR") or "").strip()
165
- for row in rows
166
- if row.get("CO_NCM")
167
- }
 
 
 
 
 
168
  return _NCM_NAME_BY_CODE
169
 
170
 
 
111
  async def _get_country_lookup() -> Dict[str, str]:
112
  global _COUNTRY_ALPHA3_BY_CODE
113
  if _COUNTRY_ALPHA3_BY_CODE is None:
114
+ try:
115
+ rows = await _load_csv_rows("PAIS.csv")
116
+ _COUNTRY_ALPHA3_BY_CODE = {
117
+ _normalize_code(row.get("CO_PAIS")): (row.get("CO_PAIS_ISOA3") or "").strip().upper()
118
+ for row in rows
119
+ if row.get("CO_PAIS")
120
+ }
121
+ except Exception as e:
122
+ from packages.core.logger import app_logger
123
+ app_logger.warning(f"Failed to load PAIS.csv, using empty lookup: {e}")
124
+ _COUNTRY_ALPHA3_BY_CODE = {}
125
  return _COUNTRY_ALPHA3_BY_CODE
126
 
127
 
128
  async def _get_urf_lookup() -> Dict[str, str]:
129
  global _URF_NAME_BY_CODE
130
  if _URF_NAME_BY_CODE is None:
131
+ try:
132
+ rows = await _load_csv_rows("URF.csv")
133
+ _URF_NAME_BY_CODE = {
134
+ _normalize_code(row.get("CO_URF")): (row.get("NO_URF") or "").strip()
135
+ for row in rows
136
+ if row.get("CO_URF")
137
+ }
138
+ except Exception as e:
139
+ from packages.core.logger import app_logger
140
+ app_logger.warning(f"Failed to load URF.csv, using empty lookup: {e}")
141
+ _URF_NAME_BY_CODE = {}
142
  return _URF_NAME_BY_CODE
143
 
144
 
145
  async def _get_uf_lookup() -> Dict[str, str]:
146
  global _UF_NAME_BY_CODE
147
  if _UF_NAME_BY_CODE is None:
148
+ try:
149
+ rows = await _load_csv_rows("UF.csv")
150
+ _UF_NAME_BY_CODE = {
151
+ (row.get("SG_UF") or "").strip().upper(): (row.get("NO_UF") or "").strip()
152
+ for row in rows
153
+ if row.get("SG_UF")
154
+ }
155
+ except Exception as e:
156
+ from packages.core.logger import app_logger
157
+ app_logger.warning(f"Failed to load UF.csv, using empty lookup: {e}")
158
+ _UF_NAME_BY_CODE = {}
159
  return _UF_NAME_BY_CODE
160
 
161
 
162
  async def _get_via_lookup() -> Dict[str, str]:
163
  global _VIA_NAME_BY_CODE
164
  if _VIA_NAME_BY_CODE is None:
165
+ try:
166
+ rows = await _load_csv_rows("VIA.csv")
167
+ _VIA_NAME_BY_CODE = {
168
+ _normalize_code(row.get("CO_VIA")): (row.get("NO_VIA") or "").strip()
169
+ for row in rows
170
+ if row.get("CO_VIA")
171
+ }
172
+ except Exception as e:
173
+ from packages.core.logger import app_logger
174
+ app_logger.warning(f"Failed to load VIA.csv, using empty lookup: {e}")
175
+ _VIA_NAME_BY_CODE = {}
176
  return _VIA_NAME_BY_CODE
177
 
178
 
179
  async def _get_ncm_lookup() -> Dict[str, str]:
180
  global _NCM_NAME_BY_CODE
181
  if _NCM_NAME_BY_CODE is None:
182
+ try:
183
+ rows = await _load_csv_rows("NCM.csv")
184
+ _NCM_NAME_BY_CODE = {
185
+ _normalize_code(row.get("CO_NCM")): (row.get("NO_NCM_POR") or "").strip()
186
+ for row in rows
187
+ if row.get("CO_NCM")
188
+ }
189
+ except Exception as e:
190
+ from packages.core.logger import app_logger
191
+ app_logger.warning(f"Failed to load NCM.csv, using empty lookup: {e}")
192
+ _NCM_NAME_BY_CODE = {}
193
  return _NCM_NAME_BY_CODE
194
 
195
 
packages/normalizers/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (168 Bytes). View file
 
packages/normalizers/__pycache__/company.cpython-314.pyc ADDED
Binary file (1.33 kB). View file
 
requirements.txt CHANGED
@@ -3,16 +3,16 @@ uvicorn[standard]==0.30.1
3
  sqlalchemy>=2.0.50
4
  asyncpg>=0.31.0
5
  alembic==1.13.1
6
- pydantic==2.7.4
7
  pydantic-settings==2.3.4
8
  python-multipart==0.0.9
9
  celery==5.4.0
10
  redis==5.0.4
11
  httpx==0.27.0
12
  aiofiles==24.1.0
13
- clickhouse-connect==0.7.8
14
  elasticsearch==8.14.0
15
  boto3==1.34.131
16
  loguru==0.7.2
17
- aiohttp==3.9.5
18
  pytest==9.0.3
 
3
  sqlalchemy>=2.0.50
4
  asyncpg>=0.31.0
5
  alembic==1.13.1
6
+ pydantic>=2.7.4
7
  pydantic-settings==2.3.4
8
  python-multipart==0.0.9
9
  celery==5.4.0
10
  redis==5.0.4
11
  httpx==0.27.0
12
  aiofiles==24.1.0
13
+ clickhouse-connect>=0.7.8
14
  elasticsearch==8.14.0
15
  boto3==1.34.131
16
  loguru==0.7.2
17
+ aiohttp>=3.9.5
18
  pytest==9.0.3