gatsby 3 недель назад
Родитель
Сommit
9106710647

+ 37 - 1
backend/app/api/v1/module_system/auth/controller.py

@@ -19,7 +19,9 @@ from .schema import (
     JWTOutSchema,
     JWTOutSchema,
     LoginMiniRequestSchema,
     LoginMiniRequestSchema,
     LogoutPayloadSchema,
     LogoutPayloadSchema,
-    RefreshTokenPayloadSchema, SmsCodeSchema,
+    RefreshTokenPayloadSchema, 
+    SmsCodeSchema,
+    SmsLoginRequestSchema,
 )
 )
 from .service import AutoLoginService, CaptchaService, LoginService, SmsCodeService
 from .service import AutoLoginService, CaptchaService, LoginService, SmsCodeService
 
 
@@ -73,6 +75,40 @@ async def login_mini_controller(
     return SuccessResponse(data=login_token.model_dump(), msg="登录成功")
     return SuccessResponse(data=login_token.model_dump(), msg="登录成功")
 
 
 
 
+@AuthRouter.post(
+    "/login/sms",
+    summary="短信登录",
+    description="使用手机号和验证码登录",
+    response_model=JWTOutSchema,
+)
+async def login_sms_controller(
+    request: Request,
+    login_form: SmsLoginRequestSchema,
+    redis: Annotated[Redis, Depends(redis_getter)],
+    db: Annotated[AsyncSession, Depends(db_getter)],
+) -> JSONResponse:
+    """
+    短信登录
+
+    参数:
+    - request (Request): FastAPI请求对象
+    - login_form (SmsLoginRequestSchema): 短信登录表单(手机号和验证码)
+    - redis (Redis): Redis客户端对象
+    - db (AsyncSession): 数据库会话对象
+
+    返回:
+    - JWTOutSchema: 包含访问令牌和刷新令牌的响应模型
+
+    异常:
+    - CustomException: 认证失败时抛出异常。
+    """
+    login_token = await LoginService.authenticate_sms_user_service(
+        request=request, redis=redis, login_form=login_form, db=db
+    )
+    log.info(f"用户{login_form.mobile}短信登录成功")
+    return SuccessResponse(data=login_token.model_dump(), msg="登录成功")
+
+
 @AuthRouter.post(
 @AuthRouter.post(
     "/login",
     "/login",
     summary="登录",
     summary="登录",

+ 10 - 2
backend/app/api/v1/module_system/auth/schema.py

@@ -6,15 +6,18 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator, field_valida
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 
 
 from app.api.v1.module_system.user.model import UserModel
 from app.api.v1.module_system.user.model import UserModel
+from app.utils.sms_util import SmsTemplateEnum
+
 
 
 class SmsCodeSchema(BaseModel):
 class SmsCodeSchema(BaseModel):
     # 验证手机号格式
     # 验证手机号格式
     mobile: str = Field(..., description="手机号")
     mobile: str = Field(..., description="手机号")
     # 短信模版
     # 短信模版
-    template_code: str = Field(..., description="短信模版")
-    # 验证码
+    template_name: str = Field(..., description="短信模版")
+    # # 验证码
     code: Optional[str] = Field(default=None, description="验证码")
     code: Optional[str] = Field(default=None, description="验证码")
 
 
+
     @field_validator("mobile", mode="before")
     @field_validator("mobile", mode="before")
     @classmethod
     @classmethod
     def validate_mobile(cls, v: str) -> str:
     def validate_mobile(cls, v: str) -> str:
@@ -129,3 +132,8 @@ class LoginMiniRequestSchema(BaseModel):
     username: str = Field(..., min_length=1, description="用户名")
     username: str = Field(..., min_length=1, description="用户名")
     password: str = Field(..., min_length=1, description="密码")
     password: str = Field(..., min_length=1, description="密码")
     login_type: Optional[str] = Field(default="mini", description="登录类型")
     login_type: Optional[str] = Field(default="mini", description="登录类型")
+
+
+class SmsLoginRequestSchema(SmsCodeSchema):
+
+    code: str = Field(..., description="验证码")

+ 84 - 6
backend/app/api/v1/module_system/auth/service.py

@@ -22,9 +22,10 @@ from app.core.security import (
     decode_access_token,
     decode_access_token,
 )
 )
 from app.utils.captcha_util import CaptchaUtil
 from app.utils.captcha_util import CaptchaUtil
-from app.utils.common_util import get_random_character
+from app.utils.common_util import get_random_character, generate_random_code
 from app.utils.hash_bcrpy_util import PwdUtil
 from app.utils.hash_bcrpy_util import PwdUtil
 from app.utils.ip_local_util import IpLocalUtil
 from app.utils.ip_local_util import IpLocalUtil
+from app.utils.sms_util import SendSmsRequest, SmsTemplateEnum, SmsSender
 
 
 from .schema import (
 from .schema import (
     AuthSchema,
     AuthSchema,
@@ -35,7 +36,9 @@ from .schema import (
     JWTPayloadSchema,
     JWTPayloadSchema,
     LoginMiniRequestSchema,
     LoginMiniRequestSchema,
     LogoutPayloadSchema,
     LogoutPayloadSchema,
-    RefreshTokenPayloadSchema, SmsCodeSchema,
+    RefreshTokenPayloadSchema, 
+    SmsCodeSchema,
+    SmsLoginRequestSchema,
 )
 )
 
 
 CaptchaKey = NewType("CaptchaKey", str)
 CaptchaKey = NewType("CaptchaKey", str)
@@ -155,6 +158,62 @@ class LoginService:
 
 
         return token
         return token
 
 
+    @classmethod
+    async def authenticate_sms_user_service(
+        cls,
+        request: Request,
+        redis: Redis,
+        login_form: SmsLoginRequestSchema,
+        db: AsyncSession,
+    ) -> JWTOutSchema:
+        """
+        短信验证码登录
+
+        参数:
+        - request (Request): FastAPI请求对象
+        - redis (Redis): Redis客户端对象
+        - login_form (SmsLoginRequestSchema): 短信登录表单数据
+        - db (AsyncSession): 数据库会话对象
+
+        返回:
+        - JWTOutSchema: 包含访问令牌和刷新令牌的响应模型
+
+        异常:
+        - CustomException: 认证失败时抛出异常。
+        """
+        mobile = login_form.mobile
+        code = login_form.code
+
+        # 验证验证码
+        verify_result = await SmsCodeService.verify_sms_code_service(login_form, redis)
+        if not verify_result:
+            raise CustomException(msg="验证码已过期或错误")
+
+        # 根据手机号查找用户
+        auth = AuthSchema(db=db)
+        user = await UserCRUD(auth).get_by_mobile_crud(mobile=mobile)
+
+        if not user:
+            raise CustomException(msg="用户不存在")
+
+        if user.status == "1":
+            raise CustomException(msg="用户已被停用")
+
+        # 更新最后登录时间
+        user = await UserCRUD(auth).update_last_login_crud(id=user.id)
+        if not user:
+            raise CustomException(msg="用户不存在")
+
+        # 创建token
+        token = await cls.create_token_service(
+            request=request,
+            redis=redis,
+            user=user,
+            login_type="sms",
+        )
+
+        return token
+
     @classmethod
     @classmethod
     async def create_token_service(
     async def create_token_service(
         cls, request: Request, redis: Redis, user: UserModel, login_type: str
         cls, request: Request, redis: Redis, user: UserModel, login_type: str
@@ -610,12 +669,12 @@ class AutoLoginService:
 class SmsCodeService:
 class SmsCodeService:
     """短信验证码服务"""
     """短信验证码服务"""
     SMS_CODE_PREFIX = "sms_code"
     SMS_CODE_PREFIX = "sms_code"
-    SMS_CODE_EXPIRE = 60 * 5
+    SMS_CODE_EXPIRE = 60 * 10
     
     
     @classmethod
     @classmethod
     async def send_sms_code_service(
     async def send_sms_code_service(
         cls, sms_code: SmsCodeSchema, redis: Redis
         cls, sms_code: SmsCodeSchema, redis: Redis
-    ) -> None:
+    ) -> bool:
         """
         """
         发送短信验证码
         发送短信验证码
 
 
@@ -626,7 +685,25 @@ class SmsCodeService:
         异常:
         异常:
         - CustomException: 验证码发送失败时抛出异常。
         - CustomException: 验证码发送失败时抛出异常。
         """
         """
-        pass
+        template = SmsTemplateEnum.get_template_by_name(sms_code.template_name)
+
+        code = generate_random_code(6)
+
+        redis_key = f"{cls.SMS_CODE_PREFIX}:{sms_code.template_name}:{sms_code.mobile}"
+        await RedisCURD(redis).set(
+            key=redis_key,
+            value=code,
+            expire=cls.SMS_CODE_EXPIRE,
+        )
+
+        request = SendSmsRequest(
+            phone_numbers=sms_code.mobile,
+            template_code=template.template_code,
+            template_param=template.template_param_fn(code=code),
+        )
+
+        return await SmsSender.send_sms(request)
+        
     
     
     @classmethod
     @classmethod
     async def verify_sms_code_service(
     async def verify_sms_code_service(
@@ -645,8 +722,9 @@ class SmsCodeService:
         异常:
         异常:
         - CustomException: 验证码验证失败时抛出异常。
         - CustomException: 验证码验证失败时抛出异常。
         """
         """
-        redis_key = f"{cls.SMS_CODE_PREFIX}:{sms_code.mobile}"
+        redis_key = f"{cls.SMS_CODE_PREFIX}:{sms_code.template_name}:{sms_code.mobile}"
         code = await RedisCURD(redis).get(redis_key)
         code = await RedisCURD(redis).get(redis_key)
         if not code or code != sms_code.code:
         if not code or code != sms_code.code:
             return False
             return False
+        await RedisCURD(redis).delete(redis_key)
         return True
         return True

+ 12 - 10
backend/app/api/v1/module_system/tenant/service.py

@@ -49,8 +49,8 @@ class TenantService:
         )
         )
 
 
     @classmethod
     @classmethod
-    async def create_service(cls, auth: AuthSchema, data: TenantCreateSchema) -> dict:
-        ensure_platform_tenant_management(auth)
+    async def create_service(cls, auth: AuthSchema, data: TenantCreateSchema, password: str = None) -> dict:
+        #ensure_platform_tenant_management(auth)
         if await TenantCRUD(auth).get(name=data.name):
         if await TenantCRUD(auth).get(name=data.name):
             raise CustomException(msg="创建失败,名称已存在")
             raise CustomException(msg="创建失败,名称已存在")
         if await TenantCRUD(auth).get(code=data.code):
         if await TenantCRUD(auth).get(code=data.code):
@@ -59,26 +59,28 @@ class TenantService:
         tenant_obj = await TenantCRUD(auth).create_crud(data=data)
         tenant_obj = await TenantCRUD(auth).create_crud(data=data)
         if not tenant_obj:
         if not tenant_obj:
             raise CustomException(msg="创建租户失败")
             raise CustomException(msg="创建租户失败")
-        log.info(f"创建租户成功: {tenant_obj.name},编码: {tenant_obj.code}")
+        log.info(f"创建租户成功: {tenant_obj.name},编码: {tenant_obj.code}, ID: {tenant_obj.id}")
         # 创建初始管理员
         # 创建初始管理员
-        await cls._create_tenant_admin_user(auth, tenant_obj)
+        await cls._create_tenant_admin_user(auth, tenant_obj, password=password)
         # 创建积分账户
         # 创建积分账户
         await PointsService.create_points_service(auth, PointsCreateSchema(tenant_id=tenant_obj.id, points=Decimal("0.00")))
         await PointsService.create_points_service(auth, PointsCreateSchema(tenant_id=tenant_obj.id, points=Decimal("0.00")))
         await auth.db.refresh(tenant_obj)
         await auth.db.refresh(tenant_obj)
         return TenantOutSchema.model_validate(tenant_obj).model_dump()
         return TenantOutSchema.model_validate(tenant_obj).model_dump()
 
 
     @classmethod
     @classmethod
-    async def _create_tenant_admin_user(cls, auth: AuthSchema, tenant_obj: TenantModel) -> None:
+    async def _create_tenant_admin_user(cls, auth: AuthSchema, tenant_obj: TenantModel, password: str = None) -> None:
         username = f"{tenant_obj.code}"
         username = f"{tenant_obj.code}"
         if await UserCRUD(auth).get_by_username_crud(username=username):
         if await UserCRUD(auth).get_by_username_crud(username=username):
             raise CustomException(msg=f"初始管理员用户名已存在: {username},请更换租户编码后重试")
             raise CustomException(msg=f"初始管理员用户名已存在: {username},请更换租户编码后重试")
 
 
-        password_length = 12
-        characters = string.ascii_letters + string.digits + "!@#$%^&*"
-        password = "".join(random.choice(characters) for _ in range(password_length))
+        # password_length = 12
+        # characters = string.ascii_letters + string.digits + "!@#$%^&*"
+        # password = "".join(random.choice(characters) for _ in range(password_length))
+        password = password or tenant_obj.code
         admin_data = {
         admin_data = {
             "username": username,
             "username": username,
-            "password": PwdUtil.set_password_hash(password=tenant_obj.code),
+            "mobile": tenant_obj.code,
+            "password": PwdUtil.set_password_hash(password=password),
             "name": f"{tenant_obj.name}管理员",
             "name": f"{tenant_obj.name}管理员",
             "tenant_id": tenant_obj.id,
             "tenant_id": tenant_obj.id,
             "status": "0",
             "status": "0",
@@ -97,7 +99,7 @@ class TenantService:
             raise CustomException(msg="创建租户初始管理员失败")
             raise CustomException(msg="创建租户初始管理员失败")
 
 
         log.info(
         log.info(
-            f"为租户[{tenant_obj.name}]创建初始管理员成功,用户名: {username},临时密码: {password}"
+            f"为租户[{tenant_obj.name}]创建初始管理员成功,用户名: {username}"
         )
         )
 
 
     @classmethod
     @classmethod

+ 4 - 2
backend/app/api/v1/module_system/user/controller.py

@@ -3,13 +3,14 @@ from typing import Annotated
 
 
 from fastapi import APIRouter, Body, Depends, Path, Request, UploadFile
 from fastapi import APIRouter, Body, Depends, Path, Request, UploadFile
 from fastapi.responses import JSONResponse, StreamingResponse
 from fastapi.responses import JSONResponse, StreamingResponse
+from redis.asyncio.client import Redis
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.ext.asyncio import AsyncSession
 
 
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
 from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
 from app.core.base_params import PaginationQueryParam
 from app.core.base_params import PaginationQueryParam
 from app.core.base_schema import BatchSetAvailable
 from app.core.base_schema import BatchSetAvailable
-from app.core.dependencies import AuthPermission, db_getter, get_current_user
+from app.core.dependencies import AuthPermission, db_getter, get_current_user, redis_getter
 from app.core.logger import log
 from app.core.logger import log
 from app.core.router_class import OperationLogRoute
 from app.core.router_class import OperationLogRoute
 from app.utils.common_util import bytes2file_response
 from app.utils.common_util import bytes2file_response
@@ -158,6 +159,7 @@ async def reset_password_controller(
 )
 )
 async def register_user_controller(
 async def register_user_controller(
     data: UserRegisterSchema,
     data: UserRegisterSchema,
+    redis: Annotated[Redis, Depends(redis_getter)],
     db: Annotated[AsyncSession, Depends(db_getter)],
     db: Annotated[AsyncSession, Depends(db_getter)],
 ) -> JSONResponse:
 ) -> JSONResponse:
     """
     """
@@ -171,7 +173,7 @@ async def register_user_controller(
     - JSONResponse: 注册用户JSON响应
     - JSONResponse: 注册用户JSON响应
     """
     """
     auth = AuthSchema(db=db)
     auth = AuthSchema(db=db)
-    user_register_result = await UserService.register_user_service(data=data, auth=auth)
+    user_register_result = await UserService.register_user_service(data=data, auth=auth, redis=redis)
     log.info(f"{data.username} 注册用户成功: {user_register_result}")
     log.info(f"{data.username} 注册用户成功: {user_register_result}")
     return SuccessResponse(data=user_register_result, msg="注册用户成功")
     return SuccessResponse(data=user_register_result, msg="注册用户成功")
 
 

+ 9 - 5
backend/app/api/v1/module_system/user/schema.py

@@ -1,3 +1,4 @@
+from typing import Optional
 from urllib.parse import urlparse
 from urllib.parse import urlparse
 
 
 from fastapi import Query
 from fastapi import Query
@@ -101,11 +102,14 @@ class CurrentUserUpdateSchema(BaseModel):
         return self
         return self
 
 
 
 
-class UserRegisterSchema(SmsCodeSchema):
+class UserRegisterSchema(BaseModel):
     """注册"""
     """注册"""
 
 
     name: str | None = Field(default=None, description="名称")
     name: str | None = Field(default=None, description="名称")
+    template_name: Optional[str] = Field(default="verify", description="模版名称")
+    invite_code: str = Field(default=None, description="邀请码")
     mobile: str = Field(default=None, description="手机号")
     mobile: str = Field(default=None, description="手机号")
+    sms_code: Optional[str] = Field(default=None, description="验证码")
     username: str = Field(..., description="账号")
     username: str = Field(..., description="账号")
     password: str = Field(..., description="密码哈希值")
     password: str = Field(..., description="密码哈希值")
     role_ids: list[int] | None = Field(default=[1], description="角色ID")
     role_ids: list[int] | None = Field(default=[1], description="角色ID")
@@ -148,10 +152,10 @@ class UserRegisterSchema(SmsCodeSchema):
         if not v:
         if not v:
             raise ValueError("账号不能为空")
             raise ValueError("账号不能为空")
         # 字母开头,允许字母数字_.-
         # 字母开头,允许字母数字_.-
-        import re
-
-        if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{2,31}$", v):
-            raise ValueError("账号需字母开头,3-32位,仅含字母/数字/_ . -")
+        # import re
+        #
+        # if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{2,31}$", v):
+        #     raise ValueError("账号需字母开头,3-32位,仅含字母/数字/_ . -")
         return v
         return v
 
 
     @model_validator(mode="after")
     @model_validator(mode="after")

+ 26 - 8
backend/app/api/v1/module_system/user/service.py

@@ -4,7 +4,7 @@ from typing import Any
 import pandas as pd
 import pandas as pd
 from fastapi import UploadFile
 from fastapi import UploadFile
 
 
-from app.api.v1.module_system.auth.schema import AuthSchema
+from app.api.v1.module_system.auth.schema import AuthSchema, SmsCodeSchema
 from app.api.v1.module_system.dept.crud import DeptCRUD
 from app.api.v1.module_system.dept.crud import DeptCRUD
 from app.api.v1.module_system.menu.crud import MenuCRUD
 from app.api.v1.module_system.menu.crud import MenuCRUD
 from app.api.v1.module_system.menu.schema import MenuOutSchema
 from app.api.v1.module_system.menu.schema import MenuOutSchema
@@ -17,6 +17,7 @@ from app.utils.common_util import traversal_to_tree
 from app.utils.excel_util import ExcelUtil
 from app.utils.excel_util import ExcelUtil
 from app.utils.hash_bcrpy_util import PwdUtil
 from app.utils.hash_bcrpy_util import PwdUtil
 from app.utils.upload_util import UploadUtil
 from app.utils.upload_util import UploadUtil
+from redis.asyncio.client import Redis
 
 
 from .crud import UserCRUD
 from .crud import UserCRUD
 from .schema import (
 from .schema import (
@@ -30,6 +31,7 @@ from .schema import (
     UserRegisterSchema,
     UserRegisterSchema,
     UserUpdateSchema,
     UserUpdateSchema,
 )
 )
+from ..auth.service import SmsCodeService
 from ..tenant.schema import TenantCreateSchema
 from ..tenant.schema import TenantCreateSchema
 from ..tenant.service import TenantService
 from ..tenant.service import TenantService
 
 
@@ -474,7 +476,7 @@ class UserService:
         return UserOutSchema.model_validate(new_user).model_dump()
         return UserOutSchema.model_validate(new_user).model_dump()
 
 
     @classmethod
     @classmethod
-    async def register_user_service(cls, auth: AuthSchema, data: UserRegisterSchema) -> dict:
+    async def register_user_service(cls, auth: AuthSchema, redis: Redis, data: UserRegisterSchema) -> dict:
         """
         """
         用户注册
         用户注册
 
 
@@ -485,12 +487,33 @@ class UserService:
         返回:
         返回:
         - Dict: 注册后的用户详情字典
         - Dict: 注册后的用户详情字典
         """
         """
+        if not data.invite_code or data.invite_code != "8888":
+            raise CustomException("无效邀请码")
+
         # 检查用户名是否存在
         # 检查用户名是否存在
         data.username = data.mobile
         data.username = data.mobile
-        username_ok = await UserCRUD(auth).get_by_username_crud(username=data.username)
+        username_ok = await UserCRUD(auth).get_by_mobile_crud(mobile=data.mobile)
         if username_ok:
         if username_ok:
             raise CustomException(msg="账号已存在")
             raise CustomException(msg="账号已存在")
 
 
+        verify_result = await SmsCodeService.verify_sms_code_service(
+            sms_code=SmsCodeSchema(
+                mobile=data.mobile,
+                template_name=data.template_name or "verify",
+                code=data.sms_code,
+            ),
+            redis=redis
+        )
+
+        if not verify_result:
+            raise CustomException("验证码过期或错误")
+
+        tenant_data = TenantCreateSchema(
+            name=data.mobile,
+            code=data.mobile,
+        )
+        return await TenantService.create_service(auth=auth, data=tenant_data, password=data.password,)
+
         # data.password = PwdUtil.set_password_hash(password=data.password)
         # data.password = PwdUtil.set_password_hash(password=data.password)
         # data.name = data.username
         # data.name = data.username
         # create_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
         # create_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
@@ -503,11 +526,6 @@ class UserService:
         # if data.role_ids:
         # if data.role_ids:
         #     await UserCRUD(auth).set_user_roles_crud(user_ids=[result.id], role_ids=data.role_ids)
         #     await UserCRUD(auth).set_user_roles_crud(user_ids=[result.id], role_ids=data.role_ids)
         # return UserOutSchema.model_validate(result).model_dump()
         # return UserOutSchema.model_validate(result).model_dump()
-        tenant_data = TenantCreateSchema(
-            name=data.mobile,
-            code=data.mobile,
-        )
-        return await TenantService.create_service(auth=auth, data=tenant_data)
 
 
     @classmethod
     @classmethod
     async def forget_password_service(
     async def forget_password_service(

+ 1 - 1
backend/app/plugin/module_payment/points/service.py

@@ -25,7 +25,7 @@ class PointsService:
         if existing_points:
         if existing_points:
             raise CustomException(msg="租户已存在积分账户")
             raise CustomException(msg="租户已存在积分账户")
 
 
-        points = await crud.create(data.model_dump(exclude_unset=True))
+        points = await crud.create(data.model_dump(exclude_unset=True), skip_tenant_id=True)
         return PointsOutSchema.model_validate(points)
         return PointsOutSchema.model_validate(points)
 
 
     @classmethod
     @classmethod

+ 14 - 0
backend/app/utils/common_util.py

@@ -77,6 +77,20 @@ def get_random_character() -> str:
     return uuid.uuid4().hex
     return uuid.uuid4().hex
 
 
 
 
+def generate_random_code(length: int = 6) -> str:
+    """
+    生成指定长度的随机数字验证码
+
+    参数:
+    - length (int): 验证码长度,默认6位
+
+    返回:
+    - str: 随机数字验证码字符串
+    """
+    import random
+    return ''.join(random.choices('0123456789', k=length))
+
+
 def uuid4_str() -> str:
 def uuid4_str() -> str:
     """
     """
     数据库引擎 UUID 类型兼容:返回无连字符的 UUID 字符串。
     数据库引擎 UUID 类型兼容:返回无连字符的 UUID 字符串。

+ 44 - 9
backend/app/utils/sms_util.py

@@ -1,7 +1,7 @@
 import enum
 import enum
 import os
 import os
 import json
 import json
-from typing import Optional
+from typing import Optional, Dict, Any
 from collections import namedtuple
 from collections import namedtuple
 from functools import lru_cache
 from functools import lru_cache
 
 
@@ -15,15 +15,25 @@ from pydantic import BaseModel, Field, AliasChoices
 from pydantic_settings import SettingsConfigDict, BaseSettings
 from pydantic_settings import SettingsConfigDict, BaseSettings
 
 
 from app.config.path_conf import ENV_DIR
 from app.config.path_conf import ENV_DIR
+from app.core.logger import log
+from app.core.exceptions import CustomException
 
 
-_SmsTemplate = namedtuple("_SmsTemplate", ["template_code", "template_param_fn"])
+_SmsTemplate = namedtuple("_SmsTemplate", ["template_name", "template_code", "template_param_fn"])
 
 
 class SmsTemplateEnum(enum.Enum):
 class SmsTemplateEnum(enum.Enum):
     VERIFICATION_CODE = _SmsTemplate(
     VERIFICATION_CODE = _SmsTemplate(
+        template_name="verify",
         template_code="SMS_333796424",
         template_code="SMS_333796424",
         template_param_fn=lambda code: f'{{"code": "{code}"}}'
         template_param_fn=lambda code: f'{{"code": "{code}"}}'
     )
     )
 
 
+    @staticmethod
+    def get_template_by_name(template_name: str) -> "_SmsTemplate":
+        for template in SmsTemplateEnum:
+            if template.value.template_name == template_name:
+                return template.value
+        raise CustomException(f"未找到模板: template_name={ template_name}")
+
 
 
 class SendSmsRequest(BaseModel):
 class SendSmsRequest(BaseModel):
     phone_numbers: str = Field(..., description="支持向不同的手机号码发送短信,手机号码之间以半角逗号(,)分隔。"
     phone_numbers: str = Field(..., description="支持向不同的手机号码发送短信,手机号码之间以半角逗号(,)分隔。"
@@ -75,16 +85,41 @@ class SmsSender:
         return Dysmsapi20170525Client(config)
         return Dysmsapi20170525Client(config)
 
 
     @classmethod
     @classmethod
-    async def send_sms(cls, sms_request: SendSmsRequest) -> None:
+    async def send_sms(cls, sms_request: SendSmsRequest) -> bool:
+        """
+        发送短信
+        
+        参数:
+        - sms_request (SendSmsRequest): 短信发送请求对象
+        
+        返回:
+        - Dict[str, Any]: 短信发送响应结果
+        
+        异常:
+        - Exception: 发送失败时抛出异常
+        """
         client = SmsSender.get_client()
         client = SmsSender.get_client()
         send_sms_request = dysmsapi_20170525_models.SendSmsRequest(**sms_request.model_dump(exclude_none=True))
         send_sms_request = dysmsapi_20170525_models.SendSmsRequest(**sms_request.model_dump(exclude_none=True))
+        
         runtime = util_models.RuntimeOptions()
         runtime = util_models.RuntimeOptions()
+        
+        phone_numbers = sms_request.phone_numbers
+        log.info(f"开始发送短信: 手机号={phone_numbers}, 模板={sms_request.template_code}, 参数={sms_request.template_param}")
+        
         try:
         try:
             resp = await client.send_sms_with_options_async(send_sms_request, runtime)
             resp = await client.send_sms_with_options_async(send_sms_request, runtime)
-            print(json.dumps(resp, default=str, indent=2))
+            # 将响应转换为字典
+            resp_dict = json.loads(json.dumps(resp, default=str))
+            # 检查发送结果
+            if hasattr(resp.body, "code") and resp.body.code == 'OK':
+                log.info("短信发送成功: 手机号={}, BizID={}, 请求ID={}",
+                         phone_numbers, resp.body.biz_id, resp.body.request_id)
+                return True
+
+            log.warning("短信发送异常: 手机号={}, 响应={}", phone_numbers, resp_dict)
+            raise CustomException(f"{resp.body.message}")
+
         except Exception as error:
         except Exception as error:
-            # 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
-            # 错误 message
-            print(error.message)
-            # 诊断地址
-            print(error.data.get("Recommend"))
+            # 打印异常栈
+            error_message = str(error.message) if hasattr(error, 'message') else str(error)
+            raise CustomException(f"短信发送失败: {error_message}")

+ 3 - 3
backend/tests/test_apikey_sign.py

@@ -5,7 +5,7 @@ class TestApiKeySign(unittest.TestCase):
 
 
     def test_qq(self):
     def test_qq(self):
         data = {
         data = {
-            "third_biz_no": "123424202604270001"
+            "third_biz_no": "123424202604270077"
         }
         }
         sign = TenantApiKeyService.generate_signature(
         sign = TenantApiKeyService.generate_signature(
             "c0764f3e3a0af4bec292ceaf8b38d054d64e41bff36333a7f24d7cafada7cba98b8ac4dcac49367907e51b0b4e1566dec473198ad729ad1cbd5734b70322d7cd",
             "c0764f3e3a0af4bec292ceaf8b38d054d64e41bff36333a7f24d7cafada7cba98b8ac4dcac49367907e51b0b4e1566dec473198ad729ad1cbd5734b70322d7cd",
@@ -16,8 +16,8 @@ class TestApiKeySign(unittest.TestCase):
         data = {
         data = {
             "account_book_id": "2088480770941200",
             "account_book_id": "2088480770941200",
             "amount": 1.00,
             "amount": 1.00,
-            "order_title": "Apikey转账6",
-            "third_biz_no": "123424202604270066",
+            "order_title": "Apikey转账7",
+            "third_biz_no": "123424202604270077",
             "payee_info": {
             "payee_info": {
                 "identity_type": "ALIPAY_ACCOUNT",
                 "identity_type": "ALIPAY_ACCOUNT",
                 "name": "钱红武",
                 "name": "钱红武",

+ 2 - 2
backend/tests/test_sms.py

@@ -8,8 +8,8 @@ class SmsTestCase(unittest.IsolatedAsyncioTestCase):
     def test_sms(self):
     def test_sms(self):
         request = SendSmsRequest(
         request = SendSmsRequest(
             phone_numbers="13874410370",
             phone_numbers="13874410370",
-            template_code=SmsTemplateEnum.VERIFICATION_CODE.value.template_code,
-            template_param=SmsTemplateEnum.VERIFICATION_CODE.value.template_param_fn(code="111111"),
+            template_code=SmsTemplateEnum.VERIFICATION_CODE.value.template_name,
+            template_param=SmsTemplateEnum.VERIFICATION_CODE.value.template_param_fn(code="222111"),
         )
         )
         asyncio.get_event_loop().run_until_complete(SmsSender.send_sms(request))
         asyncio.get_event_loop().run_until_complete(SmsSender.send_sms(request))
 
 

+ 3 - 1
frontend/src/api/module_system/auth.ts

@@ -136,10 +136,12 @@ export interface AutoLoginToken {
 /** 获取短信验证码请求 */
 /** 获取短信验证码请求 */
 export interface SmsCodeBody {
 export interface SmsCodeBody {
   mobile: string;
   mobile: string;
+  template_name: string;
 }
 }
 
 
 /** 短信登录请求 */
 /** 短信登录请求 */
 export interface SmsLoginBody {
 export interface SmsLoginBody {
-  phone: string;
+  mobile: string;
   code: string;
   code: string;
+  template_name: string;
 }
 }

+ 17 - 1
frontend/src/store/modules/user.store.ts

@@ -1,6 +1,6 @@
 import { store, useTagsViewStore, usePermissionStoreHook, useDictStoreHook } from "@/store";
 import { store, useTagsViewStore, usePermissionStoreHook, useDictStoreHook } from "@/store";
 
 
-import AuthAPI, { type LoginFormData } from "@/api/module_system/auth";
+import AuthAPI, { SmsLoginBody, type LoginFormData } from "@/api/module_system/auth";
 import UserAPI, { type UserInfo } from "@/api/module_system/user";
 import UserAPI, { type UserInfo } from "@/api/module_system/user";
 import { type TenantInfo } from "@/api/module_system/tenant";
 import { type TenantInfo } from "@/api/module_system/tenant";
 import type { MenuTable } from "@/api/module_system/menu";
 import type { MenuTable } from "@/api/module_system/menu";
@@ -123,6 +123,22 @@ export const useUserStore = defineStore("user", {
       );
       );
     },
     },
 
 
+    async sms_login(smsForm: SmsLoginBody) {
+      const response = await AuthAPI.smsLogin(smsForm);
+      if (response.data.code === ResultEnum.SUCCESS) {
+        ElNotification({
+          title: "通知",
+          message: response.data.msg,
+          type: "success",
+        });
+      }
+      Auth.setTokens(
+        response.data.data.access_token,
+        response.data.data.refresh_token,
+        this.rememberMe
+      );
+    },
+
     // 登出
     // 登出
     async logout() {
     async logout() {
       const response = await AuthAPI.logout({ token: Auth.getAccessToken() });
       const response = await AuthAPI.logout({ token: Auth.getAccessToken() });

+ 24 - 31
frontend/src/views/module_system/auth/components/Login.vue

@@ -53,12 +53,12 @@
             </div>
             </div>
           </el-form-item>
           </el-form-item>
 
 
-          <div class="flex-x-between w-full">
+          <!-- <div class="flex-x-between w-full">
             <el-checkbox v-model="loginForm.remember">{{ t("login.rememberMe") }}</el-checkbox>
             <el-checkbox v-model="loginForm.remember">{{ t("login.rememberMe") }}</el-checkbox>
-            <!-- <el-link type="primary" underline="never" @click="toOtherForm('resetPwd')">
+            <el-link type="primary" underline="never" @click="toOtherForm('resetPwd')">
               {{ t("login.forgetPassword") }}
               {{ t("login.forgetPassword") }}
-            </el-link> -->
-          </div>
+            </el-link>
+          </div> -->
 
 
           <!-- 登录按钮 -->
           <!-- 登录按钮 -->
           <el-form-item>
           <el-form-item>
@@ -70,12 +70,14 @@
       </el-tab-pane>
       </el-tab-pane>
 
 
       <!-- 短信登录 -->
       <!-- 短信登录 -->
-      <!-- <el-tab-pane label="短信登录" name="sms">
+      <el-tab-pane label="短信登录" name="sms">
         <el-form ref="smsFormRef" :model="smsForm" :rules="smsRules" size="large" :validate-on-rule-change="false">
         <el-form ref="smsFormRef" :model="smsForm" :rules="smsRules" size="large" :validate-on-rule-change="false">
-          <el-form-item prop="phone">
-            <el-input v-model.trim="smsForm.phone" placeholder="请输入手机号" clearable>
+          <el-form-item prop="mobile">
+            <el-input v-model.trim="smsForm.mobile" placeholder="请输入手机号" clearable>
               <template #prefix>
               <template #prefix>
-                <el-icon><Iphone /></el-icon>
+                <el-icon>
+                  <Iphone />
+                </el-icon>
               </template>
               </template>
             </el-input>
             </el-input>
           </el-form-item>
           </el-form-item>
@@ -90,9 +92,9 @@
                   </el-icon>
                   </el-icon>
                 </template>
                 </template>
               </el-input>
               </el-input>
-              <el-button :disabled="smsCountdown > 0 || !smsForm.phone" :loading="smsCodeLoading" class="w-32"
+              <el-button :disabled="smsCountdown > 0 || !smsForm.mobile" :loading="smsCodeLoading" class="w-32"
                 @click="getSmsCode">
                 @click="getSmsCode">
-                {{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
+                {{ smsCountdown > 0 ? `${smsCountdown}s` : "获取验证码" }}
               </el-button>
               </el-button>
             </div>
             </div>
           </el-form-item>
           </el-form-item>
@@ -103,7 +105,7 @@
             </el-button>
             </el-button>
           </el-form-item>
           </el-form-item>
         </el-form>
         </el-form>
-      </el-tab-pane> -->
+      </el-tab-pane>
 
 
       <!-- 快速登录 -->
       <!-- 快速登录 -->
       <!-- <el-tab-pane v-if="autoLoginUsers.length > 0" label="快速登录" name="quick">
       <!-- <el-tab-pane v-if="autoLoginUsers.length > 0" label="快速登录" name="quick">
@@ -148,12 +150,12 @@
       </el-tab-pane> -->
       </el-tab-pane> -->
     </el-tabs>
     </el-tabs>
 
 
-    <!-- <div flex-center gap-10px>
+    <div flex-center gap-10px>
       <el-text size="default">{{ t("login.noAccount") }}</el-text>
       <el-text size="default">{{ t("login.noAccount") }}</el-text>
       <el-link type="primary" underline="never" @click="toOtherForm('register')">
       <el-link type="primary" underline="never" @click="toOtherForm('register')">
         {{ t("login.reg") }}
         {{ t("login.reg") }}
       </el-link>
       </el-link>
-    </div> -->
+    </div>
 
 
     <!-- 第三方登录 -->
     <!-- 第三方登录 -->
     <!-- <div class="third-party-login">
     <!-- <div class="third-party-login">
@@ -214,12 +216,13 @@ const smsCodeLoading = ref(false);
 const smsCountdown = ref(0);
 const smsCountdown = ref(0);
 
 
 const smsForm = reactive({
 const smsForm = reactive({
-  phone: "",
+  mobile: "",
   code: "",
   code: "",
+  template_name: "verify",
 });
 });
 
 
 const smsRules = computed(() => ({
 const smsRules = computed(() => ({
-  phone: [
+  mobile: [
     { required: true, message: "请输入手机号", trigger: "blur" },
     { required: true, message: "请输入手机号", trigger: "blur" },
     { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号", trigger: "blur" },
     { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号", trigger: "blur" },
   ],
   ],
@@ -478,13 +481,13 @@ function checkCapsLock(event: KeyboardEvent) {
 // 获取短信验证码
 // 获取短信验证码
 async function getSmsCode() {
 async function getSmsCode() {
   try {
   try {
-    const valid = await smsFormRef.value?.validateField("phone");
+    const valid = await smsFormRef.value?.validateField("mobile");
     if (!valid) return;
     if (!valid) return;
 
 
     smsCodeLoading.value = true;
     smsCodeLoading.value = true;
 
 
     // 调用后端API获取短信验证码
     // 调用后端API获取短信验证码
-    await AuthAPI.getSmsCode({ mobile: smsForm.phone });
+    await AuthAPI.getSmsCode({ mobile: smsForm.mobile, template_name: "verify" });
 
 
     // 开始倒计时
     // 开始倒计时
     smsCountdown.value = 60;
     smsCountdown.value = 60;
@@ -495,9 +498,9 @@ async function getSmsCode() {
       }
       }
     }, 1000);
     }, 1000);
 
 
-    ElMessage.success("验证码已发送");
+    // ElMessage.success("验证码已发送");
   } catch (error: any) {
   } catch (error: any) {
-    ElMessage.error(error?.response?.data?.msg || "获取验证码失败");
+    // ElMessage.error(error?.response?.data?.msg || "获取验证码失败");
   } finally {
   } finally {
     smsCodeLoading.value = false;
     smsCodeLoading.value = false;
   }
   }
@@ -511,23 +514,13 @@ async function handleSmsLogin() {
 
 
     smsLoading.value = true;
     smsLoading.value = true;
 
 
-    // 调用后端短信登录API
-    const response = await AuthAPI.smsLogin(smsForm);
-    const loginData = response.data.data;
-
-    // 设置登录状态
-    Auth.setTokens(loginData.access_token, loginData.refresh_token, true);
-
-    // 获取用户信息
-    await userStore.getUserInfo();
+    await userStore.sms_login(smsForm);
 
 
     // 跳转
     // 跳转
     const redirect = resolveRedirectTarget(route.query);
     const redirect = resolveRedirectTarget(route.query);
     await router.replace(redirect);
     await router.replace(redirect);
 
 
-    ElMessage.success("登录成功");
-  } catch (error: any) {
-    ElMessage.error(error?.response?.data?.msg || "登录失败");
+    // @ts-ignore
   } finally {
   } finally {
     smsLoading.value = false;
     smsLoading.value = false;
   }
   }

+ 5 - 35
frontend/src/views/module_system/auth/components/Register.vue

@@ -229,6 +229,7 @@ const submit = async () => {
 
 
     loading.value = true;
     loading.value = true;
 
 
+    model.value.username = model.value.mobile;
     await UserAPI.registerUser(model.value);
     await UserAPI.registerUser(model.value);
     // 注册成功后,双向绑定回写父容器的用户名和密码,并切回登录
     // 注册成功后,双向绑定回写父容器的用户名和密码,并切回登录
     presetUsername.value = model.value.username;
     presetUsername.value = model.value.username;
@@ -251,7 +252,7 @@ async function getSmsCode() {
     smsCodeLoading.value = true;
     smsCodeLoading.value = true;
 
 
     // 调用后端API获取短信验证码
     // 调用后端API获取短信验证码
-    await AuthAPI.getSmsCode({ mobile: model.value.mobile });
+    await AuthAPI.getSmsCode({ mobile: model.value.mobile, template_name: "verify" });
 
 
     // 开始倒计时
     // 开始倒计时
     smsCountdown.value = 60;
     smsCountdown.value = 60;
@@ -262,44 +263,13 @@ async function getSmsCode() {
       }
       }
     }, 1000);
     }, 1000);
 
 
-    ElMessage.success("验证码已发送");
+    // ElMessage.success("验证码已发送");
   } catch (error: any) {
   } catch (error: any) {
-    ElMessage.error(error?.response?.data?.msg || "获取验证码失败");
+    // ElMessage.error(error?.response?.data?.msg || "获取验证码失败");
   } finally {
   } finally {
     smsCodeLoading.value = false;
     smsCodeLoading.value = false;
   }
   }
 }
 }
 
 
-// 短信登录
-async function handleSmsLogin() {
-  try {
-    // const valid = await smsFormRef.value?.validate();
-    // if (!valid) return;
-
-    // smsLoading.value = true;
-
-    // // 调用后端短信登录API
-    // const response = await AuthAPI.smsLogin(smsForm);
-    // const loginData = response.data.data;
-
-    // // 设置登录状态
-    // Auth.setTokens(loginData.access_token, loginData.refresh_token, true);
-
-    // // 获取用户信息
-    // await userStore.getUserInfo();
-
-    // // 跳转
-    // const redirect = resolveRedirectTarget(route.query);
-    // await router.replace(redirect);
-
-    ElMessage.success("登录成功");
-  } catch (error: any) {
-    ElMessage.error(error?.response?.data?.msg || "登录失败");
-  } finally {
-    smsLoading.value = false;
-  }
-}
-
-
-onMounted(() => { });
+onMounted(() => {});
 </script>
 </script>