跳到主要内容

user — 用户与认证

定位:注册(bcrypt 加密)/ 登录(JWT,2h)/ 个人资料(昵称/头像)/ 用户搜索 / 地址簿(默认地址唯一)。

实现:internal/user/(handler / service / repository / model)。

数据模型

users

字段类型说明
idBIGINT UNSIGNED PK用户 ID
usernameVARCHAR(32) UNIQUE用户名(3–32 字符)
nicknameVARCHAR(64) NULL昵称(可空,≤32 字符;空时前端回退展示 username)
avatar_urlVARCHAR(255) NULL头像托管引用(可空;POST /api/files 上传返回)
password_hashVARCHAR(255)bcrypt 哈希(默认 cost=10),日志不记录密码
roleVARCHAR(16)user / admin
default_address_idBIGINT UNSIGNED NULL默认地址唯一性指针(FK → user_addresses.id ON DELETE SET NULL)
created_at / updated_atDATETIME(3)

admin 种子账号 admin/admin123 由迁移种入(见演示账号)。

公开注册与登录在 bcrypt 前同时按可信来源 IP 和 MySQL utf8mb4_unicode_ci 排序权重账号键执行 Redis 固定窗口限流;Redis 故障时 fail closed。未知账号不会快速返回,而是使用固定 cost=10 的 dummy hash 完成一次 bcrypt 比较,使其与已知账号密码错误路径保持相近成本。

头像采用与图片消息/动态配图一致的托管引用模式:前端先 POST /api/files 上传到 MinIO 私有桶,取回 /files/<opaque-ref>,再经 PATCH /api/users/me 写入 avatar_url。user 服务通过最小媒体端口校验对象真实存在、类型为 image 且归当前用户;任意外部 URL 和他人引用均被拒。展示时前端经带 Bearer 的 GET /api/files/:reference 拉取 Blob,不依赖桶匿名访问。

user_addresses(地址簿)

字段类型说明
idBIGINT UNSIGNED PK地址 ID
user_idBIGINT UNSIGNED FK归属用户(CASCADE)
receiver / phone / province / city / district / detailVARCHAR收货信息;手机号校验 ^1[3-9]\d{9}$
is_default派生标记不落库:读取时由 users.default_address_id 指针推导

设计要点:默认地址唯一性由 users.default_address_id 单指针保证(一列只能指向一条地址),避免"多条 is_default=true"的双份状态;删除默认地址时 FK ON DELETE SET NULL 自动解除指向,随后由服务层自愈——仍有余下地址时把最新一条提为默认。

接口

HTTP(handler/user_handler.go + address_handler.go)

方法路径鉴权说明
POST/api/auth/register注册 {username, password};用户名重复 409
POST/api/auth/login登录,返回 {token, user};凭证错误 401
GET/api/users/meBearer当前用户(含 nickname/avatar_url)
PATCH/api/users/meBearer修改个人资料 {nickname?, avatar_url?}(PATCH 语义:未提交字段不动、空串清空;昵称 ≤32 字符;avatar_url 非空时必须是本人上传的托管图片;归属由 token 声明保证)
GET/api/usersBearer按用户名前缀字面搜索(% / _ 不作为通配符;username + limit,默认 10 上限 20;排除自己);响应仅含 {id, username}
GET/api/users/:idBearer指定用户(仅本人或 admin,防 IDOR)
GET/api/addressesBearer我的地址列表(默认地址排最前)
POST/api/addressesBearer新增地址(首条自动设为默认is_default=true 显式设默认)
PUT/api/addresses/:idBearer编辑地址(不触碰默认指向)
DELETE/api/addresses/:idBearer删除地址(删默认地址 → 最新余下地址提为默认)
PUT/api/addresses/:id/defaultBearer设为默认(单条 UPDATE 切换指针,旧默认自动失效)

跨模块端口(service 最小接口,进程内调用)

端口实现方消费说明
GetByIDsocial(目标存在校验)、chat(接收方校验)单用户查询
GetPublicByIDssocial(补列表用户名)单次批量读取 {id, username},避免逐条查询
GetAddressorder下单固化地址快照(owner 校验)
GetDefaultAddressflashsale 落单消费者秒杀订单地址快照(无默认地址 → 永久失败进死信)

关键流程

登录

POST /api/auth/login
→ 按来源 IP + MySQL 排序权重账号键检查固定窗口预算(超限 → 429)
→ 按 username 查用户(不存在时选择 dummy hash)
→ bcrypt.CompareHashAndPassword(不存在/不匹配统一 → 401)
→ JWT 签发(HS256,TTL 2h,sub=user_id,role 声明)
→ 返回 {token, user}

鉴权中间件:Authorization: Bearer <token>TokenVerifier.Verify(失败 401)→ Claims 写入上下文;admin 路由另加 RequireAdmin(非 admin 403)。

权威源:docs/DESIGN.md 认证与权限、迁移 000001_init / 000002_users / 000005_addresses / 000015_user_profile / 000020_user_upload_usage