Files

21 KiB
Raw Permalink Blame History

仿管家婆 SaaS · 详细实施计划(v1.0)

项目代号:dealerhub(经销商中枢) 目标:从零搭建一个"类似管家婆"的经销商 SaaS 系统,技术栈固定为 Django 5.x + Django REST Framework + ASGI + Granian,架构层面为后续扩展做好预留。 本文档是"做之前先想清楚"的总施工图,后续按阶段推进。


〇、技术选型锁定(用户指定)

层 选型 版本(推荐) 备注
语言 Python 3.11+(建议 3.12) 用户本机已有全局 Django 5.2.16 + psycopg 3.2.13
Web 框架 Django 5.2 LTS 异步 ORM 原生支持;长期支持到 2028-04
API 框架 Django REST Framework 3.15+ async views + async serializers 已 GA
异步路由 adrf 0.1.7+ DRF 的 async 兼容层,提供 async generics
ASGI 服务器 Granian 1.6+(或 2.x) 用户指定;Rust 实现的高性能 ASGI/HTTP 服务器
数据库 PostgreSQL 16 用户已有 pg-main 容器 / 本机 Win PostgreSQL 16 (5433)
缓存 Redis / KeyDB 7+ 用户已有 192.168.5.7:6379(密码在服务器 .env)
任务队列 Dramatiq / Celery dramatiq 优先 轻量,原生 asyncio
鉴权 DRF SimpleJWT 5.x access(15m) + refresh(30d)
配置 django-environ 0.11+ 12-factor
异步驱动 psycopg 3 async (AsyncConnectionPool) 3.2+ Django 5 原生支持
部署 Docker + 阿里云/华为云 - 优先部署到 lan-19216857
包管理 pip + requirements.in - 用户偏好 vendored 离线依赖(参考 EnglishDrill 的 .pylibs/)

一、项目目录结构(落地版)

dealerhub/                              # 项目根
├── backend/                            # Django 后端(单一入口)
│   ├── manage.py
│   ├── pyproject.toml                  # 现代项目元数据
│   ├── requirements.in                 # 直接依赖(手维护)
│   ├── requirements.txt                # 锁定版本(pip-compile 生成)
│   ├── .env.example                    # 环境变量样板
│   ├── .pylibs/                        # vendored wheels(可选,参考 EnglishDrill)
│   ├── pytest.ini                      # pytest 配置
│   ├── conftest.py                     # 共享 fixtures
│   │
│   ├── config/                         # Django 项目配置包
│   │   ├── __init__.py
│   │   ├── settings/
│   │   │   ├── __init__.py
│   │   │   ├── base.py                 # 共享配置
│   │   │   ├── dev.py                  # 开发环境
│   │   │   ├── prod.py                 # 生产环境
│   │   │   └── test.py                 # 测试环境(SQLite in-memory)
│   │   ├── urls.py                     # 根路由(同步 + 异步混挂)
│   │   ├── asgi.py                     # ASGI 入口(Granian 加载)
│   │   ├── wsgi.py                     # WSGI 兜底(可选)
│   │   └── routing.py                  # adrf 异步路由聚合
│   │
│   ├── apps/                           # 业务应用(domain modules)
│   │   ├── __init__.py
│   │   ├── core/                       # 跨域公共:租户/组织/工作流/插件
│   │   │   ├── models.py               # Tenant, Org, AuditLog
│   │   │   ├── middleware.py           # TenantMiddleware, OrgContextMiddleware
│   │   │   ├── permissions.py
│   │   │   ├── plugins.py              # 钩子注册器
│   │   │   ├── workflow.py             # StateMachine
│   │   │   └── management/commands/    # seed_initial_data, etc.
│   │   │
│   │   ├── catalog/                    # 商品中心
│   │   │   ├── models.py               # Category, Product, Unit, Brand
│   │   │   ├── serializers.py
│   │   │   ├── views.py                # async ViewSets
│   │   │   ├── urls.py
│   │   │   ├── filters.py
│   │   │   └── tests/
│   │   │
│   │   ├── inventory/                  # 库存中心(预留 WMS)
│   │   │   ├── models.py               # Warehouse, Stock, Batch, Movement
│   │   │   ├── services.py             # 库存扣减/锁定/释放(核心逻辑)
│   │   │   ├── views.py
│   │   │   └── tests/
│   │   │
│   │   ├── purchase/                   # 采购
│   │   │   ├── models.py               # PurchaseOrder, PurchaseBill, PurchaseReturn
│   │   │   ├── services.py             # 创建进货单 → 写入库存 → 生成应付
│   │   │   ├── views.py
│   │   │   └── tests/
│   │   │
│   │   ├── sales/                      # 销售
│   │   │   ├── models.py               # SalesOrder, SalesBill, SalesReturn
│   │   │   ├── services.py
│   │   │   ├── views.py
│   │   │   └── tests/
│   │   │
│   │   ├── partner/                    # 客户/供应商
│   │   │   ├── models.py               # Customer, Supplier, Contact, PriceLevel
│   │   │   └── ...
│   │   │
│   │   ├── finance/                    # 财务(预留:MVP 只做应收应付,深度总账后期)
│   │   │   ├── models.py               # Receivable, Payable, Statement
│   │   │   ├── adapters.py             # LedgerAdapter 接口(Mock)
│   │   │   └── ...
│   │   │
│   │   ├── report/                     # 报表(预留 BI)
│   │   │   ├── models.py               # ReportDefinition 元数据
│   │   │   └── services.py
│   │   │
│   │   ├── channel/                    # 外部对接(电商/IM,接口预留)
│   │   │   ├── adapters.py             # ChannelAdapter 接口
│   │   │   └── webhooks.py             # 内部事件 webhook
│   │   │
│   │   ├── authx/                      # 用户/角色/权限扩展
│   │   │   ├── models.py
│   │   │   └── views.py
│   │   │
│   │   └── notify/                     # 通知中心(站内/短信/微信)
│   │       └── ...
│   │
│   ├── webhooks/                       # 外部回调
│   ├── openapi/                        # 开放 API(v1 命名空间)
│   │
│   ├── static/
│   ├── media/
│   │
│   └── tests/                          # 跨 app 集成测试
│       ├── test_smoke.py
│       └── test_health.py
│
├── frontend/                           # 前端(暂留空,后续 Vue3 + uni-app)
│   └── README.md
│
├── infra/
│   ├── docker/
│   │   ├── Dockerfile.backend
│   │   └── docker-compose.yml
│   ├── nginx/
│   └── scripts/
│       ├── run_dev.sh
│       ├── run_prod_granian.sh
│       └── seed.sh
│
├── docs/
│   ├── ARCHITECTURE.md                 # 架构总览(用户已看过)
│   ├── API.md
│   ├── ROADMAP.md
│   └── CHANGELOG.md
│
├── .gitignore
├── .editorconfig
├── README.md
└── PLAN.md                             # 本文件

二、阶段划分(5 个里程碑)

阶段 0 · 准备(1–2 天)

目标:把项目骨架搭起来,能 python manage.py runserver 看到 Hello World,能 granian 启动 ASGI。

任务清单:

  • 创建项目目录 dealerhub/backend
  • pip install django==5.2 djangorestframework==3.15 adrf psycopg[binary]==3.2 grannian django-environ djangorestframework-simplejwt
  • django-admin startproject config backend
  • 改造为 config/settings/{base,dev,prod,test}.py
  • 写 config/asgi.py(基础)
  • 写 manage.py 启动命令(dev 走 runserver,prod 走 granian)
  • 第一个 endpoint:GET /api/v1/ping/ 返回 {"ok": true, "ts": ...}
  • 启动 Granian 验证:granian config.asgi:application --interface asgi --host 0.0.0.0 --port 8000

验证标准:

  • curl http://127.0.0.1:8000/api/v1/ping/ 返回 200 + JSON
  • python manage.py test 全部通过
  • Granian 启动后并发 50 个请求无报错

阶段 1 · 核心数据模型 + 多租户预留(3–5 天)

目标:核心表结构落库,多租户字段贯穿始终,跑通第一个 CRUD。

任务清单:

  • 创建 apps/core:Tenant、Org、AuditLog 模型
  • 写 TenantMiddleware:从 Header X-Tenant-Id 识别租户,注入 request.tenant
  • 创建 apps/catalog:Category、Unit、Product(含预留 tenant_id/org_id/ext_data/source_channel)
  • 抽象基类 TenantScopedModel:自动加租户过滤
  • 写第一个 async ViewSet:商品列表(GET/POST)+ DRF async pagination
  • pytest 覆盖:CRUD 流程 + 跨租户隔离测试

验证标准:

  • 能在 /api/v1/catalog/products/ 增删改查
  • 跨租户数据完全隔离
  • 所有 ViewSet 都是 async(用 pytest-asyncio 跑通)

阶段 2 · 进销存主线业务(10–15 天)

目标:跑通"采购进货 → 入库 → 销售开单 → 出库 → 应收应付"完整业务闭环。

任务清单:

  • apps/inventory:Warehouse、Stock、StockMovement 模型
  • 库存服务 inventory.services:原子操作(同事务内扣减 + 写日志)
  • apps/purchase:PurchaseOrder、Billing、Return
  • apps/sales:SalesOrder、Billing、Return
  • apps/partner:Customer、Supplier、Contact、PriceLevel
  • apps/finance:Receivable、Payable、Statement(仅应收应付,不做总账)
  • 写单据状态机 core.workflow.StateMachine:DRAFT → CONFIRMED → POSTED → CLOSED
  • 每个状态变化触发钩子 → 库存/应收应付更新
  • 集成测试:完整业务链路

验证标准:

  • 完整跑通"采购 10 件 → 入库 → 销售 7 件 → 收 1000 元 → 自动生成应收"
  • 库存数量、应收金额、单据状态、流水日志 全部一致
  • pytest 集成测试通过

阶段 3 · 预留点落地(5–7 天)

目标:把架构文档里 10 个预留点真正落到代码里。

任务清单:

  • 多组织:Org 模型 + OrgContextMiddleware + 数据按 org 过滤
  • 多币种/多税率:MoneyField(amount, currency) + 字典表
  • 财务模块对接接口:finance.adapters.LedgerAdapter + MockAdapter
  • 电商平台对接:channel.adapters.ChannelAdapter + 接口定义
  • 工作流引擎:core.workflow.StateMachine 服务化(已部分完成)
  • 插件钩子:core.plugins.HookRegistry + 在 save/delete 上挂钩子
  • 报表元数据:report.models.ReportDefinition + 简单自定义查询
  • AI Agent 接入点:在 SalesOrder 上预留 intent 字段
  • 开放 API:/openapi/v1/* 命名空间 + API key 鉴权
  • Webhook:订单事件发 webhook(HMAC 签名)

验证标准:

  • 每个预留点都有对应的接口/抽象类/服务
  • 单元测试覆盖抽象接口
  • 对应的 Mock 实现可用

阶段 4 · 上线准备(3–5 天)

目标:能部署到 lan-19216857 服务器,能跑生产。

任务清单:

  • 写 Dockerfile.backend(基于 python:3.12-slim)
  • 写 docker-compose.yml(backend + db + redis)
  • 写 infra/nginx/ 反代配置
  • 写 infra/scripts/run_prod_granian.sh
  • 部署到 192.168.5.7
  • 冒烟测试:登录 / 商品列表 / 销售开单
  • Prometheus 指标 + 健康探针
  • README + 部署文档

验证标准:

  • curl https://dealerhub.example.com/api/v1/ping/ 返回 200
  • 并发 100 个请求 P95 < 200ms
  • 日志能看到审计 trail

三、关键架构决策记录(ADR)

ADR-001 · 异步优先

决策:所有新写的 ViewSet/Serializer 必须是 async(除 Django Admin)。 理由:用户明确要求 ASGI + Granian,异步能让单进程吞吐提高 5–10 倍。 代价:同步 ORM 调用必须用 sync_to_async 包;Django Admin 不能 async。

ADR-002 · 多租户单数据库

决策:MVP 用单数据库 + tenant_id 列做隔离;不做 schema 隔离、不做数据库隔离。 理由:MVP 阶段没必要上 schema 隔离;后期可平滑迁移到 schema 隔离。 约束:所有业务表必须继承 TenantScopedModel;查询必须经过 manager 过滤。

ADR-003 · 单据状态机服务化

决策:单据状态变化走 StateMachine 服务,不在 model.save() 里散写。 理由:散写导致后期加审批流/钩子/Audit 时到处补丁。 接口:StateMachine.transition(instance, event, user) → 校验 guard → 执行 action → 发 hook。

ADR-004 · 库存服务原子化

决策:库存数量变化必须走 inventory.services 的原子函数,不允许业务层直接 update Stock。 理由:库存 = 钱,错一票就是几万;必须统一收敛。

ADR-005 · DRF + adrf 异步混挂

决策:同步路由用 path(), 异步路由用 adrf.router.ADRFFromRouter(routers.SimpleRouter())。 理由:Django Admin 必须走 sync;业务 API 全 async。

ADR-006 · 财务模块接口化

决策:深度总账/凭证不写实现,只写接口 + Mock;推荐客户继续用管家婆财贸双全/金蝶/用友。 理由:财务是合规密集区,自研风险大;MVP 把应收应付做好就够了。

ADR-007 · Webhook + 开放 API 双通道

决策:对外既提供 REST API(拉),也提供 Webhook(推)。 理由:分销 ERP 的下游/上游都需要实时同步;纯拉模式体验差。

ADR-008 · 部署走内网服务器

决策:所有部署/容器化测试走 192.168.5.7(lan-19216857)。 理由:本机 Docker Desktop 经常不可用(用户既定策略)。


四、代码骨架(最小可运行版本)

4.1 backend/requirements.in

django==5.2.*
djangorestframework==3.15.*
adrf==0.1.*
psycopg[binary]==3.2.*
granian==1.6.*
django-environ==0.11.*
djangorestframework-simplejwt==5.*
pytest==8.*
pytest-asyncio==0.23.*
pytest-django==4.*
model-bakery==1.20.*

4.2 backend/config/settings/base.py 骨架

import environ
env = environ.Env()
environ.Env.read_env()

SECRET_KEY = env("DJANGO_SECRET_KEY", default="dev-insecure-change-me")
DEBUG = env.bool("DJANGO_DEBUG", default=True)
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"])

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "rest_framework_simplejwt",
    "apps.core",
    "apps.catalog",
    # 后续阶段再加
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "apps.core.middleware.TenantMiddleware",  # 多租户识别
]

ROOT_URLCONF = "config.urls"
ASGI_APPLICATION = "config.asgi.application"

DATABASES = {
    "default": env.db_url("DATABASE_URL", default="postgres://postgres:postgres@localhost:5432/dealerhub"),
}

# async 关键配置
DATABASES["default"]["OPTIONS"] = {
    "pool": {"min_size": 2, "max_size": 10, "timeout": 10},
}

USE_TZ = True
TIME_ZONE = "Asia/Shanghai"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ),
    "DEFAULT_PERMISSION_CLASSES": (
        "rest_framework.permissions.IsAuthenticated",
    ),
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
    # 用 adrf 的 default renderer/parser(async 安全)
}

4.3 backend/config/asgi.py(Granian 加载入口)

import os
from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev")

application = get_asgi_application()

4.4 backend/config/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from apps.core.views import ping

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/ping/", ping, name="ping"),
    path("api/v1/auth/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("api/v1/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
    # 后续阶段:catalog.urls, sales.urls, etc.
]

4.5 backend/apps/core/views.py(async ping)

from datetime import datetime, timezone
from adrf.decorators import api_view
from rest_framework.response import Response

@api_view(["GET"])
async def ping(request):
    return Response({
        "ok": True,
        "service": "dealerhub",
        "ts": datetime.now(timezone.utc).isoformat(),
    })

4.6 backend/infra/scripts/run_dev.sh

#!/usr/bin/env bash
set -e
export DJANGO_SETTINGS_MODULE=config.settings.dev
python manage.py migrate --noinput
exec granian config.asgi:application \
    --interface asgi \
    --host 0.0.0.0 \
    --port 8000 \
    --reload \
    --log-level info

4.7 backend/infra/scripts/run_prod_granian.sh

#!/usr/bin/env bash
set -e
export DJANGO_SETTINGS_MODULE=config.settings.prod
python manage.py migrate --noinput
python manage.py collectstatic --noinput
exec granian config.asgi:application \
    --interface asgi \
    --host 0.0.0.0 \
    --port 8000 \
    --workers "${GRANIAN_WORKERS:-4}" \
    --threads "${GRANIAN_THREADS:-1}" \
    --process-name "dealerhub" \
    --log-level info

4.8 backend/Dockerfile.backend

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

WORKDIR /app

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

# 依赖(先 layer 缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 代码
COPY . .

EXPOSE 8000

CMD ["sh", "infra/scripts/run_prod_granian.sh"]

4.9 backend/docker-compose.yml

services:
  backend:
    build: ./backend
    image: dealerhub-backend:latest
    restart: unless-stopped
    environment:
      DJANGO_SETTINGS_MODULE: config.settings.prod
      DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY}
      DATABASE_URL: postgres://dealerhub:${POSTGRES_PASSWORD}@db:5432/dealerhub
      REDIS_URL: redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - "8000:8000"
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: dealerhub
      POSTGRES_USER: dealerhub
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dealerhub"]
      interval: 5s
      timeout: 3s
      retries: 10
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

volumes:
  pgdata:

五、立刻要做的(先把骨架跑通)

按下面顺序执行,每个步骤都有验证点:

  1. 创建项目目录 + git init
  2. 创建 venv / 直接用全局(用户偏好全局 Python + Django 5.2.16)
  3. 写 requirements.in,pip install 验证
  4. 创建 django project config,调整目录结构
  5. 写 settings/{base,dev,prod,test}.py
  6. 写 asgi.py
  7. 创建第一个 app core + ping endpoint
  8. 跑 python manage.py migrate + python manage.py test
  9. 启动 granian + curl /api/v1/ping/ 验证
  10. 写一个 pytest 集成测试(async)
  11. 写 Dockerfile + docker-compose
  12. 写 README + 部署文档

六、风险与对策

风险 概率 对策
Django 5.2 + adrf + Granian 组合兼容性 低 adrf 0.1 已稳定,Granian 1.x 兼容 Django 5
本机 Docker 不可用 高(用户已知) 部署走 192.168.5.7
Granian 1.x / 2.x API 差异 中 1.x 文档已 GA,优先 1.6+
异步 ORM 性能反不如同步 低 psycopg 3 异步连接池已成熟
用户对 Python 版本/venv 偏好 - 用户偏好全局 Python(Django 5.2.16 已装)

七、参考

  • 用户记忆:novel_reader/EnglishDrill/营养师 三个 Django+Granian 项目经验
  • 营养师数据查询项目:Django 5.1 + ADRF + Granian ASGI + PostgreSQL 16(已有完整实现)
  • 用户偏好:modern minimal UI、git init 优先、vendored .pylibs/、全局 Python
  • 部署目标:192.168.5.7 (lan-19216857)

下一步:本轮先把阶段 0 的"骨架跑通"做出来,验证 Django+DRF+adrf+Granian 的异步链路在用户本机能正常起来。然后再按阶段 1-4 推进。