feat: 增加中文注释以提升代码可读性和维护性

This commit is contained in:
2026-03-12 14:28:06 +08:00
parent 61ffab497f
commit 0e9dd1cec1

87
app.py
View File

@@ -1,3 +1,11 @@
"""库存管理 Flask 应用主文件。
中文说明:
1. 这个文件同时承担了配置加载、数据库模型、数据修复、页面路由、统计计算、AI 补货建议等职责。
2. 为了便于你后续阅读和维护,关键函数上方会保留中文解释,说明“这个函数做什么”以及“为什么这样做”。
3. 这些中文解释属于代码可读性的一部分,不应在后续维护中随意删除。
"""
import os
import re
import csv
@@ -24,11 +32,13 @@ DB_DIR = os.path.join(BASE_DIR, "data")
os.makedirs(DB_DIR, exist_ok=True)
DB_PATH = os.path.join(DB_DIR, "inventory.db")
# Flask 和 SQLAlchemy 基础初始化。
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{DB_PATH}"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
# 这里集中放全局常量,避免后面函数里散落硬编码。
LOW_STOCK_THRESHOLD = 5
BOX_TYPES_OVERRIDE_PATH = os.path.join(DB_DIR, "box_types.json")
AI_SETTINGS_PATH = os.path.join(DB_DIR, "ai_settings.json")
@@ -86,6 +96,11 @@ BOX_TYPES = deepcopy(DEFAULT_BOX_TYPES)
def _apply_box_type_overrides() -> None:
"""加载盒型覆盖配置。
中文说明:默认盒型写在代码里;如果用户在页面上修改了名称、描述、前缀,
会写入 data/box_types.json这里负责把这些覆盖项合并回运行时配置。
"""
if not os.path.exists(BOX_TYPES_OVERRIDE_PATH):
return
@@ -156,6 +171,11 @@ def _save_ai_settings(settings: dict) -> None:
def _get_ai_settings() -> dict:
"""读取并清洗 AI / 立创接口配置。
中文说明:这个函数不只是“读文件”,还会把超时、阈值、布尔值这些字段
统一修正成安全可用的格式,避免后面的业务逻辑反复判空和转类型。
"""
settings = _load_ai_settings()
try:
@@ -226,6 +246,11 @@ def _extract_lcsc_product_id_from_input(raw_identifier: str) -> int | None:
def _fetch_lcsc_product_basic(product_identifier: str, settings: dict) -> dict:
"""调用立创开放接口,按商品详情页链接读取基础资料。
中文说明:当前业务只接受“商品详情页链接”,先从链接里提取 productId
再按 JOP 鉴权规则签名请求接口,最后返回接口中的第一条商品记录。
"""
raw_identifier = (product_identifier or "").strip()
if not raw_identifier:
raise RuntimeError("立创商品链接不能为空")
@@ -299,6 +324,11 @@ def _fetch_lcsc_product_basic(product_identifier: str, settings: dict) -> dict:
def _map_lcsc_product_to_component(product: dict) -> dict:
"""把立创商品字段映射为系统内部元件字段。
中文说明:接口原始字段很多,这里只提取库存系统真正会用到的料号、名称、
规格和备注,尽量保持结果简洁且便于搜索。
"""
product_model = str(product.get("productModel") or "").strip()
product_code = str(product.get("productCode") or "").strip()
product_id = product.get("productId")
@@ -364,6 +394,7 @@ def _map_lcsc_product_to_component(product: dict) -> dict:
}
# Box 表示一个容器/盒子Component 表示盒内某个位置上的元件。
class Box(db.Model):
__tablename__ = "boxes"
@@ -421,6 +452,11 @@ def _add_column_if_missing(table_name: str, column_name: str, ddl: str) -> None:
def ensure_schema() -> None:
"""对旧数据库做最小增量迁移。
中文说明:项目早期数据库可能缺少新字段,这里用“缺什么补什么”的方式补列,
这样升级时不会强制删库重建。
"""
_add_column_if_missing(
"boxes",
"box_type",
@@ -499,6 +535,11 @@ def _find_enabled_material_conflict(
exclude_component_id: int = None,
exclude_part_no: str = "",
):
"""查找“同名 + 同规格”的启用中物料冲突。
中文说明:有些元件料号不同,但实际是同一种物料,所以不能只按 part_no 去重;
这里会把名称和规格做标准化后比对,避免重复建库存位。
"""
target_key = _material_identity_key(name, specification)
if not target_key:
return None
@@ -549,6 +590,13 @@ def _merge_into_existing_component(
source_component: Component = None,
source_box: Box = None,
) -> None:
"""把一个待保存/待导入的元件合并进已有元件记录。
中文说明:合并时会做三件事:
1. 把名称、规格、备注等信息补到目标记录上。
2. 把数量累加,并记录库存事件。
3. 如果来源位置本来就有旧记录,则删除旧记录,避免同一物料保留两份。
"""
old_target_enabled_qty = int(target.quantity or 0) if target.is_enabled else 0
target.name = incoming_name or target.name
@@ -709,6 +757,11 @@ def _parse_non_negative_int(raw_value: str, default_value: int = 0) -> int:
def normalize_legacy_data() -> None:
"""修复历史数据,使旧数据满足当前业务规则。
中文说明:这个函数主要解决历史版本遗留问题,例如空 box_type、空前缀、
旧数据没有 is_enabled 字段,以及确保“袋装清单”这个固定容器一定存在。
"""
db.session.execute(
db.text(
"UPDATE boxes SET box_type = 'small_28' WHERE box_type IS NULL OR box_type = ''"
@@ -898,6 +951,11 @@ def bad_request(message: str, box_type: str = ""):
def render_box_page(box: Box, error: str = "", notice: str = ""):
"""统一渲染盒子详情页。
中文说明:很多路由最终都要返回 box.html这里集中准备模板所需的公共数据
避免每个路由重复组织 slots、提示信息和阈值参数。
"""
slots = slot_data_for_box(box)
return render_template(
"box.html",
@@ -939,6 +997,11 @@ def log_inventory_event(
component: Component = None,
part_no: str = "",
):
"""记录库存变动日志。
中文说明:统计页的趋势图、最近活动、净变化等都依赖这张事件表,
所以凡是入库、出库、删除、合并等会改变库存的操作,都尽量走这里记账。
"""
event = InventoryEvent(
box_id=box.id if box else (component.box_id if component else None),
box_type=box.box_type if box else None,
@@ -1090,6 +1153,11 @@ def recent_events(limit: int = 20, box_type_filter: str = "all"):
def build_dashboard_context():
"""构建首页/分类页需要的聚合数据。
中文说明:这里会一次性整理盒子分组、库存统计、低库存清单等信息,
让模板层尽量只负责展示,不承担复杂的数据拼装逻辑。
"""
boxes = Box.query.all()
box_by_id = {box.id: box for box in boxes}
boxes.sort(key=box_sort_key)
@@ -1176,6 +1244,11 @@ def build_dashboard_context():
def _build_restock_payload(*, limit: int = 20, threshold: int = LOW_STOCK_THRESHOLD) -> dict:
"""生成补货分析输入数据。
中文说明AI 不直接读数据库,而是读取这里整理好的低库存清单和近 30 天出库热点,
这样可以稳定控制输入格式,也方便 AI 异常时用同一份数据做规则兜底。
"""
boxes = Box.query.all()
box_by_id = {box.id: box for box in boxes}
enabled_components = Component.query.filter_by(is_enabled=True).all()
@@ -2397,6 +2470,11 @@ def search_page():
@app.route("/ai/restock-plan", methods=["POST"])
def ai_restock_plan():
"""生成 AI 补货建议。
中文说明:优先调用 AI 输出 JSON 结构化补货方案;如果 AI 调用失败或返回格式异常,
会自动退回到规则生成的兜底方案,保证页面始终有结果可展示。
"""
ai_settings = _get_ai_settings()
data = _build_restock_payload(
limit=ai_settings["restock_limit"],
@@ -2677,6 +2755,11 @@ def quick_outbound(component_id: int):
@app.route("/stats")
def stats_page():
"""统计页。
中文说明:这里会按筛选条件汇总当前库存、低库存数量、操作次数、趋势线和分类排行,
属于展示层用到的集中统计入口。
"""
days = parse_days_value(request.args.get("days", "7"))
box_type_filter = parse_box_type_filter(request.args.get("box_type", "all"))
notice = request.args.get("notice", "").strip()
@@ -2915,6 +2998,10 @@ def clear_stats_logs():
def bootstrap() -> None:
"""应用启动时初始化数据库。
中文说明:启动顺序是“建表 -> 补字段 -> 修历史数据”,这样新旧数据库都能正常启动。
"""
with app.app_context():
db.create_all()
ensure_schema()