# Backend 渐进式优化执行清单 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在保持 `/calc` API 契约和 frontendNew 计算链路不变的前提下,逐步提升 backend 的可维护性、扩展性、计算效率与开发规范。 **Architecture:** 先建立测试安全网,再把 HTTP route 与计算编排拆到 service 层,随后用 config view 和 equipment registry 收口隐式边界。最后基于耗时观测做性能优化,并补齐 backend 文档与规范。 **Tech Stack:** Python 3.10+、FastAPI、Pydantic、SQLAlchemy、Redis、SQLite、pytest、ruff、uv。 --- ## 文件结构规划 - Create: `backend/tests/conftest.py` 提供测试用 FastAPI client、alter token、最小 calc config fixture。 - Create: `backend/tests/contract/test_calc_contract.py` 固定 `/api/calc` HTTP 响应契约。 - Create: `backend/tests/golden/test_calc_golden.py` 固定典型职业计算结果结构,后续可扩展数值快照。 - Create: `backend/services/__init__.py` 标记 service 包。 - Create: `backend/services/calc_service.py` 承接 `/calc` 应用服务编排。 - Create: `backend/core/basic/character/config_view.py` 集中处理 calc config 默认值与读取。 - Create: `backend/core/basic/equipment_registry.py` 装备版本 registry 与缓存。 - Modify: `backend/api/routes/calc.py` 改为调用 `calc_service.calculate()`,保持 route 契约不变。 - Modify: `backend/core/basic/character/calc_equip.py` 逐步使用 `CalcConfigView` 与 helper,不改计算语义。 - Modify: `backend/core/basic/equipment.py` `get_equipment()` 委托 `EquipmentRegistry`,保留兼容函数。 - Modify: `backend/pyproject.toml` 增加 `pytest`、必要时增加测试配置。 - Create or Modify: `backend/README.md` 说明运行、测试、质量检查、目录职责。 ## Task 1:建立 pytest 基础与最小 contract 测试 **Files:** - Modify: `backend/pyproject.toml` - Create: `backend/tests/conftest.py` - Create: `backend/tests/contract/test_calc_contract.py` - [ ] **Step 1: 添加 pytest 开发依赖** 在 `backend/pyproject.toml` 的 `[dependency-groups].dev` 中加入: ```toml [dependency-groups] dev = [ "pytest>=8.4.0", "ruff>=0.15.15", ] ``` - [ ] **Step 2: 同步依赖** Run: ```bash cd backend && uv sync ``` Expected: `pytest` 安装完成,`uv.lock` 更新。 - [ ] **Step 3: 创建测试 fixture** Create `backend/tests/conftest.py`: ```python import base64 import json import time import pytest from fastapi.testclient import TestClient from main import application @pytest.fixture def client(): return TestClient(application) @pytest.fixture def alter_token(): payload = { 'alter': '1.1.1', 'equVersion': '0', 'time': int(time.time() * 1000), } return base64.b64encode(json.dumps(payload).encode('utf-8')).decode('utf-8') @pytest.fixture def minimal_calc_config(): parts = [ '武器', '副武器', '称号', '上衣', '头肩', '下装', '腰带', '鞋', '手镯', '项链', '戒指', '辅助装备', '魔法石', '耳环', ] return { 'equips': { part: { 'id': '', 'reinforce': 0, 'reinforceType': 0, 'enchant': '0', 'refine': 0, 'adaptation': 0, } for part in parts }, 'oaths': {}, 'emblems': {}, 'skills': {}, 'avatar': {}, 'sundry': {}, 'jades': {}, 'options': {}, 'DSB': False, 'BUFF': False, } ``` - [ ] **Step 4: 创建 contract 测试** Create `backend/tests/contract/test_calc_contract.py`: ```python def test_calc_response_contract(client, alter_token, minimal_calc_config): response = client.post( '/api/calc', headers={'alter-token': alter_token}, json=minimal_calc_config, ) assert response.status_code == 200 payload = response.json() assert payload['code'] == 200 assert payload['message'] == 'Success' assert isinstance(payload['data'], dict) data = payload['data'] assert set(['uuid', 'skills', 'info', 'suits', 'buffer']).issubset(data) assert isinstance(data['uuid'], str) assert isinstance(data['skills'], list) assert isinstance(data['info'], list) assert isinstance(data['buffer'], bool) ``` - [ ] **Step 5: 运行测试,记录真实失败** Run: ```bash cd backend && uv run pytest backend/tests/contract/test_calc_contract.py -v ``` Expected: 如果 Redis 依赖导致失败,记录失败原因;不要绕过 `/calc` 鉴权契约。后续可在 fixture 中 monkeypatch `api.dp.get_redis_info` 或提供本地 Redis。 - [ ] **Step 6: 提交本任务** Run: ```bash git add backend/pyproject.toml backend/uv.lock backend/tests/conftest.py backend/tests/contract/test_calc_contract.py git commit -m "test: add backend calc contract coverage" ``` ## Task 2:增加 core golden smoke 测试 **Files:** - Create: `backend/tests/golden/test_calc_golden.py` - [ ] **Step 1: 创建核心计算 smoke 测试** Create `backend/tests/golden/test_calc_golden.py`: ```python from core.basic.character import createCharacter def test_character_calc_returns_result_shape(minimal_calc_config): character = createCharacter('1.1.1', '0') result = character.calc(minimal_calc_config) assert isinstance(result, dict) assert set(['uuid', 'skills', 'info', 'suits', 'buffer']).issubset(result) assert isinstance(result['skills'], list) assert isinstance(result['info'], list) assert result['buffer'] is False ``` - [ ] **Step 2: 运行 golden smoke** Run: ```bash cd backend && uv run pytest backend/tests/golden/test_calc_golden.py -v ``` Expected: PASS。若职业 `1.1.1` 不存在,改用 `core.character.adventure` 中第一个 `open=True` 的职业,并同步更新 Task 1 fixture。 - [ ] **Step 3: 运行全部测试** Run: ```bash cd backend && uv run pytest -v ``` Expected: PASS,或仅 contract 测试因外部 Redis 环境失败且原因明确。 - [ ] **Step 4: 提交本任务** Run: ```bash git add backend/tests/golden/test_calc_golden.py git commit -m "test: add backend calc golden smoke" ``` ## Task 3:引入 calc service,瘦身 API route **Files:** - Create: `backend/services/__init__.py` - Create: `backend/services/calc_service.py` - Modify: `backend/api/routes/calc.py` - Test: `backend/tests/contract/test_calc_contract.py` - Test: `backend/tests/golden/test_calc_golden.py` - [ ] **Step 1: 创建 service 包** Create `backend/services/__init__.py`: ```python """Application services for backend API orchestration.""" ``` - [ ] **Step 2: 创建 calc service** Create `backend/services/calc_service.py`: ```python from typing import Any from api.core.Auth import AlterState from core.basic.character import createCharacter def calculate(state: AlterState, config: dict[str, Any] | None) -> dict[str, Any]: safe_config = config or {} character = createCharacter(state.alter, state.equVersion) return character.calc(safe_config) ``` - [ ] **Step 3: 修改 route 调用 service** Modify `backend/api/routes/calc.py`: ```python from api.core.Gzip import register_gzip_request from fastapi import APIRouter, Body from api.core.Response import response from api.dp import AltersDep from services.calc_service import calculate router = APIRouter() register_gzip_request(router) @router.post('/calc') async def calc(state: AltersDep, config=Body(None)): return response(data=calculate(state, config)) ``` - [ ] **Step 4: 运行测试** Run: ```bash cd backend && uv run pytest backend/tests/contract/test_calc_contract.py backend/tests/golden/test_calc_golden.py -v ``` Expected: PASS。 - [ ] **Step 5: 提交本任务** Run: ```bash git add backend/services/__init__.py backend/services/calc_service.py backend/api/routes/calc.py git commit -m "refactor: route calc through service layer" ``` ## Task 4:增加 CalcConfigView,集中 config 默认值 **Files:** - Create: `backend/core/basic/character/config_view.py` - Modify: `backend/core/basic/character/calc_equip.py` - Test: `backend/tests/unit/test_calc_config_view.py` - [ ] **Step 1: 创建 config view** Create `backend/core/basic/character/config_view.py`: ```python from collections.abc import Mapping from typing import Any class CalcConfigView: def __init__(self, raw: Mapping[str, Any] | None): self.raw = dict(raw or {}) @property def equips(self) -> dict[str, dict[str, Any]]: return dict(self.raw.get('equips') or {}) @property def oaths(self) -> dict[str, dict[str, Any]]: return dict(self.raw.get('oaths') or {}) @property def emblems(self) -> dict[str, dict[str, Any]]: return dict(self.raw.get('emblems') or {}) @property def skills(self) -> dict[str, dict[str, Any]]: return dict(self.raw.get('skills') or {}) @property def avatar(self) -> dict[str, Any]: return dict(self.raw.get('avatar') or {}) @property def sundry(self) -> dict[str, Any]: return dict(self.raw.get('sundry') or {}) @property def jades(self) -> dict[str, Any]: return dict(self.raw.get('jades') or {}) @property def options(self) -> dict[str, Any]: return dict(self.raw.get('options') or {}) @property def dsb(self) -> bool: return bool(self.raw.get('DSB', False)) @property def buff(self) -> bool: return bool(self.raw.get('BUFF', False)) def as_legacy_dict(self) -> dict[str, Any]: data = dict(self.raw) data.setdefault('equips', {}) data.setdefault('oaths', {}) data.setdefault('emblems', {}) data.setdefault('skills', {}) data.setdefault('avatar', {}) data.setdefault('sundry', {}) data.setdefault('jades', {}) data.setdefault('options', {}) data.setdefault('DSB', False) data.setdefault('BUFF', False) return data ``` - [ ] **Step 2: 添加单元测试** Create `backend/tests/unit/test_calc_config_view.py`: ```python from core.basic.character.config_view import CalcConfigView def test_config_view_provides_defaults(): view = CalcConfigView(None) assert view.equips == {} assert view.oaths == {} assert view.emblems == {} assert view.skills == {} assert view.avatar == {} assert view.sundry == {} assert view.jades == {} assert view.options == {} assert view.dsb is False assert view.buff is False def test_config_view_preserves_legacy_keys(): view = CalcConfigView({'skills': {'100': {'lv': 1}}, 'BUFF': True}) data = view.as_legacy_dict() assert data['skills'] == {'100': {'lv': 1}} assert data['BUFF'] is True assert data['DSB'] is False assert data['equips'] == {} ``` - [ ] **Step 3: 在 calc_init 入口使用兼容 dict** Modify `backend/core/basic/character/calc_equip.py`: ```python from .config_view import CalcConfigView ``` 在 `calc()` 起始处改为: ```python def calc(self, setInfo: dict[str, dict]): config = CalcConfigView(setInfo).as_legacy_dict() self.calc_init(config) self.SetBaseStatus() self.calc_jade(config.get('jades', [])) self.calc_avatar(config.get('avatar', {})) self.calc_basic() self.calc_sundry(config.get('sundry', {})) self.calc_emblems() suit = self.calc_suits() self.calc_equs() self.calc_oaths() skillInfos = [] if self.buffer: skillInfos = self.calc_buffer_skills() info = self.get_buffer_info() else: if len(self.输出类型选项) > 1: temp = self.输出类型选项[0] if '固伤' in temp: if self.INT > self.STR: self.输出类型 = '魔法固伤' else: self.输出类型 = '物理固伤' else: if self.INT > self.STR and self.AtkM > self.AtkP: self.输出类型 = '魔法百分比' else: self.输出类型 = '物理百分比' skillInfos = self.calc_carry_skills(config.get('DSB', False), config.get('BUFF', False)) info = self.get_carry_info() return {'uuid': uuid1().hex, 'skills': skillInfos, 'info': info, 'suits': suit, 'buffer': self.buffer} ``` - [ ] **Step 4: 运行测试** Run: ```bash cd backend && uv run pytest backend/tests/unit/test_calc_config_view.py backend/tests/golden/test_calc_golden.py -v ``` Expected: PASS。 - [ ] **Step 5: 提交本任务** Run: ```bash git add backend/core/basic/character/config_view.py backend/core/basic/character/calc_equip.py backend/tests/unit/test_calc_config_view.py git commit -m "refactor: centralize calc config defaults" ``` ## Task 5:引入 EquipmentRegistry,替换 eval 获取装备数据 **Files:** - Create: `backend/core/basic/equipment_registry.py` - Modify: `backend/core/basic/equipment.py` - Test: `backend/tests/unit/test_equipment_registry.py` - [ ] **Step 1: 创建 registry** Create `backend/core/basic/equipment_registry.py`: ```python from functools import cache class EquipmentRegistry: def __init__(self, equipment_cls): self.equipment_cls = equipment_cls @cache def get(self, version: str = '0'): normalized = version or '0' try: return self.equipment_cls(normalized) except Exception: if normalized == '0': raise return self.get('0') ``` - [ ] **Step 2: 修改 equipment.py 兼容门面** Modify `backend/core/basic/equipment.py` 底部: ```python from core.basic.equipment_registry import EquipmentRegistry _equipment_registry = EquipmentRegistry(Equipments) def get_equipment(version: str = '0') -> Equipments: return _equipment_registry.get(version) ``` 删除或停止使用: ```python global equ0 equ0 = Equipments('0') ``` 以及 `eval(f'equ{version}'.format(version))` 相关逻辑。 - [ ] **Step 3: 添加 registry 测试** Create `backend/tests/unit/test_equipment_registry.py`: ```python from core.basic.equipment import get_equipment def test_get_equipment_returns_cached_default_version(): first = get_equipment('0') second = get_equipment('0') assert first is second assert first.version == '0' def test_get_equipment_falls_back_to_default_version(): equipment = get_equipment('missing-version') assert equipment.version == '0' ``` - [ ] **Step 4: 运行测试** Run: ```bash cd backend && uv run pytest backend/tests/unit/test_equipment_registry.py backend/tests/golden/test_calc_golden.py -v ``` Expected: PASS。 - [ ] **Step 5: 提交本任务** Run: ```bash git add backend/core/basic/equipment_registry.py backend/core/basic/equipment.py backend/tests/unit/test_equipment_registry.py git commit -m "refactor: add equipment registry" ``` ## Task 6:增加 calc service 耗时观测 **Files:** - Modify: `backend/services/calc_service.py` - Test: `backend/tests/contract/test_calc_contract.py` - [ ] **Step 1: 添加轻量日志计时** Modify `backend/services/calc_service.py`: ```python import logging import time from typing import Any from api.core.Auth import AlterState from core.basic.character import createCharacter logger = logging.getLogger('dcalc.calc') def calculate(state: AlterState, config: dict[str, Any] | None) -> dict[str, Any]: started_at = time.perf_counter() safe_config = config or {} character = createCharacter(state.alter, state.equVersion) created_at = time.perf_counter() result = character.calc(safe_config) finished_at = time.perf_counter() logger.info( 'calc completed alter=%s equVersion=%s create_ms=%.2f calc_ms=%.2f total_ms=%.2f', state.alter, state.equVersion, (created_at - started_at) * 1000, (finished_at - created_at) * 1000, (finished_at - started_at) * 1000, ) return result ``` - [ ] **Step 2: 运行 contract 测试** Run: ```bash cd backend && uv run pytest backend/tests/contract/test_calc_contract.py -v ``` Expected: PASS。响应 data 不新增耗时字段。 - [ ] **Step 3: 手动观察日志** Run: ```bash cd backend && uv run python main.py ``` Expected: 服务正常启动。发起 `/api/calc` 后可在日志处理器中接入 `dcalc.calc` logger;本任务不要求改变线上日志格式。 - [ ] **Step 4: 提交本任务** Run: ```bash git add backend/services/calc_service.py git commit -m "chore: add calc timing logs" ``` ## Task 7:抽取 effect 应用 helper **Files:** - Create: `backend/core/basic/character/effect_apply.py` - Modify: `backend/core/basic/character/calc_equip.py` - Test: `backend/tests/unit/test_effect_apply.py` - Test: `backend/tests/golden/test_calc_golden.py` - [ ] **Step 1: 创建 helper** Create `backend/core/basic/character/effect_apply.py`: ```python from typing import Any def uppercase_attrs(source: Any) -> dict[str, Any]: return { key: value for key, value in source.__dict__.items() if key and key[0].isupper() } def apply_uppercase_attrs(character: Any, source: Any) -> None: character.SetStatus(**uppercase_attrs(source)) def execute_effect(effects: Any, key: str, *args: Any) -> bool: fun = effects.execture(key) if fun is None: return False fun(*args) return True ``` - [ ] **Step 2: 添加 helper 测试** Create `backend/tests/unit/test_effect_apply.py`: ```python from core.basic.character.effect_apply import uppercase_attrs class Source: STR = 10 def __init__(self): self.INT = 20 self.name = 'ignored' def test_uppercase_attrs_returns_only_uppercase_keys(): assert uppercase_attrs(Source()) == {'INT': 20} ``` - [ ] **Step 3: 在 calc_equip.py 中逐步替换重复代码** Modify selected occurrences in `backend/core/basic/character/calc_equip.py`: ```python from .effect_apply import apply_uppercase_attrs, execute_effect, uppercase_attrs ``` 将简单重复片段: ```python filtered_dict = {k: v for k, v in suit.__dict__.items() if k[0].isupper()} self.SetStatus(**filtered_dict) ``` 替换为: ```python apply_uppercase_attrs(self, suit) ``` 将: ```python fun = effects.execture(f'equ_{equ.id}') if fun is None: continue filtered_dict = {k: v for k, v in equ.__dict__.items() if k[0].isupper()} self.SetStatus(**filtered_dict) fun(self) ``` 替换为: ```python fun = effects.execture(f'equ_{equ.id}') if fun is None: continue apply_uppercase_attrs(self, equ) fun(self) ``` - [ ] **Step 4: 运行测试** Run: ```bash cd backend && uv run pytest backend/tests/unit/test_effect_apply.py backend/tests/golden/test_calc_golden.py -v ``` Expected: PASS。 - [ ] **Step 5: 提交本任务** Run: ```bash git add backend/core/basic/character/effect_apply.py backend/core/basic/character/calc_equip.py backend/tests/unit/test_effect_apply.py git commit -m "refactor: extract character effect helpers" ``` ## Task 8:补充 backend README **Files:** - Create or Modify: `backend/README.md` - [ ] **Step 1: 编写 backend README** Create or Modify `backend/README.md`: ````markdown # dcalc backend ## Run ```bash uv sync python main.py ``` ## Test ```bash uv run pytest -v ``` ## Quality ```bash uv run ruff check . ``` ## Runtime Entry - `main.py`: FastAPI application and uvicorn entry. - `api/routes/calc.py`: `/api/calc` HTTP route. - `services/calc_service.py`: calculation orchestration. - `core/basic/character/`: character calculation implementation. - `core/basic/equipment.py`: equipment data models and compatibility access. - `core/basic/equipment_registry.py`: equipment version registry and cache. - `database/connect.py`: SQLite and Redis connections. ## Compatibility Rules - Do not change `/calc` request and response contract without frontend migration approval. - Do not change frontendNew result data shape during backend refactors. - Keep Skill and Forge business formulas behavior-compatible unless a dedicated formula task says otherwise. ```` - [ ] **Step 2: 运行文档相关检查** Run: ```bash cd backend && uv run ruff check . ``` Expected: PASS,或只出现既有遗留问题。若有既有问题,不在本任务做大范围修复。 - [ ] **Step 3: 提交本任务** Run: ```bash git add backend/README.md git commit -m "docs: document backend runtime structure" ``` ## Task 9:阶段性全量验证 **Files:** - No source changes expected. - [ ] **Step 1: 运行测试** Run: ```bash cd backend && uv run pytest -v ``` Expected: PASS。 - [ ] **Step 2: 运行 lint** Run: ```bash cd backend && uv run ruff check . ``` Expected: PASS,或输出仅包含已登记的遗留问题。 - [ ] **Step 3: 启动服务** Run: ```bash cd backend && uv run python main.py ``` Expected: 输出 `Application startup complete.`,服务监听 `config.PORT`。 - [ ] **Step 4: 手动验证 frontendNew 链路** Run: ```bash cd frontendNew && pnpm dev ``` Expected: 在 Character 模块修改 Skill/Forge 配置后,仍满足 `config 变化 -> result.execute -> saveConfig`。 - [ ] **Step 5: 提交验证记录** Run: ```bash git status --short ``` Expected: 工作区干净,所有阶段任务已独立提交。