21 KiB
仿管家婆 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-simplejwtdjango-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 + JSONpython manage.py test全部通过- Granian 启动后并发 50 个请求无报错
阶段 1 · 核心数据模型 + 多租户预留(3–5 天)
目标:核心表结构落库,多租户字段贯穿始终,跑通第一个 CRUD。
任务清单:
- 创建
apps/core:Tenant、Org、AuditLog 模型 - 写
TenantMiddleware:从 HeaderX-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、Returnapps/sales:SalesOrder、Billing、Returnapps/partner:Customer、Supplier、Contact、PriceLevelapps/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:
五、立刻要做的(先把骨架跑通)
按下面顺序执行,每个步骤都有验证点:
- 创建项目目录 + git init
- 创建 venv / 直接用全局(用户偏好全局 Python + Django 5.2.16)
- 写
requirements.in,pip install 验证 - 创建 django project
config,调整目录结构 - 写 settings/{base,dev,prod,test}.py
- 写 asgi.py
- 创建第一个 app
core+ ping endpoint - 跑
python manage.py migrate+python manage.py test - 启动 granian +
curl /api/v1/ping/验证 - 写一个 pytest 集成测试(async)
- 写 Dockerfile + docker-compose
- 写 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 推进。