news 2026/7/28 1:27:10

​​​​​​功能详解 01 · 用户注册:从表单到数据库一行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
​​​​​​功能详解 01 · 用户注册:从表单到数据库一行

🥰个人主页:会编程的土豆(欢迎来访)
💎作者简介:后端学习者
❄️个人专栏:数据结构与算法,数据库,leetcode
那些你一个人走过的夜路,终将化作照亮未来的光

系列:《影院票务 GO》功能详解 · 第 01 篇
对应代码:(代码在这个系列的文章中)templates/register.htmlstatic/js/app.jshandlers/auth.godao/user.goutils/password.go
数据库:sql/schema.sqlusers
前置

一、背景:为什么要有注册?

影院票务系统(TTMS)里,很多操作必须知道「你是谁」:

  • 选座、下单要把座位绑到你的账号;
  • 订单列表只显示自己的票;
  • 评论、评分要记录发表人。

注册就是:在系统里新建一条用户记录,以后用「用户名 + 密码」登录。

本项目的注册特点:

  1. 前后端分离式交互:页面是 HTML 表单,提交走JSON API(不是传统 form POST 刷新整页)。
  2. 密码绝不存明文:每个用户随机8 位盐,存MD5(密码+盐)
  3. 新用户默认顾客role = 0(管理员只能种子数据或人工改库,注册接口不会创建管理员)。

二、用户视角:点「注册」后发生了什么?

打开http://localhost:8080/register(端口以config为准),你会看到:

  • 用户名(必填)
  • 密码(必填,HTML 限制至少 6 位)
  • 昵称、手机(可选)

填完点「注册并登录」,前端脚本doRegister会:

  1. 阻止表单默认刷新(e.preventDefault());
  2. 把字段打包成 JSON,POST /api/register
  3. 注册成功后立刻再调POST /api/login(自动登录);
  4. 跳转到首页/

若用户名已存在或密码太短,浏览器alert弹出后端返回的中文错误信息。


三、请求链路总览

[浏览器 register.html] │ 用户提交 ▼ [app.js doRegister] │ fetch POST /api/register (JSON body) ▼ [handlers/routes.go] mux.HandleFunc("/api/register", a.APIRegister) ▼ [handlers/auth.go APIRegister] │ 校验参数 → 查重 → 哈希密码 → 组装 User ▼ [dao/user.go CreateUser] │ INSERT INTO users ... ▼ [MySQL users 表] │ 返回 LastInsertId ▼ [utils/json.go OK] → {"code":0,"message":"ok","data":{"id":3}} │ ▼ (前端继续) [app.js] POST /api/login → Session → location.href='/'

路由注册在handlers/routes.go第 18 行:

mux.HandleFunc("/api/register", a.APIRegister)

页面路由:

mux.HandleFunc("/register", a.PageRegister)

PageRegister只负责渲染模板,不做业务:

func (a *App) PageLogin(w http.ResponseWriter, r *http.Request) { ... } func (a *App) PageRegister(w http.ResponseWriter, r *http.Request) { a.Render(w, "register.html", pageData{Title: "注册", User: a.currentUser(r)}) }

四、前端:表单如何变成 JSON?

templates/register.html核心:

<form id="regForm" onsubmit="return doRegister(event)"> <label>用户名<input name="username" required /></label> <label>密码<input name="password" type="password" required minlength="6" /></label> ... </form>

static/js/app.jsdoRegister

async function doRegister(e) { e.preventDefault(); const fd = new FormData(e.target); try { await api('/api/register', { method: 'POST', body: JSON.stringify({ username: fd.get('username'), password: fd.get('password'), nickname: fd.get('nickname'), phone: fd.get('phone'), }), }); await api('/api/login', { ... }); // 注册成功后自动登录 location.href = '/'; } catch (err) { alert(err.message); } return false; }

公共封装api()做了三件事:

  1. fetch时带Content-Type: application/json
  2. credentials: 'same-origin'—— 为后续登录写 Cookie 做准备;
  3. 解析统一格式{code, message, data}code !== 0就抛错。

对应后端utils/json.go

type APIResult struct { Code int `json:"code"` Message string `json:"message"` Data interface{} `json:"data,omitempty"` } func OK(w http.ResponseWriter, data interface{}) { JSON(w, 0, "ok", data) } func Fail(w http.ResponseWriter, message string) { JSON(w, 1, message, nil) }

设计要点:业务失败时 HTTP 状态码仍是 200,靠code=1区分;前端只需看data.code,不必处理 4xx/5xx 分支(课设简化写法)。


五、后端 APIRegister:逐步拆解

完整代码在handlers/auth.go。我们按执行顺序读。

5.1 只允许 POST

if r.Method != http.MethodPost { utils.Fail(w, "方法不允许") return }

若有人用浏览器直接访问/api/register(GET),会收到 JSON 错误,不会误插入数据。

5.2 解析 JSON 到匿名结构体

var req struct { Username string `json:"username"` Password string `json:"password"` Nickname string `json:"nickname"` Phone string `json:"phone"` } if err := utils.DecodeJSON(r, &req); err != nil { utils.Fail(w, "参数错误") return }

DecodeJSONr.Bodyjson.Decode。字段 tag 的json:"username"与前端 JSON 键名对应。

5.3 参数校验

req.Username = strings.TrimSpace(req.Username) if req.Username == "" || len(req.Password) < 6 { utils.Fail(w, "用户名不能为空,密码至少6位") return }
  • 去掉用户名首尾空格,避免" alice "这种视觉重复账号;
  • 密码长度6是后端底线(前端minlength="6"是第一道防线,后端必须再验,防绕过)。

5.4 用户名唯一性

exist, err := dao.GetUserByUsername(req.Username) if err != nil { utils.Fail(w, err.Error()) return } if exist != nil { utils.Fail(w, "用户名已存在") return }

dao/user.go

func GetUserByUsername(username string) (*models.User, error) { u := &models.User{} err := db.DB.QueryRow( `SELECT id,username,password,salt,nickname,phone,role,created_at FROM users WHERE username=?`, username, ).Scan(...) if errors.Is(err, sql.ErrNoRows) { return nil, nil // 没找到:不是错误,表示可以注册 } return u, err }

sql.ErrNoRows时返回(nil, nil)是 Go + database/sql 的常见模式:「查无此人」≠ 数据库故障

表结构里还有一层保险:

username VARCHAR(50) NOT NULL UNIQUE,

即使并发两次相同用户名注册,第二条 INSERT 也会因唯一索引失败(本项目先查再插,课设足够;高并发应靠数据库约束 + 事务)。

5.5 昵称默认值

if req.Nickname == "" { req.Nickname = req.Username }

用户没填昵称时,展示名等于登录名。

5.6 密码哈希:盐 + MD5

salt := utils.RandomSalt(8) u := &models.User{ Username: req.Username, Password: utils.HashPassword(req.Password, salt), Salt: salt, Nickname: req.Nickname, Phone: req.Phone, Role: models.RoleCustomer, // 常量 0 }

utils/password.go

func HashPassword(password, salt string) string { sum := md5.Sum([]byte(password + salt)) return hex.EncodeToString(sum[:]) } func RandomSalt(n int) string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // crypto/rand 逐字符随机 ... }

为什么要盐?若只存MD5(密码),两个用户密码相同则哈希相同;攻击者可用彩虹表批量反查。每人不同盐,同样密码哈希也不同。

种子数据验证方式(sql/seed.sql):

('admin', MD5(CONCAT('admin123', 'saltadmin')), 'saltadmin', ...),

MySQL 的MD5(CONCAT(...))与 Go 的md5.Sum([]byte(password+salt))算法一致。

答辩提醒:MD5 已不适合生产密码存储(应 bcrypt/argon2);课设选 MD5 是为了算法简单、与 SQL 种子一致。详见blogs/13-密码与哈希MD5与盐.md

5.7 写入数据库

if err := dao.CreateUser(u); err != nil { utils.Fail(w, "注册失败: "+err.Error()) return } utils.OK(w, map[string]interface{}{"id": u.ID})

dao/user.go

func CreateUser(u *models.User) error { res, err := db.DB.Exec( `INSERT INTO users(username,password,salt,nickname,phone,role) VALUES(?,?,?,?,?,?)`, u.Username, u.Password, u.Salt, u.Nickname, u.Phone, u.Role, ) if err != nil { return err } u.ID, _ = res.LastInsertId() return nil }

LastInsertId()把数据库自增主键写回u.ID,响应里带给前端(前端当前没用 id,但 API 设计完整)。

models.User中密码字段带json:"-",即使将来把 User 序列化给前端,也不会泄露哈希:

type User struct { Password string `json:"-"` Salt string `json:"-"` Role int `json:"role"` ... }

六、数据库表与字段含义

sql/schema.sql

CREATE TABLE IF NOT EXISTS users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(32) NOT NULL COMMENT 'MD5(password+salt)', salt VARCHAR(16) NOT NULL, nickname VARCHAR(50) NOT NULL DEFAULT '', phone VARCHAR(20) NOT NULL DEFAULT '', role TINYINT NOT NULL DEFAULT 0, -- 0顾客 1管理员 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );
字段注册时谁赋值说明
id数据库自增用户唯一标识
username用户输入登录名
passwordHashPassword32 位十六进制
saltRandomSalt(8)明文存储,用于登录校验
nickname用户或默认用户名页面展示
phone用户可选联系方式
role固定RoleCustomer=0注册不能当管理员
created_at数据库默认注册时间

七、注册 vs 登录:别混淆

环节注册/api/register登录/api/login
密码处理生成盐 → 哈希 →存入读库盐 → 哈希输入 →比对
Session不创建Sessions.Create写 Cookie
返回{id}{id, username, nickname, role}

注册成功后前端主动调登录,用户无感;若只注册不登录,首页仍显示「登录/注册」链接。

登录校验用utils.CheckPassword

func CheckPassword(password, salt, hashed string) bool { return HashPassword(password, salt) == hashed }

同一套HashPassword,保证注册与登录算法一致。


八、常见错误与排查

现象可能原因怎么查
alert「参数错误」JSON 格式不对、Body 空浏览器 Network 看 Request Payload
「用户名已存在」重复注册SELECT * FROM users WHERE username='...'
「注册失败: …」数据库未启动、DSN 错main.go启动日志
注册成功但未登录第二步 login 失败Network 看/api/login响应
密码 5 位也能提交?只绕过前端后端应拒绝;本项目已校验len < 6

九、小结

  1. 页面register.html收集字段,JS转 JSON POST 到/api/register
  2. APIRegister校验 → 查重 →RandomSalt+HashPasswordCreateUserINSERT。
  3. 新用户role 固定为 0(顾客),密码MD5(明文+盐)存库,明文永不落盘。
  4. 前端注册成功后自动 login,建立 Session(下一篇细讲)。
  5. 统一 JSON 响应code/message/data,前后端约定清晰。

用一句话向学长描述:

「注册就是 handlers 收 JSON,dao 插 users 一行;密码先随机盐再 MD5,库里只有摘要。」


下一篇02-登录退出与Session.md—— 登录如何写 Cookiettms_sidstore/session.go如何在内存里记住你。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/28 1:27:01

5分钟掌握无损视频剪辑:用LosslessCut保护原始画质的终极指南

5分钟掌握无损视频剪辑&#xff1a;用LosslessCut保护原始画质的终极指南 【免费下载链接】lossless-cut The swiss army knife of lossless video/audio editing 项目地址: https://gitcode.com/gh_mirrors/lo/lossless-cut 你是否曾因视频剪辑导致画质下降而苦恼&…

作者头像 李华
网站建设 2026/7/28 1:20:50

使用 Taotoken 聚合服务后 API 调用延迟与稳定性的实际体验分享

使用 Taotoken 聚合服务后 API 调用延迟与稳定性的实际体验分享 1. 接入后的延迟体感观察 在实际开发过程中&#xff0c;我们通过 Taotoken 平台接入多个大模型服务&#xff0c;发现调用延迟与模型类型、请求负载和时段分布相关。以控制台记录的 7 天数据为例&#xff0c;文本…

作者头像 李华
网站建设 2026/7/28 1:19:11

利用 Taotoken 多模型能力为智能客服场景选择最佳模型

利用 Taotoken 多模型能力为智能客服场景选择最佳模型 智能客服系统需要根据对话复杂度、成本预算和响应速度选择不同的大模型。Taotoken 作为大模型聚合分发平台&#xff0c;提供了统一的 OpenAI 兼容 API 和丰富的模型选择&#xff0c;帮助开发者快速测试和切换不同模型。本文…

作者头像 李华
网站建设 2026/7/28 1:18:42

3步掌握HTTrack:将任何网站完整保存到本地的终极指南

3步掌握HTTrack&#xff1a;将任何网站完整保存到本地的终极指南 【免费下载链接】httrack HTTrack Website Copier, copy websites to your computer (Official repository) 项目地址: https://gitcode.com/gh_mirrors/ht/httrack 你是否曾需要离线访问某个网站&#x…

作者头像 李华
网站建设 2026/7/28 1:18:01

5分钟掌握Wand高级功能解锁:免费体验完整游戏修改器

5分钟掌握Wand高级功能解锁&#xff1a;免费体验完整游戏修改器 【免费下载链接】Wand-Enhancer Advanced UX and interoperability extension for Wand (WeMod) app 项目地址: https://gitcode.com/GitHub_Trending/we/Wand-Enhancer Wand-Enhancer是一个开源工具&…

作者头像 李华
网站建设 2026/7/28 1:12:01

终极指南:如何使用pkNX打造个性化的宝可梦Switch游戏体验

终极指南&#xff1a;如何使用pkNX打造个性化的宝可梦Switch游戏体验 【免费下载链接】pkNX Pokmon (Nintendo Switch) ROM Editor & Randomizer 项目地址: https://gitcode.com/gh_mirrors/pk/pkNX 想要个性化你的宝可梦Switch游戏体验吗&#xff1f;pkNX是一款功能…

作者头像 李华