浏览代码

feat: 更新开放接口

gatsby 1 月之前
父节点
当前提交
445e979494

+ 31 - 2
backend/app/api/v1/module_system/auth/controller.py

@@ -19,13 +19,42 @@ from .schema import (
     JWTOutSchema,
     LoginMiniRequestSchema,
     LogoutPayloadSchema,
-    RefreshTokenPayloadSchema,
+    RefreshTokenPayloadSchema, SmsCodeSchema,
 )
-from .service import AutoLoginService, CaptchaService, LoginService
+from .service import AutoLoginService, CaptchaService, LoginService, SmsCodeService
 
 AuthRouter = APIRouter(route_class=OperationLogRoute, prefix="/auth", tags=["认证授权"])
 
 
+@APIRouter.post(
+    '/sms-code',
+    summary="发送短信验证码",
+    description="发送短信验证码",
+)
+async def sms_code_controller(
+    sms_code: SmsCodeSchema,
+    redis: Annotated[Redis, Depends(redis_getter)],
+) -> JSONResponse:
+    """
+    发送短信验证码
+
+    参数:
+    - smsCode (SmsCodeSchema): 短信验证码请求模型
+    - redis (Redis): Redis 客户端对象
+    - db (AsyncSession): 数据库会话对象
+
+    返回:
+    - JSONResponse | dict: 包含短信验证码的响应模型
+
+    异常:
+    - CustomException: 验证码发送失败时抛出异常。
+    """
+    await SmsCodeService.send_sms_code_service(
+        sms_code=sms_code, redis=redis
+    )
+    return SuccessResponse(data={}, msg="短信验证码发送成功")
+
+
 @AuthRouter.post(
     "/login/mini",
     summary="小程序登录",

+ 28 - 1
backend/app/api/v1/module_system/auth/schema.py

@@ -1,11 +1,38 @@
+import re
 from datetime import datetime
 from typing import Optional
 
-from pydantic import BaseModel, ConfigDict, Field, model_validator
+from pydantic import BaseModel, ConfigDict, Field, model_validator, field_validator
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.api.v1.module_system.user.model import UserModel
 
+class SmsCodeSchema(BaseModel):
+    # 验证手机号格式
+    mobile: str = Field(..., description="手机号")
+    # 短信模版
+    template_code: str = Field(..., description="短信模版")
+    # 验证码
+    code: Optional[str] = Field(default=None, description="验证码")
+
+    @field_validator("mobile", mode="before")
+    @classmethod
+    def validate_mobile(cls, v: str) -> str:
+        # 移除常见地格式化字符
+        cleaned = re.sub(r'[\s\-+()()]', '', str(v))
+        # 中国大陆手机号验证
+        china_pattern = r'^1[3-9]\d{9}$'
+        # 带国际区号的手机号
+        # china_with_country_code = r'^(?:\+?86)?1[3-9]\d{9}$'
+        # if not re.match(china_with_country_code, cleaned):
+        #     raise ValueError("手机号格式错误")
+
+        if not re.match(china_pattern, cleaned):
+            raise ValueError("手机号格式错误")
+        
+        # 返回清洗后的纯数字
+        return cleaned.lstrip('+86')
+
 
 class AuthSchema(BaseModel):
     """权限认证模型"""

+ 46 - 1
backend/app/api/v1/module_system/auth/service.py

@@ -35,7 +35,7 @@ from .schema import (
     JWTPayloadSchema,
     LoginMiniRequestSchema,
     LogoutPayloadSchema,
-    RefreshTokenPayloadSchema,
+    RefreshTokenPayloadSchema, SmsCodeSchema,
 )
 
 CaptchaKey = NewType("CaptchaKey", str)
@@ -605,3 +605,48 @@ class AutoLoginService:
         log.info(f"用户{user.username}免登录成功")
 
         return jwt_token
+
+
+class SmsCodeService:
+    """短信验证码服务"""
+    SMS_CODE_PREFIX = "sms_code"
+    SMS_CODE_EXPIRE = 60 * 5
+    
+    @classmethod
+    async def send_sms_code_service(
+        cls, sms_code: SmsCodeSchema, redis: Redis
+    ) -> None:
+        """
+        发送短信验证码
+
+        参数:
+        - smsCode (SmsCodeSchema): 短信验证码请求模型
+        - redis (Redis): Redis客户端对象
+
+        异常:
+        - CustomException: 验证码发送失败时抛出异常。
+        """
+        pass
+    
+    @classmethod
+    async def verify_sms_code_service(
+        cls, sms_code: SmsCodeSchema, redis: Redis
+    ) -> bool:
+        """
+        验证短信验证码
+
+        参数:
+        - smsCode (SmsCodeSchema): 短信验证码请求模型
+        - redis (Redis): Redis客户端对象
+
+        返回:
+        - bool: 验证结果
+
+        异常:
+        - CustomException: 验证码验证失败时抛出异常。
+        """
+        redis_key = f"{cls.SMS_CODE_PREFIX}:{sms_code.mobile}"
+        code = await RedisCURD(redis).get(redis_key)
+        if not code or code != sms_code.code:
+            return False
+        return True

+ 2 - 1
backend/app/api/v1/module_system/user/schema.py

@@ -10,6 +10,7 @@ from pydantic import (
     model_validator,
 )
 
+from app.api.v1.module_system.auth.schema import SmsCodeSchema
 from app.api.v1.module_system.menu.schema import MenuOutSchema
 from app.api.v1.module_system.role.schema import RoleOutSchema
 from app.common.enums import QueueEnum
@@ -100,7 +101,7 @@ class CurrentUserUpdateSchema(BaseModel):
         return self
 
 
-class UserRegisterSchema(BaseModel):
+class UserRegisterSchema(SmsCodeSchema):
     """注册"""
 
     name: str | None = Field(default=None, description="名称")

+ 1 - 1
backend/app/core/apikey.py

@@ -131,6 +131,6 @@ class TenantApiKeyAuth(Generic[T]):
             api_key=api_key_obj.api_key,
             api_secret=api_key_obj.api_secret,
             tenant_id=api_key_obj.tenant_id,
-            auth=AuthSchema(user=None, db=db, tenant_id=api_key_obj.tenant_id, check_data_scope=False),
+            auth=AuthSchema(user=None, db=db, tenant_id=api_key_obj.tenant_id),
             data=parsed_data,
         )

+ 27 - 0
backend/app/core/base_crud.py

@@ -68,6 +68,33 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
         except Exception as e:
             raise CustomException(msg=f"获取查询失败: {e!s}")
 
+    async def get_first(self, preload: list[str | Any] | None = None) -> ModelType | None:
+        """
+        获取第一个匹配的对象(不指定条件,仅按权限过滤)
+
+        参数:
+        - preload (Optional[List[Union[str, Any]]]): 预加载关系,支持关系名字符串或SQLAlchemy loader option
+
+        返回:
+        - Optional[ModelType]: 对象实例
+
+        异常:
+        - CustomException: 查询失败时抛出异常
+        """
+        try:
+            sql = select(self.model)
+            # 应用可配置的预加载选项
+            for opt in self.__loader_options(preload):
+                sql = sql.options(opt)
+
+            sql = await self.__filter_permissions(sql)
+
+            result: Result = await self.auth.db.execute(sql)
+            obj = result.scalars().first()
+            return obj
+        except Exception as e:
+            raise CustomException(msg=f"获取查询失败: {e!s}")
+
     async def list(
         self,
         search: dict | None = None,

+ 66 - 7
backend/app/plugin/module_payment/account/schema.py

@@ -56,9 +56,10 @@ class PayeeInfoSchema(BaseModel):
             return "BANK_CARD"
         elif v == "book":
             return "BOOK"
-        else:
-            raise ValueError(f"identity_type must be one of {['alipay', 'bank', 'book']}")
-        
+        # else:
+        #     raise ValueError(f"identity_type must be one of {['alipay', 'bank', 'book']}")
+        return v
+
     @model_validator(mode='after')
     def validate_bankcard_ext_info(self) -> 'PayeeInfoSchema':
         """验证银行卡类型时bankcard_ext_info必填"""
@@ -200,7 +201,7 @@ class AccountTransferOutSchema(BaseModel):
     status: Optional[str] = Field(description="转账状态,SUCCESS:成功(对转账到银行卡的单据, 该状态可能变为退票[REFUND]); FAIL:失败; DEALING:处理中(转账到支付宝账户不涉及); REFUND:退票(转账到支付宝账户不涉及)")
     order_no: Optional[str] = Field(default=None, description="支付宝转账单号")
     fund_order_id: Optional[str] = Field(default=None, description="宝支付资金流水号")
-
+    out_biz_no: Optional[str] = Field(default=None, description="平台流水号")
 
 class AccountTransferBatchSchema(BaseModel):
     """批量资金专户转账请求"""
@@ -286,14 +287,43 @@ class TransferOutSchema(BaseModel):
     account_book_id: Optional[str] = Field(default=None, description="付款方资金专户号")
     amount: Decimal = Field(max_digits=10, decimal_places=2, gt=0, description="转账金额")
     order_title: Optional[str] = Field(default=None, description="转账标题")
-    payee_info: Optional[dict] = Field(default=None, description="收款方信息")
+    payee_info: Optional[PayeeInfoSchema] = Field(default=None, description="收款方信息")
     status: str = Field(description="转账状态")
     order_no: Optional[str] = Field(default=None, description="支付宝转账单号")
     error_code: Optional[str] = Field(default=None, description="错误码")
     error_msg: Optional[str] = Field(default=None, description="错误信息")
-    ext_info: Optional[dict] = Field(default=None, description="扩展信息")
+    # ext_info: Optional[dict] = Field(default=None, description="扩展信息")
     created_time: datetime = Field(description="创建时间")
 
+    @field_validator("payee_info", mode="after")
+    def normal_payee_info(cls, v: Optional["PayeeInfoSchema"]) -> Optional["PayeeInfoSchema"]:
+        if v is None:
+            return v
+        # 脱敏处理
+        if v.identity_type == "BANK_CARD" and v.identity:
+            # 银行卡号:保留前4位和后4位
+            if len(v.identity) >= 8:
+                v.identity = v.identity[:4] + "*" * (len(v.identity) - 8) + v.identity[-4:]
+        elif v.identity_type == "ALIPAY_ACCOUNT" and v.identity:
+            # 支付宝账号脱敏
+            if "@" in v.identity:
+                # 邮箱格式:保留@前第一个字符和域名
+                parts = v.identity.split("@")
+                if len(parts[0]) > 1:
+                    v.identity = parts[0][0] + "*" * 3 + "@" + parts[1]
+            else:
+                # 手机号格式:保留前3位和后4位
+                if len(v.identity) >= 7:
+                    v.identity = v.identity[:3] + "*" * (len(v.identity) - 7) + v.identity[-4:]
+        # 姓名脱敏:只保留第一个字
+        if v.name and len(v.name) > 1:
+            v.name = v.name[0] + "*" * (len(v.name) - 1)
+        # 银行卡扩展信息脱敏
+        if v.bankcard_ext_info:
+            if v.bankcard_ext_info.inst_name:
+                v.bankcard_ext_info.inst_name = "***"
+        return v
+
 
 class TransferListOutSchema(BaseModel):
     """转账记录列表响应"""
@@ -308,9 +338,38 @@ class TransferListOutSchema(BaseModel):
     status: str = Field(description="转账状态")
     order_no: Optional[str] = Field(default=None, description="支付宝转账单号")
     fund_order_id: Optional[str] = Field(default=None, description="宝支付资金流水号")
-    payee_info: Optional[dict] = Field(default=None, description="收款方信息")
+    payee_info: Optional[PayeeInfoSchema] = Field(default=None, description="收款方信息")
     created_time: datetime = Field(description="创建时间")
 
+    @field_validator("payee_info", mode="after")
+    def normal_payee_info(cls, v: Optional["PayeeInfoSchema"]) -> Optional["PayeeInfoSchema"]:
+        if v is None:
+            return v
+        # 脱敏处理
+        if v.identity_type == "BANK_CARD" and v.identity:
+            # 银行卡号:保留前4位和后4位
+            if len(v.identity) >= 8:
+                v.identity = v.identity[:4] + "*" * (len(v.identity) - 8) + v.identity[-4:]
+        elif v.identity_type == "ALIPAY_ACCOUNT" and v.identity:
+            # 支付宝账号脱敏
+            if "@" in v.identity:
+                # 邮箱格式:保留@前第一个字符和域名
+                parts = v.identity.split("@")
+                if len(parts[0]) > 1:
+                    v.identity = parts[0][0] + "*" * 3 + "@" + parts[1]
+            else:
+                # 手机号格式:保留前3位和后4位
+                if len(v.identity) >= 7:
+                    v.identity = v.identity[:3] + "*" * (len(v.identity) - 7) + v.identity[-4:]
+        # 姓名脱敏:只保留第一个字
+        if v.name and len(v.name) > 1:
+            v.name = v.name[0] + "*" * (len(v.name) - 1)
+        # 银行卡扩展信息脱敏
+        if v.bankcard_ext_info:
+            if v.bankcard_ext_info.inst_name:
+                v.bankcard_ext_info.inst_name = "***"
+        return v
+
 
 class DepositOutSchema(BaseModel):
     """充值记录响应"""

+ 1 - 0
backend/app/plugin/module_payment/account/service.py

@@ -294,6 +294,7 @@ class AccountService:
             status=result.status,
             order_no=result.order_no,
             fund_order_id=result.fund_order_id,
+            out_biz_no=model.out_biz_no,
         )
 
     @classmethod

+ 64 - 12
backend/app/plugin/module_payment/openapi/controller.py

@@ -1,19 +1,18 @@
-from typing import Annotated
+from typing import Annotated, Optional
 
 from fastapi import APIRouter, Depends
 from starlette.responses import JSONResponse
 
-from app.core.dependencies import db_getter
+from app.core.dependencies import AuthPermission
 from app.core.router_class import OperationLogRoute
 from app.common.response import ResponseSchema, SuccessResponse, ErrorResponse
 from app.core.apikey import TenantApiKeyAuth
-from app.plugin.module_payment.account import AccountService
-from app.plugin.module_payment.account.schema import AccountTransferSchema
 from app.api.v1.module_system.auth.schema import AuthSchema
 
-from sqlalchemy.ext.asyncio import AsyncSession
-from app.core.logger import log
 from app.plugin.module_payment.apikey.schema import ApiKeyPayload
+from app.plugin.module_payment.openapi.service import OpenConfService, OpenTransferService
+from app.plugin.module_payment.openapi.schema import OpenConfOutSchema, OpenConfUpdateSchema, OpenTransferSchema, \
+    OpenTransferOutSchema, OpenTransferQuerySchema
 
 OpenapiRouter = APIRouter(
     route_class=OperationLogRoute,
@@ -26,10 +25,10 @@ OpenapiRouter = APIRouter(
     "/account/transfer",
     summary="资金专户转账",
     description="从资金专户转账到支付宝账户/银行卡/资金专户",
-    response_model=ResponseSchema[dict],
+    response_model=ResponseSchema[OpenTransferOutSchema],
 )
-async def openapi_transfer_controller(
-    apikey: Annotated[ApiKeyPayload[AccountTransferSchema], Depends(TenantApiKeyAuth(AccountTransferSchema))],
+async def open_transfer_controller(
+    apikey: Annotated[ApiKeyPayload[OpenTransferSchema], Depends(TenantApiKeyAuth(OpenTransferSchema))],
 ) -> JSONResponse:
     """
     租户资金专户转账接口
@@ -41,7 +40,60 @@ async def openapi_transfer_controller(
     if transfer_data is None:
         return ErrorResponse(msg="缺少必需参数")
 
-    # 执行转账
-    result = await AccountService.transfer_service(auth=auth, data=transfer_data)
-    log.info(f"租户资金专户转账发起成功: 企业: {transfer_data.enterprise_id}, 金额: {transfer_data.amount}")
+    result = OpenTransferService.open_transfer_service(auth=auth, data=transfer_data)
     return SuccessResponse(data=result, msg="转账申请已提交")
+
+
+@OpenapiRouter.post(
+    "/account/transfer/query",
+    summary="查询转账",
+    description="查询转账详情",
+)
+async def open_transfer_query_controller(
+    apikey: Annotated[ApiKeyPayload[OpenTransferQuerySchema], Depends(TenantApiKeyAuth(OpenTransferQuerySchema))],
+) -> JSONResponse:
+    auth = apikey.auth
+    query_data = apikey.data
+    if query_data is None:
+        return ErrorResponse(msg="缺少必需参数")
+
+    result = OpenTransferService.open_query_service(auth=auth, query=query_data)
+    return SuccessResponse(data=result, msg="查询成功")
+
+# =====================================================================
+
+@OpenapiRouter.get(
+    "/conf",
+    summary="查询开放配置",
+    description="查询当前企业的开放接口配置",
+    response_model=ResponseSchema[OpenConfOutSchema],
+)
+async def get_open_conf_controller(
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:conf:query"]))],
+) -> JSONResponse:
+    """
+    查询开放配置接口
+    
+    获取当前企业的支付宝开放接口配置信息
+    """
+    result = await OpenConfService.get_conf_service(auth=auth)
+    return SuccessResponse(data=result, msg="查询成功")
+
+
+@OpenapiRouter.post(
+    "/conf",
+    summary="创建/更新开放配置",
+    description="创建或更新当前企业的开放接口配置(前端只允许配置回调地址)",
+    response_model=ResponseSchema[OpenConfOutSchema],
+)
+async def save_open_conf_controller(
+    data: OpenConfUpdateSchema,
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_system:tenant:conf:update"]))],
+) -> JSONResponse:
+    """
+    创建/更新开放配置接口
+    
+    如果配置已存在则更新,不存在则创建(仅更新回调地址)
+    """
+    result = await OpenConfService.save_conf_service(auth=auth, data=data)
+    return SuccessResponse(data=result, msg="配置更新成功")

+ 15 - 0
backend/app/plugin/module_payment/openapi/crud.py

@@ -0,0 +1,15 @@
+from typing import Any
+
+from app.core.base_crud import CRUDBase
+
+from app.plugin.module_payment.openapi.model import OpenTransferModel, OpenConfModel
+
+
+class OpenTransferCRUD(CRUDBase[OpenTransferModel, Any, Any]):
+    def __init__(self, auth):
+        super().__init__(model=OpenTransferModel, auth=auth)
+
+
+class OpenConfCRUD(CRUDBase[OpenConfModel, Any, Any]):
+    def __init__(self, auth):
+        super().__init__(model=OpenConfModel, auth=auth)

+ 102 - 0
backend/app/plugin/module_payment/openapi/model.py

@@ -0,0 +1,102 @@
+from sqlalchemy import String, UniqueConstraint, ForeignKey
+from sqlalchemy.orm import Mapped, mapped_column
+
+from app.common.enums import PermissionFilterStrategy
+from app.core.base_model import PaymentModelMixin, TenantMixin, EnterpriseMixin
+
+
+class OpenTransferModel(PaymentModelMixin, TenantMixin, EnterpriseMixin):
+
+    __tablename__ = "open_transfer"
+    __table_args__ = (
+        UniqueConstraint('third_biz_no', 'tenant_id', name='uq_third_biz_tenant'),
+        {"comment": "三方转账表"}  # 字典必须放在元组的最后
+    )
+    __permission_strategy__ = PermissionFilterStrategy.ENTERPRISE_BASED
+
+    third_biz_no: Mapped[str] = mapped_column(
+        String(64), nullable=False, index=True, comment="三方订单号"
+    )
+
+    out_biz_no: Mapped[str] = mapped_column(
+        String(64),
+        # ForeignKey("pay_transfer.out_biz_no", ondelete="RESTRICT", onupdate="CASCADE"),
+        nullable=False,
+        index=True,
+        comment="平台订单号",
+    )
+
+
+class OpenConfModel(PaymentModelMixin, TenantMixin):
+    __tablename__ = "open_conf"
+    __table_args__ = (
+        {"comment": "三方配置表"}
+    )
+    __permission_strategy__ = PermissionFilterStrategy.ENTERPRISE_BASED
+
+    # 支付宝应用配置
+    app_id: Mapped[str] = mapped_column(
+        String(64), unique=True, nullable=False, comment="应用ID"
+    )
+    
+    # 密钥配置
+    private_key: Mapped[str] = mapped_column(
+        String(2048), nullable=True, comment="应用私钥(PKCS8格式)"
+    )
+    
+    public_key: Mapped[str] = mapped_column(
+        String(2048), nullable=True, comment="平台公钥"
+    )
+    
+    # 网关配置
+    gateway_url: Mapped[str] = mapped_column(
+        String(256), nullable=True, comment="支付宝网关地址"
+    )
+    
+    # 签名配置
+    sign_type: Mapped[str] = mapped_column(
+        String(32), nullable=True, default="RSA2", comment="签名类型: RSA/RSA2"
+    )
+    
+    # 编码配置
+    charset: Mapped[str] = mapped_column(
+        String(32), nullable=True,default="UTF-8", comment="字符编码"
+    )
+    
+    format: Mapped[str] = mapped_column(
+        String(32), nullable=True, default="JSON", comment="数据格式"
+    )
+    
+    # 回调配置
+    notify_url: Mapped[str] = mapped_column(
+        String(512), nullable=True, comment="异步通知地址"
+    )
+    
+    return_url: Mapped[str] = mapped_column(
+        String(512), nullable=True, comment="同步返回地址"
+    )
+    
+    # 证书配置(证书模式)
+    app_cert_sn: Mapped[str] = mapped_column(
+        String(64), nullable=True, comment="应用证书序列号"
+    )
+    
+    alipay_root_cert_sn: Mapped[str] = mapped_column(
+        String(64), nullable=True, comment="支付宝根证书序列号"
+    )
+    
+    # 加密配置
+    encrypt_key: Mapped[str] = mapped_column(
+        String(64), nullable=True, comment="AES加密密钥"
+    )
+    
+    # 状态配置
+    status: Mapped[str] = mapped_column(
+        String(16), nullable=False, default="ENABLED", comment="状态: ENABLED/DISABLED"
+    )
+    
+    # 描述
+    description: Mapped[str] = mapped_column(
+        String(256), nullable=True, comment="配置描述"
+    )
+

+ 41 - 0
backend/app/plugin/module_payment/openapi/schema.py

@@ -0,0 +1,41 @@
+from typing import Optional
+from datetime import datetime
+from pydantic import BaseModel, Field, ConfigDict
+
+from app.plugin.module_payment.account.schema import AccountTransferSchema
+
+
+class OpenTransferSchema(AccountTransferSchema):
+    third_biz_no: Optional[str] = Field(default=None, description="三方订单号")
+
+
+class OpenConfOutSchema(BaseModel):
+    """开放配置响应模型"""
+    model_config = ConfigDict(from_attributes=True)
+
+    id: int = Field(description="配置ID")
+    app_id: str = Field(description="应用ID")
+    gateway_url: Optional[str] = Field(default=None, description="平台网关地址")
+    notify_url: Optional[str] = Field(default=None, description="异步通知地址")
+    return_url: Optional[str] = Field(default=None, description="同步返回地址")
+    status: str = Field(description="状态")
+    description: Optional[str] = Field(default=None, description="配置描述")
+    created_time: Optional[datetime] = Field(default=None, description="创建时间")
+    updated_time: Optional[datetime] = Field(default=None, description="更新时间")
+
+
+class OpenConfUpdateSchema(BaseModel):
+    """开放配置更新模型(前端只允许配置回调地址)"""
+    notify_url: Optional[str] = Field(default=None, description="异步通知地址")
+    return_url: Optional[str] = Field(default=None, description="同步返回地址")
+
+
+class OpenTransferOutSchema(BaseModel):
+    """开放转账响应模型"""
+    status: Optional[str] = Field(default=None, description="转账状态")
+    order_no: Optional[str] = Field(default=None, description="支付宝转账单号")
+    third_biz_no: Optional[str] = Field(default=None, description="三方订单号")
+
+
+class OpenTransferQuerySchema(BaseModel):
+    third_biz_no: str = Field(description="三方订单号")

+ 110 - 0
backend/app/plugin/module_payment/openapi/service.py

@@ -0,0 +1,110 @@
+from app.api.v1.module_system.auth.schema import AuthSchema
+from app.core.exceptions import CustomException
+from app.core.logger import log
+from app.utils.snowflake import get_snowflake_id_str
+from typing import Optional
+
+from .crud import OpenConfCRUD, OpenTransferCRUD
+from .schema import OpenConfOutSchema, OpenConfUpdateSchema, OpenTransferSchema, OpenTransferOutSchema, \
+    OpenTransferQuerySchema
+from app.plugin.module_payment.account import AccountService
+
+class OpenTransferService:
+
+    @classmethod
+    async def open_query_service(
+        cls,
+        auth: AuthSchema,
+        query: OpenTransferQuerySchema
+    ) -> dict:
+        crud = OpenTransferCRUD(auth)
+
+        transfer_data = await crud.get(third_biz_no=query.third_biz_no)
+        if transfer_data is None:
+            raise CustomException("三方订单号不存在")
+        
+        result = await AccountService.transfer_detail_service(auth=auth, out_biz_no=transfer_data.out_biz_no)
+        return result.model_dump(exclude_none=True)
+
+    @classmethod
+    async def open_transfer_service(
+        cls,
+        auth: AuthSchema,
+        data: OpenTransferSchema
+    ) -> OpenTransferOutSchema:
+        third_biz_no = data.third_biz_no
+        if not third_biz_no:
+            raise CustomException("三方订单号不能为空")
+
+        # 先查询是否存在三方订单号
+        crud = OpenTransferCRUD(auth)
+        existing = await crud.get(third_biz_no=third_biz_no)
+        if existing:
+            raise CustomException("三方订单号已存在")
+
+        # 执行转账记录创建
+        result = await AccountService.transfer_service(auth=auth, data=data)
+        log.info(f"租户资金专户转账发起成功: 企业: {data.enterprise_id}, 金额: {data.amount}")
+
+        # 保存三方订单号关联记录
+        create_data = {
+            "third_biz_no": third_biz_no,
+            "out_biz_no": result.out_biz_no,
+        }
+        await crud.create(create_data)
+
+        return OpenTransferOutSchema(
+            status=result.status,
+            order_no=result.order_no,
+            third_biz_no=third_biz_no,
+        )
+
+
+
+class OpenConfService:
+    """开放配置服务层"""
+
+    @classmethod
+    async def get_conf_service(
+        cls,
+        auth: AuthSchema,
+    ) -> Optional[OpenConfOutSchema]:
+        """
+        查询开放配置
+        """
+        crud = OpenConfCRUD(auth)
+        result = await crud.get_first()
+
+        if result is not None:
+             return OpenConfOutSchema.model_validate(result)
+        return None
+
+    @classmethod
+    async def save_conf_service(
+        cls,
+        auth: AuthSchema,
+        data: OpenConfUpdateSchema,
+    ) -> OpenConfOutSchema:
+        """
+        创建/更新开放配置(前端只允许配置回调地址)
+        """
+        # 先查询是否存在配置
+
+        update_data = {
+            "notify_url": data.notify_url,
+            "return_url": data.return_url,
+        }
+
+        crud = OpenConfCRUD(auth)
+        existing = await crud.get_first()
+
+        if not existing:
+            update_data["app_id"] = get_snowflake_id_str(auth.tenant_id)
+            update_data["gateway_url"] = "https://api.qcsj88888.com"
+            result = await crud.create(update_data)
+            log.info(f"开放配置创建成功: 租户ID: {auth.tenant_id}")
+        else:
+            result = await crud.update(existing.id, update_data)
+            log.info(f"开放配置更新成功: 租户ID: {auth.tenant_id}")
+
+        return OpenConfOutSchema.model_validate(result)

+ 37 - 0
frontend/src/api/module_payment/openapi.ts

@@ -0,0 +1,37 @@
+import request from "@/utils/request";
+
+export interface OpenConfForm {
+  notify_url?: string;
+  return_url?: string;
+}
+
+export interface OpenConfResponse {
+  id: number;
+  app_id: string;
+  gateway_url: string;
+  notify_url?: string;
+  return_url?: string;
+  status: string;
+  description?: string;
+  created_time?: string;
+  updated_time?: string;
+}
+
+const OpenAPI = {
+  getOpenConf: () => {
+    return request({
+      url: "/payment/openapi/conf",
+      method: "GET",
+    });
+  },
+
+  saveOpenConf: (data?: OpenConfForm) => {
+    return request({
+      url: "/payment/openapi/conf",
+      method: "POST",
+      data,
+    });
+  },
+};
+
+export default OpenAPI;

+ 29 - 0
frontend/src/api/module_system/auth.ts

@@ -62,6 +62,24 @@ const AuthAPI = {
       params: { token },
     });
   },
+
+  /** 获取短信验证码 */
+  getSmsCode(body: SmsCodeBody) {
+    return request<ApiResponse>({
+      url: `${API_PATH}/sms-code`,
+      method: "post",
+      data: body,
+    });
+  },
+
+  /** 短信登录 */
+  smsLogin(body: SmsLoginBody) {
+    return request<ApiResponse<LoginResult>>({
+      url: `${API_PATH}/login/sms`,
+      method: "post",
+      data: body,
+    });
+  },
 };
 
 export default AuthAPI;
@@ -114,3 +132,14 @@ export interface AutoLoginToken {
   token: string;
   user: AutoLoginUser;
 }
+
+/** 获取短信验证码请求 */
+export interface SmsCodeBody {
+  mobile: string;
+}
+
+/** 短信登录请求 */
+export interface SmsLoginBody {
+  phone: string;
+  code: string;
+}

+ 4 - 0
frontend/src/api/module_system/user.ts

@@ -1,6 +1,7 @@
 import request from "@/utils/request";
 import { MenuTable, MenuForm } from "@/api/module_system/menu";
 import { TenantInfo } from "./tenant";
+import { s } from "vue-router/dist/router-CWoNjPRp.mjs";
 
 const API_PATH = "/system/user";
 
@@ -147,6 +148,9 @@ export interface ForgetPasswordForm {
 }
 
 export interface RegisterForm {
+  sms_code: string;
+  mobile: string;
+  invite_code: string;
   username: string;
   password: string;
   confirmPassword: string;

+ 328 - 8
frontend/src/views/module_payment/apikey/index.vue

@@ -253,6 +253,113 @@
         </EnhancedDialog>
       </el-tab-pane>
 
+      <!-- 应用配置 -->
+      <el-tab-pane label="应用配置" name="config">
+        <div class="config-layout">
+          <!-- 查询中显示loading -->
+          <el-card v-if="configLoading" v-loading="configLoading" class="config-card">
+            <template #header>
+              <div class="card-header">
+                <span>开放平台配置</span>
+              </div>
+            </template>
+          </el-card>
+
+          <!-- 配置不存在时显示创建按钮 -->
+          <el-card v-else-if="!configExists" class="config-card">
+            <template #header>
+              <div class="card-header">
+                <span>开放平台配置</span>
+              </div>
+            </template>
+            <div class="config-empty">
+              <el-empty description="暂无开放平台配置" />
+              <p style="margin: 16px 0; color: #606266;">
+                请先创建开放平台配置,才能收到平台回调通知。
+              </p>
+              <el-button
+                type="primary"
+                :loading="configLoading"
+                @click="handleCreateConfig"
+              >
+                创建配置
+              </el-button>
+            </div>
+          </el-card>
+
+          <!-- 配置存在时显示编辑表单 -->
+          <el-card v-else class="config-card">
+            <template #header>
+              <div class="card-header">
+                <span>开放平台配置</span>
+              </div>
+            </template>
+            <el-form
+              ref="configFormRef"
+              :model="configForm"
+              label-width="140px"
+              label-position="right"
+              style="max-width: 600px"
+            >
+              <el-form-item label="应用ID">
+                <el-input v-model="configForm.app_id" disabled>
+                  <template #append>
+                    <el-button @click="copyToClipboard(configForm.app_id)">
+                      <el-icon><CopyDocument /></el-icon>
+                    </el-button>
+                  </template>
+                </el-input>
+              </el-form-item>
+              <el-form-item label="网关地址">
+                <el-input v-model="configForm.gateway_url" disabled>
+                  <template #append>
+                    <el-button @click="copyToClipboard(configForm.gateway_url)">
+                      <el-icon><CopyDocument /></el-icon>
+                    </el-button>
+                  </template>
+                </el-input>        
+              </el-form-item>
+              <!-- <el-form-item label="异步通知地址">
+                <el-input
+                  v-model="configForm.notify_url"
+                  placeholder="请输入异步通知地址"
+                  clearable
+                />
+              </el-form-item> -->
+              <el-form-item label="同步返回地址">
+                <el-input
+                  v-model="configForm.return_url"
+                  placeholder="请输入同步返回地址"
+                  clearable
+                />
+              </el-form-item>
+              <el-form-item label="状态">
+                <el-tag :type="configForm.status === 'ENABLED' ? 'success' : 'info'">
+                  {{ configForm.status === 'ENABLED' ? '启用' : '禁用' }}
+                </el-tag>
+              </el-form-item>
+              <!-- <el-form-item label="描述">
+                <el-input
+                  v-model="configForm.description"
+                  type="textarea"
+                  :rows="2"
+                  disabled
+                />
+              </el-form-item> -->
+              <el-form-item>
+                <el-button
+                  type="primary"
+                  :loading="configLoading"
+                  @click="handleSaveConfig"
+                >
+                  保存配置
+                </el-button>
+              </el-form-item>
+            </el-form>
+          </el-card>
+        </div>
+      </el-tab-pane>
+
       <!-- 接入文档 -->
       <el-tab-pane label="接入文档" name="docs">
         <div class="docs-layout">
@@ -270,7 +377,8 @@
               <div class="sidebar-section">
                 <h3>账户接口</h3>
                 <ul>
-                  <li @click="activeSection = 'transfer'" :class="{ active: activeSection === 'transfer' }">转账接口</li>
+                  <li @click="activeSection = 'transfer'" :class="{ active: activeSection === 'transfer' }">发起转账</li>
+                  <li @click="activeSection = 'transfer_query'" :class="{ active: activeSection === 'transfer_query' }">查询转账</li>
                 </ul>
               </div>
               <div class="sidebar-section">
@@ -376,7 +484,7 @@ Signature: {signature}</code></pre>
               </div>
               
               <div v-else-if="activeSection === 'transfer'" class="section-content">
-                <h2>4. 转账接口</h2>
+                <h2>4. 发起转账</h2>
                 <h3>4.1 接口说明</h3>
                 <p>从资金账户转账到支付宝账户/银行卡</p>
                 
@@ -418,6 +526,12 @@ Signature: {signature}</code></pre>
                       <td>否</td>
                       <td>转账备注</td>
                     </tr>
+                    <tr>
+                      <td>third_biz_no</td>
+                      <td>string</td>
+                      <td>是</td>
+                      <td>三方订单号(商户侧唯一标识,不可重复)</td>
+                    </tr>
                     <tr>
                       <td>payee_info</td>
                       <td>object</td>
@@ -536,17 +650,101 @@ Signature: {signature}</code></pre>
     "account_book_id": "资金账号",
     "amount": 100.00,
     "order_title": "转账标题",
+    "third_biz_no": "商户订单号202604270001",
     "payee_info": {
-      "identity_type": "alipay",
+      "identity_type": "ALIPAY_ACCOUNT",
       "name": "收款人姓名",
       "identity": "收款人支付宝账号"
     }
   }'</code></pre>
                 
                 <h4>响应示例</h4>
-                <pre><code>{"code": 200, "message": "转账申请已提交", "data": {"status": "DEALING", "order_no": "2026042711122334455", "fund_order_id": "2026042711122334455"}}</code></pre>
+                <pre><code>{"code": 200, "message": "转账申请已提交", "data": {"status": "DEALING", "order_no": "2026042711122334455", "third_biz_no": "商户订单号202604270001"}}</code></pre>
               </div>
               
+              <div v-else-if="activeSection === 'transfer_query'" class="section-content">
+                <h2>5. 查询转账</h2>
+                <h3>5.1 接口说明</h3>
+                <p>根据三方订单号查询转账状态和详情</p>
+                
+                <h4>API接口地址</h4>
+                <p><code>POST https://api.qcsj88888.com/payment/openapi/account/transfer/query</code></p>
+                
+                <h4>请求参数</h4>
+                <table class="api-table">
+                  <thead>
+                    <tr>
+                      <th>参数名</th>
+                      <th>类型</th>
+                      <th>是否必填</th>
+                      <th>描述</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr>
+                      <td>third_biz_no</td>
+                      <td>string</td>
+                      <td>是</td>
+                      <td>三方订单号(发起转账时传入的商户侧唯一标识)</td>
+                    </tr>
+                  </tbody>
+                </table>
+                
+                <h4>请求示例</h4>
+                <pre><code>curl -X POST 'https://api.qcsj88888.com/payment/openapi/account/transfer/query' \
+  -H 'Authorization: ApiKey your_api_key' \
+  -H 'Signature: your_signature' \
+  -H 'Content-Type: application/json' \
+  -d '{
+    "third_biz_no": "商户订单号202604270001"
+  }'</code></pre>
+                
+                <h4>响应示例</h4>
+                <pre><code>{
+  "code": 200,
+  "message": "查询成功",
+  "data": {
+    "status": "SUCCESS",
+    "order_no": "2026042711122334455",
+    "amount": 100.00,
+    "payee_info": {
+      "identity_type": "ALIPAY_ACCOUNT",
+      "name": "张*",
+      "identity": "z****@example.com"
+    },
+    "created_time": "2026-04-27 11:22:33",
+    "updated_time": "2026-04-27 11:25:45"
+  }
+}</code></pre>
+
+                <h4>状态说明</h4>
+                <table class="api-table">
+                  <thead>
+                    <tr>
+                      <th>状态码</th>
+                      <th>描述</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr>
+                      <td>DEALING</td>
+                      <td>处理中</td>
+                    </tr>
+                    <tr>
+                      <td>SUCCESS</td>
+                      <td>成功</td>
+                    </tr>
+                    <tr>
+                      <td>FAIL</td>
+                      <td>失败</td>
+                    </tr>
+                    <tr>
+                      <td>REFUND</td>
+                      <td>已退款</td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
               
               <div v-else-if="activeSection === 'errors'" class="section-content">
                 <h2>8. 常见错误</h2>
@@ -568,12 +766,13 @@ Signature: {signature}</code></pre>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive } from "vue";
+import { ref, reactive, onMounted } from "vue";
 import ApiKeyAPI, {
   ApiKeyCreateForm,
   ApiKeyPageQuery,
   ApiKeyResponse,
 } from "@/api/module_payment/apikey";
+import OpenAPI from "@/api/module_payment/openapi";
 import CrudToolbarLeft from "@/components/CURD/CrudToolbarLeft.vue";
 import CrudToolbarRight from "@/components/CURD/CrudToolbarRight.vue";
 import PageSearch from "@/components/CURD/PageSearch.vue";
@@ -582,6 +781,8 @@ import EnhancedDialog from "@/components/CURD/EnhancedDialog.vue";
 import { useCrudList } from "@/components/CURD/useCrudList";
 import type { IContentConfig, ISearchConfig } from "@/components/CURD/types";
 import { ElMessage } from "element-plus";
+import { Loading, CopyDocument } from "@element-plus/icons-vue";
+import { watch } from "vue";
 
 defineOptions({
   name: "ApiKey",
@@ -594,11 +795,89 @@ const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList }
 const dataFormRef = ref();
 const submitLoading = ref(false);
 const apiKeyDetailVisible = ref(false);
+const configFormRef = ref();
+const configLoading = ref(true);
 const expandedSections = ref({
   payee_info: false,
   bankcard_ext_info: false
 });
 
+const configExists = ref(false);
+
+const configForm = reactive({
+  app_id: "",
+  gateway_url: "",
+  notify_url: "",
+  return_url: "",
+  status: "ENABLED",
+  description: "",
+});
+
+watch(activeTab, (newTab) => {
+  if (newTab === "config") {
+    loadOpenConf();
+  }
+});
+
+async function loadOpenConf() {
+  configLoading.value = true;
+  configExists.value = false;
+  try {
+    const res = await OpenAPI.getOpenConf();
+    if (res.data.code === 0 && res.data.data) {
+      Object.assign(configForm, res.data.data);
+      configExists.value = true;
+    } else {
+      configExists.value = false;
+    }
+  } catch (error: any) {
+    console.error("加载配置失败:", error);
+    // 如果返回"配置不存在"错误,则显示创建按钮
+    if (error?.response?.data?.msg?.includes("配置不存在")) {
+      configExists.value = false;
+    }
+  } finally {
+    configLoading.value = false;
+  }
+}
+
+async function handleCreateConfig() {
+  configLoading.value = true;
+  try {
+    const res = await OpenAPI.saveOpenConf({
+      notify_url: "",
+      return_url: "",
+    });
+    if (res.data.code === 0 && res.data.data) {
+      Object.assign(configForm, res.data.data);
+      configExists.value = true;
+      ElMessage.success("创建成功");
+    }
+  } catch (error) {
+    console.error(error);
+    ElMessage.error("创建失败");
+  } finally {
+    configLoading.value = false;
+  }
+}
+
+async function handleSaveConfig() {
+  configLoading.value = true;
+  try {
+    const res = await OpenAPI.saveOpenConf({
+      notify_url: configForm.notify_url,
+      return_url: configForm.return_url,
+    });
+    if (res.data.code === 0 && res.data.data) {
+      Object.assign(configForm, res.data.data);
+    }
+  } catch (error) {
+    console.error(error);
+  } finally {
+    configLoading.value = false;
+  }
+}
+
 function toggleExpand(section: string) {
   expandedSections.value[section] = !expandedSections.value[section];
 }
@@ -608,7 +887,8 @@ function getSectionTitle() {
     auth: "认证方式",
     signature: "签名验证",
     notes: "注意事项",
-    transfer: "转账接口",
+    transfer: "发起转账",
+    transfer_query: "查询转账",
     errors: "常见错误"
   };
   return titles[activeSection.value] || "API文档";
@@ -836,6 +1116,38 @@ function downloadApiKeyCsv() {
   min-width: 0;
 }
 
+.config-loading-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 0;
+  color: #909399;
+}
+
+.config-loading-content .loading-icon {
+  font-size: 32px;
+  margin-bottom: 16px;
+  animation: rotate 1s linear infinite;
+}
+
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.config-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 0;
+}
+
 .docs-layout {
   display: flex;
   height: calc(100vh - 120px);
@@ -898,6 +1210,14 @@ function downloadApiKeyCsv() {
   padding: 0 20px;
 }
 
+.config-layout {
+  padding: 20px;
+}
+
+.config-card {
+  max-width: 700px;
+}
+
 .section-content {
   line-height: 1.6;
   padding: 20px 0;
@@ -1031,9 +1351,9 @@ function downloadApiKeyCsv() {
   align-items: center;
   justify-content: space-between;
   padding: 12px 20px;
-  border-bottom: 1px solid #e4e7ed;
+  /* border-bottom: 1px solid #e4e7ed; */
   margin: -20px -20px 20px -20px;
-  background-color: #fafafa;
+  /* background-color: #fafafa; */
 }
 
 .card-header span {

+ 145 - 47
frontend/src/views/module_system/auth/components/Login.vue

@@ -2,25 +2,18 @@
   <div>
     <h3 text-center m-0 mb-20px>{{ t("login.login") }}</h3>
 
-    <el-tabs v-model="activeTab" class="login-tabs">
+    <el-tabs v-model="activeTab" class="login-tabs" type="card">
       <!-- 账号登录 -->
       <el-tab-pane label="账号登录" name="password">
-        <el-form
-          ref="loginFormRef"
-          :model="loginForm"
-          :rules="loginRules"
-          size="large"
-          :validate-on-rule-change="false"
-        >
+        <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" size="large"
+          :validate-on-rule-change="false">
           <!-- 用户名 -->
           <el-form-item prop="username">
-            <el-input
-              v-model.trim="loginForm.username"
-              :placeholder="t('login.username')"
-              clearable
-            >
+            <el-input v-model.trim="loginForm.username" :placeholder="t('login.username')" clearable>
               <template #prefix>
-                <el-icon><User /></el-icon>
+                <el-icon>
+                  <User />
+                </el-icon>
               </template>
             </el-input>
           </el-form-item>
@@ -28,17 +21,12 @@
           <!-- 密码 -->
           <el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
             <el-form-item prop="password">
-              <el-input
-                v-model.trim="loginForm.password"
-                :placeholder="t('login.password')"
-                type="password"
-                show-password
-                clearable
-                @keyup="checkCapsLock"
-                @keyup.enter="handleLoginSubmit"
-              >
+              <el-input v-model.trim="loginForm.password" :placeholder="t('login.password')" type="password"
+                show-password clearable @keyup="checkCapsLock" @keyup.enter="handleLoginSubmit">
                 <template #prefix>
-                  <el-icon><Lock /></el-icon>
+                  <el-icon>
+                    <Lock />
+                  </el-icon>
                 </template>
               </el-input>
             </el-form-item>
@@ -47,13 +35,8 @@
           <!-- 验证码 -->
           <el-form-item v-if="captchaState.enable" prop="captcha">
             <div flex items-center gap-10px class="flex-1">
-              <el-input
-                v-model.trim="loginForm.captcha"
-                :placeholder="t('login.captchaCode')"
-                clearable
-                class="flex-1"
-                @keyup.enter="handleLoginSubmit"
-              >
+              <el-input v-model.trim="loginForm.captcha" :placeholder="t('login.captchaCode')" clearable class="flex-1"
+                @keyup.enter="handleLoginSubmit">
                 <template #prefix>
                   <div class="i-svg:captcha" />
                 </template>
@@ -62,15 +45,9 @@
                 <el-icon v-if="codeLoading" class="is-loading" size="20">
                   <Loading />
                 </el-icon>
-                <el-image
-                  v-else-if="captchaState.img_base"
-                  border-rd-4px
-                  object-cover
-                  shadow="[0_0_0_1px_var(--el-border-color)_inset]"
-                  :src="captchaState.img_base"
-                  class="w-full h-full"
-                  @click="getCaptcha"
-                />
+                <el-image v-else-if="captchaState.img_base" border-rd-4px object-cover
+                  shadow="[0_0_0_1px_var(--el-border-color)_inset]" :src="captchaState.img_base" class="w-full h-full"
+                  @click="getCaptcha" />
                 <el-text v-else type="info" size="small">点击获取验证码</el-text>
               </div>
             </div>
@@ -90,13 +67,45 @@
             </el-button>
           </el-form-item>
         </el-form>
+      </el-tab-pane>
+
+      <!-- 短信登录 -->
+      <el-tab-pane label="短信登录" name="sms">
+        <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>
+              <template #prefix>
+                <el-icon><Iphone /></el-icon>
+              </template>
+            </el-input>
+          </el-form-item>
+
+          <!-- 验证码 -->
+          <el-form-item prop="code">
+            <div class="flex gap-10px">
+              <el-input v-model.trim="smsForm.code" placeholder="请输入验证码" clearable class="flex-1"
+                @keyup.enter="handleSmsLogin">
+                <template #prefix>
+                  <el-icon>
+                    <Message />
+                  </el-icon>
+                </template>
+              </el-input>
+              <el-button :disabled="smsCountdown > 0 || !smsForm.phone" :loading="smsCodeLoading" class="w-32"
+                @click="getSmsCode">
+                {{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
+              </el-button>
+            </div>
+          </el-form-item>
 
-        <!-- <div flex-center gap-10px>
-          <el-text size="default">{{ t("login.noAccount") }}</el-text>
-          <el-link type="primary" underline="never" @click="toOtherForm('register')">
-            {{ t("login.reg") }}
-          </el-link>
-        </div> -->
+          <!-- 登录按钮 -->
+          <el-form-item>
+            <el-button :loading="smsLoading" type="primary" class="w-full" @click="handleSmsLogin">
+              {{ t("login.login") }}
+            </el-button>
+          </el-form-item>
+        </el-form>
       </el-tab-pane>
 
       <!-- 快速登录 -->
@@ -142,6 +151,13 @@
       </el-tab-pane> -->
     </el-tabs>
 
+    <div flex-center gap-10px>
+      <el-text size="default">{{ t("login.noAccount") }}</el-text>
+      <el-link type="primary" underline="never" @click="toOtherForm('register')">
+        {{ t("login.reg") }}
+      </el-link>
+    </div>
+
     <!-- 第三方登录 -->
     <!-- <div class="third-party-login">
       <div class="divider-container">
@@ -175,10 +191,12 @@ import AuthAPI, {
   type AutoLoginUser,
   type LoginFormData,
   type CaptchaInfo,
+  type SmsCodeBody,
+  type SmsLoginBody,
 } from "@/api/module_system/auth";
 import { useAppStore, useUserStore, useSettingsStore } from "@/store";
 import CommonWrapper from "@/components/CommonWrapper/index.vue";
-import { User, Loading, Lock } from "@element-plus/icons-vue";
+import { User, Loading, Lock, Phone, Message } from "@element-plus/icons-vue";
 import { Auth } from "@/utils/auth";
 
 const { t } = useI18n();
@@ -192,6 +210,28 @@ const activeTab = ref("password");
 // 选择的用户ID(用于下拉选择)
 const selectedUserId = ref<number | null>(null);
 
+// 短信登录表单
+const smsFormRef = ref<FormInstance>();
+const smsLoading = ref(false);
+const smsCodeLoading = ref(false);
+const smsCountdown = ref(0);
+
+const smsForm = reactive({
+  phone: "",
+  code: "",
+});
+
+const smsRules = computed(() => ({
+  phone: [
+    { required: true, message: "请输入手机号", trigger: "blur" },
+    { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号", trigger: "blur" },
+  ],
+  code: [
+    { required: true, message: "请输入验证码", trigger: "blur" },
+    { length: 6, message: "验证码长度为6位", trigger: "blur" },
+  ],
+}));
+
 // 来自父容器的预填用户名和密码
 const props = defineProps<{ presetUsername?: string; presetPassword?: string }>();
 
@@ -438,6 +478,64 @@ function checkCapsLock(event: KeyboardEvent) {
   }
 }
 
+// 获取短信验证码
+async function getSmsCode() {
+  try {
+    const valid = await smsFormRef.value?.validateField("phone");
+    if (!valid) return;
+
+    smsCodeLoading.value = true;
+
+    // 调用后端API获取短信验证码
+    await AuthAPI.getSmsCode({ mobile: smsForm.phone });
+
+    // 开始倒计时
+    smsCountdown.value = 60;
+    const timer = setInterval(() => {
+      smsCountdown.value--;
+      if (smsCountdown.value <= 0) {
+        clearInterval(timer);
+      }
+    }, 1000);
+
+    ElMessage.success("验证码已发送");
+  } catch (error: any) {
+    ElMessage.error(error?.response?.data?.msg || "获取验证码失败");
+  } finally {
+    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;
+  }
+}
+
 const emit = defineEmits(["update:modelValue"]);
 function toOtherForm(type: "register" | "resetPwd") {
   emit("update:modelValue", type);

+ 149 - 30
frontend/src/views/module_system/auth/components/Register.vue

@@ -2,29 +2,58 @@
   <div>
     <h3 text-center m-0 mb-20px>{{ t("login.reg") }}</h3>
     <el-form ref="formRef" :model="model" :rules="rules" size="large" label-suffix=":">
+      <!-- 邀请码 -->
+      <el-form-item prop="invite_code">
+        <el-input v-model.trim="model.invite_code" placeholder="请输入邀请码" clearable>
+          <template #prefix>
+            <el-icon><SetUp /></el-icon>
+          </template>
+        </el-input>
+      </el-form-item>
+
+      <el-form-item prop="mobile">
+        <el-input v-model.trim="model.mobile" placeholder="请输入手机号" clearable>
+          <template #prefix>
+            <el-icon><Iphone /></el-icon>
+          </template>
+        </el-input>
+      </el-form-item>
+
+      <el-form-item prop="sms_code">
+        <div class="flex gap-10px">
+          <el-input v-model.trim="model.sms_code" placeholder="请输入验证码" clearable class="flex-1"
+            @keyup.enter="">
+            <template #prefix>
+              <el-icon>
+                <Message />
+              </el-icon>
+            </template>
+          </el-input>
+          <el-button :disabled="smsCountdown > 0 || !model.mobile" :loading="smsCodeLoading" class="w-32"
+            @click="getSmsCode">
+            {{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
+          </el-button>
+        </div>
+      </el-form-item>
+
       <!-- 账号 -->
-      <el-form-item prop="username">
+      <!-- <el-form-item prop="username">
         <el-input v-model.trim="model.username" :placeholder="t('login.username')" clearable>
           <template #prefix>
             <el-icon><User /></el-icon>
           </template>
         </el-input>
-      </el-form-item>
+      </el-form-item> -->
 
       <!-- 密码 -->
       <el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
         <el-form-item prop="password">
-          <el-input
-            v-model.trim="model.password"
-            :placeholder="t('login.password')"
-            type="password"
-            show-password
-            clearable
-            @keyup="checkCapsLock"
-            @keyup.enter="submit"
-          >
+          <el-input v-model.trim="model.password" :placeholder="t('login.password')" type="password" show-password
+            clearable @keyup="checkCapsLock" @keyup.enter="submit">
             <template #prefix>
-              <el-icon><Lock /></el-icon>
+              <el-icon>
+                <Lock />
+              </el-icon>
             </template>
           </el-input>
         </el-form-item>
@@ -33,17 +62,12 @@
       <!-- 确认密码 -->
       <el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
         <el-form-item prop="confirmPassword">
-          <el-input
-            v-model.trim="model.confirmPassword"
-            :placeholder="t('login.message.password.confirm')"
-            type="password"
-            show-password
-            clearable
-            @keyup="checkCapsLock"
-            @keyup.enter="submit"
-          >
+          <el-input v-model.trim="model.confirmPassword" :placeholder="t('login.message.password.confirm')"
+            type="password" show-password clearable @keyup="checkCapsLock" @keyup.enter="submit">
             <template #prefix>
-              <el-icon><Lock /></el-icon>
+              <el-icon>
+                <Lock />
+              </el-icon>
             </template>
           </el-input>
         </el-form-item>
@@ -52,12 +76,8 @@
       <el-form-item>
         <div class="flex-y-center w-full gap-10px">
           <el-checkbox v-model="isRead">{{ t("login.agree") }}</el-checkbox>
-          <el-link
-            type="primary"
-            underline="never"
-            :href="configStore.configData.sys_web_clause.config_value"
-            target="_blank"
-          >
+          <el-link type="primary" underline="never" :href="configStore.configData.sys_web_clause.config_value"
+            target="_blank">
             {{ t("login.userAgreement") }}
           </el-link>
         </div>
@@ -65,7 +85,7 @@
 
       <!-- 注册按钮 -->
       <el-form-item>
-        <el-button :loading="loading" type="success" class="w-full" @click="submit">
+        <el-button :loading="loading" type="primary" class="w-full" @click="submit">
           {{ t("login.register") }}
         </el-button>
       </el-form-item>
@@ -82,6 +102,10 @@ import { Lock } from "@element-plus/icons-vue";
 import UserAPI, { type RegisterForm } from "@/api/module_system/user";
 import { useConfigStore } from "@/store";
 import { useI18n } from "vue-i18n";
+import AuthAPI, {
+
+} from "@/api/module_system/auth";
+import { Auth } from "@/utils/auth";
 
 const { t } = useI18n();
 
@@ -100,7 +124,15 @@ const loading = ref(false); // 按钮 loading 状态
 const isCapsLock = ref(false); // 是否大写锁定
 const isRead = ref(false);
 
+const smsLoading = ref(false);
+const smsCodeLoading = ref(false);
+const smsCountdown = ref(0);
+
+
 const model = ref<RegisterForm>({
+  invite_code: "",
+  mobile: "",
+  sms_code: "",
   username: "",
   password: "",
   confirmPassword: "",
@@ -108,6 +140,33 @@ const model = ref<RegisterForm>({
 
 const rules = computed(() => {
   return {
+    invite_code: [
+      {
+        required: true,
+        trigger: "blur",
+        message: "请输入邀请码",
+      },
+    ],
+    // 验证国内手机号格式
+    mobile: [
+      {
+        required: true,
+        trigger: "blur",
+        message: "请输入手机号",
+      },
+      {
+        pattern: /^1[3456789]\d{9}$/,
+        trigger: "blur",
+        message: "请输入正确的手机号格式",
+      },
+    ],
+    sms_code: [
+      {
+        required: true,
+        trigger: "blur",
+        message: "请输入验证码",
+      },
+    ],
     username: [
       {
         required: true,
@@ -182,5 +241,65 @@ const submit = async () => {
   }
 };
 
-onMounted(() => {});
+
+// 获取短信验证码
+async function getSmsCode() {
+  try {
+    const valid = await formRef.value?.validateField("mobile");
+    if (!valid) return;
+
+    smsCodeLoading.value = true;
+
+    // 调用后端API获取短信验证码
+    await AuthAPI.getSmsCode({ mobile: model.value.mobile });
+
+    // 开始倒计时
+    smsCountdown.value = 60;
+    const timer = setInterval(() => {
+      smsCountdown.value--;
+      if (smsCountdown.value <= 0) {
+        clearInterval(timer);
+      }
+    }, 1000);
+
+    ElMessage.success("验证码已发送");
+  } catch (error: any) {
+    ElMessage.error(error?.response?.data?.msg || "获取验证码失败");
+  } finally {
+    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(() => { });
 </script>

+ 1 - 1
frontend/src/views/module_system/auth/index.vue

@@ -341,7 +341,7 @@ onBeforeUnmount(() => {
   gap: 1.5rem;
   justify-content: flex-start;
   justify-self: end;
-  width: min(520px, 100%);
+  width: min(500px, 100%);
   padding: clamp(2rem, 3vw, 2.75rem);
   margin-inline: auto;
   background: var(--el-bg-color-overlay);