This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
"""结算服务:推荐激活(72h 窗口+次日登录)/ 创作收益增量结算 / 订单超时关单"""
import uuid
from datetime import date, datetime, timedelta
from decimal import Decimal
import pytest
from sqlalchemy import select
from app.models.billing import RechargeOrder, WatchRecord
from app.models.incentive import AssetWallet, Referral, VideoEarning
from app.models.user import User, UserMembership, UserWallet
from app.services import settle_service
# config.yaml:register_reward_points=10 / activate_watch_count=3 / activate_hours=72
REWARD = 10
WATCH_N = 3
async def _user(db, *, member=False, last_login_at=None):
u = User(password_hash="x", register_type=1, last_login_at=last_login_at,
invite_code=uuid.uuid4().hex[:8].upper())
db.add(u)
await db.flush()
if member:
db.add(UserMembership(user_id=u.id, is_member=1, is_lifetime=1))
await db.commit()
return u
async def _referral(db, inviter_id, invitee_id, *, created_at=None, status=0):
r = Referral(
inviter_id=inviter_id, invitee_id=invitee_id, invite_code="CODE123",
status=status, created_at=created_at or datetime.now(),
)
db.add(r)
await db.commit()
return r
async def _watches(db, user_id, n):
for i in range(n):
db.add(WatchRecord(
subject_type=1, subject_id=str(user_id), video_id=100 + i,
watch_date=date.today(), cost_type=0,
))
await db.commit()
# ---------- 推荐激活结算 ----------
@pytest.mark.asyncio
async def test_referral_activated_and_rewarded(db):
inviter = await _user(db, member=True)
invitee = await _user(db, last_login_at=datetime.now())
r = await _referral(db, inviter.id, invitee.id, created_at=datetime.now() - timedelta(days=1))
await _watches(db, invitee.id, WATCH_N)
result = await settle_service.settle_referral_activations(db)
assert result["activated"] == 1
await db.refresh(r)
assert r.status == 1 and r.activated_at is not None
# 推荐人是会员 → 发放注册奖励积分(biz_type=8)
assert (await db.get(UserWallet, inviter.id)).points == REWARD
@pytest.mark.asyncio
async def test_referral_expired_after_72h(db):
inviter = await _user(db, member=True)
invitee = await _user(db, last_login_at=datetime.now())
r = await _referral(db, inviter.id, invitee.id, created_at=datetime.now() - timedelta(hours=73))
await _watches(db, invitee.id, WATCH_N)
result = await settle_service.settle_referral_activations(db)
assert result["expired"] == 1 and result["activated"] == 0
await db.refresh(r)
assert r.status == 2
assert await db.get(UserWallet, inviter.id) is None # 未发奖
@pytest.mark.asyncio
async def test_referral_watches_not_enough(db):
inviter = await _user(db, member=True)
invitee = await _user(db, last_login_at=datetime.now())
r = await _referral(db, inviter.id, invitee.id, created_at=datetime.now() - timedelta(days=1))
await _watches(db, invitee.id, WATCH_N - 1)
result = await settle_service.settle_referral_activations(db)
assert result["activated"] == 0
await db.refresh(r)
assert r.status == 0 # 窗口内继续等待
@pytest.mark.asyncio
async def test_referral_requires_next_day_login(db):
inviter = await _user(db, member=True)
# 注册与登录同一天 → 不满足「次日登录」
same_day = datetime.now()
invitee = await _user(db, last_login_at=same_day)
r = await _referral(db, inviter.id, invitee.id, created_at=same_day)
await _watches(db, invitee.id, WATCH_N)
result = await settle_service.settle_referral_activations(db)
assert result["activated"] == 0
await db.refresh(r)
assert r.status == 0
@pytest.mark.asyncio
async def test_referral_non_member_inviter_no_reward(db):
inviter = await _user(db, member=False)
invitee = await _user(db, last_login_at=datetime.now())
r = await _referral(db, inviter.id, invitee.id, created_at=datetime.now() - timedelta(days=1))
await _watches(db, invitee.id, WATCH_N)
result = await settle_service.settle_referral_activations(db)
assert result["activated"] == 1 # 激活照常
await db.refresh(r)
assert r.status == 1
assert await db.get(UserWallet, inviter.id) is None # 非会员不发奖
# ---------- 视频创作收益结算 ----------
async def _earning(db, author_id, *, views=0, views_settled=0, likes=0, likes_settled=0):
e = VideoEarning(
video_id=999, author_id=author_id, complete_views=views,
views_settled=views_settled, likes_accrued=likes, likes_settled=likes_settled,
)
db.add(e)
await db.commit()
return e
@pytest.mark.asyncio
async def test_earnings_settle_full_scale(db):
author = await _user(db, member=True)
# 满 2 个观看刻度(2000/1000)+ 2 个点赞刻度(200/100),余数保留
e = await _earning(db, author.id, views=2500, likes=250)
result = await settle_service.settle_video_earnings(db)
assert result["granted_assets"] == 4.0
await db.refresh(e)
assert e.views_settled == 2000 and e.likes_settled == 200
wallet = await db.get(AssetWallet, author.id)
assert wallet.assets == Decimal(4)
# 再跑一轮:余数不足刻度,不再发放(幂等)
result2 = await settle_service.settle_video_earnings(db)
assert result2["settled_videos"] == 0
await db.refresh(wallet)
assert wallet.assets == Decimal(4)
@pytest.mark.asyncio
async def test_earnings_non_member_not_settled(db):
author = await _user(db, member=False)
e = await _earning(db, author.id, views=5000, likes=500)
result = await settle_service.settle_video_earnings(db)
assert result["settled_videos"] == 0
await db.refresh(e)
assert e.views_settled == 0 and e.likes_settled == 0 # 刻度不推进
assert await db.get(AssetWallet, author.id) is None
# ---------- 订单超时关单 ----------
@pytest.mark.asyncio
async def test_close_expired_orders(db):
def _order(no, status, expire_at):
return RechargeOrder(
order_no=no, user_id=1, amount_fen=100, points=10,
pay_method=1, status=status, expire_at=expire_at,
)
db.add(_order("R_EXP", 0, datetime.now() - timedelta(minutes=1))) # 待支付已过期 → 关单
db.add(_order("R_PEN", 0, datetime.now() + timedelta(minutes=30))) # 待支付未过期 → 保留
db.add(_order("R_PAY", 1, datetime.now() - timedelta(minutes=1))) # 已支付 → 不动
await db.commit()
result = await settle_service.close_expired_orders(db)
assert result == {"closed": 1}
status_map = {
o.order_no: o.status
for o in (await db.execute(select(RechargeOrder))).scalars()
}
assert status_map == {"R_EXP": 2, "R_PEN": 0, "R_PAY": 1}