fastapi:管理后台:用户注册

一,要注意的环节:

用户的密码保存时不能用明文,要用bcrypt加密保存

有写库操作,要做try except,发生异常时要做写日志,

有多个参数,要用wtforms做参数的校验

二,代码

python代码:

# 得到bcrypt加密后的密码
def get_password_hash(password: str) -> str:
    """Hash a password using bcrypt"""
    return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")

# 1. 定义 WTForms 表单
class RegistrationForm(WTForm):
    account = StringField('用户名', [validators.Length(min=4, max=25)])
    password = PasswordField('密码', validators=[
            DataRequired(message="密码不能为空!"),
            Length(min=6, max=20, message="用户名长度必须在 6 到 20 之间")
        ])
    # 用 EqualTo 指向需要对比的字段名 (fieldname='password')
    password_confirm = PasswordField('确认密码', [
        DataRequired(message="请再次输入密码以确认"),
        EqualTo(fieldname='password', message='两次输入的密码不一致,请重新输入')
    ])
    nickname = StringField('昵称', [validators.Length(min=2, max=25)])

# 2. GET 路由:渲染空表单
@router.get("/register", response_class=HTMLResponse)
async def get_register(request: Request):
    form = RegistrationForm()
    return templates.TemplateResponse(
        request=request,
        name="account/register.html",
        context={"request": request, "form": form,"title": "注册用户"})

# 3. POST 路由:处理表单提交
@router.post("/register")
async def post_register(request: Request,db: AsyncSession = Depends(get_db)):
    # 将 FastAPI 接收到的 FormData 转换为 WTForms 需要的格式
    form_data = await request.form()

    form = RegistrationForm(form_data)

    if form.validate():

        # 检查是否已注册
        count_stmt = select(func.count()).select_from(User).where(User.username == form.account.data)
        total_items = await db.scalar(count_stmt)
        if total_items>0:
            raise HTTPException(
                status_code=400,
                detail="用户名已被占用,请换一个"
            )

        # 2. 密码哈希处理
        hashed_password = get_password_hash(form.password.data)

        # 3. 构建新的 User 实体
        new_user = User(
            username=form.account.data,
            password=hashed_password,  # 存入哈希值
            nickname=form.nickname.data
        )
        try:
            # 4. 写入数据库
            db.add(new_user)
            await db.commit()
            await db.refresh(new_user)  # 刷新以获取数据库生成的 id
        except Exception as err:
            await db.rollback()
            stack = traceback.format_exc()
            mylogger.bind(log_type="system").info(f'注册用户失败: {err}')
            mylogger.bind(log_type="system").info(stack)
            raise HTTPException(
                status_code=400,
                detail="注册用户失败"
            )
        # 返回成功
        return JSONResponse({"status_code":200,"detail": "用户注册成功", "data":{}})
    else:
        print(form.errors)
        error_msg = "; ".join([f"{field}: {', '.join(errors)}" for field, errors in form.errors.items()])
        # 返回异常 
        return JSONResponse({"status_code":400,"detail": error_msg,"data":{}})

html代码:

{% extends "layout/column.html" %}
{% block title %}{{ title }}{% endblock %}

{% block layout_css %}
<style type="text/css">

  body {
    background-color: #f5f5f5;
  }

  .auth-login {
    display: flex;
    align-items: center;
    padding-top: 40px;
    padding-bottom: 40px;
    min-height: 780px;
  }

  .form-signin {
    max-width: 330px;
    padding: 15px;
  }

  .form-signin .form-floating:focus-within {
    z-index: 2;
  }

  .bd-placeholder-img {
    font-size: 1.125rem;
    text-anchor: middle;
    -webkit-user-select: none;
    -moz-user-select: none;
    user-select: none;
  }

  @media (min-width: 768px) {
    .bd-placeholder-img-lg {
      font-size: 3.5rem;
    }
  }

  .b-example-divider {
    height: 3rem;
    background-color: rgba(0, 0, 0, .1);
    border: solid rgba(0, 0, 0, .15);
    border-width: 1px 0;
    box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
  }

  .b-example-vr {
    flex-shrink: 0;
    width: 1.5rem;
    height: 100vh;
  }

  .bi {
    vertical-align: -.125em;
    fill: currentColor;
  }

  .nav-scroller {
    position: relative;
    z-index: 2;
    height: 2.75rem;
    overflow-y: hidden;
  }

  .nav-scroller .nav {
    display: flex;
    flex-wrap: nowrap;
    padding-bottom: 1rem;
    margin-top: -1px;
    overflow-x: auto;
    text-align: center;
    white-space: nowrap;
    -webkit-overflow-scrolling: touch;
  }

  form .error {
    padding-left:120px;
    font-size: 14px;
  }
</style>
{% endblock %}

{% block layout_js %}
<script src="{{ url_for('static', path='js/jquery/jquery.form.min.js') }}"></script>
<script src="{{ url_for('static', path='js/jquery/jquery.validate.min.js') }}"></script>

<script type="text/javascript">

$("#item-form").validate({
  //ignore: ".ignore",
  //debug: true,
  rules: {
      account: {
          required: true,
      },
      password: {
          required: true,
      },
      password_confirm: {
          required: true,
          equalTo: "#password",
      },
      nickname: {
          required: true,
      },
  },
  messages: {
      account: {
          required: "请输入账号",
      },
      password: {
          required: "请输入密码",
      },
      password_confirm: {
          required: "请再次输入密码",
          equalTo: '密码不一致',
      },
      nickname: {
          required: "请输入昵称",
      },
  },
  submitHandler: function(form) {
    $(form).ajaxSubmit({
      dataType: 'json',
      beforeSubmit: function(){
      },
      success: function(rs){
          if(rs.status_code == 200){
              phenix.show_success_note('注册成功,正在跳转登录页面...')
              setTimeout(() => {
                  window.location.href = "{{ url_for('get_login') }}"
              }, 2000);
          }else{
              phenix.show_error_note(rs.msg)
          }

      },
      error: function(xhr, status, err){
          phenix.show_error_note(xhr.responseJSON.meta.message)
      }
    });
  }
 });

</script>
{% endblock %}

{% block content %}
<div class="auth-login text-center">
  <main class="form-signin w-100 m-auto text-align">
    <form id="item-form" action="{{ url_for('post_register') }}" method="POST">
      <img class="mb-4 rounded-circle" src="{{ url_for('static', path='images/logo/logo_300.jpg') }}" alt="" width="100">
      <h1 class="h3 mb-3 fw-normal">注册</h1>

      <div class="form-floating">
        <input type="text" name="account" class="form-control" placeholder="">
        <label for="account">账号</label>
      </div>
      <div class="form-floating mt-2">
        <input id="password" type="password" name="password" class="form-control" placeholder="Password">
        <label for="password">密码</label>
      </div>
      <div class="form-floating mt-2">
        <input type="password" name="password_confirm" class="form-control" placeholder="Password Confirm">
        <label for="password_confirm">验证密码</label>
      </div>
      <div class="form-floating mt-2">
        <input type="text" name="nickname" class="form-control" placeholder="">
        <label for="nickname">昵称</label>
      </div>

      <button class="w-100 btn btn-lg btn-primary mt-3" type="submit">注册</button>
    </form>
  </main>
</div>
{% endblock %}

三,测试效果:

image

 

posted @ 2026-07-18 22:39  刘宏缔的架构森林  阅读(7)  评论(0)    收藏  举报