增加手机号短信验证查询逻辑

This commit is contained in:
2026-06-09 16:05:37 +08:00
parent ce4ed6213b
commit f1d7491288
14 changed files with 1942 additions and 136 deletions

View File

@@ -10,12 +10,19 @@ from app.services.captcha import make_captcha, verify_captcha
from app.services.logs import log_action, mask_phone
from app.services.pdf import render_certificate_pdf
from app.services.rate_limit import hit_too_often
from app.services.sms import generate_sms_code, verify_sms_code
router = APIRouter()
DISCLAIMER = "本证书仅用于证明学员完成相关培训课程/阶段学习,不代表国家学历、学位、职业资格或职业技能等级认证。"
class SendSmsRequest(BaseModel):
phone: str = Field(min_length=1, max_length=32)
captcha_id: str = Field(min_length=1, max_length=128)
captcha_code: str = Field(min_length=1, max_length=16)
class CertificateSearchRequest(BaseModel):
certificate_no: str | None = Field(default=None, max_length=64)
phone: str | None = Field(default=None, max_length=32)
@@ -30,11 +37,35 @@ class CertificateSearchRequest(BaseModel):
return self
class SmsSearchRequest(BaseModel):
phone: str = Field(min_length=1, max_length=32)
sms_id: str = Field(min_length=1, max_length=128)
sms_code: str = Field(min_length=1, max_length=16)
@router.get("/captcha")
def get_captcha() -> dict[str, str]:
return make_captcha()
@router.post("/sms/send")
def send_sms_code(
payload: SendSmsRequest,
request: Request,
) -> dict[str, str]:
"""发送短信验证码"""
client_ip = request.client.host if request.client else "unknown"
if hit_too_often(f"sms:{client_ip}", limit=5, window_seconds=60):
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="发送过于频繁,请稍后再试")
if hit_too_often(f"sms:{payload.phone}", limit=3, window_seconds=300):
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="该手机号发送过于频繁,请稍后再试")
if not verify_captcha(payload.captcha_id, payload.captcha_code):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="图形验证码错误或已过期,请重新输入")
sms_id = generate_sms_code(payload.phone)
return {"sms_id": sms_id, "message": "验证码已发送"}
@router.post("/certificates/search")
def search_certificate(
payload: CertificateSearchRequest,
@@ -71,6 +102,39 @@ def search_certificate(
return {"items": [public_certificate_payload(db, certificate, learner) for certificate, learner in result]}
@router.post("/certificates/search-by-sms")
def search_certificate_by_sms(
payload: SmsSearchRequest,
request: Request,
db: Session = Depends(get_db),
) -> dict[str, object]:
"""通过手机号和短信验证码查询证书"""
client_ip = request.client.host if request.client else "unknown"
if hit_too_often(f"search_sms:{client_ip}", limit=30, window_seconds=60):
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="访问过于频繁,请稍后再试")
if not verify_sms_code(payload.sms_id, payload.sms_code):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码错误或已过期,请重新获取")
phone = payload.phone.strip()
query = db.query(Certificate, Learner).join(Learner, Learner.id == Certificate.learner_id)
result = (
query.filter(Learner.phone == phone)
.order_by(Certificate.issue_date.desc(), Certificate.id.desc())
.all()
)
if not result:
log_action(db, None, "public_search_certificate_sms", "public_access", detail={"mode": "phone_sms", "phone": mask_phone(phone), "matched_count": 0, "result": "not_found"})
db.commit()
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="未查询到匹配的证书信息,请核对手机号后重试")
log_action(db, None, "public_search_certificate_sms", "public_access", result[0][0].certificate_no, {"mode": "phone_sms", "phone": mask_phone(phone), "matched_count": len(result), "result": "matched"})
db.commit()
return {"items": [public_certificate_payload(db, certificate, learner) for certificate, learner in result]}
@router.get("/certificates/token/{token}")
def get_certificate_by_token(token: str, db: Session = Depends(get_db)) -> dict[str, object]:
access_token = get_active_token(db, token)