feat: 添加短信验证码绕过功能并更新API权限同步

- 添加SMS_BYPASS_CODE配置允许特定验证码绕过验证
- 实现角色与API权限的自动同步功能
- 更新评估模型的时间字段为可空
- 移除前端PC路由配置
- 更新Docker镜像版本至v2.6
- 切换开发环境API基础地址
This commit is contained in:
邹方成 2025-12-02 16:41:03 +08:00
parent 253ed14c87
commit 01ed8fb25b
9 changed files with 74 additions and 39 deletions

View File

@ -132,6 +132,10 @@ async def verify_code(payload: VerifyCodeRequest) -> BasicResponse[dict]:
Returns:
验证结果字典
"""
from app.settings import settings
if settings.SMS_BYPASS_CODE and payload.code == settings.SMS_BYPASS_CODE:
logger.info("sms.verify_code bypass phone={}", payload.phone)
return Success(data={"status": "OK", "message": "verified"})
ok, reason = store.can_verify(payload.phone)
if not ok:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=str(reason))
@ -161,6 +165,9 @@ class SMSLoginRequest(BaseModel):
@router.post("/login", summary="短信验证码登录", response_model=BasicResponse[dict])
async def sms_login(payload: SMSLoginRequest) -> BasicResponse[dict]:
from app.settings import settings
bypass = settings.SMS_BYPASS_CODE and payload.verification_code == settings.SMS_BYPASS_CODE
if not bypass:
ok, reason = store.can_verify(payload.phone_number)
if not ok:
raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=str(reason))
@ -187,6 +194,7 @@ async def sms_login(payload: SMSLoginRequest) -> BasicResponse[dict]:
await app_user_controller.update_last_login(user.id)
access_token = create_app_user_access_token(user_id=user.id, phone=user.phone)
if not bypass:
store.clear_code(payload.phone_number)
store.reset_failures(payload.phone_number)
logger.info("sms.login success phone={}", payload.phone_number)

View File

@ -621,12 +621,17 @@ class ValuationController:
if 'certificate_url' in update_data and update_data.get('certificate_url'):
from datetime import datetime
update_data['audited_at'] = datetime.now()
update_data['updated_at'] = datetime.now()
else:
from datetime import datetime
update_data['updated_at'] = datetime.now()
await valuation.update_from_dict(update_data)
await valuation.save()
from datetime import datetime
valuation.status ="pending"
if not getattr(valuation, "audited_at", None):
valuation.audited_at = datetime.now()
valuation.updated_at = datetime.now()
await valuation.save()
out = ValuationAssessmentOut.model_validate(valuation)
@ -643,12 +648,17 @@ class ValuationController:
if 'certificate_url' in update_data and update_data.get('certificate_url'):
from datetime import datetime
update_data['audited_at'] = datetime.now()
update_data['updated_at'] = datetime.now()
else:
from datetime import datetime
update_data['updated_at'] = datetime.now()
await valuation.update_from_dict(update_data)
await valuation.save()
from datetime import datetime
valuation.status ="success"
if not getattr(valuation, "audited_at", None):
valuation.audited_at = datetime.now()
valuation.updated_at = datetime.now()
await valuation.save()
out = ValuationAssessmentOut.model_validate(valuation)
@ -803,7 +813,7 @@ class ValuationController:
return None
from datetime import datetime
update_data = {"status": "pending", "audited_at": datetime.now()}
update_data = {"status": "pending", "audited_at": datetime.now(), "updated_at": datetime.now()}
if admin_notes:
update_data["admin_notes"] = admin_notes
@ -818,7 +828,7 @@ class ValuationController:
return None
from datetime import datetime
update_data = {"status": "rejected", "audited_at": datetime.now()}
update_data = {"status": "rejected", "audited_at": datetime.now(), "updated_at": datetime.now()}
if admin_notes:
update_data["admin_notes"] = admin_notes
@ -832,7 +842,8 @@ class ValuationController:
if not valuation:
return None
await valuation.update_from_dict({"admin_notes": admin_notes}).save()
from datetime import datetime
await valuation.update_from_dict({"admin_notes": admin_notes, "updated_at": datetime.now()}).save()
out = ValuationAssessmentOut.model_validate(valuation)
return await self._attach_user_phone(out)

View File

@ -279,11 +279,33 @@ async def init_menus():
async def init_apis():
apis = await api_controller.model.exists()
if not apis:
await api_controller.refresh_api()
async def sync_role_api_bindings():
"""确保角色与API权限绑定是最新的管理员拥有全部API普通用户拥有基础API"""
from tortoise.expressions import Q
try:
admin_role = await Role.filter(name="管理员").first()
if admin_role:
all_apis = await Api.all()
current = await admin_role.apis.all()
current_keys = {(a.method, a.path) for a in current}
missing = [a for a in all_apis if (a.method, a.path) not in current_keys]
if missing:
await admin_role.apis.add(*missing)
user_role = await Role.filter(name="普通用户").first()
if user_role:
basic_apis = await Api.filter(Q(method__in=["GET"]) | Q(tags="基础模块"))
current_u = await user_role.apis.all()
current_u_keys = {(a.method, a.path) for a in current_u}
missing_u = [a for a in basic_apis if (a.method, a.path) not in current_u_keys]
if missing_u:
await user_role.apis.add(*missing_u)
except Exception:
pass
async def _ensure_unique_index():
"""确保 valuation_calculation_steps 表的唯一索引存在"""
try:
@ -599,4 +621,5 @@ async def init_data():
await init_menus()
await init_apis()
await init_roles()
await sync_role_api_bindings()
await init_demo_transactions()

View File

@ -85,7 +85,7 @@ class ValuationAssessment(Model):
status = fields.CharField(max_length=20, default="pending", description="评估状态: pending(待审核), success(已通过), rejected(已拒绝)")
admin_notes = fields.TextField(null=True, description="管理员备注")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
updated_at = fields.DatetimeField(null=True, description="更新时间")
audited_at = fields.DatetimeField(null=True, description="审核时间")
is_active = fields.BooleanField(default=True, description="是否激活")
@ -114,7 +114,7 @@ class ValuationCalculationStep(Model):
status = fields.CharField(max_length=20, default="processing", description="步骤状态: processing, completed, failed")
error_message = fields.TextField(null=True, description="错误信息")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
updated_at = fields.DatetimeField(null=True, description="更新时间")
class Meta:
table = "valuation_calculation_steps"

View File

@ -105,6 +105,7 @@ class Settings(BaseSettings):
SMS_DEBUG_LOG_CODE: bool = True
ALIYUN_USE_DEFAULT_CREDENTIALS: bool = False
ALIYUN_SMS_TEMPLATE_PARAM_CODE_KEY: typing.Optional[str] = "code"
SMS_BYPASS_CODE: typing.Optional[str] = "202511"
SMTP_HOST: typing.Optional[str] = "smtp.qiye.aliyun.com"
SMTP_PORT: typing.Optional[int] = 465

View File

@ -21,15 +21,7 @@ server {
index index.html index.htm;
try_files $uri /index.html;
}
# PC 前端(/pc/ 前缀)
location = /pc {
return 302 /pc/;
}
location ^~ /pc/ {
alias /opt/vue-fastapi-admin/web1/dist/;
index index.html;
try_files $uri $uri/ /index.html;
}
location ^~ /api/ {
proxy_pass http://127.0.0.1:9999;
proxy_set_header Host $host;

View File

@ -5,5 +5,5 @@ VITE_PUBLIC_PATH = '/'
VITE_USE_PROXY = true
# base api
VITE_BASE_API = 'http://139.224.70.152:9990/api/v1'
# VITE_BASE_API = 'https://value.cdcee.net/api/v1'
# VITE_BASE_API = 'http://139.224.70.152:9990/api/v1'
VITE_BASE_API = 'https://value.cdcee.net/api/v1'

Binary file not shown.

View File

@ -37,8 +37,8 @@
export DOCKER_DEFAULT_PLATFORM=linux/amd64
docker build -t zfc931912343/guzhi-fastapi-admin:v2.1 .
docker push zfc931912343/guzhi-fastapi-admin:v2.1
docker build -t zfc931912343/guzhi-fastapi-admin:v2.6 .
docker push zfc931912343/guzhi-fastapi-admin:v2.6
# 运行容器
@ -75,4 +75,4 @@ docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v1.4 &&
docker pull nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v2.5 && docker rm -f guzhi_pro && docker run -itd --name=guzhi_pro -p 8080:80 -v ~/guzhi-data/static/images:/opt/vue-fastapi-admin/app/static/images --restart=unless-stopped -e TZ=Asia/Shanghai nbg2akd8w5diy8.xuanyuan.run/zfc931912343/guzhi-fastapi-admin:v2.5