codex/backend-optimization #2

Merged
imelon merged 11 commits from codex/backend-optimization into master 2026-06-05 11:36:19 +08:00
2 changed files with 1145 additions and 0 deletions
Showing only changes of commit 98261248a2 - Show all commits
@@ -0,0 +1,237 @@
# Backend 渐进式优化设计
> 本文面向 dcalc backend 的渐进式架构优化。目标是在不改变 `/calc` API 契约、不破坏 frontendNew 计算链路的前提下,逐步提升后端可维护性、扩展性、计算效率与开发规范。
## 背景
当前仓库前端迁移主线是 `frontend -> frontendNew`,backend 必须优先服务 Dcalc Character 模块的稳定计算链路:equipment、skill、forge、result。backend 现有结构已经完成过一次 `core/basic/character_old.py` 到 `core/basic/character/` 多文件 mixin 的拆分,说明当前更适合继续做小步收口,而不是推倒重写。
关键入口:
- `/calc` 路由:`backend/api/routes/calc.py`
- 应用入口:`backend/main.py`
- 角色计算核心:`backend/core/basic/character/`
- 装备数据装载:`backend/core/basic/equipment.py`
- 数据库连接:`backend/database/connect.py`
- Redis 预热:`backend/api/core/RedisPreheat.py`
## 非目标
- 不改 `/calc` 的请求语义、响应包装、`data` 主体字段结构。
- 不改 frontendNew 依赖的 IResult 数据消费方式。
- 不重写 Skill、Forge、装备效果、职业技能的核心业务逻辑。
- 不在 backend 优化中引入新的 UI、前端状态管理或前端接口契约调整。
- 不一次性格式化全仓 Python 文件,避免制造大规模无关 diff。
## 现状问题
### API 与计算耦合
`backend/api/routes/calc.py` 直接 `createCharacter(...).calc(config)`。这让鉴权、请求适配、计算编排、错误处理和未来耗时统计都挤在 API 调用路径上,不利于扩展。
### config 读取分散
计算过程中大量直接访问 `setInfo.get(...)`、`info['equips']`、`info['oaths']`。默认值、兼容逻辑和输入容错散落在 mixin 中,后续 Skill/Forge 配置演进时容易漏字段。
### 装备数据全局缓存边界不清
`backend/core/basic/equipment.py` 启动时构造 `equ0 = Equipments('0')`,`get_equipment()` 通过动态变量和 `eval` 获取版本。该方式对当前性能有帮助,但多版本扩展、测试隔离、热更新和错误定位较弱。
### 计算对象状态可变
`Character` 实例在 `calc_init -> calc_* -> calc_*_skills` 过程中持续变异属性、技能等级、装备效果和输出类型。该模式符合当前业务写法,但缺少明确的一次性计算上下文,测试和复用成本偏高。
### 性能缺少量化
已有内存化装备数据、技能类缓存、Redis 预热等优化点,但 `/calc` 的耗时瓶颈没有分段统计。后续不应凭感觉优化。
### 测试安全网不足
当前仅看到 `backend/test.py`,没有正式 `tests/` 体系。后续重构计算链路时,缺少 golden case 来确认字段与数值没有回退。
## 优化原则
1. 接口冻结:先用测试固定 `/calc` 外部行为,再改内部结构。
2. 小步可回滚:每个阶段能单独合并、单独验证。
3. 先边界后算法:优先整理 service、config view、repository/registry,再考虑公式与缓存。
4. 只搬运不改义:拆文件、封 helper 时不改变公式含义。
5. 先量化后性能:先加耗时指标,再按热点优化。
## 目标架构
```text
backend/
api/
routes/
calc.py # 只负责 HTTP 层:依赖注入、Body、response
services/
calc_service.py # /calc 应用服务:创建角色、执行计算、计时、错误上下文
schemas/
calc.py # 宽松 config schema 与响应类型别名
response.py # 通用响应类型,保持旧格式兼容
core/
basic/
character/
config_view.py # CalcConfigView:统一默认值、兼容旧 config
effect_apply.py # 统一属性提取与 effect 执行 helper
equipment.py # 保留现有模型,逐步委托给 registry
equipment_registry.py # EquipmentRegistry:装备版本懒加载与缓存
tests/
contract/
test_calc_contract.py
golden/
test_calc_golden.py
unit/
test_equipment_registry.py
test_calc_config_view.py
```
## 数据流
```text
HTTP POST /api/calc
-> AltersDep 解析 alter/equVersion
-> calc_service.calculate(state, raw_config)
-> CalcConfigView.from_raw(raw_config)
-> createCharacter(state.alter, state.equVersion)
-> character.calc(config_view 或兼容 dict)
-> calc_service 返回原始计算 data
-> response(data=data)
```
短期内 `Character.calc()` 仍接收原 `dict`,`CalcConfigView` 先作为内部读取辅助逐步接入。等测试稳定后,再考虑让 `Character.calc()` 接收 `CalcConfigView`。
## 分阶段设计
### Phase 0:测试安全网
建立 pytest 测试结构,固定 `/calc` 契约和典型职业结果结构。
重点:
- 增加 `pytest` 开发依赖。
- 增加可复用的最小 config fixture。
- 增加 `/api/calc` HTTP contract 测试。
- 增加 `createCharacter(...).calc(...)` golden smoke 测试。
成功标准:
- `uv run pytest` 可执行。
- `/calc` 响应仍包含 `code/message/data`。
- `data` 至少包含 `uuid/skills/info/suits/buffer`。
### Phase 1:API service 边界
新增 `backend/services/calc_service.py`,API route 不再直接编排计算。
重点:
- route 保持原路径、请求体和响应格式。
- service 负责 `createCharacter`、空 config 兜底、计算执行。
- 为后续日志、耗时、缓存预留 service 层。
成功标准:
- `/api/calc` 行为不变。
- route 文件只保留 HTTP 层职责。
- Phase 0 测试全部通过。
### Phase 2:config view 与计算输入收口
新增 `CalcConfigView`,集中处理 config 的默认值与字段读取。
重点:
- `equips/oaths/emblems/skills/avatar/sundry/jades/options` 都有稳定默认值。
- 先在 `calc_init()`、`SetDetail()`、`SetSkills()` 等入口使用,不一次性替换所有 mixin。
- 保留向后兼容,仍支持原始 dict。
成功标准:
- 缺少可选字段时不抛出 KeyError。
- Skill/Forge 配置链路不变:config 变化 -> result.execute -> saveConfig。
- golden case 结果字段不变。
### Phase 3:装备数据 registry
新增 `EquipmentRegistry`,替换 `get_equipment()` 内部的 `eval` 与模块全局变量访问。
重点:
- `get_equipment(version)` 作为兼容门面继续存在。
- registry 内部使用 `functools.cache` 或显式 dict 缓存。
- 版本不存在时保持回退 `0` 的现有行为,但记录原因。
成功标准:
- 现有调用方无需一次性改动。
- 多版本装备数据加载路径清晰。
- 单元测试覆盖缓存、回退和不存在版本。
### Phase 4:计算核心 helper 化
抽取重复且无业务争议的 helper,降低 mixin 复杂度。
重点:
- `apply_uppercase_attrs(character, source)` 统一处理首字母大写属性应用。
- `execute_effect(effects, key, *args)` 统一处理效果函数不存在时的行为。
- 优先处理 `calc_suits/calc_equs/calc_oaths/calc_emblems` 中重复逻辑。
成功标准:
- helper 有单元测试。
- 业务公式位置不变或仅做等价搬迁。
- `calc_equip.py` 可读性提升,结果不变。
### Phase 5:性能观测与热点优化
在 service 层引入轻量计时,再针对热点做优化。
重点:
- 记录 `createCharacter`、`calc` 总耗时。
- 可选记录 `calc_*` 分段耗时,默认不影响响应结构。
- 优化重复 `获取基础属性()` 调用。
- 评估技能实例模板缓存,但必须避免可变技能状态污染请求。
成功标准:
- 能拿到典型配置的平均耗时和 p95。
- 优化前后 golden case 一致。
- 性能优化有数据支撑。
### Phase 6:规范与文档
清理运行缓存、废弃文件和文档入口。
重点:
- 确认 `.venv/.ruff_cache/__pycache__/.DS_Store` 不进入 git。
- 评估 `character_old.py` 移到归档或删除。
- 补充 backend README:运行、测试、质量检查、目录职责。
- 不在本阶段大规模格式化老文件。
成功标准:
- 新人可通过文档理解 backend 入口和测试方式。
- 废弃代码不会继续误导维护。
- 规范变更不产生大规模业务 diff。
## 风险与缓解
| 风险 | 缓解 |
| --- | --- |
| 重构导致 `/calc` 字段变化 | Phase 0 先做 contract/golden 测试 |
| 技能对象可变状态污染缓存 | 技能模板缓存必须后置,且只缓存类或不可变模板 |
| 装备版本回退行为改变 | `get_equipment()` 保留兼容门面,并测试回退 |
| config 字段默认值与前端不一致 | `CalcConfigView` 先只集中现有默认值,不新增语义 |
| 性能统计污染响应 | 默认只写日志,不改变 API data |
## 验收总标准
- `uv run pytest` 通过。
- `/api/calc` 响应契约不变。
- frontendNew 的 result.execute 与 saveConfig 链路无需调整。
- 典型职业 golden case 在重构前后字段一致,关键数值在允许误差内。
- backend 文档说明清晰,后续任务可以分阶段执行。
@@ -0,0 +1,908 @@
# 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: 工作区干净,所有阶段任务已独立提交。