Files
shaguabijia-app-server/tests/test_admin_config.py
T
marco 07c0b7502c feat(welfare): 提现人工审核流程 + 看广告每日时长限流(防刷主闸) (#15)
提现人工审核(提现不再即时打款):
- models/wallet.py: WithdrawOrder 状态机改为
  reviewing →(审核通过)→ pending → success/failed;reviewing →(驳回)→ rejected(已退款),
  默认态 pending → reviewing(扣现金建单但不打款);新增 user_name 列(实名, 微信达额转账要求)
- admin/routers/withdraw.py + admin/schemas/wallet.py: 运营后台审核接口(通过触发微信转账 / 驳回退款)
- api/v1/wallet.py + repositories/wallet.py: 发起提现进 reviewing 态, 驳回退回现金并写 withdraw_refund
- schemas/welfare.py: 提现出参增 reviewing/rejected 状态 + fail_reason

看广告每日时长限流(防刷主闸 + 次数兜底):
- 新增 models/ad_watch_log.py: 按 (user_id, watch_date) 聚合当日观看秒数
- 新增 repositories/ad_watch.py: watched_seconds_today / add_watch_seconds(夹 [0, MAX_SINGLE_WATCH_SECONDS])
- api/v1/ad.py: 新增 POST /ad/watch-report(JWT 取 user, 落时长返当日累计);
  /ad/reward-status 增 watched_seconds_today / limit / remaining
- schemas/ad.py: WatchReportIn/Out
- core/rewards.py: 新增 DAILY_AD_WATCH_SECONDS_LIMIT=50 分钟(主闸)+ MAX_SINGLE_WATCH_SECONDS=120;
  DAILY_AD_REWARD_LIMIT 20→200 改作次数兜底(防前端少报时长绕过时长闸, 又不误伤看短广告的正常用户)
- repositories/ad_reward.py + models/__init__.py: 接入新表与时长闸

alembic: withdraw_review_and_ad_watch 迁移(withdraw_order.user_name 列 + ad_watch_log 表)
tests: 补 test_ad_reward / test_withdraw

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: pure <pure@192.168.0.104>
Reviewed-on: #15
2026-06-06 04:51:50 +08:00

131 lines
4.3 KiB
Python

"""Admin M5 配置后台化测试:列出/改配置 + 改配**真生效** + 校验 + 审计。
autouse 清理每个用例后清空 app_config,避免改配污染其他文件的福利测试。
"""
from __future__ import annotations
from collections.abc import Iterator
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import delete, select
from app.admin.main import admin_app
from app.admin.repositories import admin_user as admin_repo
from app.db.session import SessionLocal
from app.models.admin import AdminAuditLog
from app.models.app_config import AppConfig
from app.repositories import ad_reward, signin
from app.repositories import user as user_repo
@pytest.fixture()
def admin_client() -> TestClient:
return TestClient(admin_app)
@pytest.fixture()
def token() -> str:
db = SessionLocal()
try:
if admin_repo.get_by_username(db, "cfg_admin") is None:
admin_repo.create_admin(
db, username="cfg_admin", password="cfgpass12", role="super_admin"
)
finally:
db.close()
c = TestClient(admin_app)
return c.post(
"/admin/api/auth/login", json={"username": "cfg_admin", "password": "cfgpass12"}
).json()["access_token"]
@pytest.fixture(autouse=True)
def _clean_config() -> Iterator[None]:
"""每个用例后清空 app_config,避免改配污染其他文件的福利测试(它们假设默认值)。"""
yield
db = SessionLocal()
try:
db.execute(delete(AppConfig))
db.commit()
finally:
db.close()
def _auth(t: str) -> dict:
return {"Authorization": f"Bearer {t}"}
def _seed_user(phone: str) -> int:
db = SessionLocal()
try:
return user_repo.upsert_user_for_login(db, phone=phone, register_channel="sms").id
finally:
db.close()
def test_list_config(admin_client: TestClient, token: str) -> None:
r = admin_client.get("/admin/api/config", headers=_auth(token))
assert r.status_code == 200, r.text
items = {i["key"]: i for i in r.json()}
assert "signin_rewards" in items and "ad_daily_limit" in items
assert items["signin_rewards"]["value"] == [10, 20, 30, 50, 80, 120, 200]
assert items["signin_rewards"]["overridden"] is False
def test_update_signin_takes_effect(admin_client: TestClient, token: str) -> None:
r = admin_client.patch(
"/admin/api/config/signin_rewards",
json={"value": [100, 200, 300, 400, 500, 600, 700]},
headers=_auth(token),
)
assert r.status_code == 200, r.text
assert r.json()["value"][0] == 100 and r.json()["overridden"] is True
# 业务真的用上新值(配置后台化的核心验证)
uid = _seed_user("13912340001")
db = SessionLocal()
try:
st = signin.get_status(db, uid)
assert st.steps[0].coin == 100
assert st.today_coin == 100 # 首签=第 1 天=100(新值)
logs = db.execute(
select(AdminAuditLog).where(
AdminAuditLog.action == "config.set",
AdminAuditLog.target_id == "signin_rewards",
)
).scalars().all()
assert len(logs) == 1 and logs[0].detail["after"][0] == 100
finally:
db.close()
def test_update_ad_limit_takes_effect(admin_client: TestClient, token: str) -> None:
r = admin_client.patch(
"/admin/api/config/ad_daily_limit", json={"value": 5}, headers=_auth(token)
)
assert r.status_code == 200, r.text
uid = _seed_user("13912340002")
db = SessionLocal()
try:
# today_status 现返 7 元组(末两位为观看时长闸:已看秒数 / 上限),取次数上限位
_used, limit, _coin, _rc, _cd, _ws, _wl = ad_reward.today_status(db, uid)
assert limit == 5 # 新配置生效
finally:
db.close()
def test_config_validation(admin_client: TestClient, token: str) -> None:
# 签到档位长度≠7
assert admin_client.patch(
"/admin/api/config/signin_rewards", json={"value": [1, 2, 3]}, headers=_auth(token)
).status_code == 400
# int 负数
assert admin_client.patch(
"/admin/api/config/ad_daily_limit", json={"value": -5}, headers=_auth(token)
).status_code == 400
# 未知 key
assert admin_client.patch(
"/admin/api/config/nope", json={"value": 1}, headers=_auth(token)
).status_code == 404