Files

555 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 仿管家婆 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 推进。