|
@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
from app.models.license import SuperAdminLicense
|
|
from app.models.license import SuperAdminLicense
|
|
|
from app.models.monitoring import SuperAdmin
|
|
from app.models.monitoring import SuperAdmin
|
|
|
from app.models.domain import MonitoredDomain
|
|
from app.models.domain import MonitoredDomain
|
|
|
|
|
+from app.models.visitor import VisitorInfo
|
|
|
from app.schemas.license import (
|
|
from app.schemas.license import (
|
|
|
LicenseCreate,
|
|
LicenseCreate,
|
|
|
LicenseResponse,
|
|
LicenseResponse,
|
|
@@ -15,6 +16,10 @@ from app.schemas.license import (
|
|
|
SuperAdminOption,
|
|
SuperAdminOption,
|
|
|
LicenseListResponse,
|
|
LicenseListResponse,
|
|
|
)
|
|
)
|
|
|
|
|
+from app.services.sms import send_license_expired, send_license_restored
|
|
|
|
|
+import logging
|
|
|
|
|
+
|
|
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
def _to_str(val) -> str:
|
|
def _to_str(val) -> str:
|
|
@@ -25,6 +30,54 @@ def _to_str(val) -> str:
|
|
|
return str(val)
|
|
return str(val)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+async def _get_contact_for_license(db: AsyncSession, lic: SuperAdminLicense) -> tuple[str | None, str | None]:
|
|
|
|
|
+ """根据 License 获取关联的联系人 phone 和 company 名称。返回 (phone, company)"""
|
|
|
|
|
+ domain_result = await db.execute(
|
|
|
|
|
+ select(MonitoredDomain).where(
|
|
|
|
|
+ MonitoredDomain.super_admin_id == lic.super_admin_id,
|
|
|
|
|
+ MonitoredDomain.is_active == True,
|
|
|
|
|
+ ).limit(1)
|
|
|
|
|
+ )
|
|
|
|
|
+ domain = domain_result.scalar_one_or_none()
|
|
|
|
|
+ if not domain:
|
|
|
|
|
+ return None, None
|
|
|
|
|
+
|
|
|
|
|
+ sa_result = await db.execute(
|
|
|
|
|
+ select(SuperAdmin).where(SuperAdmin.id == lic.super_admin_id)
|
|
|
|
|
+ )
|
|
|
|
|
+ sa = sa_result.scalar_one_or_none()
|
|
|
|
|
+ company = sa.remark or sa.username if sa else str(lic.super_admin_id)
|
|
|
|
|
+
|
|
|
|
|
+ visitor_result = await db.execute(
|
|
|
|
|
+ select(VisitorInfo).where(VisitorInfo.domain_id == domain.id)
|
|
|
|
|
+ )
|
|
|
|
|
+ visitor = visitor_result.scalar_one_or_none()
|
|
|
|
|
+ if not visitor or not visitor.phone:
|
|
|
|
|
+ return None, company
|
|
|
|
|
+
|
|
|
|
|
+ return visitor.phone, company
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _notify_on_expired(db: AsyncSession, lic: SuperAdminLicense):
|
|
|
|
|
+ """License 过期时发送短信通知(静默失败,不阻断主流程)"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ phone, company = await _get_contact_for_license(db, lic)
|
|
|
|
|
+ if phone:
|
|
|
|
|
+ await send_license_expired(phone, company)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ logger.exception("发送过期短信失败,license_id=%d", lic.id)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def _notify_on_restored(db: AsyncSession, lic: SuperAdminLicense):
|
|
|
|
|
+ """License 恢复时发送短信通知(静默失败,不阻断主流程)"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ phone, company = await _get_contact_for_license(db, lic)
|
|
|
|
|
+ if phone:
|
|
|
|
|
+ await send_license_restored(phone, company)
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ logger.exception("发送恢复短信失败,license_id=%d", lic.id)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _calc_days_left(expires_at: datetime) -> int:
|
|
def _calc_days_left(expires_at: datetime) -> int:
|
|
|
now = datetime.now(timezone.utc) if expires_at.tzinfo else datetime.now()
|
|
now = datetime.now(timezone.utc) if expires_at.tzinfo else datetime.now()
|
|
|
delta = expires_at - now
|
|
delta = expires_at - now
|
|
@@ -109,12 +162,23 @@ async def list_licenses(
|
|
|
|
|
|
|
|
# 查询关联的域名
|
|
# 查询关联的域名
|
|
|
domain_result = await db.execute(
|
|
domain_result = await db.execute(
|
|
|
- select(MonitoredDomain.domain).where(
|
|
|
|
|
|
|
+ select(MonitoredDomain).where(
|
|
|
MonitoredDomain.super_admin_id == r.super_admin_id,
|
|
MonitoredDomain.super_admin_id == r.super_admin_id,
|
|
|
MonitoredDomain.is_active == True,
|
|
MonitoredDomain.is_active == True,
|
|
|
).limit(1)
|
|
).limit(1)
|
|
|
)
|
|
)
|
|
|
- domain_name = domain_result.scalar_one_or_none()
|
|
|
|
|
|
|
+ domain_row = domain_result.scalar_one_or_none()
|
|
|
|
|
+ domain_name = domain_row.domain if domain_row else None
|
|
|
|
|
+
|
|
|
|
|
+ # 查询关联的联系人信息
|
|
|
|
|
+ contact = None
|
|
|
|
|
+ if domain_row:
|
|
|
|
|
+ visitor_result = await db.execute(
|
|
|
|
|
+ select(VisitorInfo).where(VisitorInfo.domain_id == domain_row.id)
|
|
|
|
|
+ )
|
|
|
|
|
+ visitor = visitor_result.scalar_one_or_none()
|
|
|
|
|
+ if visitor:
|
|
|
|
|
+ contact = {"name": visitor.name, "phone": visitor.phone, "email": visitor.email}
|
|
|
|
|
|
|
|
items.append(LicenseResponse(
|
|
items.append(LicenseResponse(
|
|
|
id=r.id,
|
|
id=r.id,
|
|
@@ -129,6 +193,7 @@ async def list_licenses(
|
|
|
created_at=_to_str(r.created_at),
|
|
created_at=_to_str(r.created_at),
|
|
|
updated_at=_to_str(r.updated_at),
|
|
updated_at=_to_str(r.updated_at),
|
|
|
domain=domain_name,
|
|
domain=domain_name,
|
|
|
|
|
+ contact=contact,
|
|
|
))
|
|
))
|
|
|
|
|
|
|
|
return LicenseListResponse(total=total, items=items)
|
|
return LicenseListResponse(total=total, items=items)
|
|
@@ -146,9 +211,11 @@ async def get_license_status(
|
|
|
raise ValueError("License 不存在")
|
|
raise ValueError("License 不存在")
|
|
|
|
|
|
|
|
days_left = _calc_days_left(lic.expires_at)
|
|
days_left = _calc_days_left(lic.expires_at)
|
|
|
- if days_left <= 0:
|
|
|
|
|
|
|
+ if lic.status == "active" and days_left <= 0:
|
|
|
lic.status = "expired"
|
|
lic.status = "expired"
|
|
|
await db.commit()
|
|
await db.commit()
|
|
|
|
|
+ # 触发过期通知
|
|
|
|
|
+ await _notify_on_expired(db, lic)
|
|
|
|
|
|
|
|
sa_result = await db.execute(
|
|
sa_result = await db.execute(
|
|
|
select(SuperAdmin).where(SuperAdmin.id == lic.super_admin_id)
|
|
select(SuperAdmin).where(SuperAdmin.id == lic.super_admin_id)
|
|
@@ -199,12 +266,18 @@ async def update_license(
|
|
|
lic.license_key = payload.license_key
|
|
lic.license_key = payload.license_key
|
|
|
changed = True
|
|
changed = True
|
|
|
if payload.expires_at is not None:
|
|
if payload.expires_at is not None:
|
|
|
|
|
+ old_status = lic.status
|
|
|
lic.expires_at = datetime.fromisoformat(payload.expires_at)
|
|
lic.expires_at = datetime.fromisoformat(payload.expires_at)
|
|
|
if _calc_days_left(lic.expires_at) > 0:
|
|
if _calc_days_left(lic.expires_at) > 0:
|
|
|
lic.status = "active"
|
|
lic.status = "active"
|
|
|
else:
|
|
else:
|
|
|
lic.status = "expired"
|
|
lic.status = "expired"
|
|
|
changed = True
|
|
changed = True
|
|
|
|
|
+ # 状态变化时发送短信通知
|
|
|
|
|
+ if old_status == "active" and lic.status == "expired":
|
|
|
|
|
+ await _notify_on_expired(db, lic)
|
|
|
|
|
+ elif old_status == "expired" and lic.status == "active":
|
|
|
|
|
+ await _notify_on_restored(db, lic)
|
|
|
if not changed:
|
|
if not changed:
|
|
|
raise ValueError("没有需要更新的字段")
|
|
raise ValueError("没有需要更新的字段")
|
|
|
await db.commit()
|
|
await db.commit()
|
|
@@ -225,6 +298,8 @@ async def restore_license(
|
|
|
raise ValueError("仅可恢复已吊销的 License")
|
|
raise ValueError("仅可恢复已吊销的 License")
|
|
|
lic.status = "active"
|
|
lic.status = "active"
|
|
|
await db.commit()
|
|
await db.commit()
|
|
|
|
|
+ # 触发恢复通知
|
|
|
|
|
+ await _notify_on_restored(db, lic)
|
|
|
return {"message": "License已恢复"}
|
|
return {"message": "License已恢复"}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -323,6 +398,8 @@ async def check_license_by_referer(
|
|
|
if lic.status == "active" and days_left <= 0:
|
|
if lic.status == "active" and days_left <= 0:
|
|
|
lic.status = "expired"
|
|
lic.status = "expired"
|
|
|
await db.commit()
|
|
await db.commit()
|
|
|
|
|
+ # 触发过期通知
|
|
|
|
|
+ await _notify_on_expired(db, lic)
|
|
|
|
|
|
|
|
sa_result = await db.execute(
|
|
sa_result = await db.execute(
|
|
|
select(SuperAdmin).where(SuperAdmin.id == lic.super_admin_id)
|
|
select(SuperAdmin).where(SuperAdmin.id == lic.super_admin_id)
|