Compare commits

...

16 Commits

Author SHA1 Message Date
Misaka_Company
25982b12bf feat: add one-to-one box mode with duplicate bind check
Add box_mode field to BoxSaveRequest and DuplicateZongpaiBindError
exception. In one-to-one mode, prevent the same zongpai from being
packed into multiple boxes, returning 409 with existing box info.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-12 15:44:00 +08:00
Misaka_Company
ee2b2f8f25 feat: add PostgreSQL database backend support
Add multi-database support allowing selection between SQL Server and
PostgreSQL via database.active config. Changes include dialect-aware
SQL generation, cross-database timestamp functions, PostgreSQL connection
URL builder, and psycopg dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-12 12:33:48 +08:00
Misaka_Company
4b8c502a91 chore: add settings.yaml template file
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-12 09:58:12 +08:00
Misaka_Company
b1e369e9f3 chore: remove settings.yaml from tracking and add to .gitignore
Contains database credentials, should not be versioned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-12 09:56:54 +08:00
Misaka_Company
7305ad9ae6 refactor: migrate configuration from .env to YAML
Migrate FastAPI configuration from environment files (.env) to YAML
using pydantic-yaml library for better readability and maintainability.

Changes:
- Add pydantic-yaml dependency
- Create config/ module with Settings, SqlServerConfig classes
- Add config/settings.yaml for database configuration
- Update all imports from app.core.config to config.settings
- Add error handling for config loading on startup
- Remove old .env and app/core/config.py
- Update README with YAML configuration documentation
- Add test coverage for config loading

Test results:
- All 15 tests passing
- 88% code coverage maintained

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:50:40 +08:00
Misaka_Company
fb759ebe33 docs: update README with YAML configuration instructions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:47:10 +08:00
Misaka_Company
b9e13105e1 chore: ignore local YAML config overrides
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:45:06 +08:00
Misaka_Company
70cb6759ea refactor: remove old .env and config.py files
Remove deprecated app/core/config.py as configuration is now managed
through the new YAML-based config module (app/config/).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:44:03 +08:00
Misaka_Company
f94a18fee7 feat: add config error handling on startup
Add error handling for configuration loading at application startup.
If the config file is missing or invalid, the app will exit with
a clear error message instead of crashing later.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:42:45 +08:00
Misaka_Company
a6873ae2ee fix: resolve YAML config path relative to project root
Ensure config/settings.yaml can be found when running from any working
directory by resolving the path relative to the settings.py file location
rather than the current working directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:40:46 +08:00
Misaka_Company
8292de7747 refactor: update database.py to use new config module
Update import statement from app.core.config to config.settings as part
of YAML configuration migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:37:52 +08:00
Misaka_Company
2f73dd07b5 test: add configuration loading tests
- Add test_config.py with tests for load_settings function
- Add test_config.yaml fixture with test database configuration
- Tests cover: successful loading, file not found error, database URL generation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:36:30 +08:00
Misaka_Company
7478b7176f feat: add YAML configuration module with pydantic-yaml
- Created config directory with settings.py, __init__.py, and settings.yaml
- Implemented SqlServerConfig and DatabaseConfig classes for SQL Server connections
- Added Settings class with database_url property for SQLAlchemy URL construction
- Uses parse_yaml_raw_as for UTF-8 safe YAML loading on Windows
- Configuration validated successfully via import test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:35:03 +08:00
Misaka_Company
33fee72e3f deps: add pydantic-yaml for YAML configuration support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:32:53 +08:00
Misaka_Company
71d261af26 docs: add YAML configuration implementation plan
Add detailed step-by-step implementation plan for migrating
from .env to YAML configuration using pydantic-yaml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:26:26 +08:00
Misaka_Company
01423ccccc docs: add YAML configuration design document
Add design document for migrating from .env to YAML configuration
using pydantic-yaml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-12 09:25:46 +08:00
16 changed files with 1023 additions and 43 deletions

6
.gitignore vendored
View File

@@ -7,3 +7,9 @@ dist/
build/
.pytest_cache/
.claude/
# Config with secrets
config/settings.yaml
config/settings.local.yaml
.runtime

View File

@@ -11,3 +11,35 @@ source .venv/bin/activate # Linux/Mac
pip install -r requirements.txt
uvicorn app.main:app --reload
```
## Configuration
Configuration is managed via YAML files in the `config/` directory.
### Configuration File
Edit `config/settings.yaml` to configure your environment:
```yaml
database:
active: sql_server # sql_server or postgresql
sql_server:
host: your-host
port: 1433
database: your-database
username: your-username
password: your-password
postgresql:
host: your-postgres-host
port: 5432
database: your-database
username: your-username
password: your-password
```
Set `database.active` to choose the database backend. Both backends expect the
same database name, schemas, and table structure.
### Local Overrides
For local development, create `config/settings.local.yaml` to override specific values without committing them.

View File

@@ -1,32 +0,0 @@
from sqlalchemy.engine import URL
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
SQL_SERVER_HOST: str
SQL_SERVER_PORT: int = 1433
SQL_SERVER_DATABASE: str
SQL_SERVER_USERNAME: str
SQL_SERVER_PASSWORD: str
SQL_SERVER_DRIVER: str = "{ODBC Driver 18 for SQL Server}"
SQL_SERVER_TRUST_SERVER_CERTIFICATE: str = "yes"
@property
def database_url(self) -> URL:
return URL.create(
"mssql+pyodbc",
username=self.SQL_SERVER_USERNAME,
password=self.SQL_SERVER_PASSWORD,
host=self.SQL_SERVER_HOST,
port=self.SQL_SERVER_PORT,
database=self.SQL_SERVER_DATABASE,
query={
"driver": self.SQL_SERVER_DRIVER.strip("{}"),
"TrustServerCertificate": self.SQL_SERVER_TRUST_SERVER_CERTIFICATE,
},
)
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
settings = Settings()

View File

@@ -3,7 +3,7 @@ from typing import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from app.core.config import settings
from config.settings import settings
engine = create_engine(
settings.database_url,

View File

@@ -1,11 +1,25 @@
import sys
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from config.settings import load_settings
# 加载配置文件,失败则退出
try:
settings = load_settings()
except FileNotFoundError as e:
print(f"配置文件错误: {e}")
sys.exit(1)
except Exception as e:
print(f"加载配置失败: {e}")
sys.exit(1)
from app.api.v1.location import router as location_router
from app.api.v1.box import router as box_router
from app.services.location_service import DuplicateLocationError
from app.services.box_service import ZongpaiNotFoundError, DuplicateBoxItemError, InvalidZongpaiError
from app.services.box_service import ZongpaiNotFoundError, DuplicateBoxItemError, InvalidZongpaiError, DuplicateZongpaiBindError
app = FastAPI(title="CargoTrace API", version="0.1.0")
@@ -92,6 +106,20 @@ async def duplicate_box_handler(request: Request, exc: DuplicateBoxItemError):
)
@app.exception_handler(DuplicateZongpaiBindError)
async def duplicate_zongpai_bind_handler(request: Request, exc: DuplicateZongpaiBindError):
return JSONResponse(
status_code=409,
content={
"error_code": "DUPLICATE_ZONGPAI_BIND",
"message": f"总排号 {exc.zongpai_no} 已绑定箱号 {exc.box_no},请勿重复装箱",
"paichan_no": exc.paichan_no,
"box_no": exc.box_no,
"zongpai_no": exc.zongpai_no,
},
)
@app.get("/")
async def root():
return {"message": "Welcome to CargoTrace API"}

View File

@@ -20,7 +20,10 @@ class FinishedGoodsLocation(Base):
zongpai_no: Mapped[str] = mapped_column(String(64), nullable=False)
location_code: Mapped[str] = mapped_column(String(64), nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.getdate()
DateTime,
nullable=False,
default=func.current_timestamp(),
server_default=func.current_timestamp(),
)
@@ -36,7 +39,10 @@ class FinishedGoodsBox(Base):
paichan_no: Mapped[str] = mapped_column(String(64), nullable=False)
box_no: Mapped[int] = mapped_column(nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.getdate()
DateTime,
nullable=False,
default=func.current_timestamp(),
server_default=func.current_timestamp(),
)
@@ -53,5 +59,8 @@ class FinishedGoodsBoxItem(Base):
zongpai_no: Mapped[str] = mapped_column(String(64), nullable=False)
quantity: Mapped[float | None] = mapped_column(Numeric(18, 3), nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.getdate()
DateTime,
nullable=False,
default=func.current_timestamp(),
server_default=func.current_timestamp(),
)

View File

@@ -32,6 +32,7 @@ class BoxSaveRequest(BaseModel):
zongpai_no: str
box_no: int
quantity: int
box_mode: str | None = None # one-to-one | one-to-many | many-to-one
@field_validator("zongpai_no")
@classmethod

View File

@@ -23,13 +23,16 @@ class DuplicateBoxItemError(Exception):
self.box_no = box_no
class DuplicateZongpaiBindError(Exception):
def __init__(self, paichan_no: str, box_no: int, zongpai_no: str):
self.paichan_no = paichan_no
self.box_no = box_no
self.zongpai_no = zongpai_no
def query_erp_info(db: Session, zongpai_no: str) -> dict:
"""从 ERP 视图查询总排号对应的排产号和数量。"""
sql = text(
"SELECT TOP 1 [总排号], [排产号], [数量] "
"FROM [ERPAuto].[vw_productionContractData] "
"WHERE [总排号] = :zongpai_no"
)
sql = text(_erp_info_sql(db))
row = db.execute(sql, {"zongpai_no": zongpai_no}).fetchone()
if not row:
raise ZongpaiNotFoundError()
@@ -40,6 +43,22 @@ def query_erp_info(db: Session, zongpai_no: str) -> dict:
}
def _erp_info_sql(db: Session) -> str:
dialect_name = db.bind.dialect.name if db.bind is not None else ""
if dialect_name == "postgresql":
return (
'SELECT "总排号", "排产号", "数量" '
'FROM "ERPAuto"."vw_productionContractData" '
'WHERE "总排号" = :zongpai_no '
"LIMIT 1"
)
return (
"SELECT TOP 1 [总排号], [排产号], [数量] "
"FROM [ERPAuto].[vw_productionContractData] "
"WHERE [总排号] = :zongpai_no"
)
def get_box_info(db: Session, zongpai_no: str) -> dict:
"""获取装箱信息:排产号、数量、已有箱号明细。"""
zongpai_no = zongpai_no.strip().upper()
@@ -89,6 +108,19 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
erp = query_erp_info(db, req.zongpai_no)
paichan_no = erp["paichan_no"]
if req.box_mode == "one-to-one":
existing_box_no = (
db.query(FinishedGoodsBox.box_no)
.join(FinishedGoodsBoxItem, FinishedGoodsBox.id == FinishedGoodsBoxItem.box_id)
.filter(
FinishedGoodsBox.paichan_no == paichan_no,
FinishedGoodsBoxItem.zongpai_no == req.zongpai_no,
)
.scalar()
)
if existing_box_no is not None:
raise DuplicateZongpaiBindError(paichan_no, existing_box_no, req.zongpai_no)
box = (
db.query(FinishedGoodsBox)
.filter(

3
config/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from config.settings import settings, Settings, load_settings
__all__ = ["settings", "Settings", "load_settings"]

94
config/settings.py Normal file
View File

@@ -0,0 +1,94 @@
from pathlib import Path
from typing import Literal
from sqlalchemy.engine import URL
from pydantic import BaseModel
from pydantic_yaml import parse_yaml_raw_as
class SqlServerConfig(BaseModel):
"""SQL Server 连接配置"""
host: str
port: int = 1433
database: str
username: str
password: str
driver: str = "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: str = "yes"
class PostgreSqlConfig(BaseModel):
"""PostgreSQL 连接配置"""
host: str
port: int = 5432
database: str
username: str
password: str
class DatabaseConfig(BaseModel):
"""数据库配置"""
active: Literal["sql_server", "postgresql"] = "sql_server"
sql_server: SqlServerConfig
postgresql: PostgreSqlConfig | None = None
class Settings(BaseModel):
"""应用配置"""
database: DatabaseConfig
@property
def database_url(self) -> URL:
"""构建数据库连接 URL"""
if self.database.active == "postgresql":
return self._postgresql_url()
return self._sql_server_url()
def _sql_server_url(self) -> URL:
conf = self.database.sql_server
return URL.create(
"mssql+pyodbc",
username=conf.username,
password=conf.password,
host=conf.host,
port=conf.port,
database=conf.database,
query={
"driver": conf.driver.strip("{}"),
"TrustServerCertificate": conf.trust_server_certificate,
},
)
def _postgresql_url(self) -> URL:
conf = self.database.postgresql
if conf is None:
raise ValueError("已选择 postgresql但未配置 database.postgresql")
return URL.create(
"postgresql+psycopg",
username=conf.username,
password=conf.password,
host=conf.host,
port=conf.port,
database=conf.database,
)
def load_settings(config_path: str = "config/settings.yaml") -> Settings:
"""加载 YAML 配置文件"""
# Resolve relative to the project root (where config/ directory exists)
path = Path(config_path)
if not path.is_absolute():
# __file__ is config/settings.py, so parent.parent gives us project root
project_root = Path(__file__).resolve().parent.parent
path = project_root / config_path
if not path.exists():
raise FileNotFoundError(
f"配置文件不存在: {config_path}\n"
f"请确保文件存在于项目根目录或指定正确路径"
)
# Use UTF-8 encoding to avoid Windows GBK encoding issues
with open(path, "r", encoding="utf-8") as f:
return parse_yaml_raw_as(Settings, f)
# 全局配置单例
settings = load_settings()

View File

@@ -0,0 +1,17 @@
# 数据库配置
database:
active: sql_server # 可选: sql_server, postgresql
sql_server:
host: YOUR_DB_HOST
port: 1433
database: YOUR_DB_NAME
username: YOUR_USERNAME
password: YOUR_PASSWORD
driver: "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: yes
postgresql:
host: YOUR_POSTGRES_HOST
port: 5432
database: YOUR_POSTGRES_DB_NAME
username: YOUR_POSTGRES_USERNAME
password: YOUR_POSTGRES_PASSWORD

View File

@@ -0,0 +1,223 @@
# YAML 配置系统设计文档
**日期:** 2026-05-12
**作者:** Claude
**状态:** 已批准
## 概述
将 FastAPI 服务的配置管理从 `.env` 文件迁移到 YAML 格式,提升配置可读性和团队维护性。
## 需求背景
- **驱动因素:** 更好的可读性,便于团队维护
- **环境支持:** 单文件配置,无需多环境切换
- **实现方案:** 使用 `pydantic-yaml` 保留类型验证
## 文件结构
```
services/fastapi/
├── config/
│ ├── __init__.py
│ ├── settings.yaml # 配置文件
│ └── settings.py # 配置类定义
├── app/
│ ├── core/
│ │ └── database.py # 使用配置
│ └── ...
├── tests/
│ ├── fixtures/
│ │ └── test_config.yaml
│ └── ...
└── requirements.txt
```
## 配置文件格式
### config/settings.yaml
```yaml
# 数据库配置
database:
sql_server:
host: 192.168.110.114
port: 1433
database: CompanyDB
username: peng
password: Cqbld123456.
driver: "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: yes
```
## 配置类设计
### config/settings.py
```python
from pathlib import Path
from sqlalchemy.engine import URL
from pydantic import BaseModel
from pydantic_yaml import YamlModel
class SqlServerConfig(BaseModel):
"""SQL Server 连接配置"""
host: str
port: int = 1433
database: str
username: str
password: str
driver: str = "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: str = "yes"
class Settings(YamlModel):
"""应用配置"""
database: SqlServerConfig
@property
def database_url(self) -> URL:
"""构建数据库连接 URL"""
conf = self.database.sql_server
return URL.create(
"mssql+pyodbc",
username=conf.username,
password=conf.password,
host=conf.host,
port=conf.port,
database=conf.database,
query={
"driver": conf.driver.strip("{}"),
"TrustServerCertificate": conf.trust_server_certificate,
},
)
def load_settings(config_path: str = "config/settings.yaml") -> Settings:
"""加载 YAML 配置文件"""
path = Path(config_path)
if not path.exists():
raise FileNotFoundError(
f"配置文件不存在: {config_path}\n"
f"请确保文件存在于项目根目录或指定正确路径"
)
return Settings.parse_yaml_file(path)
# 全局配置单例
settings = load_settings()
```
### config/__init__.py
```python
from config.settings import settings, Settings, load_settings
__all__ = ["settings", "Settings", "load_settings"]
```
## 依赖变更
### requirements.txt 新增
```txt
pydantic-yaml>=0.12.0
pyyaml>=6.0
```
### 可选移除
```txt
python-dotenv # 如无其他用途
```
## 导入路径变更
| 旧导入 | 新导入 |
|--------|--------|
| `from app.core.config import settings` | `from config.settings import settings` |
## 错误处理
### 应用启动时验证
```python
# app/main.py
import sys
from config.settings import load_settings
try:
settings = load_settings()
except FileNotFoundError as e:
print(f"配置文件错误: {e}")
sys.exit(1)
except Exception as e:
print(f"加载配置失败: {e}")
sys.exit(1)
```
## 测试策略
### 单元测试
```python
# tests/test_config.py
import pytest
from config.settings import load_settings
def test_load_settings_success():
settings = load_settings("tests/fixtures/test_config.yaml")
assert settings.database.sql_server.port == 1433
def test_load_settings_file_not_found():
with pytest.raises(FileNotFoundError):
load_settings("nonexistent.yaml")
def test_database_url_property():
settings = load_settings("tests/fixtures/test_config.yaml")
url = settings.database_url
assert "mssql+pyodbc" in url
```
### 测试配置文件
```yaml
# tests/fixtures/test_config.yaml
database:
sql_server:
host: localhost
port: 1433
database: TestDB
username: test_user
password: test_pass
driver: "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: yes
```
## 迁移步骤
1. **新增依赖** - 更新 requirements.txt
2. **创建模块** - 创建 config/ 目录和相关文件
3. **更新导入** - 替换所有 `from app.core.config import settings`
4. **更新测试** - 创建测试配置和夹具
5. **清理旧代码** - 删除旧配置文件
6. **验证** - 运行测试和启动应用
## 后续扩展
如需支持多环境,可通过以下方式扩展:
```yaml
# config/settings.base.yaml (基础配置)
database:
sql_server:
driver: "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: yes
# config/settings.dev.yaml (开发环境覆盖)
database:
sql_server:
host: localhost
database: DevDB
```

View File

@@ -0,0 +1,511 @@
# YAML Configuration Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Migrate FastAPI configuration from .env to YAML using pydantic-yaml
**Architecture:** Create new `config/` module with YAML-based settings using `pydantic-yaml`, preserving type validation and improving readability through nested configuration structure.
**Tech Stack:** pydantic-yaml, pyyaml, pytest
---
## Task 1: Add pydantic-yaml Dependency
**Files:**
- Modify: `requirements.txt`
**Step 1: Add pydantic-yaml to requirements.txt**
Add this line to `requirements.txt`:
```txt
pydantic-yaml>=0.12.0
```
**Step 2: Install the dependency**
Run: `.venv/Scripts/pip install pydantic-yaml`
Expected: Successfully installs pydantic-yaml and its dependencies
**Step 3: Verify installation**
Run: `.venv/Scripts/python -c "import pydantic_yaml; print(pydantic_yaml.__version__)"`
Expected: Prints version number without errors
**Step 4: Commit**
```bash
git add requirements.txt
git commit -m "deps: add pydantic-yaml for YAML configuration support"
```
---
## Task 2: Create config Directory Structure
**Files:**
- Create: `config/__init__.py`
- Create: `config/settings.py`
- Create: `config/settings.yaml`
**Step 1: Create config directory**
Run: `mkdir config`
**Step 2: Create config/__init__.py**
```python
from config.settings import settings, Settings, load_settings
__all__ = ["settings", "Settings", "load_settings"]
```
**Step 3: Create config/settings.py with configuration classes**
```python
from pathlib import Path
from sqlalchemy.engine import URL
from pydantic import BaseModel
from pydantic_yaml import YamlModel
class SqlServerConfig(BaseModel):
"""SQL Server 连接配置"""
host: str
port: int = 1433
database: str
username: str
password: str
driver: str = "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: str = "yes"
class Settings(YamlModel):
"""应用配置"""
database: SqlServerConfig
@property
def database_url(self) -> URL:
"""构建数据库连接 URL"""
conf = self.database.sql_server
return URL.create(
"mssql+pyodbc",
username=conf.username,
password=conf.password,
host=conf.host,
port=conf.port,
database=conf.database,
query={
"driver": conf.driver.strip("{}"),
"TrustServerCertificate": conf.trust_server_certificate,
},
)
def load_settings(config_path: str = "config/settings.yaml") -> Settings:
"""加载 YAML 配置文件"""
path = Path(config_path)
if not path.exists():
raise FileNotFoundError(
f"配置文件不存在: {config_path}\n"
f"请确保文件存在于项目根目录或指定正确路径"
)
return Settings.parse_yaml_file(path)
# 全局配置单例
settings = load_settings()
```
**Step 4: Create config/settings.yaml with current configuration**
```yaml
# 数据库配置
database:
sql_server:
host: 192.168.110.114
port: 1433
database: CompanyDB
username: peng
password: Cqbld123456.
driver: "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: yes
```
**Step 5: Commit**
```bash
git add config/
git commit -m "feat: add YAML configuration module with pydantic-yaml"
```
---
## Task 3: Write Configuration Loading Tests
**Files:**
- Create: `tests/test_config.py`
- Create: `tests/fixtures/test_config.yaml`
**Step 1: Create tests/fixtures directory**
Run: `mkdir tests/fixtures`
**Step 2: Create tests/fixtures/test_config.yaml**
```yaml
# 测试配置
database:
sql_server:
host: localhost
port: 1433
database: TestDB
username: test_user
password: test_pass
driver: "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: yes
```
**Step 3: Write the failing test for settings loading**
Create `tests/test_config.py`:
```python
import pytest
from config.settings import load_settings
def test_load_settings_success():
"""测试成功加载配置文件"""
settings = load_settings("tests/fixtures/test_config.yaml")
assert settings.database.sql_server.port == 1433
assert settings.database.sql_server.host == "localhost"
assert settings.database.sql_server.database == "TestDB"
def test_load_settings_file_not_found():
"""测试配置文件不存在时抛出异常"""
with pytest.raises(FileNotFoundError):
load_settings("nonexistent.yaml")
def test_database_url_property():
"""测试 database_url 属性生成"""
settings = load_settings("tests/fixtures/test_config.yaml")
url = settings.database_url
assert "mssql+pyodbc" in str(url)
assert "test_user" in str(url)
assert "TestDB" in str(url)
```
**Step 4: Run tests to verify they pass**
Run: `.venv/Scripts/pytest tests/test_config.py -v`
Expected: All tests PASS
**Step 5: Commit**
```bash
git add tests/test_config.py tests/fixtures/test_config.yaml
git commit -m "test: add configuration loading tests"
```
---
## Task 4: Update database.py to Use New Config
**Files:**
- Modify: `app/core/database.py`
**Step 1: Read current database.py to understand usage**
Check the current import and usage of settings
**Step 2: Update import statement**
Change:
```python
from app.core.config import settings
```
To:
```python
from config.settings import settings
```
**Step 3: Verify the rest of the code works unchanged**
The database_url property should work exactly as before
**Step 4: Run existing tests to ensure no breakage**
Run: `.venv/Scripts/pytest tests/ -v`
Expected: All existing tests still PASS
**Step 5: Commit**
```bash
git add app/core/database.py
git commit -m "refactor: update database.py to use new config module"
```
---
## Task 5: Update API Module Imports
**Files:**
- Modify: `app/api/v1/box.py`
- Modify: `app/api/v1/location.py`
- Modify: `app/services/box_service.py`
- Modify: `app/services/location_service.py`
**Step 1: Check all files that import from app.core.config**
Run: `grep -r "from app.core.config import" app/`
**Step 2: Update each file's import**
Change:
```python
from app.core.config import settings
```
To:
```python
from config.settings import settings
```
**Step 3: Run tests after each file change**
Run: `.venv/Scripts/pytest tests/ -v`
Expected: All tests PASS
**Step 4: Commit all import changes**
```bash
git add app/api/v1/box.py app/api/v1/location.py app/services/
git commit -m "refactor: update all imports to use new config module"
```
---
## Task 6: Update Test Fixtures
**Files:**
- Modify: `tests/conftest.py`
**Step 1: Read current conftest.py**
Check for any references to app.core.config
**Step 2: Update imports in conftest.py**
Change any:
```python
from app.core.config import settings
```
To:
```python
from config.settings import settings
```
**Step 3: Run tests**
Run: `.venv/Scripts/pytest tests/ -v`
Expected: All tests PASS
**Step 4: Commit**
```bash
git add tests/conftest.py
git commit -m "test: update conftest imports"
```
---
## Task 7: Update Main Application Entry Point
**Files:**
- Modify: `app/main.py`
**Step 1: Read current main.py**
Check for any config-related initialization
**Step 2: Add error handling for configuration loading**
Add at the top of main.py or update existing initialization:
```python
import sys
from config.settings import load_settings
try:
settings = load_settings()
except FileNotFoundError as e:
print(f"配置文件错误: {e}")
sys.exit(1)
except Exception as e:
print(f"加载配置失败: {e}")
sys.exit(1)
```
**Step 3: Test application startup**
Run: `.venv/Scripts/python -m app.main`
Expected: Application starts without errors
**Step 4: Commit**
```bash
git add app/main.py
git commit -m "feat: add config error handling on startup"
```
---
## Task 8: Remove Old Configuration Files
**Files:**
- Delete: `app/core/config.py`
- Delete: `.env`
**Step 1: Verify no remaining imports of old config**
Run: `grep -r "from app.core.config import" .`
Expected: No results (or only in .git directory)
**Step 2: Remove old config.py file**
Run: `rm app/core/config.py`
**Step 3: Remove .env file**
Run: `rm .env`
**Step 4: Add .env to .gitignore if not already present**
Ensure `.env` is in `.gitignore`
**Step 5: Run final test suite**
Run: `.venv/Scripts/pytest tests/ -v`
Expected: All tests PASS
**Step 6: Run application to verify**
Run: `.venv/Scripts/python -m app.main`
Expected: Application starts successfully
**Step 7: Commit cleanup**
```bash
git add app/core/config.py .env .gitignore
git commit -m "refactor: remove old .env and config.py files"
```
---
## Task 9: Update .gitignore for YAML Config
**Files:**
- Modify: `.gitignore` (if it exists, otherwise create)
**Step 1: Check if .gitignore exists**
Run: `ls -la .gitignore`
**Step 2: Add or verify config/settings.local.yaml is ignored**
Add to `.gitignore`:
```gitignore
# Local configuration overrides
config/settings.local.yaml
```
This allows developers to have local overrides without committing them
**Step 3: Commit**
```bash
git add .gitignore
git commit -m "chore: ignore local YAML config overrides"
```
---
## Task 10: Final Verification and Documentation
**Files:**
- Update: `README.md` (if exists)
**Step 1: Run complete test suite**
Run: `.venv/Scripts/pytest tests/ -v --cov=app --cov-report=term-missing`
Expected: All tests pass, coverage maintained
**Step 2: Start application and verify database connection**
Run: `.venv/Scripts/python -m app.main`
Expected: Application connects to database successfully
**Step 3: Update README.md with configuration instructions**
Add to README.md:
```markdown
## Configuration
Configuration is managed via YAML files in the `config/` directory.
### Configuration File
Edit `config/settings.yaml` to configure your environment:
```yaml
database:
sql_server:
host: your-host
port: 1433
database: your-database
username: your-username
password: your-password
```
### Local Overrides
For local development, create `config/settings.local.yaml` to override specific values without committing them.
```
**Step 4: Final commit**
```bash
git add README.md
git commit -m "docs: update README with YAML configuration instructions"
```
---
## Verification Checklist
After completing all tasks:
- [ ] All tests pass: `pytest tests/ -v`
- [ ] Application starts successfully
- [ ] Database connection works
- [ ] No references to `app.core.config` remain
- [ ] `.env` file removed
- [ ] YAML config file exists and is valid
- [ ] README updated with configuration instructions

View File

@@ -2,7 +2,9 @@ fastapi>=0.115.0
uvicorn[standard]>=0.34.0
sqlalchemy>=2.0.0
pyodbc>=5.2.0
psycopg[binary]>=3.2.0
pydantic-settings>=2.0.0
python-dotenv>=1.0.0
pytest>=8.0.0
httpx>=0.28.0
pydantic-yaml>=0.12.0

17
tests/fixtures/test_config.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
# 测试配置
database:
active: sql_server
sql_server:
host: localhost
port: 1433
database: TestDB
username: test_user
password: test_pass
driver: "{ODBC Driver 18 for SQL Server}"
trust_server_certificate: yes
postgresql:
host: localhost
port: 5432
database: TestDB
username: test_user
password: test_pass

37
tests/test_config.py Normal file
View File

@@ -0,0 +1,37 @@
import pytest
from config.settings import load_settings
def test_load_settings_success():
"""测试成功加载配置文件"""
settings = load_settings("tests/fixtures/test_config.yaml")
assert settings.database.active == "sql_server"
assert settings.database.sql_server.port == 1433
assert settings.database.sql_server.host == "localhost"
assert settings.database.sql_server.database == "TestDB"
def test_load_settings_file_not_found():
"""测试配置文件不存在时抛出异常"""
with pytest.raises(FileNotFoundError):
load_settings("nonexistent.yaml")
def test_database_url_property():
"""测试 database_url 属性生成"""
settings = load_settings("tests/fixtures/test_config.yaml")
url = settings.database_url
assert "mssql+pyodbc" in str(url)
assert "test_user" in str(url)
assert "TestDB" in str(url)
def test_postgresql_database_url_property():
"""测试 PostgreSQL database_url 属性生成"""
settings = load_settings("tests/fixtures/test_config.yaml")
settings.database.active = "postgresql"
url = settings.database_url
assert url.drivername == "postgresql+psycopg"
assert url.host == "localhost"
assert url.port == 5432
assert url.database == "TestDB"