File size: 14,357 Bytes
b4f2238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# 法律事务管理系统 - Backend Makefile
# ============================================================

check: lint typecheck ## 运行所有检查

# ============================================================
# GitHub CI 镜像检查(推送前必跑,避免反复修 7 轮)
# ============================================================

ci-check: ## 模拟 GitHub backend (3.12) job 的核心检查(changed-files 模式)
	@echo "$(GREEN)=== GitHub CI 预检 (changed-files 模式) ===$(NC)"
	@echo ""
	@if [ -n "$$CI_CHECK_BASE" ]; then \
		BASE="$$CI_CHECK_BASE"; \
	else \
		UPSTREAM=$$(git rev-parse --abbrev-ref @{u} 2>/dev/null | sed 's|origin/||' || true); \
		if [ -n "$$UPSTREAM" ]; then \
			BASE="$$UPSTREAM"; \
		else \
			BASE="main"; \
		fi; \
	fi; \
	if ! git rev-parse "$$BASE" &>/dev/null 2>&1; then \
		echo "⚠️  基准 '$$BASE' 不存在,尝试 merge-base..."; \
		BASE=$$(git merge-base HEAD HEAD~1 2>/dev/null || echo ""); \
	fi; \
	echo "📋 基准: $(YELLOW)$$BASE$(NC) → $(YELLOW)HEAD$(NC)"; \
	CHANGED=$$(git diff --name-only --diff-filter=ACMRT $$BASE HEAD 2>/dev/null | grep -E '^apps/.*\.py$$' | sed 's|^apps/||' || true); \
	if [ -z "$$CHANGED" ]; then \
		echo "✅ 无变更 Python 文件"; \
	else \
		echo "🔍 [1/3] Ruff (changed files): $$CHANGED"; \
		$(PYTHON) -m ruff check $$CHANGED --config ruff.toml; \
		echo ""; \
		echo "🔍 [2/3] Mypy (changed files, follow-imports=skip): $$CHANGED"; \
		PYTHONPATH=apiSystem:. $(PYTHON) -m mypy --config-file mypy.ini --follow-imports=skip $$CHANGED; \
		echo ""; \
	fi
	@echo "🔍 [3/3] 测试收集 (structure smoke):"
	$(TEST_ENV) $(PYTEST) tests/ci/structure/ --co -q 2>&1 | tail -3
	@echo ""
	@echo "$(GREEN)✅ GitHub CI 预检通过! 可以放心推送了$(NC)"

ci-check-full: ## 完整 CI 预检 (含 unit/integration/property smoke)
	$(MAKE) ci-check
	@echo "🧪 运行 CI 测试套件..."
	$(TEST_ENV) $(PYTEST) tests/ci/unit/ -q $(ARGS)
	$(TEST_ENV) $(PYTEST) tests/ci/integration/ -q $(ARGS)
	$(TEST_ENV) HYPOTHESIS_PROFILE=default $(PYTEST) tests/ci/property/ -q $(ARGS)

.PHONY: help install run test migrate shell lint clean ci-check ci-check-full qcluster-dev

# 默认 Python 和虚拟环境
VENV_DIR ?= .venv
UV := uv
PYTHON := $(VENV_DIR)/bin/python
PYTEST := $(VENV_DIR)/bin/pytest
MANAGE := $(PYTHON) apiSystem/manage.py
UVICORN := $(VENV_DIR)/bin/uvicorn
WATCHFILES := $(VENV_DIR)/bin/watchfiles
PORT ?= 8002
RUN_DEV_FORCE_POLLING ?= true
RUN_DEV_POLL_DELAY_MS ?= 300
QCLUSTER_DEV_FORCE_POLLING ?= true
QCLUSTER_DEV_POLL_DELAY_MS ?= 300
QCLUSTER_WATCH_PATHS ?= apps apiSystem

# 数据库配置(默认 PostgreSQL,可通过 DB_ENGINE=sqlite 切换)
DB_ENGINE ?= postgresql
DATABASE_PATH ?=
DB_NAME ?= fachuan_dev
DB_USER ?= postgres
DB_PASSWORD ?= postgres
DB_HOST ?= 127.0.0.1
DB_PORT ?= 5432

# 测试数据库配置
TEST_DB_ENGINE ?= postgresql
TEST_DB_PATH ?= /tmp/fachuan_pytest.sqlite3
TEST_DB_NAME ?= fachuan_test
TEST_DB_USER ?= postgres
TEST_DB_PASSWORD ?= postgres
TEST_DB_HOST ?= 127.0.0.1
TEST_DB_PORT ?= 5432

export DB_ENGINE DATABASE_PATH DB_NAME DB_USER DB_PASSWORD DB_HOST DB_PORT

# 颜色定义
GREEN := \033[0;32m
YELLOW := \033[0;33m
NC := \033[0m # No Color

TEST_ENV = DJANGO_DEBUG=1 DB_ENGINE=$(TEST_DB_ENGINE) DATABASE_PATH=$(TEST_DB_PATH) DB_NAME=$(TEST_DB_NAME) DB_USER=$(TEST_DB_USER) DB_PASSWORD=$(TEST_DB_PASSWORD) DB_HOST=$(TEST_DB_HOST) DB_PORT=$(TEST_DB_PORT) PYTHONPATH=apiSystem:.

help: ## 显示帮助信息
	@echo "$(GREEN)法律事务管理系统 - Backend$(NC)"
	@echo ""
	@echo "可用命令:"
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "  $(YELLOW)%-15s$(NC) %s\n", $$1, $$2}'

# ============================================================
# 安装和配置
# ============================================================

install: ## 安装生产依赖
	$(UV) sync --no-dev

install-dev: ## 安装全部依赖(含 dev + test)
	$(UV) sync

venv: ## 创建虚拟环境
	$(UV) venv $(VENV_DIR) --python 3.12
	@echo "$(GREEN)虚拟环境已创建,请运行: source $(VENV_DIR)/bin/activate$(NC)"

# ============================================================
# 开发服务器
# ============================================================

run: migrate ## 启动开发服务器 (默认端口 8002, 使用 uvicorn, 可通过 PORT=8003 修改)
	cd apiSystem && ../$(UVICORN) apiSystem.asgi:application --host 0.0.0.0 --port $(PORT)

run-dev: migrate ## 启动开发服务器 (热重载稳定模式:默认启用 polling,避免与 qcluster 并行时卡住)
	cd apiSystem && WATCHFILES_FORCE_POLLING=$(RUN_DEV_FORCE_POLLING) WATCHFILES_POLL_DELAY_MS=$(RUN_DEV_POLL_DELAY_MS) ../$(UVICORN) apiSystem.asgi:application --host 0.0.0.0 --port $(PORT) --reload --reload-delay 0.5 --reload-dir ../apps --reload-dir . --reload-include '*.html'

run-port: migrate ## 启动开发服务器 (自定义端口, 使用 PORT=8001)
	cd apiSystem && ../$(UVICORN) apiSystem.asgi:application --host 0.0.0.0 --port $(PORT)

# ============================================================
# 数据库
# ============================================================

migrate: ## 运行数据库迁移
	$(MANAGE) migrate

makemigrations: ## 创建迁移文件
	$(MANAGE) makemigrations

migrations: makemigrations migrate ## 创建并运行迁移

showmigrations: ## 显示迁移状态
	$(MANAGE) showmigrations

resetdb: ## 重置数据库 (危险!)
	@echo "$(YELLOW)警告: 这将删除所有数据!$(NC)"
	@read -p "确认继续? [y/N] " confirm && [ "$$confirm" = "y" ]
	@if [ "$(DB_ENGINE)" = "sqlite" ]; then \
		rm -f apiSystem/db.sqlite3; \
		$(MANAGE) migrate; \
	elif [ "$(DB_ENGINE)" = "postgresql" ]; then \
		$(MANAGE) flush --noinput; \
		$(MANAGE) migrate; \
	else \
		echo "$(YELLOW)未知 DB_ENGINE=$(DB_ENGINE),使用 flush + migrate 兜底$(NC)"; \
		$(MANAGE) flush --noinput; \
		$(MANAGE) migrate; \
	fi
	@echo "$(GREEN)数据库已重置 (DB_ENGINE=$(DB_ENGINE))$(NC)"

# ============================================================
# 用户管理
# ============================================================

superuser: ## 创建超级用户
	$(MANAGE) createsuperuser

# ============================================================
# 测试
# ============================================================

test: ## 运行测试
	$(TEST_ENV) $(PYTEST) tests -v --tb=short $(ARGS)

test-cov: ## 运行测试 (带覆盖率)
	$(TEST_ENV) $(PYTEST) tests -v --tb=short --cov=apps --cov-report=html --cov-report=term

test-fast: ## 快速测试 (不显示详情)
	$(TEST_ENV) $(PYTEST) tests -q

test-unit: ## 运行单元测试
	$(TEST_ENV) $(PYTEST) tests/unit -v --tb=short $(ARGS)

test-integration: ## 运行集成测试
	$(TEST_ENV) $(PYTEST) tests/integration -v --tb=short $(ARGS)

test-property: ## 运行 Property-Based Tests
	$(TEST_ENV) $(PYTEST) tests/property -v --tb=short $(ARGS)

test-structure: ## 运行结构验证测试
	$(TEST_ENV) $(PYTEST) tests/structure -v --tb=short --no-cov $(ARGS)

test-admin: ## 运行 Admin 测试
	$(TEST_ENV) $(PYTEST) tests/admin -v --tb=short $(ARGS)

# ============================================================
# 代码质量
# ============================================================

RUFF_RATCHET_PATHS = \
	apps/core/security/scrub.py \
	apps/core/infrastructure/cache.py \
	apps/automation/models/token.py \
	apps/automation/services/automation_service_adapter.py \
	apps/automation/services/scraper/core/token_service.py \
	apps/automation/services/token/cache_manager.py \
	apps/automation/services/token/history_recorder.py \
	apps/automation/services/token/token_acquisition_orchestrator.py \
	apps/automation/services/token/trigger_reasons.py \
	apps/client/services/wiring.py \
	apps/chat_records/services/wiring.py \
	apps/core/services/wiring.py \
	apps/core/services/conversation_service.py \
	apps/core/services/cause_court_initialization_service.py \
	apps/core/services/court_tokens/baoquan_token_service.py \
	apps/litigation_ai/services/wiring.py \
	apps/litigation_ai/agent/factory.py \
	apps/litigation_ai/agent/llm_provider.py \
	apps/litigation_ai/agent/middleware.py \
	apps/litigation_ai/chains/document_type_parse_chain.py \
	apps/litigation_ai/chains/litigation_draft_chain.py \
	apps/litigation_ai/chains/litigation_goal_intake_chain.py \
	apps/litigation_ai/chains/user_choice_parse_chain.py

lint-ratchet: ## 代码检查(仅棘轮路径)
	$(PYTHON) -m ruff check $(RUFF_RATCHET_PATHS) --config ruff.toml

lint: ## 全量代码检查 apps/
	$(PYTHON) -m ruff check apps --config ruff.toml

lint-full: lint ## 全量代码检查(lint 别名)

format: ## 格式化代码
	$(PYTHON) -m black apps --line-length=120
	$(PYTHON) -m isort apps

typecheck-ratchet: ## 类型检查(仅棘轮路径)
	$(PYTHON) -m mypy \
		apps/core/infrastructure/throttling.py \
		apps/core/middleware_request_id.py \
		apps/core/exceptions_handlers.py \
		apps/core/httpx_clients.py \
		apps/core/config/django_runtime.py \
		apps/core/protocols/common_protocols.py \
		apps/core/dto/auth.py \
		apps/core/llm/config.py \
		apps/core/llm/service.py \
		apps/core/llm/router.py \
		apps/core/llm/backends/base.py \
		apps/core/services/wiring.py \
		apps/core/services/system_config_service.py \
		apps/core/security/secret_codec.py \
		apps/core/dependencies/automation_sms.py \
		apps/core/security/scrub.py \
		apps/automation/services/document/document_processing_service_adapter.py \
		apps/automation/services/token/trigger_reasons.py \
		apps/automation/services/scraper/core/token_service.py \
		apps/cases/services/case/case_service.py \
		apps/contracts/services/contract/contract_service_adapter.py \
		apps/organization/services/org_access_computation_service.py \
		apps/organization/services/wiring.py \
		--follow-imports=skip \
		--config-file mypy.ini

typecheck: ## 全量类型检查 apps/
	$(PYTHON) -m mypy apps --config-file mypy.ini

typecheck-full: typecheck ## 全量类型检查(typecheck 别名)

check: lint typecheck ## 运行所有检查

# ============================================================
# Django 工具
# ============================================================

shell: ## Django Shell
	$(MANAGE) shell

dbshell: ## 数据库 Shell
	$(MANAGE) dbshell

collectstatic: ## 收集静态文件
	$(MANAGE) collectstatic --noinput

# ============================================================
# 后台任务
# ============================================================

qcluster: migrate ## 启动 Django-Q 任务队列(与 Django Web 启动顺序不限)
	@pkill -9 -f 'python.*manage.py qcluster' 2>/dev/null || true
	@sleep 1
	$(MANAGE) qcluster

qcluster-start: migrate ## 启动 Django-Q 并处理待处理任务(与 Django Web 启动顺序不限)
	@pkill -9 -f 'python.*manage.py qcluster' 2>/dev/null || true
	@sleep 1
	$(MANAGE) process_pending_tasks --reset
	$(MANAGE) qcluster

qcluster-dev: migrate ## 启动 Django-Q(文件变化自动重启)
	@echo "$(GREEN)启动 qcluster-dev: 检测 Python 文件变化后自动重启 qcluster$(NC)"
	@echo "$(YELLOW)监听目录: $(QCLUSTER_WATCH_PATHS)$(NC)"
	@WATCHFILES_FORCE_POLLING=$(QCLUSTER_DEV_FORCE_POLLING) WATCHFILES_POLL_DELAY_MS=$(QCLUSTER_DEV_POLL_DELAY_MS) $(WATCHFILES) --target-type command --filter python --ignore-paths ".venv,node_modules,.git,__pycache__,media,staticfiles,logs,htmlcov,.pytest_cache" "$(MANAGE) qcluster" $(QCLUSTER_WATCH_PATHS)

process-tasks: ## 处理待处理的爬虫任务
	$(MANAGE) process_pending_tasks

process-tasks-reset: ## 重置卡住任务并处理待处理任务
	$(MANAGE) process_pending_tasks --reset

process-tasks-dry: ## 显示待处理任务(不执行)
	$(MANAGE) process_pending_tasks --dry-run

# ============================================================
# 爬虫测试
# ============================================================

test-court-login: ## 测试法院"一张网"登录
	$(PYTHON) apps/automation/tests/test_court_zxfw_login.py

check-admin: ## 检查 Admin 配置是否正确
	$(PYTHON) scripts/check_admin_config.py

# ============================================================
# Token 管理
# ============================================================

token-example: ## 运行 Token 使用示例
	$(PYTHON) scripts/example_use_token.py

test-token: ## 测试 Token 服务
	PYTHONPATH=apiSystem:. $(PYTEST) apps/automation/tests/test_token_service.py -v

migrate-token: ## 执行 Token 相关的数据库迁移
	$(MANAGE) migrate automation
	@echo "$(GREEN)Token 数据库迁移完成$(NC)"

debug-token: ## 调试 Token 捕获问题
	$(PYTHON) scripts/debug_token_capture.py

# ============================================================
# 清理
# ============================================================

clean: ## 清理临时文件
	find . -type f -name "*.pyc" -delete
	find . -type d -name "__pycache__" -delete
	find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
	find . -type f -name ".DS_Store" -delete
	rm -rf htmlcov/
	rm -rf .coverage
	@echo "$(GREEN)清理完成$(NC)"

clean-logs: ## 清理日志文件
	rm -f logs/*.log
	@echo "$(GREEN)日志已清理$(NC)"

# ============================================================
# Docker (可选)
# ============================================================

docker-build: ## 构建 Docker 镜像
	docker build -t lawfirm-backend .

docker-run: ## 运行 Docker 容器
	docker run -p 8000:8000 lawfirm-backend

# ============================================================
# 健康检查
# ============================================================

health: ## 检查服务健康状态
	@curl -sf http://localhost:$(PORT)/health/ && echo " OK" || echo "服务未运行"

health-detail: ## 详细健康检查
	@curl -sf http://localhost:$(PORT)/health/ && echo " OK (db connected)" || echo "服务未运行或数据库不可用"

# ============================================================
# 国际化 (i18n)
# ============================================================

messages: ## 提取翻译字符串,生成 .po 文件
	cd apiSystem && ../$(PYTHON) manage.py makemessages -l en -l zh_Hans --ignore='.venv/*' --ignore='venv312/*'

compilemessages: ## 编译 .po 为 .mo 文件
	cd apiSystem && ../$(PYTHON) manage.py compilemessages