feat: 支持网站目标分组轻量管理(新增、编辑、描述展示),不修改和不提交倍率
This commit is contained in:
@@ -30,6 +30,8 @@ from app.schemas.website import (
|
||||
TestResult,
|
||||
WebsiteCreate,
|
||||
WebsiteGroupResponse,
|
||||
WebsiteGroupCreate,
|
||||
WebsiteGroupUpdate,
|
||||
WebsiteResponse,
|
||||
WebsiteSyncLogResponse,
|
||||
WebsiteUpdate,
|
||||
@@ -291,6 +293,49 @@ def list_website_groups(wid: int, db: Session = Depends(get_db), _=Depends(get_c
|
||||
raise HTTPException(502, str(exc))
|
||||
|
||||
|
||||
@router.post("/api/websites/{wid}/groups", response_model=dict)
|
||||
def create_website_group(
|
||||
wid: int,
|
||||
body: WebsiteGroupCreate,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(get_current_user),
|
||||
):
|
||||
row = db.query(Website).filter(Website.id == wid).first()
|
||||
if not row:
|
||||
raise HTTPException(404, "website not found")
|
||||
try:
|
||||
with _client(row) as c:
|
||||
payload = {
|
||||
"name": body.name,
|
||||
"description": body.description,
|
||||
}
|
||||
return c.create_group(payload, row.groups_endpoint)
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, str(exc))
|
||||
|
||||
|
||||
@router.put("/api/websites/{wid}/groups/{group_id}", response_model=dict)
|
||||
def update_website_group(
|
||||
wid: int,
|
||||
group_id: str,
|
||||
body: WebsiteGroupUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
_=Depends(get_current_user),
|
||||
):
|
||||
row = db.query(Website).filter(Website.id == wid).first()
|
||||
if not row:
|
||||
raise HTTPException(404, "website not found")
|
||||
try:
|
||||
with _client(row) as c:
|
||||
payload = {
|
||||
"name": body.name,
|
||||
"description": body.description,
|
||||
}
|
||||
return c.update_group(row.group_update_endpoint, group_id, payload)
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, str(exc))
|
||||
|
||||
|
||||
@router.post("/api/websites/{wid}/groups/import-from-upstream/{upstream_id}", response_model=ImportGroupsResponse)
|
||||
def import_groups_from_upstream(
|
||||
wid: int,
|
||||
|
||||
@@ -64,9 +64,20 @@ class WebsiteGroupResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
rate_multiplier: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
raw: dict[str, Any] = {}
|
||||
|
||||
|
||||
class WebsiteGroupCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1)
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class WebsiteGroupUpdate(BaseModel):
|
||||
name: str = Field(..., min_length=1)
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class BindingSourceGroup(BaseModel):
|
||||
upstream_id: int
|
||||
group_id: str
|
||||
|
||||
@@ -113,7 +113,13 @@ def normalize_groups(value: Any) -> list[dict[str, Any]]:
|
||||
groups: list[dict[str, Any]] = []
|
||||
for item in raw:
|
||||
if isinstance(item, str):
|
||||
groups.append({"id": item, "name": item, "rate_multiplier": None, "raw": {"id": item, "name": item}})
|
||||
groups.append({
|
||||
"id": item,
|
||||
"name": item,
|
||||
"rate_multiplier": None,
|
||||
"description": None,
|
||||
"raw": {"id": item, "name": item}
|
||||
})
|
||||
continue
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
@@ -122,10 +128,12 @@ def normalize_groups(value: Any) -> list[dict[str, Any]]:
|
||||
continue
|
||||
name = item.get("name") or item.get("group_name") or str(gid)
|
||||
rate = item.get("rate_multiplier") or item.get("rateMultiplier") or item.get("ratio")
|
||||
desc = item.get("description") or item.get("desc") or item.get("remark")
|
||||
groups.append({
|
||||
"id": str(gid),
|
||||
"name": str(name),
|
||||
"rate_multiplier": fixed_decimal_string(rate, 2) if rate is not None else None,
|
||||
"description": str(desc) if desc is not None else None,
|
||||
"raw": item,
|
||||
})
|
||||
return groups
|
||||
@@ -262,6 +270,12 @@ class Sub2ApiWebsiteClient:
|
||||
path = endpoint_template.replace("{id}", quote(group_id, safe=""))
|
||||
return self._request("PUT", path, {"rate_multiplier": fixed_decimal_number(rate, 2)})
|
||||
|
||||
def update_group(self, endpoint_template: str, group_id: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
path = endpoint_template.replace("{id}", quote(group_id, safe=""))
|
||||
resp = self._request("PUT", path, body)
|
||||
data = _unwrap_data(resp)
|
||||
return data if isinstance(data, dict) else {"value": data}
|
||||
|
||||
def create_group(self, body: dict[str, Any], endpoint: str = "/groups") -> dict[str, Any]:
|
||||
resp = self._request("POST", endpoint, body)
|
||||
data = _unwrap_data(resp)
|
||||
|
||||
@@ -49,6 +49,7 @@ def test_normalize_groups_unwraps_wrapped_list_response():
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"rate_multiplier": "2.00",
|
||||
"description": None,
|
||||
"raw": {"id": "default", "name": "Default", "rateMultiplier": "2.0"},
|
||||
}]
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.database import Base
|
||||
from app.models.website import Website
|
||||
from app.routers.websites import create_website_group, update_website_group
|
||||
from app.schemas.website import WebsiteGroupCreate, WebsiteGroupUpdate
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def db_session():
|
||||
engine = create_engine(
|
||||
"sqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
from app.models import admin_user, upstream, snapshot, webhook_config, notification_log, custom_page, website, revoked_token, upstream_key
|
||||
Base.metadata.create_all(bind=engine)
|
||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
db = TestingSessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
|
||||
|
||||
def test_create_and_update_website_group(db_session, monkeypatch):
|
||||
w = Website(
|
||||
name="W1",
|
||||
site_type="sub2api",
|
||||
base_url="http://w1",
|
||||
enabled=True,
|
||||
auth_config_json="{}",
|
||||
timeout_seconds=30
|
||||
)
|
||||
db_session.add(w)
|
||||
db_session.commit()
|
||||
db_session.refresh(w)
|
||||
|
||||
called_create = []
|
||||
called_update = []
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
def __enter__(self):
|
||||
return self
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
def create_group(self, body, endpoint):
|
||||
called_create.append((body, endpoint))
|
||||
return {"id": "new-gid", "name": body["name"], "description": body.get("description")}
|
||||
def update_group(self, endpoint_template, group_id, body):
|
||||
called_update.append((endpoint_template, group_id, body))
|
||||
return {"id": group_id, "name": body["name"], "description": body.get("description")}
|
||||
|
||||
monkeypatch.setattr("app.routers.websites._client", lambda website: FakeClient())
|
||||
|
||||
# 1. Test create_website_group
|
||||
body_create = WebsiteGroupCreate(name="NewGroup", description="NewDesc")
|
||||
res_create = create_website_group(wid=w.id, body=body_create, db=db_session)
|
||||
assert res_create["id"] == "new-gid"
|
||||
assert res_create["name"] == "NewGroup"
|
||||
assert res_create["description"] == "NewDesc"
|
||||
assert len(called_create) == 1
|
||||
assert called_create[0][0] == {"name": "NewGroup", "description": "NewDesc"}
|
||||
|
||||
# 2. Test update_website_group
|
||||
body_update = WebsiteGroupUpdate(name="UpdatedGroup", description="UpdatedDesc")
|
||||
res_update = update_website_group(wid=w.id, group_id="g123", body=body_update, db=db_session)
|
||||
assert res_update["id"] == "g123"
|
||||
assert res_update["name"] == "UpdatedGroup"
|
||||
assert res_update["description"] == "UpdatedDesc"
|
||||
assert len(called_update) == 1
|
||||
assert called_update[0][1] == "g123"
|
||||
assert called_update[0][2] == {"name": "UpdatedGroup", "description": "UpdatedDesc"}
|
||||
Reference in New Issue
Block a user