909 lines
21 KiB
Markdown
909 lines
21 KiB
Markdown
# 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: 工作区干净,所有阶段任务已独立提交。
|