Compare commits
11 Commits
worktree-y
...
53abead657
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53abead657 | ||
|
|
7fe1fe7ef5 | ||
|
|
25982b12bf | ||
|
|
ee2b2f8f25 | ||
|
|
4b8c502a91 | ||
|
|
b1e369e9f3 | ||
|
|
7305ad9ae6 | ||
|
|
fb759ebe33 | ||
|
|
b9e13105e1 | ||
|
|
70cb6759ea | ||
|
|
f94a18fee7 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -6,4 +6,10 @@ __pycache__/
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.claude/
|
.claude/
|
||||||
|
.agents/
|
||||||
|
# Config with secrets
|
||||||
|
config/settings.yaml
|
||||||
|
config/settings.local.yaml
|
||||||
|
|
||||||
|
.runtime
|
||||||
32
README.md
32
README.md
@@ -11,3 +11,35 @@ source .venv/bin/activate # Linux/Mac
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
uvicorn app.main:app --reload
|
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.
|
||||||
|
|||||||
@@ -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()
|
|
||||||
30
app/main.py
30
app/main.py
@@ -1,11 +1,25 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import RequestValidationError
|
||||||
from fastapi.responses import JSONResponse
|
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.location import router as location_router
|
||||||
from app.api.v1.box import router as box_router
|
from app.api.v1.box import router as box_router
|
||||||
from app.services.location_service import DuplicateLocationError
|
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")
|
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("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return {"message": "Welcome to CargoTrace API"}
|
return {"message": "Welcome to CargoTrace API"}
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ class FinishedGoodsLocation(Base):
|
|||||||
zongpai_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
zongpai_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
location_code: Mapped[str] = mapped_column(String(64), nullable=False)
|
location_code: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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)
|
paichan_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
box_no: Mapped[int] = mapped_column(nullable=False)
|
box_no: Mapped[int] = mapped_column(nullable=False)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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)
|
zongpai_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
quantity: Mapped[float | None] = mapped_column(Numeric(18, 3), nullable=True)
|
quantity: Mapped[float | None] = mapped_column(Numeric(18, 3), nullable=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
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(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ ZONGPAI_PATTERN = re.compile(r"^(\d{2}(B|C|T)\d+|\d{2}(BW|CW)\d{4})$")
|
|||||||
class BoxItemDetail(BaseModel):
|
class BoxItemDetail(BaseModel):
|
||||||
"""箱号下某个总排号的明细"""
|
"""箱号下某个总排号的明细"""
|
||||||
zongpai_no: str
|
zongpai_no: str
|
||||||
|
work_order_no: str | None = None
|
||||||
quantity: int
|
quantity: int
|
||||||
|
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ class BoxInfoResponse(BaseModel):
|
|||||||
"""GET /box/info 响应"""
|
"""GET /box/info 响应"""
|
||||||
zongpai_no: str
|
zongpai_no: str
|
||||||
paichan_no: str
|
paichan_no: str
|
||||||
|
work_order_no: str | None = None
|
||||||
quantity: int
|
quantity: int
|
||||||
existing_boxes: list[BoxDetail]
|
existing_boxes: list[BoxDetail]
|
||||||
max_box_no: int
|
max_box_no: int
|
||||||
@@ -32,6 +34,7 @@ class BoxSaveRequest(BaseModel):
|
|||||||
zongpai_no: str
|
zongpai_no: str
|
||||||
box_no: int
|
box_no: int
|
||||||
quantity: int
|
quantity: int
|
||||||
|
box_mode: str | None = None # one-to-one | one-to-many | many-to-one
|
||||||
|
|
||||||
@field_validator("zongpai_no")
|
@field_validator("zongpai_no")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import bindparam, text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.models.finished_goods import FinishedGoodsBox, FinishedGoodsBoxItem
|
from app.models.finished_goods import FinishedGoodsBox, FinishedGoodsBoxItem
|
||||||
@@ -23,13 +23,16 @@ class DuplicateBoxItemError(Exception):
|
|||||||
self.box_no = box_no
|
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:
|
def query_erp_info(db: Session, zongpai_no: str) -> dict:
|
||||||
"""从 ERP 视图查询总排号对应的排产号和数量。"""
|
"""从 ERP 视图查询总排号对应的排产号、工令号和数量。"""
|
||||||
sql = text(
|
sql = text(_erp_info_sql(db))
|
||||||
"SELECT TOP 1 [总排号], [排产号], [数量] "
|
|
||||||
"FROM [ERPAuto].[vw_productionContractData] "
|
|
||||||
"WHERE [总排号] = :zongpai_no"
|
|
||||||
)
|
|
||||||
row = db.execute(sql, {"zongpai_no": zongpai_no}).fetchone()
|
row = db.execute(sql, {"zongpai_no": zongpai_no}).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
raise ZongpaiNotFoundError()
|
raise ZongpaiNotFoundError()
|
||||||
@@ -37,9 +40,51 @@ def query_erp_info(db: Session, zongpai_no: str) -> dict:
|
|||||||
"zongpai_no": row[0],
|
"zongpai_no": row[0],
|
||||||
"paichan_no": row[1],
|
"paichan_no": row[1],
|
||||||
"quantity": int(row[2]),
|
"quantity": int(row[2]),
|
||||||
|
"work_order_no": row[3],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 query_erp_work_order_map(db: Session, zongpai_nos: list[str]) -> dict[str, str | None]:
|
||||||
|
"""批量查询总排号对应的工令号。"""
|
||||||
|
if not zongpai_nos:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
dialect_name = db.bind.dialect.name if db.bind is not None else ""
|
||||||
|
if dialect_name == "postgresql":
|
||||||
|
sql = text(
|
||||||
|
'SELECT "总排号", "工令号" '
|
||||||
|
'FROM "ERPAuto"."vw_productionContractData" '
|
||||||
|
'WHERE "总排号" IN :zongpai_nos'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sql = text(
|
||||||
|
"SELECT [总排号], [工令号] "
|
||||||
|
"FROM [ERPAuto].[vw_productionContractData] "
|
||||||
|
"WHERE [总排号] IN :zongpai_nos"
|
||||||
|
)
|
||||||
|
rows = db.execute(
|
||||||
|
sql.bindparams(bindparam("zongpai_nos", expanding=True)),
|
||||||
|
{"zongpai_nos": sorted(set(zongpai_nos))},
|
||||||
|
).fetchall()
|
||||||
|
return {row[0]: row[1] for row in rows}
|
||||||
|
|
||||||
|
|
||||||
def get_box_info(db: Session, zongpai_no: str) -> dict:
|
def get_box_info(db: Session, zongpai_no: str) -> dict:
|
||||||
"""获取装箱信息:排产号、数量、已有箱号明细。"""
|
"""获取装箱信息:排产号、数量、已有箱号明细。"""
|
||||||
zongpai_no = zongpai_no.strip().upper()
|
zongpai_no = zongpai_no.strip().upper()
|
||||||
@@ -56,18 +101,31 @@ def get_box_info(db: Session, zongpai_no: str) -> dict:
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_boxes = []
|
box_items = {}
|
||||||
max_box_no = 0
|
item_zongpai_nos = []
|
||||||
for box in boxes:
|
for box in boxes:
|
||||||
items = (
|
items = (
|
||||||
db.query(FinishedGoodsBoxItem)
|
db.query(FinishedGoodsBoxItem)
|
||||||
.filter(FinishedGoodsBoxItem.box_id == box.id)
|
.filter(FinishedGoodsBoxItem.box_id == box.id)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
box_items[box.id] = items
|
||||||
|
item_zongpai_nos.extend(item.zongpai_no for item in items)
|
||||||
|
|
||||||
|
work_order_map = query_erp_work_order_map(db, item_zongpai_nos)
|
||||||
|
|
||||||
|
existing_boxes = []
|
||||||
|
max_box_no = 0
|
||||||
|
for box in boxes:
|
||||||
|
items = box_items[box.id]
|
||||||
existing_boxes.append({
|
existing_boxes.append({
|
||||||
"box_no": box.box_no,
|
"box_no": box.box_no,
|
||||||
"items": [
|
"items": [
|
||||||
{"zongpai_no": item.zongpai_no, "quantity": int(item.quantity or 0)}
|
{
|
||||||
|
"zongpai_no": item.zongpai_no,
|
||||||
|
"work_order_no": work_order_map.get(item.zongpai_no),
|
||||||
|
"quantity": int(item.quantity or 0),
|
||||||
|
}
|
||||||
for item in items
|
for item in items
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -77,6 +135,7 @@ def get_box_info(db: Session, zongpai_no: str) -> dict:
|
|||||||
return {
|
return {
|
||||||
"zongpai_no": zongpai_no,
|
"zongpai_no": zongpai_no,
|
||||||
"paichan_no": paichan_no,
|
"paichan_no": paichan_no,
|
||||||
|
"work_order_no": erp["work_order_no"],
|
||||||
"quantity": quantity,
|
"quantity": quantity,
|
||||||
"existing_boxes": existing_boxes,
|
"existing_boxes": existing_boxes,
|
||||||
"max_box_no": max_box_no,
|
"max_box_no": max_box_no,
|
||||||
@@ -89,6 +148,19 @@ def save_box_record(db: Session, req: BoxSaveRequest) -> dict:
|
|||||||
erp = query_erp_info(db, req.zongpai_no)
|
erp = query_erp_info(db, req.zongpai_no)
|
||||||
paichan_no = erp["paichan_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 = (
|
box = (
|
||||||
db.query(FinishedGoodsBox)
|
db.query(FinishedGoodsBox)
|
||||||
.filter(
|
.filter(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Literal
|
||||||
from sqlalchemy.engine import URL
|
from sqlalchemy.engine import URL
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel
|
||||||
from pydantic_yaml import parse_yaml_raw_as
|
from pydantic_yaml import parse_yaml_raw_as
|
||||||
|
|
||||||
|
|
||||||
@@ -16,9 +16,20 @@ class SqlServerConfig(BaseModel):
|
|||||||
trust_server_certificate: str = "yes"
|
trust_server_certificate: str = "yes"
|
||||||
|
|
||||||
|
|
||||||
|
class PostgreSqlConfig(BaseModel):
|
||||||
|
"""PostgreSQL 连接配置"""
|
||||||
|
host: str
|
||||||
|
port: int = 5432
|
||||||
|
database: str
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig(BaseModel):
|
class DatabaseConfig(BaseModel):
|
||||||
"""数据库配置"""
|
"""数据库配置"""
|
||||||
|
active: Literal["sql_server", "postgresql"] = "sql_server"
|
||||||
sql_server: SqlServerConfig
|
sql_server: SqlServerConfig
|
||||||
|
postgresql: PostgreSqlConfig | None = None
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseModel):
|
class Settings(BaseModel):
|
||||||
@@ -28,6 +39,11 @@ class Settings(BaseModel):
|
|||||||
@property
|
@property
|
||||||
def database_url(self) -> URL:
|
def database_url(self) -> URL:
|
||||||
"""构建数据库连接 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
|
conf = self.database.sql_server
|
||||||
return URL.create(
|
return URL.create(
|
||||||
"mssql+pyodbc",
|
"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:
|
def load_settings(config_path: str = "config/settings.yaml") -> Settings:
|
||||||
"""加载 YAML 配置文件"""
|
"""加载 YAML 配置文件"""
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
# 数据库配置
|
|
||||||
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
|
|
||||||
17
config/settings.yaml.example
Normal file
17
config/settings.yaml.example
Normal 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
|
||||||
@@ -2,6 +2,7 @@ fastapi>=0.115.0
|
|||||||
uvicorn[standard]>=0.34.0
|
uvicorn[standard]>=0.34.0
|
||||||
sqlalchemy>=2.0.0
|
sqlalchemy>=2.0.0
|
||||||
pyodbc>=5.2.0
|
pyodbc>=5.2.0
|
||||||
|
psycopg[binary]>=3.2.0
|
||||||
pydantic-settings>=2.0.0
|
pydantic-settings>=2.0.0
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
pytest>=8.0.0
|
pytest>=8.0.0
|
||||||
|
|||||||
7
tests/fixtures/test_config.yaml
vendored
7
tests/fixtures/test_config.yaml
vendored
@@ -1,5 +1,6 @@
|
|||||||
# 测试配置
|
# 测试配置
|
||||||
database:
|
database:
|
||||||
|
active: sql_server
|
||||||
sql_server:
|
sql_server:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 1433
|
port: 1433
|
||||||
@@ -8,3 +9,9 @@ database:
|
|||||||
password: test_pass
|
password: test_pass
|
||||||
driver: "{ODBC Driver 18 for SQL Server}"
|
driver: "{ODBC Driver 18 for SQL Server}"
|
||||||
trust_server_certificate: yes
|
trust_server_certificate: yes
|
||||||
|
postgresql:
|
||||||
|
host: localhost
|
||||||
|
port: 5432
|
||||||
|
database: TestDB
|
||||||
|
username: test_user
|
||||||
|
password: test_pass
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ def test_box_info_success(client: TestClient):
|
|||||||
data = resp.json()
|
data = resp.json()
|
||||||
assert data["zongpai_no"] == "26BW0011"
|
assert data["zongpai_no"] == "26BW0011"
|
||||||
assert data["paichan_no"] == "W00009"
|
assert data["paichan_no"] == "W00009"
|
||||||
|
assert "work_order_no" in data
|
||||||
|
assert data["work_order_no"] is None or isinstance(data["work_order_no"], str)
|
||||||
assert data["quantity"] == 80
|
assert data["quantity"] == 80
|
||||||
assert "existing_boxes" in data
|
assert "existing_boxes" in data
|
||||||
assert "max_box_no" in data
|
assert "max_box_no" in data
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from config.settings import load_settings
|
|||||||
def test_load_settings_success():
|
def test_load_settings_success():
|
||||||
"""测试成功加载配置文件"""
|
"""测试成功加载配置文件"""
|
||||||
settings = load_settings("tests/fixtures/test_config.yaml")
|
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.port == 1433
|
||||||
assert settings.database.sql_server.host == "localhost"
|
assert settings.database.sql_server.host == "localhost"
|
||||||
assert settings.database.sql_server.database == "TestDB"
|
assert settings.database.sql_server.database == "TestDB"
|
||||||
@@ -23,3 +24,14 @@ def test_database_url_property():
|
|||||||
assert "mssql+pyodbc" in str(url)
|
assert "mssql+pyodbc" in str(url)
|
||||||
assert "test_user" in str(url)
|
assert "test_user" in str(url)
|
||||||
assert "TestDB" 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user