555 lines
21 KiB
Markdown
555 lines
21 KiB
Markdown
# 仿管家婆 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 推进。
|