商品管理与库存系统

目录

  • 电商平台商品管理与库存系统API设计
    • [1. 引言](#1. 引言)
    • [2. 系统需求分析](#2. 系统需求分析)
      • [2.1 功能需求](#2.1 功能需求)
      • [2.2 非功能需求](#2.2 非功能需求)
    • [3. 系统架构设计](#3. 系统架构设计)
      • [3.1 整体架构图](#3.1 整体架构图)
      • [3.2 技术栈选择](#3.2 技术栈选择)
    • [4. 数据模型设计](#4. 数据模型设计)
      • [4.1 实体关系图](#4.1 实体关系图)
      • [4.2 核心表设计](#4.2 核心表设计)
        • [4.2.1 商品表 (products)](#4.2.1 商品表 (products))
        • [4.2.2 SKU表 (skus)](#4.2.2 SKU表 (skus))
        • [4.2.3 库存表 (inventory)](#4.2.3 库存表 (inventory))
    • [5. 核心算法与公式](#5. 核心算法与公式)
      • [5.1 库存计算](#5.1 库存计算)
      • [5.2 库存预警](#5.2 库存预警)
    • [6. 系统详细设计](#6. 系统详细设计)
      • [6.1 库存扣减流程](#6.1 库存扣减流程)
      • [6.2 库存数据一致性保障](#6.2 库存数据一致性保障)
    • [7. API接口设计](#7. API接口设计)
      • [7.1 RESTful API设计](#7.1 RESTful API设计)
        • [7.1.1 商品管理API](#7.1.1 商品管理API)
        • [7.1.2 库存管理API](#7.1.2 库存管理API)
        • [7.1.3 分类管理API](#7.1.3 分类管理API)
    • [8. 代码实现](#8. 代码实现)
      • [8.1 项目结构](#8.1 项目结构)
      • [8.2 核心代码实现](#8.2 核心代码实现)
        • [8.2.1 数据模型 (app/models/inventory.py)](#8.2.1 数据模型 (app/models/inventory.py))
        • [8.2.2 Pydantic模型 (app/schemas/inventory.py)](#8.2.2 Pydantic模型 (app/schemas/inventory.py))
        • [8.2.3 库存服务 (app/core/inventory_service.py)](#8.2.3 库存服务 (app/core/inventory_service.py))
        • [8.2.4 分布式锁 (app/core/redis_lock.py)](#8.2.4 分布式锁 (app/core/redis_lock.py))
        • [8.2.5 库存API路由 (app/api/v1/inventory.py)](#8.2.5 库存API路由 (app/api/v1/inventory.py))
        • [8.2.6 商品API路由 (app/api/v1/products.py)](#8.2.6 商品API路由 (app/api/v1/products.py))
    • [9. 性能优化策略](#9. 性能优化策略)
      • [9.1 缓存策略](#9.1 缓存策略)
      • [9.2 数据库优化](#9.2 数据库优化)
    • [10. 测试策略](#10. 测试策略)
      • [10.1 单元测试](#10.1 单元测试)
      • [10.2 压力测试](#10.2 压力测试)
    • [11. 部署与监控](#11. 部署与监控)
      • [11.1 Docker部署配置](#11.1 Docker部署配置)
      • [11.2 监控指标](#11.2 监控指标)
    • [12. 总结与展望](#12. 总结与展望)
      • [12.1 核心亮点](#12.1 核心亮点)
      • [12.2 未来扩展方向](#12.2 未来扩展方向)
      • [12.3 注意事项](#12.3 注意事项)

『宝藏代码胶囊开张啦!』------ 我的 CodeCapsule 来咯!✨写代码不再头疼!我的新站点 CodeCapsule 主打一个 "白菜价"+"量身定制 "!无论是卡脖子的毕设/课设/文献复现 ,需要灵光一现的算法改进 ,还是想给项目加个"外挂",这里都有便宜又好用的代码方案等你发现!低成本,高适配,助你轻松通关!速来围观 👉 CodeCapsule官网

电商平台商品管理与库存系统API设计

1. 引言

商品管理与库存系统是电商平台的核心组件,直接关系到平台的运营效率、用户体验和盈利能力。一个高效的库存管理系统不仅需要实时跟踪库存变化,还需要支持复杂的商品管理、多仓库调度、库存预警等功能。本文将从需求分析、架构设计到代码实现,全面介绍如何构建一个高可用、高性能的电商商品管理与库存系统。

2. 系统需求分析

2.1 功能需求

  1. 商品管理

    • 商品分类管理(多级分类)
    • 商品属性管理(规格、参数等)
    • 商品信息管理(增删改查)
    • 商品SKU管理(库存量单位)
    • 商品图片/视频管理
  2. 库存管理

    • 多仓库库存管理
    • 实时库存查询与更新
    • 库存预警与补货提醒
    • 库存锁定与释放
    • 库存流水记录
  3. 价格管理

    • 商品定价策略
    • 促销价格管理
    • 会员价格体系
  4. 数据统计与分析

    • 商品销售统计
    • 库存周转率分析
    • 滞销商品预警

2.2 非功能需求

  1. 性能需求

    • 库存查询响应时间 < 50ms
    • 库存更新操作 < 100ms
    • 支持每秒1000+的库存查询请求
    • 支持每秒100+的库存更新操作
  2. 可用性需求

    • 系统可用性达到99.99%
    • 支持7×24小时不间断服务
  3. 数据一致性需求

    • 保证库存数据的最终一致性
    • 防止超卖问题

3. 系统架构设计

3.1 整体架构图

基础设施
数据层
服务层
API网关层
客户端层
Web前端
移动端
商家后台
第三方系统
API网关
负载均衡
认证鉴权
商品服务
库存服务
价格服务
搜索服务
商品数据库
库存数据库
缓存集群
搜索引擎
消息队列
分布式锁
配置中心
监控告警

3.2 技术栈选择

  • 后端框架: FastAPI + SQLAlchemy + Pydantic
  • 数据库: PostgreSQL (主数据) + Redis (缓存) + Elasticsearch (搜索)
  • 消息队列: RabbitMQ / Kafka (异步处理)
  • 分布式锁: Redis Redlock
  • 监控: Prometheus + Grafana
  • 容器化: Docker + Kubernetes

4. 数据模型设计

4.1 实体关系图

contains
has
tracks
stores
has
defines
displays
records
CATEGORIES
uuid
id
PK
string
name
uuid
parent_id
FK
integer
level
string
path
integer
sort_order
boolean
is_active
PRODUCTS
uuid
id
PK
string
product_code
UK
string
name
uuid
category_id
FK
uuid
brand_id
FK
decimal
market_price
decimal
cost_price
string
description
json
specifications
integer
status
integer
sales_count
integer
view_count
timestamp
created_at
timestamp
updated_at
SKUS
uuid
id
PK
uuid
product_id
FK
string
sku_code
UK
string
sku_name
json
specifications
decimal
price
decimal
cost_price
string
barcode
string
unit
integer
weight
string
thumbnail
boolean
is_active
INVENTORY
uuid
id
PK
uuid
sku_id
FK
uuid
warehouse_id
FK
integer
quantity
integer
locked_quantity
integer
available_quantity
integer
safety_stock
integer
warning_threshold
timestamp
last_updated
WAREHOUSES
uuid
id
PK
string
code
UK
string
name
string
address
string
contact
string
phone
integer
type
integer
status
boolean
is_default
PRODUCT_ATTRIBUTES
ATTRIBUTES
PRODUCT_IMAGES
INVENTORY_TRANSACTIONS

4.2 核心表设计

4.2.1 商品表 (products)
字段名 类型 说明
id UUID 主键
product_code VARCHAR(50) 商品编码,唯一
name VARCHAR(200) 商品名称
category_id UUID 分类ID
brand_id UUID 品牌ID
market_price DECIMAL(10,2) 市场价
cost_price DECIMAL(10,2) 成本价
description TEXT 商品描述
specifications JSON 商品规格参数
status INTEGER 状态(0:下架,1:上架,2:待审核)
sales_count INTEGER 销售数量
view_count INTEGER 浏览量
created_at TIMESTAMP 创建时间
updated_at TIMESTAMP 更新时间
4.2.2 SKU表 (skus)
字段名 类型 说明
id UUID 主键
product_id UUID 商品ID
sku_code VARCHAR(50) SKU编码,唯一
sku_name VARCHAR(200) SKU名称
specifications JSON SKU规格属性
price DECIMAL(10,2) 销售价格
cost_price DECIMAL(10,2) 成本价格
barcode VARCHAR(50) 条形码
unit VARCHAR(20) 单位
weight INTEGER 重量(克)
thumbnail VARCHAR(500) 缩略图
is_active BOOLEAN 是否启用
4.2.3 库存表 (inventory)
字段名 类型 说明
id UUID 主键
sku_id UUID SKU ID
warehouse_id UUID 仓库ID
quantity INTEGER 总库存数量
locked_quantity INTEGER 锁定库存数量
available_quantity INTEGER 可用库存数量
safety_stock INTEGER 安全库存
warning_threshold INTEGER 库存预警阈值
last_updated TIMESTAMP 最后更新时间

5. 核心算法与公式

5.1 库存计算

可用库存计算公式:

available_quantity = quantity − locked_quantity \text{available\_quantity} = \text{quantity} - \text{locked\_quantity} available_quantity=quantity−locked_quantity

库存周转率计算公式:

inventory_turnover = cost_of_goods_sold average_inventory \text{inventory\_turnover} = \frac{\text{cost\_of\_goods\_sold}}{\text{average\_inventory}} inventory_turnover=average_inventorycost_of_goods_sold

其中平均库存为:

average_inventory = beginning_inventory + ending_inventory 2 \text{average\_inventory} = \frac{\text{beginning\_inventory} + \text{ending\_inventory}}{2} average_inventory=2beginning_inventory+ending_inventory

5.2 库存预警

库存预警条件:

{ warning = true , if available_quantity ≤ safety_stock warning = false , otherwise \begin{cases} \text{warning} = \text{true}, & \text{if } \text{available\_quantity} \leq \text{safety\_stock} \\ \text{warning} = \text{false}, & \text{otherwise} \end{cases} {warning=true,warning=false,if available_quantity≤safety_stockotherwise

补货数量建议:

reorder_quantity = max ( safety_stock × 2 − available_quantity , minimum_order_quantity ) \text{reorder\_quantity} = \text{max}( \text{safety\_stock} \times 2 - \text{available\_quantity}, \text{minimum\_order\_quantity} ) reorder_quantity=max(safety_stock×2−available_quantity,minimum_order_quantity)

6. 系统详细设计

6.1 库存扣减流程

消息队列 数据库 Redis 库存服务 API网关 客户端 消息队列 数据库 Redis 库存服务 API网关 客户端 alt [库存充足] [库存不足] alt [锁获取成功] [锁获取失败] 下单请求 库存预扣请求 获取分布式锁 锁获取成功 查询库存缓存 返回库存信息 预扣库存 记录库存流水(预扣) 更新数据库库存 发送库存变更消息 释放分布式锁 返回预扣成功 下单成功 释放分布式锁 返回库存不足 下单失败 返回系统繁忙 请重试

6.2 库存数据一致性保障

为保证库存数据一致性,我们采用以下策略:

  1. 分布式锁机制: 使用Redis Redlock算法实现分布式锁
  2. 乐观锁机制: 数据库层面使用版本号控制
  3. 最终一致性: 通过消息队列保证数据最终一致性
  4. 补偿机制: 实现库存操作的回滚机制

7. API接口设计

7.1 RESTful API设计

7.1.1 商品管理API
复制代码
GET    /api/v1/products                   # 获取商品列表
POST   /api/v1/products                   # 创建商品
GET    /api/v1/products/{product_id}      # 获取商品详情
PUT    /api/v1/products/{product_id}      # 更新商品
DELETE /api/v1/products/{product_id}      # 删除商品
POST   /api/v1/products/{product_id}/skus # 添加SKU
GET    /api/v1/products/search            # 搜索商品
7.1.2 库存管理API
复制代码
GET    /api/v1/inventory/{sku_id}              # 查询库存
POST   /api/v1/inventory/deduct               # 扣减库存
POST   /api/v1/inventory/revert               # 回滚库存
POST   /api/v1/inventory/adjust               # 调整库存
GET    /api/v1/inventory/transactions         # 查询库存流水
POST   /api/v1/inventory/warning/settings     # 设置库存预警
GET    /api/v1/inventory/warnings             # 获取库存预警
7.1.3 分类管理API
复制代码
GET    /api/v1/categories                     # 获取分类树
POST   /api/v1/categories                     # 创建分类
PUT    /api/v1/categories/{category_id}       # 更新分类
DELETE /api/v1/categories/{category_id}       # 删除分类
GET    /api/v1/categories/{category_id}/products # 获取分类下商品

8. 代码实现

8.1 项目结构

复制代码
ecommerce-product-inventory/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   ├── database.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── product.py
│   │   ├── sku.py
│   │   ├── category.py
│   │   ├── inventory.py
│   │   └── warehouse.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── product.py
│   │   ├── inventory.py
│   │   └── category.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── v1/
│   │   │   ├── __init__.py
│   │   │   ├── products.py
│   │   │   ├── inventory.py
│   │   │   └── categories.py
│   ├── core/
│   │   ├── __init__.py
│   │   ├── inventory_service.py
│   │   ├── cache.py
│   │   ├── redis_lock.py
│   │   └── mq.py
│   ├── crud/
│   │   ├── __init__.py
│   │   ├── product.py
│   │   └── inventory.py
│   └── utils/
│       ├── __init__.py
│       ├── validators.py
│       └── helpers.py
├── tests/
├── requirements.txt
└── docker-compose.yml

8.2 核心代码实现

8.2.1 数据模型 (app/models/inventory.py)
python 复制代码
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Optional
from sqlalchemy import Column, String, Integer, Boolean, DateTime, DECIMAL, ForeignKey, JSON, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base


class Product(Base):
    """商品模型"""
    
    __tablename__ = "products"
    
    # 主键
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
    
    # 基本信息
    product_code = Column(String(50), unique=True, index=True, nullable=False)
    name = Column(String(200), nullable=False)
    category_id = Column(UUID(as_uuid=True), ForeignKey("categories.id"), index=True, nullable=False)
    brand_id = Column(UUID(as_uuid=True), ForeignKey("brands.id"), index=True, nullable=True)
    
    # 价格信息
    market_price = Column(DECIMAL(10, 2), nullable=False, default=0)
    cost_price = Column(DECIMAL(10, 2), nullable=False, default=0)
    
    # 描述信息
    description = Column(Text, nullable=True)
    specifications = Column(JSON, nullable=True)  # 规格参数
    features = Column(Text, nullable=True)  # 商品特色
    
    # 状态信息
    status = Column(Integer, nullable=False, default=0)  # 0:下架, 1:上架, 2:待审核
    is_hot = Column(Boolean, default=False)
    is_new = Column(Boolean, default=False)
    is_recommended = Column(Boolean, default=False)
    
    # 统计信息
    sales_count = Column(Integer, default=0)
    view_count = Column(Integer, default=0)
    favorite_count = Column(Integer, default=0)
    
    # 时间戳
    created_at = Column(DateTime, default=func.now())
    updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
    
    # 关系
    category = relationship("Category", back_populates="products")
    brand = relationship("Brand", back_populates="products")
    skus = relationship("SKU", back_populates="product", cascade="all, delete-orphan")
    images = relationship("ProductImage", back_populates="product", cascade="all, delete-orphan")
    inventory_records = relationship("Inventory", back_populates="product", cascade="all, delete-orphan")
    
    def __repr__(self) -> str:
        return f"<Product(id={self.id}, name={self.name}, code={self.product_code})>"
    
    @property
    def min_price(self) -> Decimal:
        """获取最低价格"""
        if not self.skus:
            return self.market_price
        return min(sku.price for sku in self.skus if sku.is_active)
    
    @property
    def max_price(self) -> Decimal:
        """获取最高价格"""
        if not self.skus:
            return self.market_price
        return max(sku.price for sku in self.skus if sku.is_active)
    
    @property
    def total_stock(self) -> int:
        """获取总库存"""
        return sum(inv.available_quantity for inv in self.inventory_records)


class SKU(Base):
    """SKU模型"""
    
    __tablename__ = "skus"
    
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
    product_id = Column(UUID(as_uuid=True), ForeignKey("products.id"), index=True, nullable=False)
    
    # SKU信息
    sku_code = Column(String(50), unique=True, index=True, nullable=False)
    sku_name = Column(String(200), nullable=False)
    specifications = Column(JSON, nullable=True)  # SKU规格属性
    
    # 价格信息
    price = Column(DECIMAL(10, 2), nullable=False)
    cost_price = Column(DECIMAL(10, 2), nullable=False)
    promotion_price = Column(DECIMAL(10, 2), nullable=True)  # 促销价
    
    # 物理属性
    barcode = Column(String(50), unique=True, nullable=True)
    unit = Column(String(20), nullable=False, default="件")
    weight = Column(Integer, nullable=False, default=0)  # 重量(克)
    volume = Column(DECIMAL(10, 2), nullable=True)  # 体积(立方厘米)
    
    # 显示信息
    thumbnail = Column(String(500), nullable=True)
    sort_order = Column(Integer, default=0)
    
    # 状态
    is_active = Column(Boolean, default=True)
    is_default = Column(Boolean, default=False)  # 是否默认SKU
    
    # 时间戳
    created_at = Column(DateTime, default=func.now())
    updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
    
    # 关系
    product = relationship("Product", back_populates="skus")
    inventory_records = relationship("Inventory", back_populates="sku", cascade="all, delete-orphan")
    
    def __repr__(self) -> str:
        return f"<SKU(id={self.id}, sku_code={self.sku_code}, product_id={self.product_id})>"
    
    @property
    def actual_price(self) -> Decimal:
        """获取实际价格(优先使用促销价)"""
        return self.promotion_price if self.promotion_price else self.price


class Category(Base):
    """商品分类模型"""
    
    __tablename__ = "categories"
    
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
    
    # 分类信息
    name = Column(String(100), nullable=False)
    parent_id = Column(UUID(as_uuid=True), ForeignKey("categories.id"), index=True, nullable=True)
    level = Column(Integer, nullable=False, default=1)
    path = Column(String(500), nullable=True)  # 分类路径,如"1/2/3"
    
    # 显示信息
    description = Column(Text, nullable=True)
    image_url = Column(String(500), nullable=True)
    icon = Column(String(100), nullable=True)
    
    # 排序与状态
    sort_order = Column(Integer, default=0)
    is_active = Column(Boolean, default=True)
    is_show = Column(Boolean, default=True)
    
    # SEO信息
    seo_title = Column(String(200), nullable=True)
    seo_keywords = Column(String(500), nullable=True)
    seo_description = Column(Text, nullable=True)
    
    # 时间戳
    created_at = Column(DateTime, default=func.now())
    updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
    
    # 关系
    parent = relationship("Category", remote_side=[id], backref="children")
    products = relationship("Product", back_populates="category")
    
    def __repr__(self) -> str:
        return f"<Category(id={self.id}, name={self.name}, level={self.level})>"
    
    def update_path(self) -> None:
        """更新分类路径"""
        if self.parent_id:
            parent = self.parent
            self.path = f"{parent.path}/{self.id}" if parent.path else str(self.id)
            self.level = parent.level + 1
        else:
            self.path = str(self.id)
            self.level = 1


class Warehouse(Base):
    """仓库模型"""
    
    __tablename__ = "warehouses"
    
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
    
    # 仓库信息
    code = Column(String(50), unique=True, index=True, nullable=False)
    name = Column(String(100), nullable=False)
    type = Column(Integer, nullable=False, default=1)  # 1:自营仓, 2:供应商仓, 3:虚拟仓
    status = Column(Integer, nullable=False, default=1)  # 1:启用, 0:停用
    
    # 联系信息
    address = Column(String(500), nullable=False)
    contact_person = Column(String(50), nullable=True)
    phone = Column(String(20), nullable=True)
    email = Column(String(100), nullable=True)
    
    # 配置信息
    is_default = Column(Boolean, default=False)
    priority = Column(Integer, default=0)  # 优先级,数字越大优先级越高
    capacity = Column(Integer, nullable=True)  # 仓库容量
    
    # 时间戳
    created_at = Column(DateTime, default=func.now())
    updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
    
    # 关系
    inventory_records = relationship("Inventory", back_populates="warehouse")
    
    def __repr__(self) -> str:
        return f"<Warehouse(id={self.id}, name={self.name}, code={self.code})>"


class Inventory(Base):
    """库存模型"""
    
    __tablename__ = "inventory"
    
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
    sku_id = Column(UUID(as_uuid=True), ForeignKey("skus.id"), index=True, nullable=False)
    warehouse_id = Column(UUID(as_uuid=True), ForeignKey("warehouses.id"), index=True, nullable=False)
    
    # 库存数量
    quantity = Column(Integer, nullable=False, default=0)  # 总库存
    locked_quantity = Column(Integer, nullable=False, default=0)  # 锁定库存
    available_quantity = Column(Integer, nullable=False, default=0)  # 可用库存
    
    # 库存配置
    safety_stock = Column(Integer, nullable=False, default=0)  # 安全库存
    warning_threshold = Column(Integer, nullable=True)  # 预警阈值
    max_stock = Column(Integer, nullable=True)  # 最大库存
    
    # 库存状态
    status = Column(Integer, nullable=False, default=1)  # 1:正常, 0:停用
    last_check_date = Column(DateTime, nullable=True)  # 最后盘点日期
    
    # 时间戳
    created_at = Column(DateTime, default=func.now())
    updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
    last_updated = Column(DateTime, default=func.now(), onupdate=func.now())
    
    # 关系
    sku = relationship("SKU", back_populates="inventory_records")
    warehouse = relationship("Warehouse", back_populates="inventory_records")
    product = relationship("Product", secondary="skus", primaryjoin="Inventory.sku_id == SKU.id", 
                         secondaryjoin="SKU.product_id == Product.id", viewonly=True)
    
    def __repr__(self) -> str:
        return f"<Inventory(id={self.id}, sku_id={self.sku_id}, warehouse_id={self.warehouse_id}, available={self.available_quantity})>"
    
    def update_available_quantity(self) -> None:
        """更新可用库存"""
        self.available_quantity = self.quantity - self.locked_quantity
        self.last_updated = func.now()
    
    def is_low_stock(self) -> bool:
        """判断是否低库存"""
        if self.warning_threshold is not None:
            return self.available_quantity <= self.warning_threshold
        return self.available_quantity <= self.safety_stock
    
    def get_reorder_quantity(self, minimum_order: int = 10) -> int:
        """获取建议补货数量
        
        参数:
        - minimum_order: 最小订购量
        
        返回:
        - 建议补货数量
        """
        if self.max_stock:
            reorder_qty = self.max_stock - self.available_quantity
        else:
            reorder_qty = max(self.safety_stock * 2 - self.available_quantity, 0)
        
        return max(reorder_qty, minimum_order)


class InventoryTransaction(Base):
    """库存流水模型"""
    
    __tablename__ = "inventory_transactions"
    
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
    sku_id = Column(UUID(as_uuid=True), ForeignKey("skus.id"), index=True, nullable=False)
    warehouse_id = Column(UUID(as_uuid=True), ForeignKey("warehouses.id"), index=True, nullable=False)
    
    # 流水信息
    transaction_type = Column(Integer, nullable=False)  # 1:入库, 2:出库, 3:调整, 4:锁定, 5:解锁
    change_quantity = Column(Integer, nullable=False)  # 变化数量(正数增加,负数减少)
    before_quantity = Column(Integer, nullable=False)  # 变化前数量
    after_quantity = Column(Integer, nullable=False)  # 变化后数量
    
    # 关联信息
    reference_type = Column(Integer, nullable=True)  # 关联类型(1:订单, 2:采购单, 3:调拨单)
    reference_id = Column(String(100), nullable=True)  # 关联ID
    reference_no = Column(String(100), nullable=True)  # 关联单号
    
    # 操作信息
    operator_id = Column(UUID(as_uuid=True), nullable=True)
    operator_name = Column(String(100), nullable=True)
    remark = Column(Text, nullable=True)
    
    # 时间戳
    created_at = Column(DateTime, default=func.now())
    
    # 关系
    sku = relationship("SKU")
    warehouse = relationship("Warehouse")
    
    def __repr__(self) -> str:
        return f"<InventoryTransaction(id={self.id}, sku_id={self.sku_id}, type={self.transaction_type}, change={self.change_quantity})>"


class ProductImage(Base):
    """商品图片模型"""
    
    __tablename__ = "product_images"
    
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    product_id = Column(UUID(as_uuid=True), ForeignKey("products.id"), index=True, nullable=False)
    sku_id = Column(UUID(as_uuid=True), ForeignKey("skus.id"), index=True, nullable=True)
    
    # 图片信息
    url = Column(String(500), nullable=False)
    thumbnail_url = Column(String(500), nullable=True)
    alt_text = Column(String(200), nullable=True)
    
    # 显示信息
    sort_order = Column(Integer, default=0)
    is_main = Column(Boolean, default=False)  # 是否主图
    
    # 时间戳
    created_at = Column(DateTime, default=func.now())
    
    # 关系
    product = relationship("Product", back_populates="images")
    sku = relationship("SKU")
    
    def __repr__(self) -> str:
        return f"<ProductImage(id={self.id}, product_id={self.product_id}, url={self.url})>"
8.2.2 Pydantic模型 (app/schemas/inventory.py)
python 复制代码
from typing import Optional, List, Dict, Any
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel, Field, validator, condecimal, conint
import re


class CategoryBase(BaseModel):
    """分类基础模型"""
    name: str = Field(..., max_length=100, description="分类名称")
    parent_id: Optional[str] = Field(None, description="父分类ID")
    description: Optional[str] = Field(None, description="分类描述")
    image_url: Optional[str] = Field(None, description="分类图片")
    sort_order: int = Field(0, ge=0, description="排序")
    is_active: bool = Field(True, description="是否启用")


class CategoryCreate(CategoryBase):
    """分类创建模型"""
    pass


class CategoryUpdate(BaseModel):
    """分类更新模型"""
    name: Optional[str] = Field(None, max_length=100)
    description: Optional[str] = None
    image_url: Optional[str] = None
    sort_order: Optional[int] = Field(None, ge=0)
    is_active: Optional[bool] = None


class CategoryInDB(CategoryBase):
    """数据库中的分类模型"""
    id: str
    level: int
    path: Optional[str]
    created_at: datetime
    updated_at: datetime
    
    class Config:
        orm_mode = True


class CategoryTree(CategoryInDB):
    """分类树模型"""
    children: List["CategoryTree"] = []
    
    class Config:
        orm_mode = True


class ProductBase(BaseModel):
    """商品基础模型"""
    product_code: str = Field(..., max_length=50, description="商品编码")
    name: str = Field(..., max_length=200, description="商品名称")
    category_id: str = Field(..., description="分类ID")
    brand_id: Optional[str] = Field(None, description="品牌ID")
    
    market_price: Decimal = Field(..., ge=0, description="市场价")
    cost_price: Decimal = Field(..., ge=0, description="成本价")
    
    description: Optional[str] = Field(None, description="商品描述")
    specifications: Optional[Dict[str, Any]] = Field(None, description="规格参数")
    features: Optional[str] = Field(None, description="商品特色")
    
    status: int = Field(1, ge=0, le=2, description="状态(0:下架,1:上架,2:待审核)")
    is_hot: bool = Field(False, description="是否热销")
    is_new: bool = Field(False, description="是否新品")
    is_recommended: bool = Field(False, description="是否推荐")
    
    @validator('product_code')
    def validate_product_code(cls, v):
        """验证商品编码格式"""
        if not re.match(r'^[A-Za-z0-9_-]+$', v):
            raise ValueError('商品编码只能包含字母、数字、下划线和连字符')
        return v


class ProductCreate(ProductBase):
    """商品创建模型"""
    pass


class ProductUpdate(BaseModel):
    """商品更新模型"""
    name: Optional[str] = Field(None, max_length=200)
    category_id: Optional[str] = None
    brand_id: Optional[str] = None
    market_price: Optional[Decimal] = Field(None, ge=0)
    cost_price: Optional[Decimal] = Field(None, ge=0)
    description: Optional[str] = None
    specifications: Optional[Dict[str, Any]] = None
    status: Optional[int] = Field(None, ge=0, le=2)
    is_hot: Optional[bool] = None
    is_new: Optional[bool] = None
    is_recommended: Optional[bool] = None


class ProductInDB(ProductBase):
    """数据库中的商品模型"""
    id: str
    sales_count: int
    view_count: int
    favorite_count: int
    created_at: datetime
    updated_at: datetime
    
    class Config:
        orm_mode = True


class ProductDetail(ProductInDB):
    """商品详情模型"""
    category: Optional[CategoryInDB] = None
    skus: List["SKUInDB"] = []
    images: List["ProductImageInDB"] = []
    inventory_summary: Optional[Dict[str, int]] = None
    
    class Config:
        orm_mode = True


class SKUBase(BaseModel):
    """SKU基础模型"""
    sku_code: str = Field(..., max_length=50, description="SKU编码")
    sku_name: str = Field(..., max_length=200, description="SKU名称")
    specifications: Optional[Dict[str, Any]] = Field(None, description="SKU规格")
    
    price: Decimal = Field(..., ge=0, description="销售价")
    cost_price: Decimal = Field(..., ge=0, description="成本价")
    promotion_price: Optional[Decimal] = Field(None, ge=0, description="促销价")
    
    barcode: Optional[str] = Field(None, max_length=50, description="条形码")
    unit: str = Field("件", max_length=20, description="单位")
    weight: int = Field(0, ge=0, description="重量(克)")
    
    thumbnail: Optional[str] = Field(None, description="缩略图")
    is_active: bool = Field(True, description="是否启用")
    is_default: bool = Field(False, description="是否默认SKU")
    
    @validator('sku_code')
    def validate_sku_code(cls, v):
        """验证SKU编码格式"""
        if not re.match(r'^[A-Za-z0-9_-]+$', v):
            raise ValueError('SKU编码只能包含字母、数字、下划线和连字符')
        return v


class SKUCreate(SKUBase):
    """SKU创建模型"""
    product_id: str = Field(..., description="商品ID")


class SKUUpdate(BaseModel):
    """SKU更新模型"""
    sku_name: Optional[str] = Field(None, max_length=200)
    specifications: Optional[Dict[str, Any]] = None
    price: Optional[Decimal] = Field(None, ge=0)
    cost_price: Optional[Decimal] = Field(None, ge=0)
    promotion_price: Optional[Decimal] = Field(None, ge=0)
    barcode: Optional[str] = Field(None, max_length=50)
    unit: Optional[str] = Field(None, max_length=20)
    weight: Optional[int] = Field(None, ge=0)
    thumbnail: Optional[str] = None
    is_active: Optional[bool] = None
    is_default: Optional[bool] = None


class SKUInDB(SKUBase):
    """数据库中的SKU模型"""
    id: str
    product_id: str
    created_at: datetime
    updated_at: datetime
    
    class Config:
        orm_mode = True


class SKUDetail(SKUInDB):
    """SKU详情模型"""
    product: Optional[ProductInDB] = None
    inventory: List["InventoryInDB"] = []
    
    class Config:
        orm_mode = True


class InventoryBase(BaseModel):
    """库存基础模型"""
    sku_id: str = Field(..., description="SKU ID")
    warehouse_id: str = Field(..., description="仓库ID")
    quantity: int = Field(0, ge=0, description="总库存")
    safety_stock: int = Field(0, ge=0, description="安全库存")
    warning_threshold: Optional[int] = Field(None, ge=0, description="预警阈值")
    max_stock: Optional[int] = Field(None, ge=0, description="最大库存")


class InventoryCreate(InventoryBase):
    """库存创建模型"""
    pass


class InventoryUpdate(BaseModel):
    """库存更新模型"""
    quantity: Optional[int] = Field(None, ge=0)
    safety_stock: Optional[int] = Field(None, ge=0)
    warning_threshold: Optional[int] = Field(None, ge=0)
    max_stock: Optional[int] = Field(None, ge=0)
    status: Optional[int] = Field(None, ge=0, le=1)


class InventoryInDB(InventoryBase):
    """数据库中的库存模型"""
    id: str
    locked_quantity: int
    available_quantity: int
    status: int
    last_updated: datetime
    created_at: datetime
    updated_at: datetime
    
    class Config:
        orm_mode = True


class InventoryDetail(InventoryInDB):
    """库存详情模型"""
    sku: Optional[SKUInDB] = None
    warehouse: Optional["WarehouseInDB"] = None
    
    class Config:
        orm_mode = True


class WarehouseBase(BaseModel):
    """仓库基础模型"""
    code: str = Field(..., max_length=50, description="仓库编码")
    name: str = Field(..., max_length=100, description="仓库名称")
    type: int = Field(1, ge=1, le=3, description="仓库类型(1:自营,2:供应商,3:虚拟)")
    address: str = Field(..., max_length=500, description="仓库地址")
    contact_person: Optional[str] = Field(None, max_length=50, description="联系人")
    phone: Optional[str] = Field(None, max_length=20, description="联系电话")
    is_default: bool = Field(False, description="是否默认仓库")
    priority: int = Field(0, description="优先级")


class WarehouseCreate(WarehouseBase):
    """仓库创建模型"""
    pass


class WarehouseUpdate(BaseModel):
    """仓库更新模型"""
    name: Optional[str] = Field(None, max_length=100)
    type: Optional[int] = Field(None, ge=1, le=3)
    address: Optional[str] = Field(None, max_length=500)
    contact_person: Optional[str] = Field(None, max_length=50)
    phone: Optional[str] = Field(None, max_length=20)
    is_default: Optional[bool] = None
    priority: Optional[int] = None
    status: Optional[int] = Field(None, ge=0, le=1)


class WarehouseInDB(WarehouseBase):
    """数据库中的仓库模型"""
    id: str
    status: int
    created_at: datetime
    updated_at: datetime
    
    class Config:
        orm_mode = True


class ProductImageInDB(BaseModel):
    """商品图片模型"""
    id: str
    product_id: str
    sku_id: Optional[str]
    url: str
    thumbnail_url: Optional[str]
    alt_text: Optional[str]
    sort_order: int
    is_main: bool
    created_at: datetime
    
    class Config:
        orm_mode = True


# 库存操作相关模型
class InventoryDeductRequest(BaseModel):
    """库存扣减请求模型"""
    sku_id: str = Field(..., description="SKU ID")
    warehouse_id: Optional[str] = Field(None, description="仓库ID,不指定时使用默认仓库")
    quantity: int = Field(..., gt=0, description="扣减数量")
    order_id: str = Field(..., description="订单ID")
    order_item_id: str = Field(..., description="订单项ID")
    deduct_type: int = Field(1, ge=1, le=2, description="扣减类型(1:预扣,2:实扣)")


class InventoryDeductResponse(BaseModel):
    """库存扣减响应模型"""
    success: bool
    message: str
    transaction_id: Optional[str] = None
    available_quantity: Optional[int] = None
    deducted_quantity: Optional[int] = None


class InventoryRevertRequest(BaseModel):
    """库存回滚请求模型"""
    sku_id: str = Field(..., description="SKU ID")
    warehouse_id: Optional[str] = Field(None, description="仓库ID")
    quantity: int = Field(..., gt=0, description="回滚数量")
    transaction_id: str = Field(..., description="原交易ID")
    reason: str = Field(..., description="回滚原因")


class InventoryAdjustRequest(BaseModel):
    """库存调整请求模型"""
    sku_id: str = Field(..., description="SKU ID")
    warehouse_id: str = Field(..., description="仓库ID")
    quantity: int = Field(..., description="调整数量(正数增加,负数减少)")
    reason: str = Field(..., description="调整原因")
    reference_type: int = Field(1, ge=1, le=3, description="关联类型(1:盘点,2:报损,3:其他)")
    reference_no: Optional[str] = Field(None, description="关联单号")


# 分页和查询模型
class ProductQueryParams(BaseModel):
    """商品查询参数"""
    keyword: Optional[str] = Field(None, description="关键词")
    category_id: Optional[str] = Field(None, description="分类ID")
    brand_id: Optional[str] = Field(None, description="品牌ID")
    status: Optional[int] = Field(None, ge=0, le=2, description="状态")
    min_price: Optional[Decimal] = Field(None, ge=0, description="最低价")
    max_price: Optional[Decimal] = Field(None, ge=0, description="最高价")
    is_hot: Optional[bool] = Field(None, description="是否热销")
    is_new: Optional[bool] = Field(None, description="是否新品")
    is_recommended: Optional[bool] = Field(None, description="是否推荐")
    page: int = Field(1, ge=1, description="页码")
    size: int = Field(20, ge=1, le=100, description="每页数量")
    sort_by: str = Field("created_at", description="排序字段")
    sort_order: str = Field("desc", description="排序方向")


class ProductListResponse(BaseModel):
    """商品列表响应"""
    success: bool = True
    message: str = "查询成功"
    data: List[ProductInDB]
    total: int
    page: int
    size: int
    total_pages: int


# 更新前向引用
CategoryTree.update_forward_refs()
8.2.3 库存服务 (app/core/inventory_service.py)
python 复制代码
import uuid
from datetime import datetime
from decimal import Decimal
from typing import Optional, List, Dict, Any, Tuple
import asyncio
import json

from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func
import redis

from app import models, schemas
from app.core.redis_lock import RedisDistributedLock
from app.core.cache import get_redis_client
from app.core.mq import MessageQueue
from app.config import settings


class InventoryService:
    """库存服务"""
    
    def __init__(self, db: Session):
        self.db = db
        self.redis = get_redis_client()
        self.mq = MessageQueue()
        self.lock_timeout = 10  # 锁超时时间(秒)
        self.lock_retry_delay = 0.1  # 锁重试延迟(秒)
    
    async def deduct_inventory(self, request: schemas.InventoryDeductRequest) -> schemas.InventoryDeductResponse:
        """扣减库存
        
        参数:
        - request: 库存扣减请求
        
        返回:
        - 库存扣减响应
        """
        
        # 获取仓库ID
        warehouse_id = request.warehouse_id
        if not warehouse_id:
            warehouse_id = await self._get_default_warehouse_id()
        
        # 构建锁键
        lock_key = f"inventory:lock:{request.sku_id}:{warehouse_id}"
        
        try:
            # 获取分布式锁
            async with RedisDistributedLock(self.redis, lock_key, timeout=self.lock_timeout):
                # 查询库存记录
                inventory = self.db.query(models.Inventory).filter(
                    and_(
                        models.Inventory.sku_id == uuid.UUID(request.sku_id),
                        models.Inventory.warehouse_id == uuid.UUID(warehouse_id),
                        models.Inventory.status == 1
                    )
                ).with_for_update().first()
                
                if not inventory:
                    return schemas.InventoryDeductResponse(
                        success=False,
                        message="库存记录不存在"
                    )
                
                # 检查库存是否充足
                if inventory.available_quantity < request.quantity:
                    return schemas.InventoryDeductResponse(
                        success=False,
                        message=f"库存不足,当前可用库存: {inventory.available_quantity}",
                        available_quantity=inventory.available_quantity
                    )
                
                # 记录操作前的数量
                before_quantity = inventory.quantity
                before_locked = inventory.locked_quantity
                
                # 更新库存
                if request.deduct_type == 1:
                    # 预扣库存:锁定库存
                    inventory.locked_quantity += request.quantity
                else:
                    # 实扣库存:直接减少总库存
                    inventory.quantity -= request.quantity
                
                # 更新可用库存
                inventory.update_available_quantity()
                inventory.last_updated = datetime.utcnow()
                
                # 创建库存流水记录
                transaction = models.InventoryTransaction(
                    sku_id=uuid.UUID(request.sku_id),
                    warehouse_id=uuid.UUID(warehouse_id),
                    transaction_type=4 if request.deduct_type == 1 else 2,  # 4:锁定, 2:出库
                    change_quantity=-request.quantity,
                    before_quantity=before_quantity,
                    after_quantity=inventory.quantity,
                    reference_type=1,  # 订单
                    reference_id=request.order_id,
                    reference_no=request.order_id,
                    operator_name="system",
                    remark=f"订单扣减: {request.order_item_id}"
                )
                
                self.db.add(transaction)
                self.db.commit()
                
                # 更新缓存
                await self._update_inventory_cache(inventory)
                
                # 发送库存变更消息
                await self.mq.publish_inventory_change({
                    "sku_id": request.sku_id,
                    "warehouse_id": warehouse_id,
                    "change_type": "deduct",
                    "quantity": request.quantity,
                    "available_quantity": inventory.available_quantity,
                    "order_id": request.order_id,
                    "timestamp": datetime.utcnow().isoformat()
                })
                
                # 检查库存预警
                if inventory.is_low_stock():
                    await self._send_low_stock_warning(inventory)
                
                return schemas.InventoryDeductResponse(
                    success=True,
                    message="库存扣减成功",
                    transaction_id=str(transaction.id),
                    available_quantity=inventory.available_quantity,
                    deducted_quantity=request.quantity
                )
                
        except Exception as e:
            self.db.rollback()
            return schemas.InventoryDeductResponse(
                success=False,
                message=f"库存扣减失败: {str(e)}"
            )
    
    async def revert_inventory(self, request: schemas.InventoryRevertRequest) -> schemas.InventoryDeductResponse:
        """回滚库存(释放锁定库存或恢复库存)
        
        参数:
        - request: 库存回滚请求
        
        返回:
        - 库存回滚响应
        """
        
        # 获取仓库ID
        warehouse_id = request.warehouse_id
        if not warehouse_id:
            warehouse_id = await self._get_default_warehouse_id()
        
        # 构建锁键
        lock_key = f"inventory:lock:{request.sku_id}:{warehouse_id}"
        
        try:
            # 获取分布式锁
            async with RedisDistributedLock(self.redis, lock_key, timeout=self.lock_timeout):
                # 查询原交易记录
                original_transaction = self.db.query(models.InventoryTransaction).filter(
                    models.InventoryTransaction.id == uuid.UUID(request.transaction_id)
                ).first()
                
                if not original_transaction:
                    return schemas.InventoryDeductResponse(
                        success=False,
                        message="原交易记录不存在"
                    )
                
                # 查询库存记录
                inventory = self.db.query(models.Inventory).filter(
                    and_(
                        models.Inventory.sku_id == uuid.UUID(request.sku_id),
                        models.Inventory.warehouse_id == uuid.UUID(warehouse_id),
                        models.Inventory.status == 1
                    )
                ).with_for_update().first()
                
                if not inventory:
                    return schemas.InventoryDeductResponse(
                        success=False,
                        message="库存记录不存在"
                    )
                
                # 记录操作前的数量
                before_quantity = inventory.quantity
                before_locked = inventory.locked_quantity
                
                # 根据原交易类型进行回滚
                if original_transaction.transaction_type == 4:  # 锁定
                    # 释放锁定库存
                    if inventory.locked_quantity < request.quantity:
                        request.quantity = inventory.locked_quantity  # 只能释放已有的锁定库存
                    
                    inventory.locked_quantity -= request.quantity
                    revert_type = 5  # 解锁
                else:  # 出库
                    # 恢复总库存
                    inventory.quantity += request.quantity
                    revert_type = 1  # 入库
                
                # 更新可用库存
                inventory.update_available_quantity()
                inventory.last_updated = datetime.utcnow()
                
                # 创建库存流水记录
                transaction = models.InventoryTransaction(
                    sku_id=uuid.UUID(request.sku_id),
                    warehouse_id=uuid.UUID(warehouse_id),
                    transaction_type=revert_type,
                    change_quantity=request.quantity,
                    before_quantity=before_quantity,
                    after_quantity=inventory.quantity,
                    reference_type=1,  # 订单
                    reference_id=original_transaction.reference_id,
                    reference_no=original_transaction.reference_no,
                    operator_name="system",
                    remark=f"库存回滚: {request.reason}, 原交易: {request.transaction_id}"
                )
                
                self.db.add(transaction)
                self.db.commit()
                
                # 更新缓存
                await self._update_inventory_cache(inventory)
                
                # 发送库存变更消息
                await self.mq.publish_inventory_change({
                    "sku_id": request.sku_id,
                    "warehouse_id": warehouse_id,
                    "change_type": "revert",
                    "quantity": request.quantity,
                    "available_quantity": inventory.available_quantity,
                    "reason": request.reason,
                    "timestamp": datetime.utcnow().isoformat()
                })
                
                return schemas.InventoryDeductResponse(
                    success=True,
                    message="库存回滚成功",
                    transaction_id=str(transaction.id),
                    available_quantity=inventory.available_quantity
                )
                
        except Exception as e:
            self.db.rollback()
            return schemas.InventoryDeductResponse(
                success=False,
                message=f"库存回滚失败: {str(e)}"
            )
    
    async def adjust_inventory(self, request: schemas.InventoryAdjustRequest) -> schemas.InventoryDeductResponse:
        """调整库存
        
        参数:
        - request: 库存调整请求
        
        返回:
        - 库存调整响应
        """
        
        lock_key = f"inventory:lock:{request.sku_id}:{request.warehouse_id}"
        
        try:
            # 获取分布式锁
            async with RedisDistributedLock(self.redis, lock_key, timeout=self.lock_timeout):
                # 查询库存记录
                inventory = self.db.query(models.Inventory).filter(
                    and_(
                        models.Inventory.sku_id == uuid.UUID(request.sku_id),
                        models.Inventory.warehouse_id == uuid.UUID(request.warehouse_id),
                        models.Inventory.status == 1
                    )
                ).with_for_update().first()
                
                if not inventory:
                    # 如果库存记录不存在,创建新的记录
                    inventory = models.Inventory(
                        sku_id=uuid.UUID(request.sku_id),
                        warehouse_id=uuid.UUID(request.warehouse_id),
                        quantity=max(request.quantity, 0),
                        locked_quantity=0,
                        safety_stock=0,
                        status=1
                    )
                    inventory.update_available_quantity()
                    self.db.add(inventory)
                    before_quantity = 0
                else:
                    before_quantity = inventory.quantity
                
                # 更新库存
                inventory.quantity += request.quantity
                if inventory.quantity < 0:
                    inventory.quantity = 0
                
                # 更新可用库存
                inventory.update_available_quantity()
                inventory.last_updated = datetime.utcnow()
                
                # 创建库存流水记录
                transaction = models.InventoryTransaction(
                    sku_id=uuid.UUID(request.sku_id),
                    warehouse_id=uuid.UUID(request.warehouse_id),
                    transaction_type=3,  # 调整
                    change_quantity=request.quantity,
                    before_quantity=before_quantity,
                    after_quantity=inventory.quantity,
                    reference_type=request.reference_type,
                    reference_no=request.reference_no,
                    operator_name="system",
                    remark=request.reason
                )
                
                self.db.add(transaction)
                self.db.commit()
                
                # 更新缓存
                await self._update_inventory_cache(inventory)
                
                # 发送库存变更消息
                await self.mq.publish_inventory_change({
                    "sku_id": request.sku_id,
                    "warehouse_id": request.warehouse_id,
                    "change_type": "adjust",
                    "quantity": request.quantity,
                    "available_quantity": inventory.available_quantity,
                    "reason": request.reason,
                    "timestamp": datetime.utcnow().isoformat()
                })
                
                # 检查库存预警
                if request.quantity < 0 and inventory.is_low_stock():
                    await self._send_low_stock_warning(inventory)
                
                return schemas.InventoryDeductResponse(
                    success=True,
                    message="库存调整成功",
                    transaction_id=str(transaction.id),
                    available_quantity=inventory.available_quantity
                )
                
        except Exception as e:
            self.db.rollback()
            return schemas.InventoryDeductResponse(
                success=False,
                message=f"库存调整失败: {str(e)}"
            )
    
    async def get_inventory(self, sku_id: str, warehouse_id: Optional[str] = None) -> Optional[models.Inventory]:
        """获取库存信息
        
        参数:
        - sku_id: SKU ID
        - warehouse_id: 仓库ID,不指定时返回默认仓库库存
        
        返回:
        - 库存记录
        """
        
        # 优先从缓存获取
        cache_key = f"inventory:{sku_id}"
        if warehouse_id:
            cache_key = f"{cache_key}:{warehouse_id}"
        
        cached_data = await self.redis.get(cache_key)
        if cached_data:
            # 这里简化处理,实际应该反序列化为模型对象
            return json.loads(cached_data)
        
        # 查询数据库
        query = self.db.query(models.Inventory).filter(
            models.Inventory.sku_id == uuid.UUID(sku_id),
            models.Inventory.status == 1
        )
        
        if warehouse_id:
            query = query.filter(models.Inventory.warehouse_id == uuid.UUID(warehouse_id))
        else:
            # 获取默认仓库
            default_warehouse = await self._get_default_warehouse_id()
            query = query.filter(models.Inventory.warehouse_id == uuid.UUID(default_warehouse))
        
        inventory = query.first()
        
        if inventory:
            # 更新缓存
            await self._update_inventory_cache(inventory)
        
        return inventory
    
    async def batch_deduct_inventory(self, requests: List[schemas.InventoryDeductRequest]) -> List[schemas.InventoryDeductResponse]:
        """批量扣减库存
        
        参数:
        - requests: 库存扣减请求列表
        
        返回:
        - 库存扣减响应列表
        """
        
        results = []
        for request in requests:
            result = await self.deduct_inventory(request)
            results.append(result)
            
            # 如果某个扣减失败,可以决定是否继续
            if not result.success:
                # 这里可以根据业务需求决定是否中断
                pass
        
        return results
    
    async def get_inventory_summary(self, sku_id: str) -> Dict[str, Any]:
        """获取库存汇总信息
        
        参数:
        - sku_id: SKU ID
        
        返回:
        - 库存汇总信息
        """
        
        # 查询所有仓库的库存
        inventories = self.db.query(models.Inventory).filter(
            models.Inventory.sku_id == uuid.UUID(sku_id),
            models.Inventory.status == 1
        ).all()
        
        total_quantity = sum(inv.quantity for inv in inventories)
        total_locked = sum(inv.locked_quantity for inv in inventories)
        total_available = sum(inv.available_quantity for inv in inventories)
        
        # 检查是否有低库存预警
        low_stock_warehouses = [
            {
                "warehouse_id": str(inv.warehouse_id),
                "warehouse_name": inv.warehouse.name if inv.warehouse else "",
                "available_quantity": inv.available_quantity,
                "safety_stock": inv.safety_stock
            }
            for inv in inventories if inv.is_low_stock()
        ]
        
        return {
            "sku_id": sku_id,
            "total_quantity": total_quantity,
            "total_locked": total_locked,
            "total_available": total_available,
            "warehouse_count": len(inventories),
            "low_stock_warehouses": low_stock_warehouses,
            "has_low_stock": len(low_stock_warehouses) > 0
        }
    
    async def get_low_stock_products(self, threshold: Optional[int] = None) -> List[Dict[str, Any]]:
        """获取低库存商品
        
        参数:
        - threshold: 预警阈值,不指定时使用库存记录中的设置
        
        返回:
        - 低库存商品列表
        """
        
        query = self.db.query(
            models.Inventory,
            models.SKU,
            models.Product,
            models.Warehouse
        ).join(
            models.SKU, models.Inventory.sku_id == models.SKU.id
        ).join(
            models.Product, models.SKU.product_id == models.Product.id
        ).join(
            models.Warehouse, models.Inventory.warehouse_id == models.Warehouse.id
        ).filter(
            models.Inventory.status == 1,
            models.Product.status == 1,
            models.SKU.is_active == True
        )
        
        if threshold:
            # 使用指定的阈值
            query = query.filter(models.Inventory.available_quantity <= threshold)
        else:
            # 使用库存记录中的预警阈值或安全库存
            query = query.filter(
                or_(
                    models.Inventory.available_quantity <= models.Inventory.warning_threshold,
                    and_(
                        models.Inventory.warning_threshold.is_(None),
                        models.Inventory.available_quantity <= models.Inventory.safety_stock
                    )
                )
            )
        
        results = query.all()
        
        low_stock_items = []
        for inventory, sku, product, warehouse in results:
            low_stock_items.append({
                "product_id": str(product.id),
                "product_name": product.name,
                "product_code": product.product_code,
                "sku_id": str(sku.id),
                "sku_code": sku.sku_code,
                "sku_name": sku.sku_name,
                "warehouse_id": str(warehouse.id),
                "warehouse_name": warehouse.name,
                "available_quantity": inventory.available_quantity,
                "safety_stock": inventory.safety_stock,
                "warning_threshold": inventory.warning_threshold,
                "reorder_quantity": inventory.get_reorder_quantity(),
                "last_updated": inventory.last_updated.isoformat() if inventory.last_updated else None
            })
        
        return low_stock_items
    
    async def _get_default_warehouse_id(self) -> str:
        """获取默认仓库ID"""
        
        cache_key = "warehouse:default"
        cached_id = await self.redis.get(cache_key)
        if cached_id:
            return cached_id.decode()
        
        # 查询数据库
        warehouse = self.db.query(models.Warehouse).filter(
            models.Warehouse.is_default == True,
            models.Warehouse.status == 1
        ).first()
        
        if not warehouse:
            # 如果没有默认仓库,使用第一个启用的仓库
            warehouse = self.db.query(models.Warehouse).filter(
                models.Warehouse.status == 1
            ).first()
            
            if not warehouse:
                raise Exception("没有可用的仓库")
        
        warehouse_id = str(warehouse.id)
        
        # 更新缓存
        await self.redis.setex(cache_key, 3600, warehouse_id)  # 缓存1小时
        
        return warehouse_id
    
    async def _update_inventory_cache(self, inventory: models.Inventory) -> None:
        """更新库存缓存"""
        
        cache_key = f"inventory:{inventory.sku_id}:{inventory.warehouse_id}"
        inventory_data = {
            "id": str(inventory.id),
            "sku_id": str(inventory.sku_id),
            "warehouse_id": str(inventory.warehouse_id),
            "quantity": inventory.quantity,
            "locked_quantity": inventory.locked_quantity,
            "available_quantity": inventory.available_quantity,
            "safety_stock": inventory.safety_stock,
            "warning_threshold": inventory.warning_threshold,
            "last_updated": inventory.last_updated.isoformat() if inventory.last_updated else None
        }
        
        # 缓存5分钟
        await self.redis.setex(cache_key, 300, json.dumps(inventory_data))
        
        # 同时更新SKU维度的汇总缓存
        summary_key = f"inventory:summary:{inventory.sku_id}"
        await self.redis.delete(summary_key)  # 删除汇总缓存,下次查询时重新计算
    
    async def _send_low_stock_warning(self, inventory: models.Inventory) -> None:
        """发送低库存预警"""
        
        # 获取SKU和商品信息
        sku = self.db.query(models.SKU).filter(models.SKU.id == inventory.sku_id).first()
        if not sku:
            return
        
        product = self.db.query(models.Product).filter(models.Product.id == sku.product_id).first()
        warehouse = self.db.query(models.Warehouse).filter(models.Warehouse.id == inventory.warehouse_id).first()
        
        warning_data = {
            "type": "low_stock_warning",
            "sku_id": str(inventory.sku_id),
            "sku_code": sku.sku_code if sku else "",
            "sku_name": sku.sku_name if sku else "",
            "product_id": str(product.id) if product else "",
            "product_name": product.name if product else "",
            "product_code": product.product_code if product else "",
            "warehouse_id": str(inventory.warehouse_id),
            "warehouse_name": warehouse.name if warehouse else "",
            "available_quantity": inventory.available_quantity,
            "safety_stock": inventory.safety_stock,
            "warning_threshold": inventory.warning_threshold,
            "reorder_suggestion": inventory.get_reorder_quantity(),
            "timestamp": datetime.utcnow().isoformat()
        }
        
        # 发送到消息队列
        await self.mq.publish_warning(warning_data)
        
        # 也可以发送邮件、短信等通知
        # await self._send_notification(warning_data)
    
    async def calculate_inventory_turnover(self, sku_id: str, period_days: int = 30) -> Dict[str, Any]:
        """计算库存周转率
        
        参数:
        - sku_id: SKU ID
        - period_days: 统计周期(天)
        
        返回:
        - 库存周转率信息
        """
        
        from datetime import timedelta
        
        end_date = datetime.utcnow()
        start_date = end_date - timedelta(days=period_days)
        
        # 计算销售成本(这里简化处理,实际应该从订单系统获取)
        # 假设通过库存流水计算出库数量
        outbound_transactions = self.db.query(models.InventoryTransaction).filter(
            models.InventoryTransaction.sku_id == uuid.UUID(sku_id),
            models.InventoryTransaction.transaction_type == 2,  # 出库
            models.InventoryTransaction.created_at >= start_date,
            models.InventoryTransaction.created_at <= end_date
        ).all()
        
        total_outbound = sum(abs(tx.change_quantity) for tx in outbound_transactions)
        
        # 获取SKU成本价
        sku = self.db.query(models.SKU).filter(models.SKU.id == uuid.UUID(sku_id)).first()
        if not sku:
            return {"error": "SKU不存在"}
        
        cost_of_goods_sold = total_outbound * float(sku.cost_price)
        
        # 计算平均库存
        # 这里简化处理,实际应该计算周期内的每日平均库存
        inventory = self.db.query(models.Inventory).filter(
            models.Inventory.sku_id == uuid.UUID(sku_id)
        ).first()
        
        if not inventory:
            average_inventory = 0
        else:
            # 这里简化计算,实际应该查询历史库存记录
            average_inventory = float(inventory.quantity)
        
        # 计算库存周转率
        if average_inventory > 0:
            turnover_rate = cost_of_goods_sold / average_inventory
            turnover_days = period_days / turnover_rate if turnover_rate > 0 else float('inf')
        else:
            turnover_rate = float('inf')
            turnover_days = 0
        
        return {
            "sku_id": sku_id,
            "period_days": period_days,
            "start_date": start_date.isoformat(),
            "end_date": end_date.isoformat(),
            "total_outbound": total_outbound,
            "cost_of_goods_sold": cost_of_goods_sold,
            "average_inventory": average_inventory,
            "turnover_rate": turnover_rate,
            "turnover_days": turnover_days,
            "interpretation": self._interpret_turnover_rate(turnover_rate)
        }
    
    def _interpret_turnover_rate(self, rate: float) -> str:
        """解释库存周转率"""
        if rate == float('inf'):
            return "库存为0,周转率为无限大"
        elif rate > 12:  # 年周转12次以上
            return "周转率很高,库存管理优秀"
        elif rate > 6:   # 年周转6-12次
            return "周转率良好"
        elif rate > 3:   # 年周转3-6次
            return "周转率一般"
        elif rate > 1:   # 年周转1-3次
            return "周转率较低,库存可能积压"
        else:            # 年周转1次以下
            return "周转率很低,库存严重积压"
8.2.4 分布式锁 (app/core/redis_lock.py)
python 复制代码
import asyncio
import time
import uuid
from typing import Optional
import redis.asyncio as redis
from contextlib import asynccontextmanager


class RedisDistributedLock:
    """Redis分布式锁"""
    
    def __init__(self, redis_client: redis.Redis, lock_key: str, timeout: int = 10):
        """
        初始化分布式锁
        
        参数:
        - redis_client: Redis客户端
        - lock_key: 锁的键名
        - timeout: 锁超时时间(秒)
        """
        self.redis = redis_client
        self.lock_key = lock_key
        self.timeout = timeout
        self.identifier = str(uuid.uuid4())
    
    async def acquire(self, retry_count: int = 3, retry_delay: float = 0.1) -> bool:
        """获取锁
        
        参数:
        - retry_count: 重试次数
        - retry_delay: 重试延迟(秒)
        
        返回:
        - 是否获取成功
        """
        for attempt in range(retry_count):
            # 尝试获取锁
            acquired = await self.redis.set(
                self.lock_key,
                self.identifier,
                ex=self.timeout,
                nx=True  # 只在键不存在时设置
            )
            
            if acquired:
                return True
            
            if attempt < retry_count - 1:
                await asyncio.sleep(retry_delay)
        
        return False
    
    async def release(self) -> bool:
        """释放锁
        
        使用Lua脚本确保原子性操作
        
        返回:
        - 是否释放成功
        """
        lua_script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
        """
        
        try:
            result = await self.redis.eval(lua_script, 1, self.lock_key, self.identifier)
            return bool(result)
        except Exception:
            return False
    
    async def __aenter__(self):
        """上下文管理器入口"""
        acquired = await self.acquire()
        if not acquired:
            raise Exception(f"Failed to acquire lock for key: {self.lock_key}")
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """上下文管理器出口"""
        await self.release()
    
    async def renew(self, additional_time: int = None) -> bool:
        """续期锁
        
        参数:
        - additional_time: 续期时间,不指定时使用原超时时间
        
        返回:
        - 是否续期成功
        """
        if additional_time is None:
            additional_time = self.timeout
        
        lua_script = """
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("expire", KEYS[1], ARGV[2])
        else
            return 0
        end
        """
        
        try:
            result = await self.redis.eval(lua_script, 1, self.lock_key, self.identifier, additional_time)
            return bool(result)
        except Exception:
            return False


class InventoryLockManager:
    """库存锁管理器"""
    
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
        self.locks = {}
    
    async def lock_inventory(self, sku_id: str, warehouse_id: str, timeout: int = 10) -> Optional[RedisDistributedLock]:
        """锁定库存记录
        
        参数:
        - sku_id: SKU ID
        - warehouse_id: 仓库ID
        - timeout: 锁超时时间
        
        返回:
        - 锁对象
        """
        lock_key = f"inventory:lock:{sku_id}:{warehouse_id}"
        lock = RedisDistributedLock(self.redis, lock_key, timeout)
        
        if await lock.acquire():
            # 记录锁,用于后续管理
            lock_id = f"{sku_id}:{warehouse_id}"
            self.locks[lock_id] = lock
            return lock
        
        return None
    
    async def unlock_inventory(self, sku_id: str, warehouse_id: str) -> bool:
        """解锁库存记录
        
        参数:
        - sku_id: SKU ID
        - warehouse_id: 仓库ID
        
        返回:
        - 是否解锁成功
        """
        lock_id = f"{sku_id}:{warehouse_id}"
        if lock_id in self.locks:
            lock = self.locks[lock_id]
            result = await lock.release()
            del self.locks[lock_id]
            return result
        
        # 如果内存中没有记录,尝试直接释放
        lock_key = f"inventory:lock:{sku_id}:{warehouse_id}"
        try:
            await self.redis.delete(lock_key)
            return True
        except Exception:
            return False
    
    async def unlock_all(self) -> None:
        """释放所有锁"""
        for lock_id, lock in list(self.locks.items()):
            try:
                await lock.release()
            except Exception:
                pass
            finally:
                del self.locks[lock_id]
    
    @asynccontextmanager
    async def inventory_lock(self, sku_id: str, warehouse_id: str, timeout: int = 10):
        """库存锁上下文管理器
        
        参数:
        - sku_id: SKU ID
        - warehouse_id: 仓库ID
        - timeout: 锁超时时间
        """
        lock = await self.lock_inventory(sku_id, warehouse_id, timeout)
        if not lock:
            raise Exception(f"Failed to acquire lock for {sku_id}:{warehouse_id}")
        
        try:
            yield lock
        finally:
            await self.unlock_inventory(sku_id, warehouse_id)
8.2.5 库存API路由 (app/api/v1/inventory.py)
python 复制代码
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, status, Query, BackgroundTasks
from sqlalchemy.orm import Session
import uuid

from app import schemas, models
from app.api import deps
from app.core.inventory_service import InventoryService
from app.crud import crud_product, crud_inventory, crud_sku

router = APIRouter()


@router.get("/{sku_id}", response_model=schemas.InventoryDetail)
async def get_inventory(
    sku_id: str,
    warehouse_id: Optional[str] = Query(None, description="仓库ID"),
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """获取库存信息"""
    
    try:
        uuid.UUID(sku_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的SKU ID格式"
        )
    
    # 验证SKU是否存在
    sku = crud_sku.get(db, id=sku_id)
    if not sku:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="SKU不存在"
        )
    
    # 创建库存服务实例
    inventory_service = InventoryService(db)
    
    # 获取库存信息
    inventory = await inventory_service.get_inventory(sku_id, warehouse_id)
    
    if not inventory:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="库存记录不存在"
        )
    
    return inventory


@router.post("/deduct", response_model=schemas.InventoryDeductResponse)
async def deduct_inventory(
    *,
    db: Session = Depends(deps.get_db),
    deduct_request: schemas.InventoryDeductRequest,
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """扣减库存"""
    
    # 验证SKU是否存在
    sku = crud_sku.get(db, id=deduct_request.sku_id)
    if not sku:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="SKU不存在"
        )
    
    # 验证仓库是否存在(如果指定了仓库)
    if deduct_request.warehouse_id:
        warehouse = crud_inventory.get_warehouse(db, id=deduct_request.warehouse_id)
        if not warehouse:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="仓库不存在"
            )
    
    # 创建库存服务实例
    inventory_service = InventoryService(db)
    
    # 执行库存扣减
    result = await inventory_service.deduct_inventory(deduct_request)
    
    if not result.success:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=result.message
        )
    
    return result


@router.post("/revert", response_model=schemas.InventoryDeductResponse)
async def revert_inventory(
    *,
    db: Session = Depends(deps.get_db),
    revert_request: schemas.InventoryRevertRequest,
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """回滚库存"""
    
    # 验证SKU是否存在
    sku = crud_sku.get(db, id=revert_request.sku_id)
    if not sku:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="SKU不存在"
        )
    
    # 创建库存服务实例
    inventory_service = InventoryService(db)
    
    # 执行库存回滚
    result = await inventory_service.revert_inventory(revert_request)
    
    if not result.success:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=result.message
        )
    
    return result


@router.post("/adjust", response_model=schemas.InventoryDeductResponse)
async def adjust_inventory(
    *,
    db: Session = Depends(deps.get_db),
    adjust_request: schemas.InventoryAdjustRequest,
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """调整库存"""
    
    # 验证SKU是否存在
    sku = crud_sku.get(db, id=adjust_request.sku_id)
    if not sku:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="SKU不存在"
        )
    
    # 验证仓库是否存在
    warehouse = crud_inventory.get_warehouse(db, id=adjust_request.warehouse_id)
    if not warehouse:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="仓库不存在"
        )
    
    # 创建库存服务实例
    inventory_service = InventoryService(db)
    
    # 执行库存调整
    result = await inventory_service.adjust_inventory(adjust_request)
    
    if not result.success:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=result.message
        )
    
    return result


@router.post("/batch-deduct", response_model=List[schemas.InventoryDeductResponse])
async def batch_deduct_inventory(
    *,
    db: Session = Depends(deps.get_db),
    deduct_requests: List[schemas.InventoryDeductRequest],
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """批量扣减库存"""
    
    # 验证所有SKU是否存在
    for request in deduct_requests:
        sku = crud_sku.get(db, id=request.sku_id)
        if not sku:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"SKU不存在: {request.sku_id}"
            )
        
        # 验证仓库是否存在(如果指定了仓库)
        if request.warehouse_id:
            warehouse = crud_inventory.get_warehouse(db, id=request.warehouse_id)
            if not warehouse:
                raise HTTPException(
                    status_code=status.HTTP_404_NOT_FOUND,
                    detail=f"仓库不存在: {request.warehouse_id}"
                )
    
    # 创建库存服务实例
    inventory_service = InventoryService(db)
    
    # 执行批量库存扣减
    results = await inventory_service.batch_deduct_inventory(deduct_requests)
    
    return results


@router.get("/{sku_id}/summary")
async def get_inventory_summary(
    sku_id: str,
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """获取库存汇总信息"""
    
    try:
        uuid.UUID(sku_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的SKU ID格式"
        )
    
    # 验证SKU是否存在
    sku = crud_sku.get(db, id=sku_id)
    if not sku:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="SKU不存在"
        )
    
    # 创建库存服务实例
    inventory_service = InventoryService(db)
    
    # 获取库存汇总
    summary = await inventory_service.get_inventory_summary(sku_id)
    
    return {
        "success": True,
        "data": summary
    }


@router.get("/warnings/low-stock")
async def get_low_stock_warnings(
    threshold: Optional[int] = Query(None, ge=0, description="预警阈值"),
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """获取低库存预警列表"""
    
    # 检查权限(通常只有管理员可以查看)
    if not current_user.is_superuser and not current_user.is_staff:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="权限不足"
        )
    
    # 创建库存服务实例
    inventory_service = InventoryService(db)
    
    # 获取低库存商品
    low_stock_items = await inventory_service.get_low_stock_products(threshold)
    
    return {
        "success": True,
        "data": low_stock_items,
        "total": len(low_stock_items),
        "timestamp": datetime.utcnow().isoformat()
    }


@router.get("/{sku_id}/turnover")
async def get_inventory_turnover(
    sku_id: str,
    period_days: int = Query(30, ge=1, le=365, description="统计周期(天)"),
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """获取库存周转率"""
    
    try:
        uuid.UUID(sku_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的SKU ID格式"
        )
    
    # 验证SKU是否存在
    sku = crud_sku.get(db, id=sku_id)
    if not sku:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="SKU不存在"
        )
    
    # 创建库存服务实例
    inventory_service = InventoryService(db)
    
    # 计算库存周转率
    turnover_data = await inventory_service.calculate_inventory_turnover(sku_id, period_days)
    
    if "error" in turnover_data:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=turnover_data["error"]
        )
    
    return {
        "success": True,
        "data": turnover_data
    }


@router.get("/transactions")
async def get_inventory_transactions(
    sku_id: Optional[str] = Query(None, description="SKU ID"),
    warehouse_id: Optional[str] = Query(None, description="仓库ID"),
    transaction_type: Optional[int] = Query(None, ge=1, le=5, description="交易类型"),
    start_date: Optional[str] = Query(None, description="开始日期"),
    end_date: Optional[str] = Query(None, description="结束日期"),
    page: int = Query(1, ge=1, description="页码"),
    size: int = Query(20, ge=1, le=100, description="每页数量"),
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """查询库存流水"""
    
    # 检查权限(通常只有管理员可以查看)
    if not current_user.is_superuser and not current_user.is_staff:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="权限不足"
        )
    
    # 构建查询
    query = db.query(models.InventoryTransaction)
    
    # 添加过滤条件
    if sku_id:
        try:
            query = query.filter(models.InventoryTransaction.sku_id == uuid.UUID(sku_id))
        except ValueError:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="无效的SKU ID格式"
            )
    
    if warehouse_id:
        try:
            query = query.filter(models.InventoryTransaction.warehouse_id == uuid.UUID(warehouse_id))
        except ValueError:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="无效的仓库ID格式"
            )
    
    if transaction_type:
        query = query.filter(models.InventoryTransaction.transaction_type == transaction_type)
    
    if start_date:
        query = query.filter(models.InventoryTransaction.created_at >= start_date)
    
    if end_date:
        query = query.filter(models.InventoryTransaction.created_at <= end_date)
    
    # 计算总数
    total = query.count()
    
    # 分页查询
    transactions = query.order_by(
        models.InventoryTransaction.created_at.desc()
    ).offset((page - 1) * size).limit(size).all()
    
    # 格式化结果
    transaction_list = []
    for tx in transactions:
        transaction_list.append({
            "id": str(tx.id),
            "sku_id": str(tx.sku_id),
            "warehouse_id": str(tx.warehouse_id),
            "transaction_type": tx.transaction_type,
            "transaction_type_name": self._get_transaction_type_name(tx.transaction_type),
            "change_quantity": tx.change_quantity,
            "before_quantity": tx.before_quantity,
            "after_quantity": tx.after_quantity,
            "reference_type": tx.reference_type,
            "reference_no": tx.reference_no,
            "operator_name": tx.operator_name,
            "remark": tx.remark,
            "created_at": tx.created_at.isoformat()
        })
    
    return {
        "success": True,
        "data": transaction_list,
        "total": total,
        "page": page,
        "size": size,
        "total_pages": (total + size - 1) // size
    }


def _get_transaction_type_name(self, transaction_type: int) -> str:
    """获取交易类型名称"""
    type_names = {
        1: "入库",
        2: "出库",
        3: "调整",
        4: "锁定",
        5: "解锁"
    }
    return type_names.get(transaction_type, "未知")


@router.post("/warehouses", response_model=Dict[str, Any])
async def create_warehouse(
    *,
    db: Session = Depends(deps.get_db),
    warehouse_in: schemas.WarehouseCreate,
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """创建仓库"""
    
    # 检查权限
    if not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只有管理员可以创建仓库"
        )
    
    # 检查仓库编码是否已存在
    existing = crud_inventory.get_warehouse_by_code(db, code=warehouse_in.code)
    if existing:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="仓库编码已存在"
        )
    
    # 如果设置为默认仓库,需要先取消其他仓库的默认设置
    if warehouse_in.is_default:
        db.query(models.Warehouse).filter(
            models.Warehouse.is_default == True
        ).update({"is_default": False})
    
    # 创建仓库
    warehouse = crud_inventory.create_warehouse(db, obj_in=warehouse_in)
    
    # 清除缓存
    redis_client = get_redis_client()
    await redis_client.delete("warehouse:default")
    await redis_client.delete("warehouse:list")
    
    return {
        "success": True,
        "message": "仓库创建成功",
        "data": warehouse
    }


@router.get("/warehouses", response_model=List[schemas.WarehouseInDB])
async def list_warehouses(
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """获取仓库列表"""
    
    # 检查缓存
    redis_client = get_redis_client()
    cache_key = "warehouse:list"
    cached_data = await redis_client.get(cache_key)
    
    if cached_data:
        import json
        return json.loads(cached_data)
    
    # 查询数据库
    warehouses = crud_inventory.get_warehouses(db)
    
    # 更新缓存
    warehouse_list = [warehouse.to_dict() for warehouse in warehouses]
    await redis_client.setex(cache_key, 3600, json.dumps(warehouse_list))  # 缓存1小时
    
    return warehouse_list


@router.post("/inventory/settings/{inventory_id}")
async def update_inventory_settings(
    *,
    db: Session = Depends(deps.get_db),
    inventory_id: str,
    settings_in: Dict[str, Any],
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """更新库存设置"""
    
    # 检查权限
    if not current_user.is_superuser and not current_user.is_staff:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="权限不足"
        )
    
    try:
        inventory_uuid = uuid.UUID(inventory_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的库存ID格式"
        )
    
    # 查询库存记录
    inventory = db.query(models.Inventory).filter(models.Inventory.id == inventory_uuid).first()
    if not inventory:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="库存记录不存在"
        )
    
    # 更新设置
    if "safety_stock" in settings_in:
        inventory.safety_stock = settings_in["safety_stock"]
    
    if "warning_threshold" in settings_in:
        inventory.warning_threshold = settings_in["warning_threshold"]
    
    if "max_stock" in settings_in:
        inventory.max_stock = settings_in["max_stock"]
    
    # 更新可用库存
    inventory.update_available_quantity()
    inventory.last_updated = datetime.utcnow()
    
    db.commit()
    
    # 清除缓存
    cache_key = f"inventory:{inventory.sku_id}:{inventory.warehouse_id}"
    await redis_client.delete(cache_key)
    
    return {
        "success": True,
        "message": "库存设置更新成功",
        "data": inventory
    }
8.2.6 商品API路由 (app/api/v1/products.py)
python 复制代码
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, status, Query, File, UploadFile
from sqlalchemy.orm import Session
import uuid
from datetime import datetime

from app import schemas, models
from app.api import deps
from app.crud import crud_product, crud_sku, crud_category
from app.core.inventory_service import InventoryService

router = APIRouter()


@router.get("/", response_model=schemas.ProductListResponse)
async def list_products(
    keyword: Optional[str] = Query(None, description="关键词"),
    category_id: Optional[str] = Query(None, description="分类ID"),
    brand_id: Optional[str] = Query(None, description="品牌ID"),
    status: Optional[int] = Query(None, ge=0, le=2, description="状态"),
    min_price: Optional[float] = Query(None, ge=0, description="最低价"),
    max_price: Optional[float] = Query(None, ge=0, description="最高价"),
    is_hot: Optional[bool] = Query(None, description="是否热销"),
    is_new: Optional[bool] = Query(None, description="是否新品"),
    is_recommended: Optional[bool] = Query(None, description="是否推荐"),
    page: int = Query(1, ge=1, description="页码"),
    size: int = Query(20, ge=1, le=100, description="每页数量"),
    sort_by: str = Query("created_at", description="排序字段"),
    sort_order: str = Query("desc", description="排序方向"),
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """获取商品列表"""
    
    # 构建查询参数
    query_params = schemas.ProductQueryParams(
        keyword=keyword,
        category_id=category_id,
        brand_id=brand_id,
        status=status,
        min_price=min_price,
        max_price=max_price,
        is_hot=is_hot,
        is_new=is_new,
        is_recommended=is_recommended,
        page=page,
        size=size,
        sort_by=sort_by,
        sort_order=sort_order
    )
    
    # 查询商品
    products, total = crud_product.get_multi(db, query_params=query_params)
    
    # 获取每个商品的库存汇总
    for product in products:
        inventory_service = InventoryService(db)
        summary = await inventory_service.get_inventory_summary(str(product.id))
        product.inventory_summary = summary
    
    return {
        "success": True,
        "data": products,
        "total": total,
        "page": page,
        "size": size,
        "total_pages": (total + size - 1) // size
    }


@router.post("/", response_model=schemas.ProductDetail, status_code=status.HTTP_201_CREATED)
async def create_product(
    *,
    db: Session = Depends(deps.get_db),
    product_in: schemas.ProductCreate,
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """创建商品"""
    
    # 检查权限
    if not current_user.is_superuser and not current_user.is_staff:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只有管理员可以创建商品"
        )
    
    # 检查商品编码是否已存在
    existing = crud_product.get_by_code(db, code=product_in.product_code)
    if existing:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="商品编码已存在"
        )
    
    # 检查分类是否存在
    if product_in.category_id:
        category = crud_category.get(db, id=product_in.category_id)
        if not category:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="分类不存在"
            )
    
    # 创建商品
    product = crud_product.create(db, obj_in=product_in)
    
    # 创建默认SKU
    default_sku = schemas.SKUCreate(
        product_id=str(product.id),
        sku_code=f"{product_in.product_code}-DEFAULT",
        sku_name=product_in.name,
        price=product_in.market_price,
        cost_price=product_in.cost_price,
        is_default=True
    )
    
    sku = crud_sku.create(db, obj_in=default_sku)
    
    # 获取默认仓库
    inventory_service = InventoryService(db)
    try:
        warehouse_id = await inventory_service._get_default_warehouse_id()
        
        # 创建初始库存记录
        inventory = models.Inventory(
            sku_id=sku.id,
            warehouse_id=uuid.UUID(warehouse_id),
            quantity=0,
            locked_quantity=0,
            safety_stock=0,
            status=1
        )
        inventory.update_available_quantity()
        
        db.add(inventory)
        db.commit()
    except Exception as e:
        # 如果创建库存失败,回滚商品创建
        db.rollback()
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"创建库存失败: {str(e)}"
        )
    
    return product


@router.get("/{product_id}", response_model=schemas.ProductDetail)
async def get_product(
    product_id: str,
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """获取商品详情"""
    
    try:
        uuid.UUID(product_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的商品ID格式"
        )
    
    product = crud_product.get(db, id=product_id)
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="商品不存在"
        )
    
    # 增加浏览量
    product.view_count += 1
    db.commit()
    
    # 获取库存汇总
    inventory_service = InventoryService(db)
    summary = await inventory_service.get_inventory_summary(product_id)
    product.inventory_summary = summary
    
    return product


@router.put("/{product_id}", response_model=schemas.ProductDetail)
async def update_product(
    *,
    db: Session = Depends(deps.get_db),
    product_id: str,
    product_in: schemas.ProductUpdate,
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """更新商品"""
    
    # 检查权限
    if not current_user.is_superuser and not current_user.is_staff:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只有管理员可以更新商品"
        )
    
    try:
        uuid.UUID(product_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的商品ID格式"
        )
    
    product = crud_product.get(db, id=product_id)
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="商品不存在"
        )
    
    # 如果更新分类,检查分类是否存在
    if product_in.category_id:
        category = crud_category.get(db, id=product_in.category_id)
        if not category:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="分类不存在"
            )
    
    # 更新商品
    product = crud_product.update(db, db_obj=product, obj_in=product_in)
    
    # 清除商品缓存
    redis_client = get_redis_client()
    await redis_client.delete(f"product:{product_id}")
    
    return product


@router.delete("/{product_id}")
async def delete_product(
    *,
    db: Session = Depends(deps.get_db),
    product_id: str,
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """删除商品(软删除)"""
    
    # 检查权限
    if not current_user.is_superuser:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只有超级管理员可以删除商品"
        )
    
    try:
        uuid.UUID(product_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的商品ID格式"
        )
    
    product = crud_product.get(db, id=product_id)
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="商品不存在"
        )
    
    # 软删除:将状态改为下架
    product.status = 0
    product.updated_at = datetime.utcnow()
    db.commit()
    
    # 清除缓存
    redis_client = get_redis_client()
    await redis_client.delete(f"product:{product_id}")
    
    return {
        "success": True,
        "message": "商品已下架"
    }


@router.post("/{product_id}/skus", response_model=schemas.SKUDetail)
async def create_sku(
    *,
    db: Session = Depends(deps.get_db),
    product_id: str,
    sku_in: schemas.SKUCreate,
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """为商品创建SKU"""
    
    # 检查权限
    if not current_user.is_superuser and not current_user.is_staff:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只有管理员可以创建SKU"
        )
    
    try:
        uuid.UUID(product_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的商品ID格式"
        )
    
    # 检查商品是否存在
    product = crud_product.get(db, id=product_id)
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="商品不存在"
        )
    
    # 检查SKU编码是否已存在
    existing = crud_sku.get_by_code(db, code=sku_in.sku_code)
    if existing:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="SKU编码已存在"
        )
    
    # 如果设置为默认SKU,需要先取消其他SKU的默认设置
    if sku_in.is_default:
        db.query(models.SKU).filter(
            models.SKU.product_id == uuid.UUID(product_id),
            models.SKU.is_default == True
        ).update({"is_default": False})
    
    # 创建SKU
    sku = crud_sku.create(db, obj_in=sku_in)
    
    # 获取默认仓库并创建库存记录
    inventory_service = InventoryService(db)
    try:
        warehouse_id = await inventory_service._get_default_warehouse_id()
        
        # 创建初始库存记录
        inventory = models.Inventory(
            sku_id=sku.id,
            warehouse_id=uuid.UUID(warehouse_id),
            quantity=0,
            locked_quantity=0,
            safety_stock=0,
            status=1
        )
        inventory.update_available_quantity()
        
        db.add(inventory)
        db.commit()
    except Exception as e:
        # 如果创建库存失败,回滚SKU创建
        db.rollback()
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"创建库存失败: {str(e)}"
        )
    
    return sku


@router.get("/{product_id}/skus", response_model=List[schemas.SKUDetail])
async def list_product_skus(
    product_id: str,
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """获取商品下的所有SKU"""
    
    try:
        uuid.UUID(product_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的商品ID格式"
        )
    
    # 检查商品是否存在
    product = crud_product.get(db, id=product_id)
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="商品不存在"
        )
    
    # 获取SKU列表
    skus = crud_sku.get_by_product(db, product_id=product_id)
    
    # 为每个SKU获取库存信息
    inventory_service = InventoryService(db)
    for sku in skus:
        inventory = await inventory_service.get_inventory(str(sku.id))
        sku.inventory = inventory
    
    return skus


@router.post("/{product_id}/images")
async def upload_product_image(
    *,
    db: Session = Depends(deps.get_db),
    product_id: str,
    image: UploadFile = File(...),
    is_main: bool = Query(False, description="是否主图"),
    sort_order: int = Query(0, description="排序"),
    current_user: models.User = Depends(deps.get_current_active_user),
) -> Any:
    """上传商品图片"""
    
    # 检查权限
    if not current_user.is_superuser and not current_user.is_staff:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="只有管理员可以上传图片"
        )
    
    try:
        uuid.UUID(product_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="无效的商品ID格式"
        )
    
    # 检查商品是否存在
    product = crud_product.get(db, id=product_id)
    if not product:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="商品不存在"
        )
    
    # 这里简化处理,实际应该上传到云存储
    # 生成唯一的文件名
    import os
    from pathlib import Path
    
    # 获取文件扩展名
    file_ext = os.path.splitext(image.filename)[1]
    new_filename = f"{uuid.uuid4()}{file_ext}"
    
    # 保存文件(实际项目中应该使用云存储)
    upload_dir = Path("uploads/products")
    upload_dir.mkdir(parents=True, exist_ok=True)
    
    file_path = upload_dir / new_filename
    
    # 读取文件内容并保存
    contents = await image.read()
    with open(file_path, "wb") as f:
        f.write(contents)
    
    # 如果设置为新主图,需要先取消其他图片的主图设置
    if is_main:
        db.query(models.ProductImage).filter(
            models.ProductImage.product_id == uuid.UUID(product_id),
            models.ProductImage.is_main == True
        ).update({"is_main": False})
    
    # 创建图片记录
    image_record = models.ProductImage(
        product_id=uuid.UUID(product_id),
        url=f"/uploads/products/{new_filename}",
        is_main=is_main,
        sort_order=sort_order
    )
    
    db.add(image_record)
    db.commit()
    
    return {
        "success": True,
        "message": "图片上传成功",
        "data": {
            "id": str(image_record.id),
            "url": image_record.url,
            "is_main": image_record.is_main,
            "sort_order": image_record.sort_order
        }
    }


@router.get("/search/suggestions")
async def search_suggestions(
    keyword: str = Query(..., min_length=1, description="搜索关键词"),
    limit: int = Query(10, ge=1, le=50, description="返回数量"),
    db: Session = Depends(deps.get_db),
) -> Any:
    """搜索建议"""
    
    # 简单实现,实际应该使用Elasticsearch等搜索引擎
    suggestions = []
    
    # 搜索商品名称
    products = db.query(models.Product).filter(
        models.Product.name.ilike(f"%{keyword}%"),
        models.Product.status == 1
    ).limit(limit).all()
    
    for product in products:
        suggestions.append({
            "type": "product",
            "id": str(product.id),
            "name": product.name,
            "code": product.product_code,
            "price": float(product.market_price)
        })
    
    # 搜索SKU编码和名称
    skus = db.query(models.SKU).join(
        models.Product, models.SKU.product_id == models.Product.id
    ).filter(
        and_(
            or_(
                models.SKU.sku_code.ilike(f"%{keyword}%"),
                models.SKU.sku_name.ilike(f"%{keyword}%")
            ),
            models.Product.status == 1,
            models.SKU.is_active == True
        )
    ).limit(limit).all()
    
    for sku in skus:
        suggestions.append({
            "type": "sku",
            "id": str(sku.id),
            "name": sku.sku_name,
            "code": sku.sku_code,
            "product_id": str(sku.product_id),
            "product_name": sku.product.name if sku.product else "",
            "price": float(sku.price)
        })
    
    return {
        "success": True,
        "keyword": keyword,
        "suggestions": suggestions
    }

9. 性能优化策略

9.1 缓存策略

缓存命中
缓存未命中
库存查询请求
缓存检查
返回缓存数据
查询数据库
写入缓存
库存更新操作
更新数据库
删除缓存
发送变更消息

9.2 数据库优化

  1. 索引策略

    • SKU表:sku_code, product_id, is_active
    • 库存表:sku_id, warehouse_id, (sku_id, warehouse_id)复合索引
    • 库存流水表:sku_id, created_at, (sku_id, transaction_type)
  2. 查询优化

    • 使用分页避免全表扫描
    • 避免N+1查询问题
    • 使用EXPLAIN分析查询计划

10. 测试策略

10.1 单元测试

python 复制代码
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import asyncio

from app.main import app
from app.database import Base, get_db
from app.core.inventory_service import InventoryService

# 测试数据库
SQLALCHEMY_TEST_DATABASE_URL = "sqlite:///./test_inventory.db"
engine = create_engine(SQLALCHEMY_TEST_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def override_get_db():
    try:
        db = TestingSessionLocal()
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_get_db

client = TestClient(app)

def setup_module():
    Base.metadata.create_all(bind=engine)

def teardown_module():
    Base.metadata.drop_all(bind=engine)

def test_create_product():
    """测试创建商品"""
    product_data = {
        "product_code": "TEST001",
        "name": "测试商品",
        "category_id": "test-category-id",
        "market_price": 100.0,
        "cost_price": 50.0,
        "status": 1
    }
    
    response = client.post("/api/v1/products", json=product_data)
    assert response.status_code == 201
    data = response.json()
    assert data["success"] == True
    assert data["data"]["product_code"] == "TEST001"

def test_deduct_inventory():
    """测试库存扣减"""
    # 先创建测试数据
    product_data = {
        "product_code": "TEST002",
        "name": "测试扣减商品",
        "category_id": "test-category-id",
        "market_price": 200.0,
        "cost_price": 100.0,
        "status": 1
    }
    
    product_response = client.post("/api/v1/products", json=product_data)
    product_id = product_response.json()["data"]["id"]
    
    # 获取SKU ID
    skus_response = client.get(f"/api/v1/products/{product_id}/skus")
    sku_id = skus_response.json()[0]["id"]
    
    # 调整库存到有库存的状态
    adjust_data = {
        "sku_id": sku_id,
        "warehouse_id": "default-warehouse-id",
        "quantity": 100,
        "reason": "测试调整",
        "reference_type": 3
    }
    
    client.post("/api/v1/inventory/adjust", json=adjust_data)
    
    # 测试扣减库存
    deduct_data = {
        "sku_id": sku_id,
        "quantity": 10,
        "order_id": "test-order-001",
        "order_item_id": "test-order-item-001",
        "deduct_type": 1
    }
    
    response = client.post("/api/v1/inventory/deduct", json=deduct_data)
    assert response.status_code == 200
    data = response.json()
    assert data["success"] == True
    assert data["deducted_quantity"] == 10

def test_inventory_low_stock_warning():
    """测试低库存预警"""
    # 创建库存服务实例
    db = TestingSessionLocal()
    inventory_service = InventoryService(db)
    
    # 测试低库存判断逻辑
    class MockInventory:
        def __init__(self):
            self.available_quantity = 5
            self.safety_stock = 10
            self.warning_threshold = None
        
        def is_low_stock(self):
            return self.available_quantity <= self.safety_stock
    
    mock_inventory = MockInventory()
    assert mock_inventory.is_low_stock() == True
    
    mock_inventory.available_quantity = 15
    assert mock_inventory.is_low_stock() == False

def test_concurrent_inventory_deduction():
    """测试并发库存扣减"""
    import threading
    
    results = []
    
    def deduct_thread(sku_id, quantity, order_id):
        """扣减线程"""
        deduct_data = {
            "sku_id": sku_id,
            "quantity": quantity,
            "order_id": order_id,
            "order_item_id": f"{order_id}-item",
            "deduct_type": 1
        }
        
        response = client.post("/api/v1/inventory/deduct", json=deduct_data)
        results.append(response.json())
    
    # 创建测试商品和库存
    product_data = {
        "product_code": "CONCURRENT_TEST",
        "name": "并发测试商品",
        "category_id": "test-category-id",
        "market_price": 300.0,
        "cost_price": 150.0,
        "status": 1
    }
    
    product_response = client.post("/api/v1/products", json=product_data)
    product_id = product_response.json()["data"]["id"]
    
    skus_response = client.get(f"/api/v1/products/{product_id}/skus")
    sku_id = skus_response.json()[0]["id"]
    
    # 设置库存为50
    adjust_data = {
        "sku_id": sku_id,
        "warehouse_id": "default-warehouse-id",
        "quantity": 50,
        "reason": "并发测试准备",
        "reference_type": 3
    }
    
    client.post("/api/v1/inventory/adjust", json=adjust_data)
    
    # 创建多个线程同时扣减库存
    threads = []
    for i in range(10):
        thread = threading.Thread(
            target=deduct_thread,
            args=(sku_id, 10, f"order-concurrent-{i}")
        )
        threads.append(thread)
        thread.start()
    
    # 等待所有线程完成
    for thread in threads:
        thread.join()
    
    # 检查结果:最多只有5个成功(库存50,每次扣10)
    success_count = sum(1 for r in results if r.get("success") == True)
    assert success_count == 5  # 50/10 = 5
    
    # 检查失败的都是因为库存不足
    for r in results:
        if not r.get("success"):
            assert "库存不足" in r.get("message", "")

10.2 压力测试

python 复制代码
import asyncio
import time
import statistics
from typing import List
import aiohttp
import asyncpg

class InventoryStressTest:
    """库存压力测试"""
    
    def __init__(self, base_url: str, concurrency: int = 100):
        self.base_url = base_url
        self.concurrency = concurrency
        self.results = []
    
    async def test_concurrent_deductions(self, sku_id: str, total_requests: int = 1000) -> dict:
        """测试并发扣减"""
        
        semaphore = asyncio.Semaphore(self.concurrency)
        
        async def make_request(session, request_id: int):
            async with semaphore:
                start_time = time.time()
                
                try:
                    deduct_data = {
                        "sku_id": sku_id,
                        "quantity": 1,
                        "order_id": f"stress-test-order-{request_id}",
                        "order_item_id": f"stress-test-item-{request_id}",
                        "deduct_type": 1
                    }
                    
                    async with session.post(
                        f"{self.base_url}/inventory/deduct",
                        json=deduct_data
                    ) as response:
                        end_time = time.time()
                        elapsed = end_time - start_time
                        
                        result = {
                            "request_id": request_id,
                            "status": response.status,
                            "elapsed": elapsed,
                            "success": response.status == 200
                        }
                        
                        if response.status == 200:
                            data = await response.json()
                            result["data"] = data
                        
                        return result
                        
                except Exception as e:
                    return {
                        "request_id": request_id,
                        "status": 0,
                        "elapsed": time.time() - start_time,
                        "success": False,
                        "error": str(e)
                    }
        
        async with aiohttp.ClientSession() as session:
            tasks = [make_request(session, i) for i in range(total_requests)]
            results = await asyncio.gather(*tasks)
        
        # 分析结果
        successful = [r for r in results if r["success"]]
        failed = [r for r in results if not r["success"]]
        
        response_times = [r["elapsed"] for r in results]
        
        return {
            "total_requests": total_requests,
            "successful": len(successful),
            "failed": len(failed),
            "success_rate": len(successful) / total_requests,
            "avg_response_time": statistics.mean(response_times),
            "p95_response_time": sorted(response_times)[int(0.95 * len(response_times))],
            "max_response_time": max(response_times),
            "min_response_time": min(response_times)
        }
    
    async def test_inventory_accuracy(self, sku_id: str, warehouse_id: str, db_config: dict) -> dict:
        """测试库存准确性"""
        
        # 连接数据库
        conn = await asyncpg.connect(**db_config)
        
        try:
            # 查询数据库中的实际库存
            db_result = await conn.fetchrow(
                "SELECT quantity, locked_quantity, available_quantity FROM inventory WHERE sku_id = $1 AND warehouse_id = $2",
                sku_id, warehouse_id
            )
            
            # 查询库存流水计算的理论库存
            tx_result = await conn.fetch(
                "SELECT SUM(change_quantity) as total_change FROM inventory_transactions WHERE sku_id = $1 AND warehouse_id = $2",
                sku_id, warehouse_id
            )
            
            total_change = tx_result[0]["total_change"] or 0
            
            # 查询API返回的库存
            async with aiohttp.ClientSession() as session:
                async with session.get(
                    f"{self.base_url}/inventory/{sku_id}?warehouse_id={warehouse_id}"
                ) as response:
                    api_data = await response.json()
            
            api_quantity = api_data["data"]["available_quantity"] if api_data["success"] else None
            
            return {
                "database_quantity": db_result["available_quantity"] if db_result else None,
                "calculated_from_transactions": total_change,
                "api_quantity": api_quantity,
                "consistent": (
                    db_result and 
                    api_quantity is not None and 
                    db_result["available_quantity"] == api_quantity
                )
            }
            
        finally:
            await conn.close()

11. 部署与监控

11.1 Docker部署配置

dockerfile 复制代码
# Dockerfile
FROM python:3.9-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app

# 安装系统依赖
RUN apt-get update \
    && apt-get install -y --no-install-recommends gcc libpq-dev curl \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码
COPY . .

# 创建非root用户
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

11.2 监控指标

关键监控指标:

  1. 性能指标

    • 库存查询响应时间(P50, P95, P99)
    • 库存更新操作耗时
    • API调用成功率
    • 系统吞吐量(QPS)
  2. 业务指标

    • 商品总数和SKU总数
    • 低库存商品数量
    • 库存周转率
    • 超卖发生率
  3. 系统指标

    • 数据库连接池使用率
    • Redis内存使用率
    • 消息队列积压情况
    • 错误率和异常数量

12. 总结与展望

本文详细介绍了电商平台商品管理与库存系统的设计与实现。我们设计了一个完整的系统架构,包括商品管理、SKU管理、多仓库库存管理、库存流水记录等核心功能。通过采用分布式锁、缓存策略、消息队列等技术,保证了系统的高性能和强一致性。

12.1 核心亮点

  1. 强一致性保障:通过分布式锁和数据库事务,有效防止超卖问题
  2. 高性能设计:采用多级缓存策略,优化查询性能
  3. 可扩展架构:支持多仓库、多SKU的复杂场景
  4. 完善的监控:提供全面的业务和系统监控指标

12.2 未来扩展方向

  1. 智能补货系统:基于销售预测和库存周转率的智能补货建议
  2. 供应链协同:与供应商系统集成,实现自动补货
  3. 库存调拨优化:基于地理位置和销售数据的智能调拨策略
  4. 库存金融:支持库存质押、供应链金融等增值服务

12.3 注意事项

  1. 数据一致性:在分布式环境下,需要持续关注和优化数据一致性策略
  2. 系统监控:生产环境需要完善的监控和告警机制
  3. 容灾备份:定期备份关键数据,设计容灾恢复方案
  4. 安全防护:加强API安全防护,防止恶意攻击

本系统为电商平台提供了一个坚实的基础,可以根据具体业务需求进行扩展和优化,满足不同规模电商平台的库存管理需求。

相关推荐
了一梨19 小时前
SQLite3学习笔记4:打开和关闭数据库 + 创建表(C API)
数据库·学习·sqlite
Hgfdsaqwr1 天前
Django全栈开发入门:构建一个博客系统
jvm·数据库·python
开发者小天1 天前
python中For Loop的用法
java·服务器·python
绾樘1 天前
RHCE--基于Nginx的Web服务器配置
运维·服务器·nginx
生活很暖很治愈1 天前
Linux基础开发工具
linux·服务器·git·vim
charlotte102410241 天前
数据库概述
数据库
老百姓懂点AI1 天前
[RAG实战] 向量数据库选型与优化:智能体来了(西南总部)AI agent指挥官的长短期记忆架构设计
python
清平乐的技术专栏1 天前
HBase集群连接方式
大数据·数据库·hbase
喵手1 天前
Python爬虫零基础入门【第九章:实战项目教学·第15节】搜索页采集:关键词队列 + 结果去重 + 反爬友好策略!
爬虫·python·爬虫实战·python爬虫工程化实战·零基础python爬虫教学·搜索页采集·关键词队列
Suchadar1 天前
if判断语句——Python
开发语言·python