Files
dcalc/docs/backend/2026-06-04-backend-optimization-tasks.md
T

21 KiB

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 中加入:

[dependency-groups]
dev = [
    "pytest>=8.4.0",
    "ruff>=0.15.15",
]
  • Step 2: 同步依赖

Run:

cd backend && uv sync

Expected: pytest 安装完成,uv.lock 更新。

  • Step 3: 创建测试 fixture

Create backend/tests/conftest.py:

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:

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:

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:

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:

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:

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:

cd backend && uv run pytest -v

Expected: PASS,或仅 contract 测试因外部 Redis 环境失败且原因明确。

  • Step 4: 提交本任务

Run:

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:

"""Application services for backend API orchestration."""
  • Step 2: 创建 calc service

Create backend/services/calc_service.py:

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:

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:

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:

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:

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:

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:

from .config_view import CalcConfigView

在 calc() 起始处改为:

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:

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:

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:

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 底部:

from core.basic.equipment_registry import EquipmentRegistry


_equipment_registry = EquipmentRegistry(Equipments)


def get_equipment(version: str = '0') -> Equipments:
    return _equipment_registry.get(version)

删除或停止使用:

global equ0
equ0 = Equipments('0')

以及 eval(f'equ{version}'.format(version)) 相关逻辑。

  • Step 3: 添加 registry 测试

Create backend/tests/unit/test_equipment_registry.py:

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:

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:

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:

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:

cd backend && uv run pytest backend/tests/contract/test_calc_contract.py -v

Expected: PASS。响应 data 不新增耗时字段。

  • Step 3: 手动观察日志

Run:

cd backend && uv run python main.py

Expected: 服务正常启动。发起 /api/calc 后可在日志处理器中接入 dcalc.calc logger;本任务不要求改变线上日志格式。

  • Step 4: 提交本任务

Run:

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:

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:

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:

from .effect_apply import apply_uppercase_attrs, execute_effect, uppercase_attrs

将简单重复片段:

filtered_dict = {k: v for k, v in suit.__dict__.items() if k[0].isupper()}
self.SetStatus(**filtered_dict)

替换为:

apply_uppercase_attrs(self, suit)

将:

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)

替换为:

fun = effects.execture(f'equ_{equ.id}')
if fun is None:
    continue
apply_uppercase_attrs(self, equ)
fun(self)
  • Step 4: 运行测试

Run:

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:

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:

# 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:

cd backend && uv run ruff check .

Expected: PASS,或只出现既有遗留问题。若有既有问题,不在本任务做大范围修复。

  • Step 3: 提交本任务

Run:

git add backend/README.md
git commit -m "docs: document backend runtime structure"

Task 9:阶段性全量验证

Files:

  • No source changes expected.

  • Step 1: 运行测试

Run:

cd backend && uv run pytest -v

Expected: PASS。

  • Step 2: 运行 lint

Run:

cd backend && uv run ruff check .

Expected: PASS,或输出仅包含已登记的遗留问题。

  • Step 3: 启动服务

Run:

cd backend && uv run python main.py

Expected: 输出 Application startup complete.,服务监听 config.PORT。

  • Step 4: 手动验证 frontendNew 链路

Run:

cd frontendNew && pnpm dev

Expected: 在 Character 模块修改 Skill/Forge 配置后,仍满足 config 变化 -> result.execute -> saveConfig。

  • Step 5: 提交验证记录

Run:

git status --short

Expected: 工作区干净,所有阶段任务已独立提交。