baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+554
View File
@@ -0,0 +1,554 @@
# 仿管家婆 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 推进。