​​​​​​功能详解 01 · 用户注册:从表单到数据库一行
个人主页会编程的土豆欢迎来访作者简介后端学习者❄️个人专栏数据结构与算法数据库leetcode✨那些你一个人走过的夜路终将化作照亮未来的光系列《影院票务 GO》功能详解 · 第 01 篇对应代码代码在这个系列的文章中templates/register.html→static/js/app.js→handlers/auth.go→dao/user.go→utils/password.go数据库sql/schema.sql中users表前置一、背景为什么要有注册影院票务系统TTMS里很多操作必须知道「你是谁」选座、下单要把座位绑到你的账号订单列表只显示自己的票评论、评分要记录发表人。注册就是在系统里新建一条用户记录以后用「用户名 密码」登录。本项目的注册特点前后端分离式交互页面是 HTML 表单提交走JSON API不是传统 form POST 刷新整页。密码绝不存明文每个用户随机8 位盐存MD5(密码盐)。新用户默认顾客role 0管理员只能种子数据或人工改库注册接口不会创建管理员。二、用户视角点「注册」后发生了什么打开http://localhost:8080/register端口以config为准你会看到用户名必填密码必填HTML 限制至少 6 位昵称、手机可选填完点「注册并登录」前端脚本doRegister会阻止表单默认刷新e.preventDefault()把字段打包成 JSONPOST /api/register注册成功后立刻再调POST /api/login自动登录跳转到首页/。若用户名已存在或密码太短浏览器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)}) }四、前端表单如何变成 JSONtemplates/register.html核心form idregForm onsubmitreturn doRegister(event) label用户名input nameusername required //label label密码input namepassword typepassword required minlength6 //label ... /formstatic/js/app.js中doRegisterasync 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()做了三件事fetch时带Content-Type: application/jsoncredentials: same-origin—— 为后续登录写 Cookie 做准备解析统一格式{code, message, data}code ! 0就抛错。对应后端utils/json.gotype 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靠code1区分前端只需看data.code不必处理 4xx/5xx 分支课设简化写法。五、后端 APIRegister逐步拆解完整代码在handlers/auth.go。我们按执行顺序读。5.1 只允许 POSTif r.Method ! http.MethodPost { utils.Fail(w, 方法不允许) return }若有人用浏览器直接访问/api/registerGET会收到 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 }DecodeJSON读r.Body并json.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是后端底线前端minlength6是第一道防线后端必须再验防绕过。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.gofunc 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 密码哈希盐 MD5salt : 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.gofunc 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(passwordsalt))算法一致。答辩提醒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.gofunc 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.sqlCREATE TABLE IF NOT EXISTS users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(32) NOT NULL COMMENT MD5(passwordsalt), 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固定RoleCustomer0注册不能当管理员created_at数据库默认注册时间七、注册 vs 登录别混淆环节注册/api/register登录/api/login密码处理生成盐 → 哈希 →存入读库盐 → 哈希输入 →比对Session不创建Sessions.Create写 Cookie返回{id}{id, username, nickname, role}注册成功后前端主动调登录用户无感若只注册不登录首页仍显示「登录/注册」链接。登录校验用utils.CheckPasswordfunc 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九、小结页面register.html收集字段JS转 JSON POST 到/api/register。APIRegister校验 → 查重 →RandomSaltHashPassword→CreateUserINSERT。新用户role 固定为 0顾客密码MD5(明文盐)存库明文永不落盘。前端注册成功后自动 login建立 Session下一篇细讲。统一 JSON 响应code/message/data前后端约定清晰。用一句话向学长描述「注册就是 handlers 收 JSONdao 插 users 一行密码先随机盐再 MD5库里只有摘要。」下一篇02-登录退出与Session.md—— 登录如何写 Cookiettms_sidstore/session.go如何在内存里记住你。

相关新闻

目前 Claude / GPT 的订阅建议与反代避坑

目前 Claude / GPT 的订阅建议与反代避坑

半年来在 Claude 和 GPT 的 CLI 上累计消耗了约 30,000$ 用量的 tokens(Claude : GPT 2: 1,Fable 5 出来之后 GPT 用量为 0)。这期间关于模型对比体验的文章断断续续其实写了不少,但 Agent 相关技术和概念迭代太快,新…

2026/10/25 21:17:34 阅读更多 →
HarmonyOS应用开发实战:猫猫大作战-@Extend 的使用

HarmonyOS应用开发实战:猫猫大作战-@Extend 的使用

前言 Extend 装饰器用于为特定组件类型扩展样式方法,与 Styles 的通用属性集合不同,Extend 可以访问组件特有的属性。 本文以「猫猫大作战」的 Text 标题样式为锚点,讲解 Extend 的使用。 提示:本系列不讲 ArkTS 基础语法与环境…

2026/10/23 19:35:21 阅读更多 →
HarmonyOS应用开发实战:猫猫大作战-在 HarmonyOS 应用中,`app.json5` 位于 `AppScope/` 目录下,是整个应用的“全

HarmonyOS应用开发实战:猫猫大作战-在 HarmonyOS 应用中,`app.json5` 位于 `AppScope/` 目录下,是整个应用的“全

前言 在 HarmonyOS 应用中,app.json5 位于 AppScope/ 目录下,是整个应用的“全球身份证“——它定义了应用的唯一标识(bundleName)、版本号、全局图标和标签。系统包管理器和应用市场都依赖这个文件的配置来识别和分发应用。 本…

2026/10/21 3:26:23 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/27 17:33:32 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/27 4:06:49 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/27 7:21:53 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/27 18:20:50 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/27 6:20:47 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/27 4:06:50 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 13:55:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 7:21:50 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 19:19:37 阅读更多 →