- 新增 storage 配置(存储目录、单文件大小上限),ConfigVersion 3→4 自动补全 - internal/file:上传(sha256 秒传去重)、删除(上传者或管理员,引用中 409)、公开查看;本地存储 + 操作日志 + 引用计数,仅安全类型内联防存储型 XSS - internal/avatar:PUT/DELETE /api/me/avatar,自动管理头像文件引用与旧头像解绑 - 前端引入 vue-advanced-cropper,个人中心支持上传/更换/删除头像,裁剪输出 512×512 JPEG;http 请求支持 FormData - 导出 auth.CurrentUser、新增 model.User.IsAdmin 与 testutil 多部件上传辅助,补充接口测试并重新生成 Swagger 文档
5.6 KiB
5.6 KiB
后端开发规范
本文约定后端代码的组织方式与开发流程,重点说明通用工具模块 internal/utils 的使用。
1. 目录结构
internal/ 按功能拆分模块,职责单一:
internal/
├── api/ 仅路由装配(公开/登录/管理员三组)
├── auth/ 注册、登录、JWT、鉴权中间件、个人资料
├── user/ 用户管理
├── usergroup/ 用户组管理
├── note/ 便签
├── site/ 站点信息
├── file/ 文件上传、删除、查看与本地存储
├── avatar/ 当前用户头像
├── httpx/ HTTP 公共能力:ErrorResponse、分页、ID 解析
├── utils/ 通用工具(与业务无关的公共函数)
├── model/ 数据库模型
├── database/ 连接、迁移、种子数据
├── config/ 配置加载与自动补全
└── testutil/ 测试公共环境(仅测试引用)
依赖方向:业务模块 → httpx / utils / model,禁止反向依赖;api 只做路由装配,不写业务逻辑。
2. 通用工具模块 internal/utils
使用原则
- 只放与具体业务无关的公共函数;有业务语义的逻辑放在对应业务模块
- 一个主题一个文件(如
ip.go、random.go),函数命名语义化 - 新增函数必须附带同包单元测试(
*_test.go) - 尽量无全局可变状态;无法避免时(如可信代理配置)提供
SetXxx初始化函数、内部加锁保证并发安全,并在main.go启动时接线 utils只允许依赖标准库与 gin,不得依赖model等业务包
2.1 获取客户端 IP
统一使用 utils.ClientIP(c),不要直接使用 c.ClientIP() 或 c.Request.RemoteAddr——前者无法兼容多 CDN 且 gin 默认信任所有代理,后者拿到的是代理节点地址。
ip := utils.ClientIP(c) // 业务取 IP 的唯一入口,如操作日志
取值规则:
- 先取直连地址;只有直连地址属于可信代理时才会读取转发头,否则直接返回直连地址(防止伪造)
- 按优先级依次尝试:
CF-Connecting-IP→True-Client-IP→Ali-CDN-Real-IP→X-Real-IP→X-Client-IP→Fastly-Client-IP→Forwarded(RFC 7239) →X-Forwarded-For - 链式头(
Forwarded、X-Forwarded-For)从右往左取第一个非可信代理 IP,全部可信时取最左 - 每个候选值自动去引号、去端口并校验合法性,非法值跳过继续尝试下一个
- unix socket 连接视为可信(本地进程)
可信代理通过配置项 server.trusted_proxies 设置(IP 或 CIDR 列表):
server:
# 留空表示不信任任何代理头;部署 CDN / 自建反代时填其回源网段
trusted_proxies:
- "173.245.48.0/20" # 示例:Cloudflare 回源网段
- "127.0.0.1"
注意:main.go 启动时已同时调用 utils.SetTrustedProxies 与 r.SetTrustedProxies,业务代码不要重复调用或修改该配置。
需要直连地址(审计、排障)时使用 utils.RemoteIP(c),它不读取任何请求头。
2.2 随机字符串
安全敏感场景(初始密码、令牌等)统一使用 utils.RandomString(n),内部使用 crypto/rand 且字符集去掉了易混淆字符;禁止用 math/rand 生成此类字符串。
password, err := utils.RandomString(16)
2.3 新增工具函数的流程
- 判断是否属于与业务无关的公共逻辑
- 放入对应主题文件,或新建
xxx.go/xxx_test.go - 涉及全局初始化时提供
SetXxx,并在main.go中调用(配置类需同时加入config校验与ConfigVersion升级) - 运行
gofmt -l . && go build ./... && go vet ./... && go test ./...
3. 接口规范
- 响应文案一律英文,统一错误响应
httpx.ErrorResponse,如invalid request、record not found、internal server error、unauthorized or session expired - 分页参数用
httpx.ParsePagination(c);路径 ID 用httpx.ParseID(c)(用户组用usergroup内部的parseGroupID,允许 id 0) - Swagger 注解:
@Summary/@Description/@Tags(按权限取public/user/admin)/@Security BearerAuth/ 401、403、500 失败响应;注解修改后执行go generate ./...重新生成docs/ - 路由按权限挂载:公开(
/health、/swagger、/auth/*、GET /site、GET /files/:id)、需登录(个人资料、头像、文件上传删除、notes)、仅管理员
4. 数据库与迁移
- 模型放在
internal/model/,表名遵循 GORM 默认复数 - 迁移只能追加(
Version递增),禁止修改已发布的迁移;补数据、字段翻译等一次性操作同样通过新迁移完成 - SQLite 为单连接串行写且不建外键;多对多关联表手动维护(参考
user_group_members) - 仅日期字段使用
model.Date(JSON 为YYYY-MM-DD,未设置输出null) - 状态字段沿用
int8(status)或字符串枚举(gender、file operation),取值定义在model包
5. 测试与验证
- 接口层测试使用
internal/testutil(临时库、鉴权路由、请求辅助),业务/工具测试放在对应包 - 表驱动优先;涉及全局状态的测试用
t.Cleanup复位 - 提交前必须通过:
gofmt -l .
go build ./...
go vet ./...
go test ./...
- 修改 Swagger 注解后额外执行:
go generate ./...
6. 提交规范
- 提交信息使用简短中文标题,正文用要点列出改动
- 一个提交只做一件事;重构与功能尽量分开