# 仿管家婆 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` 骨架 ```python 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 加载入口) ```python 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` ```python 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) ```python 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` ```bash #!/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` ```bash #!/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` ```dockerfile 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` ```yaml 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 推进。