diff --git a/.gitignore b/.gitignore index 912bcc8..d27675c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ build/ # Config with secrets config/settings.yaml -config/settings.local.yaml \ No newline at end of file +config/settings.local.yaml + +.runtime \ No newline at end of file diff --git a/README.md b/README.md index 2a8b87f..5e3d48f 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,24 @@ 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. diff --git a/app/models/finished_goods.py b/app/models/finished_goods.py index 01edf05..be55334 100644 --- a/app/models/finished_goods.py +++ b/app/models/finished_goods.py @@ -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(), ) diff --git a/app/services/box_service.py b/app/services/box_service.py index d9d5f21..ab4eab3 100644 --- a/app/services/box_service.py +++ b/app/services/box_service.py @@ -25,11 +25,7 @@ class DuplicateBoxItemError(Exception): 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 +36,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() diff --git a/config/settings.py b/config/settings.py index ae083cf..54e6986 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,7 +1,7 @@ from pathlib import Path -from typing import Optional +from typing import Literal from sqlalchemy.engine import URL -from pydantic import BaseModel, Field +from pydantic import BaseModel from pydantic_yaml import parse_yaml_raw_as @@ -16,9 +16,20 @@ class SqlServerConfig(BaseModel): 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): @@ -28,6 +39,11 @@ class Settings(BaseModel): @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", @@ -42,6 +58,19 @@ class Settings(BaseModel): }, ) + 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 配置文件""" diff --git a/config/settings.yaml.example b/config/settings.yaml.example index 26ffbe2..0722830 100644 --- a/config/settings.yaml.example +++ b/config/settings.yaml.example @@ -1,5 +1,6 @@ # 数据库配置 database: + active: sql_server # 可选: sql_server, postgresql sql_server: host: YOUR_DB_HOST port: 1433 @@ -8,3 +9,9 @@ database: 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 diff --git a/requirements.txt b/requirements.txt index 89e772f..41889a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ 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 diff --git a/tests/fixtures/test_config.yaml b/tests/fixtures/test_config.yaml index 6ef1259..605c6df 100644 --- a/tests/fixtures/test_config.yaml +++ b/tests/fixtures/test_config.yaml @@ -1,5 +1,6 @@ # 测试配置 database: + active: sql_server sql_server: host: localhost port: 1433 @@ -8,3 +9,9 @@ database: 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 diff --git a/tests/test_config.py b/tests/test_config.py index 9c51154..2fa7c1a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,6 +5,7 @@ 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" @@ -23,3 +24,14 @@ def test_database_url_property(): 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"