From 2858d3c0f79d9860d5b7aa4064d8b525b2123c1a Mon Sep 17 00:00:00 2001 From: kevin Date: Sun, 20 Sep 2026 02:13:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=93=8D=E5=BA=94=E4=B8=8E?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=96=87=E6=A1=88=E5=85=A8=E9=83=A8=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E8=8B=B1=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 错误响应、健康检查等运行时文案改为英文 - 种子数据英文化,新增迁移 v5 更新存量内置数据(不覆盖手工修改) - Swagger 注释与 docs/ 文档全英文化 - 补充英文文案断言与 v5 迁移测试 --- docs/docs.go | 224 +++++++++++------------ docs/swagger.json | 224 +++++++++++------------ docs/swagger.yaml | 244 ++++++++++++++------------ internal/api/api.go | 6 +- internal/auth/auth.go | 42 ++--- internal/auth/auth_test.go | 3 + internal/database/database_test.go | 74 ++++++++ internal/database/migrate.go | 21 ++- internal/database/seed.go | 2 +- internal/httpx/httpx.go | 12 +- internal/note/note.go | 70 ++++---- internal/note/note_test.go | 4 + internal/user/user.go | 70 ++++---- internal/usergroup/user_group.go | 84 ++++----- internal/usergroup/user_group_test.go | 4 + main.go | 8 +- 16 files changed, 603 insertions(+), 489 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index e833928..d4cda1a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -17,7 +17,7 @@ const docTemplate = `{ "paths": { "/auth/login": { "post": { - "description": "使用用户名或邮箱登录,成功返回 JWT(有效期见 auth.token_ttl)与用户信息。", + "description": "Login with username or email; returns a JWT (TTL from auth.token_ttl) and the user.", "consumes": [ "application/json" ], @@ -30,7 +30,7 @@ const docTemplate = `{ "summary": "Login", "parameters": [ { - "description": "登录凭证", + "description": "Login credentials", "name": "credentials", "in": "body", "required": true, @@ -47,19 +47,19 @@ const docTemplate = `{ } }, "400": { - "description": "参数无效", + "description": "invalid request", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "账号或密码错误", + "description": "incorrect account or password", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -75,7 +75,7 @@ const docTemplate = `{ }, "/auth/register": { "post": { - "description": "公开注册,创建普通用户并默认加入普通用户组(id 1)。username 长度 3-50 且唯一,email 唯一,password 长度 6-72。", + "description": "Public registration. Creates a regular user in the default user group (id 1). username is 3-50 chars and unique; email is unique; password is 6-72 chars.", "consumes": [ "application/json" ], @@ -88,7 +88,7 @@ const docTemplate = `{ "summary": "Register", "parameters": [ { - "description": "注册信息", + "description": "Registration payload", "name": "user", "in": "body", "required": true, @@ -105,13 +105,13 @@ const docTemplate = `{ } }, "400": { - "description": "参数无效", + "description": "invalid request", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "用户名或邮箱已存在", + "description": "username or email already exists", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -127,7 +127,7 @@ const docTemplate = `{ }, "/health": { "get": { - "description": "检查服务与数据库连接状态;数据库不可用时返回 503。", + "description": "Check service and database connectivity; returns 503 when the database is unavailable.", "produces": [ "application/json" ], @@ -158,7 +158,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "分页查询便签列表,按 id 倒序返回。page 从 1 开始;page_size 取值 1-100,默认 20。", + "description": "List notes ordered by id DESC. page starts at 1; page_size is 1-100, default 20.", "produces": [ "application/json" ], @@ -170,14 +170,14 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "页码,默认 1", + "description": "Page number, default 1", "name": "page", "in": "query" }, { "type": "integer", "example": 20, - "description": "每页数量,默认 20,最大 100", + "description": "Page size, default 20, max 100", "name": "page_size", "in": "query" } @@ -190,13 +190,13 @@ const docTemplate = `{ } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -215,7 +215,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "创建便签。title 必填且最长 200 字符,content 可选。", + "description": "Create a note. title is required (max 200 chars); content is optional.", "consumes": [ "application/json" ], @@ -228,7 +228,7 @@ const docTemplate = `{ "summary": "Create a note", "parameters": [ { - "description": "便签内容", + "description": "Note payload", "name": "note", "in": "body", "required": true, @@ -245,19 +245,19 @@ const docTemplate = `{ } }, "400": { - "description": "参数无效", + "description": "invalid request", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -278,7 +278,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "按 id 查询单个便签。", + "description": "Get a note by id.", "produces": [ "application/json" ], @@ -290,7 +290,7 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "便签 ID", + "description": "Note ID", "name": "id", "in": "path", "required": true @@ -304,25 +304,25 @@ const docTemplate = `{ } }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -341,7 +341,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "全量更新便签的 title 与 content,字段校验规则同创建。", + "description": "Update title and content; validation is the same as create.", "consumes": [ "application/json" ], @@ -356,13 +356,13 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "便签 ID", + "description": "Note ID", "name": "id", "in": "path", "required": true }, { - "description": "便签内容", + "description": "Note payload", "name": "note", "in": "body", "required": true, @@ -379,25 +379,25 @@ const docTemplate = `{ } }, "400": { - "description": "参数无效或 id 无效", + "description": "invalid request or id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -416,7 +416,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "按 id 删除便签,成功时返回 204 且无响应体。", + "description": "Delete a note by id; returns 204 with no body on success.", "produces": [ "application/json" ], @@ -428,7 +428,7 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "便签 ID", + "description": "Note ID", "name": "id", "in": "path", "required": true @@ -436,28 +436,28 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "删除成功" + "description": "Deleted" }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -478,7 +478,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "分页查询用户组列表,按 id 升序返回。page 从 1 开始;page_size 取值 1-100,默认 20。", + "description": "List user groups ordered by id ASC. page starts at 1; page_size is 1-100, default 20.", "produces": [ "application/json" ], @@ -490,14 +490,14 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "页码,默认 1", + "description": "Page number, default 1", "name": "page", "in": "query" }, { "type": "integer", "example": 20, - "description": "每页数量,默认 20,最大 100", + "description": "Page size, default 20, max 100", "name": "page_size", "in": "query" } @@ -510,13 +510,13 @@ const docTemplate = `{ } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -535,7 +535,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "创建用户组。name 必填且唯一(最长 50 字符),description 可选(最长 255 字符);id 由服务端分配。", + "description": "Create a user group. name is required and unique (max 50 chars); description is optional (max 255 chars); id is assigned by the server.", "consumes": [ "application/json" ], @@ -548,7 +548,7 @@ const docTemplate = `{ "summary": "Create a user group", "parameters": [ { - "description": "用户组信息", + "description": "User group payload", "name": "group", "in": "body", "required": true, @@ -565,25 +565,25 @@ const docTemplate = `{ } }, "400": { - "description": "参数无效", + "description": "invalid request", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "用户组名称已存在", + "description": "user group name already exists", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -604,7 +604,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "按 id 查询单个用户组,id 0 为内置 admin 组。", + "description": "Get a user group by id; id 0 is the built-in admin group.", "produces": [ "application/json" ], @@ -616,7 +616,7 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "用户组 ID", + "description": "User group ID", "name": "id", "in": "path", "required": true @@ -630,25 +630,25 @@ const docTemplate = `{ } }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -667,7 +667,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "更新用户组的 name 与 description,name 必填且唯一。", + "description": "Update a user group's name and description; name is required and unique.", "consumes": [ "application/json" ], @@ -682,13 +682,13 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "用户组 ID", + "description": "User group ID", "name": "id", "in": "path", "required": true }, { - "description": "用户组信息", + "description": "User group payload", "name": "group", "in": "body", "required": true, @@ -705,31 +705,31 @@ const docTemplate = `{ } }, "400": { - "description": "参数无效或 id 无效", + "description": "invalid request or id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "用户组名称已存在", + "description": "user group name already exists", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -748,7 +748,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "按 id 删除用户组,成功时返回 204 且无响应体。系统内置组或组内仍有用户时返回 409。", + "description": "Delete a user group by id; returns 204 with no body on success. Returns 409 for system groups or when the group still has members.", "produces": [ "application/json" ], @@ -760,7 +760,7 @@ const docTemplate = `{ { "type": "integer", "example": 2, - "description": "用户组 ID", + "description": "User group ID", "name": "id", "in": "path", "required": true @@ -768,34 +768,34 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "删除成功" + "description": "Deleted" }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "系统内置组不可删除或用户组内仍有用户", + "description": "system group cannot be deleted or group still has members", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -816,7 +816,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "分页查询用户列表(含所属用户组),按 id 倒序返回。page 从 1 开始;page_size 取值 1-100,默认 20。", + "description": "List users with their groups, ordered by id DESC. page starts at 1; page_size is 1-100, default 20.", "produces": [ "application/json" ], @@ -828,14 +828,14 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "页码,默认 1", + "description": "Page number, default 1", "name": "page", "in": "query" }, { "type": "integer", "example": 20, - "description": "每页数量,默认 20,最大 100", + "description": "Page size, default 20, max 100", "name": "page_size", "in": "query" } @@ -848,13 +848,13 @@ const docTemplate = `{ } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -873,7 +873,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "创建用户并关联用户组。username、email 唯一,password 长度 6-72;不传 group_ids 时默认加入普通用户组(id 1)。", + "description": "Create a user and assign groups. username and email are unique; password is 6-72 chars; defaults to the regular user group (id 1) when group_ids is omitted.", "consumes": [ "application/json" ], @@ -886,7 +886,7 @@ const docTemplate = `{ "summary": "Create a user", "parameters": [ { - "description": "用户信息", + "description": "User payload", "name": "user", "in": "body", "required": true, @@ -903,25 +903,25 @@ const docTemplate = `{ } }, "400": { - "description": "参数无效或用户组不存在", + "description": "invalid request or user group not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "用户名或邮箱已存在", + "description": "username or email already exists", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -942,7 +942,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "按 id 查询用户(含所属用户组)。", + "description": "Get a user by id, including groups.", "produces": [ "application/json" ], @@ -954,7 +954,7 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "用户 ID", + "description": "User ID", "name": "id", "in": "path", "required": true @@ -968,25 +968,25 @@ const docTemplate = `{ } }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -1005,7 +1005,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "更新用户信息,仅更新请求中提供的字段。传 group_ids 会整体替换用户组;password 非空时重置密码。", + "description": "Update user fields. group_ids replaces all groups; a non-empty password resets the password.", "consumes": [ "application/json" ], @@ -1020,13 +1020,13 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "用户 ID", + "description": "User ID", "name": "id", "in": "path", "required": true }, { - "description": "待更新字段", + "description": "Fields to update", "name": "user", "in": "body", "required": true, @@ -1043,25 +1043,25 @@ const docTemplate = `{ } }, "400": { - "description": "参数无效、id 无效或用户组不存在", + "description": "invalid request, id, or user group not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -1080,7 +1080,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "按 id 删除用户及其用户组成员关系,成功时返回 204 且无响应体。", + "description": "Delete a user and their group memberships; returns 204 with no body on success.", "produces": [ "application/json" ], @@ -1092,7 +1092,7 @@ const docTemplate = `{ { "type": "integer", "example": 1, - "description": "用户 ID", + "description": "User ID", "name": "id", "in": "path", "required": true @@ -1100,28 +1100,28 @@ const docTemplate = `{ ], "responses": { "204": { - "description": "删除成功" + "description": "Deleted" }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -1142,7 +1142,7 @@ const docTemplate = `{ "properties": { "error": { "type": "string", - "example": "数据库不可用" + "example": "database unavailable" }, "status": { "type": "string", @@ -1215,7 +1215,7 @@ const docTemplate = `{ "properties": { "error": { "type": "string", - "example": "记录不存在" + "example": "record not found" } } }, @@ -1328,12 +1328,12 @@ const docTemplate = `{ "properties": { "content": { "type": "string", - "example": "牛奶、鸡蛋" + "example": "Milk, eggs" }, "title": { "type": "string", "maxLength": 200, - "example": "购物清单" + "example": "Shopping list" } } }, @@ -1483,19 +1483,19 @@ const docTemplate = `{ "description": { "type": "string", "maxLength": 255, - "example": "负责日常运营" + "example": "Handles daily operations" }, "name": { "type": "string", "maxLength": 50, - "example": "运营组" + "example": "Operations" } } } }, "securityDefinitions": { "BearerAuth": { - "description": "Bearer JWT,格式:Bearer {token},通过 /auth/login 获取", + "description": "Bearer JWT, format: Bearer {token}, obtained from /auth/login", "type": "apiKey", "name": "Authorization", "in": "header" @@ -1510,7 +1510,7 @@ var SwaggerInfo = &swag.Spec{ BasePath: "/api", Schemes: []string{}, Title: "Rill API", - Description: "Rill 服务端 HTTP API 文档,所有接口以配置项 api.prefix(默认 /api)为前缀,请求与响应均为 JSON。\nSwagger 页面:{prefix}/swagger/index.html;OpenAPI JSON:{prefix}/swagger/doc.json。\n除 health、swagger、auth 外,其余接口需要 Bearer JWT:先调用 /auth/login 获取 token,再在请求头携带 Authorization: Bearer {token}。", + Description: "Rill server HTTP API documentation. All endpoints are prefixed with api.prefix (default /api); requests and responses are JSON.\nSwagger UI: {prefix}/swagger/index.html; OpenAPI JSON: {prefix}/swagger/doc.json.\nExcept for health, swagger, and auth, all endpoints require a Bearer JWT: call /auth/login to get a token, then send Authorization: Bearer {token}.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index 43907cb..828b239 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "description": "Rill 服务端 HTTP API 文档,所有接口以配置项 api.prefix(默认 /api)为前缀,请求与响应均为 JSON。\nSwagger 页面:{prefix}/swagger/index.html;OpenAPI JSON:{prefix}/swagger/doc.json。\n除 health、swagger、auth 外,其余接口需要 Bearer JWT:先调用 /auth/login 获取 token,再在请求头携带 Authorization: Bearer {token}。", + "description": "Rill server HTTP API documentation. All endpoints are prefixed with api.prefix (default /api); requests and responses are JSON.\nSwagger UI: {prefix}/swagger/index.html; OpenAPI JSON: {prefix}/swagger/doc.json.\nExcept for health, swagger, and auth, all endpoints require a Bearer JWT: call /auth/login to get a token, then send Authorization: Bearer {token}.", "title": "Rill API", "contact": {}, "version": "1.0" @@ -10,7 +10,7 @@ "paths": { "/auth/login": { "post": { - "description": "使用用户名或邮箱登录,成功返回 JWT(有效期见 auth.token_ttl)与用户信息。", + "description": "Login with username or email; returns a JWT (TTL from auth.token_ttl) and the user.", "consumes": [ "application/json" ], @@ -23,7 +23,7 @@ "summary": "Login", "parameters": [ { - "description": "登录凭证", + "description": "Login credentials", "name": "credentials", "in": "body", "required": true, @@ -40,19 +40,19 @@ } }, "400": { - "description": "参数无效", + "description": "invalid request", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "账号或密码错误", + "description": "incorrect account or password", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -68,7 +68,7 @@ }, "/auth/register": { "post": { - "description": "公开注册,创建普通用户并默认加入普通用户组(id 1)。username 长度 3-50 且唯一,email 唯一,password 长度 6-72。", + "description": "Public registration. Creates a regular user in the default user group (id 1). username is 3-50 chars and unique; email is unique; password is 6-72 chars.", "consumes": [ "application/json" ], @@ -81,7 +81,7 @@ "summary": "Register", "parameters": [ { - "description": "注册信息", + "description": "Registration payload", "name": "user", "in": "body", "required": true, @@ -98,13 +98,13 @@ } }, "400": { - "description": "参数无效", + "description": "invalid request", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "用户名或邮箱已存在", + "description": "username or email already exists", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -120,7 +120,7 @@ }, "/health": { "get": { - "description": "检查服务与数据库连接状态;数据库不可用时返回 503。", + "description": "Check service and database connectivity; returns 503 when the database is unavailable.", "produces": [ "application/json" ], @@ -151,7 +151,7 @@ "BearerAuth": [] } ], - "description": "分页查询便签列表,按 id 倒序返回。page 从 1 开始;page_size 取值 1-100,默认 20。", + "description": "List notes ordered by id DESC. page starts at 1; page_size is 1-100, default 20.", "produces": [ "application/json" ], @@ -163,14 +163,14 @@ { "type": "integer", "example": 1, - "description": "页码,默认 1", + "description": "Page number, default 1", "name": "page", "in": "query" }, { "type": "integer", "example": 20, - "description": "每页数量,默认 20,最大 100", + "description": "Page size, default 20, max 100", "name": "page_size", "in": "query" } @@ -183,13 +183,13 @@ } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -208,7 +208,7 @@ "BearerAuth": [] } ], - "description": "创建便签。title 必填且最长 200 字符,content 可选。", + "description": "Create a note. title is required (max 200 chars); content is optional.", "consumes": [ "application/json" ], @@ -221,7 +221,7 @@ "summary": "Create a note", "parameters": [ { - "description": "便签内容", + "description": "Note payload", "name": "note", "in": "body", "required": true, @@ -238,19 +238,19 @@ } }, "400": { - "description": "参数无效", + "description": "invalid request", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -271,7 +271,7 @@ "BearerAuth": [] } ], - "description": "按 id 查询单个便签。", + "description": "Get a note by id.", "produces": [ "application/json" ], @@ -283,7 +283,7 @@ { "type": "integer", "example": 1, - "description": "便签 ID", + "description": "Note ID", "name": "id", "in": "path", "required": true @@ -297,25 +297,25 @@ } }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -334,7 +334,7 @@ "BearerAuth": [] } ], - "description": "全量更新便签的 title 与 content,字段校验规则同创建。", + "description": "Update title and content; validation is the same as create.", "consumes": [ "application/json" ], @@ -349,13 +349,13 @@ { "type": "integer", "example": 1, - "description": "便签 ID", + "description": "Note ID", "name": "id", "in": "path", "required": true }, { - "description": "便签内容", + "description": "Note payload", "name": "note", "in": "body", "required": true, @@ -372,25 +372,25 @@ } }, "400": { - "description": "参数无效或 id 无效", + "description": "invalid request or id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -409,7 +409,7 @@ "BearerAuth": [] } ], - "description": "按 id 删除便签,成功时返回 204 且无响应体。", + "description": "Delete a note by id; returns 204 with no body on success.", "produces": [ "application/json" ], @@ -421,7 +421,7 @@ { "type": "integer", "example": 1, - "description": "便签 ID", + "description": "Note ID", "name": "id", "in": "path", "required": true @@ -429,28 +429,28 @@ ], "responses": { "204": { - "description": "删除成功" + "description": "Deleted" }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "账号已被禁用", + "description": "account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -471,7 +471,7 @@ "BearerAuth": [] } ], - "description": "分页查询用户组列表,按 id 升序返回。page 从 1 开始;page_size 取值 1-100,默认 20。", + "description": "List user groups ordered by id ASC. page starts at 1; page_size is 1-100, default 20.", "produces": [ "application/json" ], @@ -483,14 +483,14 @@ { "type": "integer", "example": 1, - "description": "页码,默认 1", + "description": "Page number, default 1", "name": "page", "in": "query" }, { "type": "integer", "example": 20, - "description": "每页数量,默认 20,最大 100", + "description": "Page size, default 20, max 100", "name": "page_size", "in": "query" } @@ -503,13 +503,13 @@ } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -528,7 +528,7 @@ "BearerAuth": [] } ], - "description": "创建用户组。name 必填且唯一(最长 50 字符),description 可选(最长 255 字符);id 由服务端分配。", + "description": "Create a user group. name is required and unique (max 50 chars); description is optional (max 255 chars); id is assigned by the server.", "consumes": [ "application/json" ], @@ -541,7 +541,7 @@ "summary": "Create a user group", "parameters": [ { - "description": "用户组信息", + "description": "User group payload", "name": "group", "in": "body", "required": true, @@ -558,25 +558,25 @@ } }, "400": { - "description": "参数无效", + "description": "invalid request", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "用户组名称已存在", + "description": "user group name already exists", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -597,7 +597,7 @@ "BearerAuth": [] } ], - "description": "按 id 查询单个用户组,id 0 为内置 admin 组。", + "description": "Get a user group by id; id 0 is the built-in admin group.", "produces": [ "application/json" ], @@ -609,7 +609,7 @@ { "type": "integer", "example": 1, - "description": "用户组 ID", + "description": "User group ID", "name": "id", "in": "path", "required": true @@ -623,25 +623,25 @@ } }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -660,7 +660,7 @@ "BearerAuth": [] } ], - "description": "更新用户组的 name 与 description,name 必填且唯一。", + "description": "Update a user group's name and description; name is required and unique.", "consumes": [ "application/json" ], @@ -675,13 +675,13 @@ { "type": "integer", "example": 1, - "description": "用户组 ID", + "description": "User group ID", "name": "id", "in": "path", "required": true }, { - "description": "用户组信息", + "description": "User group payload", "name": "group", "in": "body", "required": true, @@ -698,31 +698,31 @@ } }, "400": { - "description": "参数无效或 id 无效", + "description": "invalid request or id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "用户组名称已存在", + "description": "user group name already exists", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -741,7 +741,7 @@ "BearerAuth": [] } ], - "description": "按 id 删除用户组,成功时返回 204 且无响应体。系统内置组或组内仍有用户时返回 409。", + "description": "Delete a user group by id; returns 204 with no body on success. Returns 409 for system groups or when the group still has members.", "produces": [ "application/json" ], @@ -753,7 +753,7 @@ { "type": "integer", "example": 2, - "description": "用户组 ID", + "description": "User group ID", "name": "id", "in": "path", "required": true @@ -761,34 +761,34 @@ ], "responses": { "204": { - "description": "删除成功" + "description": "Deleted" }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "系统内置组不可删除或用户组内仍有用户", + "description": "system group cannot be deleted or group still has members", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -809,7 +809,7 @@ "BearerAuth": [] } ], - "description": "分页查询用户列表(含所属用户组),按 id 倒序返回。page 从 1 开始;page_size 取值 1-100,默认 20。", + "description": "List users with their groups, ordered by id DESC. page starts at 1; page_size is 1-100, default 20.", "produces": [ "application/json" ], @@ -821,14 +821,14 @@ { "type": "integer", "example": 1, - "description": "页码,默认 1", + "description": "Page number, default 1", "name": "page", "in": "query" }, { "type": "integer", "example": 20, - "description": "每页数量,默认 20,最大 100", + "description": "Page size, default 20, max 100", "name": "page_size", "in": "query" } @@ -841,13 +841,13 @@ } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -866,7 +866,7 @@ "BearerAuth": [] } ], - "description": "创建用户并关联用户组。username、email 唯一,password 长度 6-72;不传 group_ids 时默认加入普通用户组(id 1)。", + "description": "Create a user and assign groups. username and email are unique; password is 6-72 chars; defaults to the regular user group (id 1) when group_ids is omitted.", "consumes": [ "application/json" ], @@ -879,7 +879,7 @@ "summary": "Create a user", "parameters": [ { - "description": "用户信息", + "description": "User payload", "name": "user", "in": "body", "required": true, @@ -896,25 +896,25 @@ } }, "400": { - "description": "参数无效或用户组不存在", + "description": "invalid request or user group not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "409": { - "description": "用户名或邮箱已存在", + "description": "username or email already exists", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -935,7 +935,7 @@ "BearerAuth": [] } ], - "description": "按 id 查询用户(含所属用户组)。", + "description": "Get a user by id, including groups.", "produces": [ "application/json" ], @@ -947,7 +947,7 @@ { "type": "integer", "example": 1, - "description": "用户 ID", + "description": "User ID", "name": "id", "in": "path", "required": true @@ -961,25 +961,25 @@ } }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -998,7 +998,7 @@ "BearerAuth": [] } ], - "description": "更新用户信息,仅更新请求中提供的字段。传 group_ids 会整体替换用户组;password 非空时重置密码。", + "description": "Update user fields. group_ids replaces all groups; a non-empty password resets the password.", "consumes": [ "application/json" ], @@ -1013,13 +1013,13 @@ { "type": "integer", "example": 1, - "description": "用户 ID", + "description": "User ID", "name": "id", "in": "path", "required": true }, { - "description": "待更新字段", + "description": "Fields to update", "name": "user", "in": "body", "required": true, @@ -1036,25 +1036,25 @@ } }, "400": { - "description": "参数无效、id 无效或用户组不存在", + "description": "invalid request, id, or user group not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -1073,7 +1073,7 @@ "BearerAuth": [] } ], - "description": "按 id 删除用户及其用户组成员关系,成功时返回 204 且无响应体。", + "description": "Delete a user and their group memberships; returns 204 with no body on success.", "produces": [ "application/json" ], @@ -1085,7 +1085,7 @@ { "type": "integer", "example": 1, - "description": "用户 ID", + "description": "User ID", "name": "id", "in": "path", "required": true @@ -1093,28 +1093,28 @@ ], "responses": { "204": { - "description": "删除成功" + "description": "Deleted" }, "400": { - "description": "id 无效", + "description": "invalid id", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "401": { - "description": "未登录或登录已过期", + "description": "unauthorized or session expired", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "403": { - "description": "需要管理员权限或账号已被禁用", + "description": "admin permission required or account disabled", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } }, "404": { - "description": "记录不存在", + "description": "record not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -1135,7 +1135,7 @@ "properties": { "error": { "type": "string", - "example": "数据库不可用" + "example": "database unavailable" }, "status": { "type": "string", @@ -1208,7 +1208,7 @@ "properties": { "error": { "type": "string", - "example": "记录不存在" + "example": "record not found" } } }, @@ -1321,12 +1321,12 @@ "properties": { "content": { "type": "string", - "example": "牛奶、鸡蛋" + "example": "Milk, eggs" }, "title": { "type": "string", "maxLength": 200, - "example": "购物清单" + "example": "Shopping list" } } }, @@ -1476,19 +1476,19 @@ "description": { "type": "string", "maxLength": 255, - "example": "负责日常运营" + "example": "Handles daily operations" }, "name": { "type": "string", "maxLength": 50, - "example": "运营组" + "example": "Operations" } } } }, "securityDefinitions": { "BearerAuth": { - "description": "Bearer JWT,格式:Bearer {token},通过 /auth/login 获取", + "description": "Bearer JWT, format: Bearer {token}, obtained from /auth/login", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1a9d535..0a5edef 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3,7 +3,7 @@ definitions: api.HealthResponse: properties: error: - example: 数据库不可用 + example: database unavailable type: string status: example: ok @@ -56,7 +56,7 @@ definitions: httpx.ErrorResponse: properties: error: - example: 记录不存在 + example: record not found type: string type: object model.Note: @@ -129,10 +129,10 @@ definitions: note.Request: properties: content: - example: 牛奶、鸡蛋 + example: Milk, eggs type: string title: - example: 购物清单 + example: Shopping list maxLength: 200 type: string required: @@ -241,11 +241,11 @@ definitions: usergroup.Request: properties: description: - example: 负责日常运营 + example: Handles daily operations maxLength: 255 type: string name: - example: 运营组 + example: Operations maxLength: 50 type: string required: @@ -254,9 +254,9 @@ definitions: info: contact: {} description: |- - Rill 服务端 HTTP API 文档,所有接口以配置项 api.prefix(默认 /api)为前缀,请求与响应均为 JSON。 - Swagger 页面:{prefix}/swagger/index.html;OpenAPI JSON:{prefix}/swagger/doc.json。 - 除 health、swagger、auth 外,其余接口需要 Bearer JWT:先调用 /auth/login 获取 token,再在请求头携带 Authorization: Bearer {token}。 + Rill server HTTP API documentation. All endpoints are prefixed with api.prefix (default /api); requests and responses are JSON. + Swagger UI: {prefix}/swagger/index.html; OpenAPI JSON: {prefix}/swagger/doc.json. + Except for health, swagger, and auth, all endpoints require a Bearer JWT: call /auth/login to get a token, then send Authorization: Bearer {token}. title: Rill API version: "1.0" paths: @@ -264,9 +264,10 @@ paths: post: consumes: - application/json - description: 使用用户名或邮箱登录,成功返回 JWT(有效期见 auth.token_ttl)与用户信息。 + description: Login with username or email; returns a JWT (TTL from auth.token_ttl) + and the user. parameters: - - description: 登录凭证 + - description: Login credentials in: body name: credentials required: true @@ -280,15 +281,15 @@ paths: schema: $ref: '#/definitions/auth.LoginResponse' "400": - description: 参数无效 + description: invalid request schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 账号或密码错误 + description: incorrect account or password schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 账号已被禁用 + description: account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -302,10 +303,11 @@ paths: post: consumes: - application/json - description: 公开注册,创建普通用户并默认加入普通用户组(id 1)。username 长度 3-50 且唯一,email 唯一,password - 长度 6-72。 + description: Public registration. Creates a regular user in the default user + group (id 1). username is 3-50 chars and unique; email is unique; password + is 6-72 chars. parameters: - - description: 注册信息 + - description: Registration payload in: body name: user required: true @@ -319,11 +321,11 @@ paths: schema: $ref: '#/definitions/model.User' "400": - description: 参数无效 + description: invalid request schema: $ref: '#/definitions/httpx.ErrorResponse' "409": - description: 用户名或邮箱已存在 + description: username or email already exists schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -335,7 +337,8 @@ paths: - auth /health: get: - description: 检查服务与数据库连接状态;数据库不可用时返回 503。 + description: Check service and database connectivity; returns 503 when the database + is unavailable. produces: - application/json responses: @@ -352,14 +355,15 @@ paths: - system /notes: get: - description: 分页查询便签列表,按 id 倒序返回。page 从 1 开始;page_size 取值 1-100,默认 20。 + description: List notes ordered by id DESC. page starts at 1; page_size is 1-100, + default 20. parameters: - - description: 页码,默认 1 + - description: Page number, default 1 example: 1 in: query name: page type: integer - - description: 每页数量,默认 20,最大 100 + - description: Page size, default 20, max 100 example: 20 in: query name: page_size @@ -372,11 +376,11 @@ paths: schema: $ref: '#/definitions/note.ListResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 账号已被禁用 + description: account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -391,9 +395,9 @@ paths: post: consumes: - application/json - description: 创建便签。title 必填且最长 200 字符,content 可选。 + description: Create a note. title is required (max 200 chars); content is optional. parameters: - - description: 便签内容 + - description: Note payload in: body name: note required: true @@ -407,15 +411,15 @@ paths: schema: $ref: '#/definitions/model.Note' "400": - description: 参数无效 + description: invalid request schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 账号已被禁用 + description: account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -429,9 +433,9 @@ paths: - notes /notes/{id}: delete: - description: 按 id 删除便签,成功时返回 204 且无响应体。 + description: Delete a note by id; returns 204 with no body on success. parameters: - - description: 便签 ID + - description: Note ID example: 1 in: path name: id @@ -441,21 +445,21 @@ paths: - application/json responses: "204": - description: 删除成功 + description: Deleted "400": - description: id 无效 + description: invalid id schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 账号已被禁用 + description: account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "404": - description: 记录不存在 + description: record not found schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -468,9 +472,9 @@ paths: tags: - notes get: - description: 按 id 查询单个便签。 + description: Get a note by id. parameters: - - description: 便签 ID + - description: Note ID example: 1 in: path name: id @@ -484,19 +488,19 @@ paths: schema: $ref: '#/definitions/model.Note' "400": - description: id 无效 + description: invalid id schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 账号已被禁用 + description: account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "404": - description: 记录不存在 + description: record not found schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -511,15 +515,15 @@ paths: put: consumes: - application/json - description: 全量更新便签的 title 与 content,字段校验规则同创建。 + description: Update title and content; validation is the same as create. parameters: - - description: 便签 ID + - description: Note ID example: 1 in: path name: id required: true type: integer - - description: 便签内容 + - description: Note payload in: body name: note required: true @@ -533,19 +537,19 @@ paths: schema: $ref: '#/definitions/model.Note' "400": - description: 参数无效或 id 无效 + description: invalid request or id schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 账号已被禁用 + description: account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "404": - description: 记录不存在 + description: record not found schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -559,14 +563,15 @@ paths: - notes /user-groups: get: - description: 分页查询用户组列表,按 id 升序返回。page 从 1 开始;page_size 取值 1-100,默认 20。 + description: List user groups ordered by id ASC. page starts at 1; page_size + is 1-100, default 20. parameters: - - description: 页码,默认 1 + - description: Page number, default 1 example: 1 in: query name: page type: integer - - description: 每页数量,默认 20,最大 100 + - description: Page size, default 20, max 100 example: 20 in: query name: page_size @@ -579,11 +584,11 @@ paths: schema: $ref: '#/definitions/usergroup.ListResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -598,9 +603,10 @@ paths: post: consumes: - application/json - description: 创建用户组。name 必填且唯一(最长 50 字符),description 可选(最长 255 字符);id 由服务端分配。 + description: Create a user group. name is required and unique (max 50 chars); + description is optional (max 255 chars); id is assigned by the server. parameters: - - description: 用户组信息 + - description: User group payload in: body name: group required: true @@ -614,19 +620,19 @@ paths: schema: $ref: '#/definitions/model.UserGroup' "400": - description: 参数无效 + description: invalid request schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "409": - description: 用户组名称已存在 + description: user group name already exists schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -640,9 +646,10 @@ paths: - user-groups /user-groups/{id}: delete: - description: 按 id 删除用户组,成功时返回 204 且无响应体。系统内置组或组内仍有用户时返回 409。 + description: Delete a user group by id; returns 204 with no body on success. + Returns 409 for system groups or when the group still has members. parameters: - - description: 用户组 ID + - description: User group ID example: 2 in: path name: id @@ -652,25 +659,25 @@ paths: - application/json responses: "204": - description: 删除成功 + description: Deleted "400": - description: id 无效 + description: invalid id schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "404": - description: 记录不存在 + description: record not found schema: $ref: '#/definitions/httpx.ErrorResponse' "409": - description: 系统内置组不可删除或用户组内仍有用户 + description: system group cannot be deleted or group still has members schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -683,9 +690,9 @@ paths: tags: - user-groups get: - description: 按 id 查询单个用户组,id 0 为内置 admin 组。 + description: Get a user group by id; id 0 is the built-in admin group. parameters: - - description: 用户组 ID + - description: User group ID example: 1 in: path name: id @@ -699,19 +706,19 @@ paths: schema: $ref: '#/definitions/model.UserGroup' "400": - description: id 无效 + description: invalid id schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "404": - description: 记录不存在 + description: record not found schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -726,15 +733,16 @@ paths: put: consumes: - application/json - description: 更新用户组的 name 与 description,name 必填且唯一。 + description: Update a user group's name and description; name is required and + unique. parameters: - - description: 用户组 ID + - description: User group ID example: 1 in: path name: id required: true type: integer - - description: 用户组信息 + - description: User group payload in: body name: group required: true @@ -748,23 +756,23 @@ paths: schema: $ref: '#/definitions/model.UserGroup' "400": - description: 参数无效或 id 无效 + description: invalid request or id schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "404": - description: 记录不存在 + description: record not found schema: $ref: '#/definitions/httpx.ErrorResponse' "409": - description: 用户组名称已存在 + description: user group name already exists schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -778,14 +786,15 @@ paths: - user-groups /users: get: - description: 分页查询用户列表(含所属用户组),按 id 倒序返回。page 从 1 开始;page_size 取值 1-100,默认 20。 + description: List users with their groups, ordered by id DESC. page starts at + 1; page_size is 1-100, default 20. parameters: - - description: 页码,默认 1 + - description: Page number, default 1 example: 1 in: query name: page type: integer - - description: 每页数量,默认 20,最大 100 + - description: Page size, default 20, max 100 example: 20 in: query name: page_size @@ -798,11 +807,11 @@ paths: schema: $ref: '#/definitions/user.ListResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -817,10 +826,11 @@ paths: post: consumes: - application/json - description: 创建用户并关联用户组。username、email 唯一,password 长度 6-72;不传 group_ids 时默认加入普通用户组(id - 1)。 + description: Create a user and assign groups. username and email are unique; + password is 6-72 chars; defaults to the regular user group (id 1) when group_ids + is omitted. parameters: - - description: 用户信息 + - description: User payload in: body name: user required: true @@ -834,19 +844,19 @@ paths: schema: $ref: '#/definitions/model.User' "400": - description: 参数无效或用户组不存在 + description: invalid request or user group not found schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "409": - description: 用户名或邮箱已存在 + description: username or email already exists schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -860,9 +870,10 @@ paths: - users /users/{id}: delete: - description: 按 id 删除用户及其用户组成员关系,成功时返回 204 且无响应体。 + description: Delete a user and their group memberships; returns 204 with no + body on success. parameters: - - description: 用户 ID + - description: User ID example: 1 in: path name: id @@ -872,21 +883,21 @@ paths: - application/json responses: "204": - description: 删除成功 + description: Deleted "400": - description: id 无效 + description: invalid id schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "404": - description: 记录不存在 + description: record not found schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -899,9 +910,9 @@ paths: tags: - users get: - description: 按 id 查询用户(含所属用户组)。 + description: Get a user by id, including groups. parameters: - - description: 用户 ID + - description: User ID example: 1 in: path name: id @@ -915,19 +926,19 @@ paths: schema: $ref: '#/definitions/model.User' "400": - description: id 无效 + description: invalid id schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "404": - description: 记录不存在 + description: record not found schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -942,15 +953,16 @@ paths: put: consumes: - application/json - description: 更新用户信息,仅更新请求中提供的字段。传 group_ids 会整体替换用户组;password 非空时重置密码。 + description: Update user fields. group_ids replaces all groups; a non-empty + password resets the password. parameters: - - description: 用户 ID + - description: User ID example: 1 in: path name: id required: true type: integer - - description: 待更新字段 + - description: Fields to update in: body name: user required: true @@ -964,19 +976,19 @@ paths: schema: $ref: '#/definitions/model.User' "400": - description: 参数无效、id 无效或用户组不存在 + description: invalid request, id, or user group not found schema: $ref: '#/definitions/httpx.ErrorResponse' "401": - description: 未登录或登录已过期 + description: unauthorized or session expired schema: $ref: '#/definitions/httpx.ErrorResponse' "403": - description: 需要管理员权限或账号已被禁用 + description: admin permission required or account disabled schema: $ref: '#/definitions/httpx.ErrorResponse' "404": - description: 记录不存在 + description: record not found schema: $ref: '#/definitions/httpx.ErrorResponse' "500": @@ -990,7 +1002,7 @@ paths: - users securityDefinitions: BearerAuth: - description: Bearer JWT,格式:Bearer {token},通过 /auth/login 获取 + description: 'Bearer JWT, format: Bearer {token}, obtained from /auth/login' in: header name: Authorization type: apiKey diff --git a/internal/api/api.go b/internal/api/api.go index b326b6d..260591b 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -75,11 +75,11 @@ func RegisterRoutes(rg *gin.RouterGroup, db *gorm.DB, cfg *config.Config) { // HealthResponse 健康检查响应。 type HealthResponse struct { Status string `json:"status" example:"ok"` - Error string `json:"error,omitempty" example:"数据库不可用"` + Error string `json:"error,omitempty" example:"database unavailable"` } // @Summary Health check -// @Description 检查服务与数据库连接状态;数据库不可用时返回 503。 +// @Description Check service and database connectivity; returns 503 when the database is unavailable. // @Tags system // @Produce json // @Success 200 {object} api.HealthResponse @@ -88,7 +88,7 @@ type HealthResponse struct { func health(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if err := database.Ping(c.Request.Context(), db); err != nil { - c.JSON(http.StatusServiceUnavailable, HealthResponse{Status: "error", Error: "数据库不可用"}) + c.JSON(http.StatusServiceUnavailable, HealthResponse{Status: "error", Error: "database unavailable"}) return } c.JSON(http.StatusOK, HealthResponse{Status: "ok"}) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 385317f..9c64052 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -54,21 +54,21 @@ type LoginResponse struct { } // @Summary Register -// @Description 公开注册,创建普通用户并默认加入普通用户组(id 1)。username 长度 3-50 且唯一,email 唯一,password 长度 6-72。 +// @Description Public registration. Creates a regular user in the default user group (id 1). username is 3-50 chars and unique; email is unique; password is 6-72 chars. // @Tags auth // @Accept json // @Produce json -// @Param user body auth.RegisterRequest true "注册信息" +// @Param user body auth.RegisterRequest true "Registration payload" // @Success 201 {object} model.User -// @Failure 400 {object} httpx.ErrorResponse "参数无效" -// @Failure 409 {object} httpx.ErrorResponse "用户名或邮箱已存在" +// @Failure 400 {object} httpx.ErrorResponse "invalid request" +// @Failure 409 {object} httpx.ErrorResponse "username or email already exists" // @Failure 500 {object} httpx.ErrorResponse // @Router /auth/register [post] func Register(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()}) + c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: " + err.Error()}) return } @@ -92,7 +92,7 @@ func Register(db *gorm.DB) gin.HandlerFunc { return user.ReplaceGroups(tx, newUser.ID, []uint{model.GroupIDUser}) }) if err != nil { - httpx.RespondDuplicateOrDBError(c, err, "用户名或邮箱已存在") + httpx.RespondDuplicateOrDBError(c, err, "username or email already exists") return } @@ -107,22 +107,22 @@ func Register(db *gorm.DB) gin.HandlerFunc { } // @Summary Login -// @Description 使用用户名或邮箱登录,成功返回 JWT(有效期见 auth.token_ttl)与用户信息。 +// @Description Login with username or email; returns a JWT (TTL from auth.token_ttl) and the user. // @Tags auth // @Accept json // @Produce json -// @Param credentials body auth.LoginRequest true "登录凭证" +// @Param credentials body auth.LoginRequest true "Login credentials" // @Success 200 {object} auth.LoginResponse -// @Failure 400 {object} httpx.ErrorResponse "参数无效" -// @Failure 401 {object} httpx.ErrorResponse "账号或密码错误" -// @Failure 403 {object} httpx.ErrorResponse "账号已被禁用" +// @Failure 400 {object} httpx.ErrorResponse "invalid request" +// @Failure 401 {object} httpx.ErrorResponse "incorrect account or password" +// @Failure 403 {object} httpx.ErrorResponse "account disabled" // @Failure 500 {object} httpx.ErrorResponse // @Router /auth/login [post] func Login(db *gorm.DB, authn *Authenticator) gin.HandlerFunc { return func(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()}) + c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: " + err.Error()}) return } @@ -135,7 +135,7 @@ func Login(db *gorm.DB, authn *Authenticator) gin.HandlerFunc { if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { _ = bcrypt.CompareHashAndPassword(dummyPasswordHash, []byte(req.Password)) - c.JSON(http.StatusUnauthorized, httpx.ErrorResponse{Error: "账号或密码错误"}) + c.JSON(http.StatusUnauthorized, httpx.ErrorResponse{Error: "incorrect account or password"}) return } httpx.RespondDBError(c, err) @@ -143,11 +143,11 @@ func Login(db *gorm.DB, authn *Authenticator) gin.HandlerFunc { } if err := bcrypt.CompareHashAndPassword([]byte(loginUser.PasswordHash), []byte(req.Password)); err != nil { - c.JSON(http.StatusUnauthorized, httpx.ErrorResponse{Error: "账号或密码错误"}) + c.JSON(http.StatusUnauthorized, httpx.ErrorResponse{Error: "incorrect account or password"}) return } if loginUser.Status != 1 { - c.JSON(http.StatusForbidden, httpx.ErrorResponse{Error: "账号已被禁用"}) + c.JSON(http.StatusForbidden, httpx.ErrorResponse{Error: "account disabled"}) return } @@ -161,7 +161,7 @@ func Login(db *gorm.DB, authn *Authenticator) gin.HandlerFunc { token, expiresAt, err := authn.Sign(loginUser.ID) if err != nil { slog.ErrorContext(ctx, "签发登录凭证失败", "err", err) - c.JSON(http.StatusInternalServerError, httpx.ErrorResponse{Error: "服务器内部错误"}) + c.JSON(http.StatusInternalServerError, httpx.ErrorResponse{Error: "internal server error"}) return } c.JSON(http.StatusOK, LoginResponse{Token: token, ExpiresAt: expiresAt, User: loginUser}) @@ -215,7 +215,7 @@ func (a *Authenticator) Parse(tokenString string) (uint, error) { claims := &jwt.RegisteredClaims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("非预期的签名算法: %v", t.Header["alg"]) + return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } return a.secret, nil }, @@ -224,12 +224,12 @@ func (a *Authenticator) Parse(tokenString string) (uint, error) { jwt.WithExpirationRequired(), ) if err != nil || !token.Valid { - return 0, errors.New("登录凭证无效") + return 0, errors.New("invalid token") } id, err := strconv.ParseUint(claims.Subject, 10, 64) if err != nil || id == 0 { - return 0, errors.New("登录凭证无效") + return 0, errors.New("invalid token") } return uint(id), nil } @@ -259,7 +259,7 @@ func (a *Authenticator) RequireAuth(db *gorm.DB) gin.HandlerFunc { return } if current.Status != 1 { - c.AbortWithStatusJSON(http.StatusForbidden, httpx.ErrorResponse{Error: "账号已被禁用"}) + c.AbortWithStatusJSON(http.StatusForbidden, httpx.ErrorResponse{Error: "account disabled"}) return } @@ -288,7 +288,7 @@ func RequireAdmin() gin.HandlerFunc { return } } - c.AbortWithStatusJSON(http.StatusForbidden, httpx.ErrorResponse{Error: "需要管理员权限"}) + c.AbortWithStatusJSON(http.StatusForbidden, httpx.ErrorResponse{Error: "admin permission required"}) } } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index de69cbf..b3ba5a6 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -148,6 +148,9 @@ func TestLogin(t *testing.T) { if w.Code != http.StatusUnauthorized { t.Errorf("密码错误状态码 = %d, 期望 %d", w.Code, http.StatusUnauthorized) } + if !strings.Contains(w.Body.String(), "incorrect account or password") { + t.Errorf("错误文案应为英文: %s", w.Body.String()) + } w = testutil.Call(t, r, http.MethodPost, "/api/auth/login", map[string]string{ "account": "nobody", "password": "secret123", diff --git a/internal/database/database_test.go b/internal/database/database_test.go index 5ff8422..d85110b 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -86,6 +86,9 @@ func TestMigrateIdempotentAndCRUD(t *testing.T) { if len(groups) != 2 || groups[0].ID != model.GroupIDAdmin || groups[1].ID != model.GroupIDUser { t.Errorf("内置组异常: %+v", groups) } + if groups[0].Description != "Administrator group" || groups[1].Description != "Regular user group" { + t.Errorf("内置组描述应为英文: %+v", groups) + } var admin model.User if err := db.WithContext(ctx).Where("username = ?", adminUsername).First(&admin).Error; err != nil { @@ -94,6 +97,9 @@ func TestMigrateIdempotentAndCRUD(t *testing.T) { if admin.Status != 1 { t.Errorf("初始管理员状态 = %d, 期望 1", admin.Status) } + if admin.Nickname != "Administrator" { + t.Errorf("初始管理员昵称应为英文: %q", admin.Nickname) + } if _, err := bcrypt.Cost([]byte(admin.PasswordHash)); err != nil { t.Errorf("初始管理员密码哈希无效: %v", err) } @@ -173,3 +179,71 @@ func TestGeneratePassword(t *testing.T) { t.Error("两次生成的密码不应相同") } } + +func TestTranslateBuiltinDataMigration(t *testing.T) { + db := openTestDB(t) + ctx := context.Background() + if err := Migrate(ctx, db); err != nil { + t.Fatalf("执行迁移失败: %v", err) + } + + var translate Migration + for _, m := range migrations { + if m.Version == 5 { + translate = m + } + } + if translate.Up == nil { + t.Fatal("未找到 v5 翻译迁移") + } + + // 还原为历史中文数据后重新执行 v5,应更新内置数据。 + if err := db.WithContext(ctx).Model(&model.UserGroup{}). + Where("id = ?", model.GroupIDAdmin).Update("description", "管理员组").Error; err != nil { + t.Fatalf("还原 admin 组描述失败: %v", err) + } + if err := db.WithContext(ctx).Model(&model.UserGroup{}). + Where("id = ?", model.GroupIDUser).Update("description", "普通用户组").Error; err != nil { + t.Fatalf("还原 user 组描述失败: %v", err) + } + if err := db.WithContext(ctx).Model(&model.User{}). + Where("username = ?", "admin").Update("nickname", "管理员").Error; err != nil { + t.Fatalf("还原 admin 昵称失败: %v", err) + } + if err := translate.Up(db.WithContext(ctx)); err != nil { + t.Fatalf("执行 v5 迁移失败: %v", err) + } + + var admin model.User + if err := db.WithContext(ctx).Where("username = ?", "admin").First(&admin).Error; err != nil { + t.Fatalf("查询管理员失败: %v", err) + } + if admin.Nickname != "Administrator" { + t.Errorf("管理员昵称 = %q, 期望 Administrator", admin.Nickname) + } + var adminGroup, userGroup model.UserGroup + if err := db.WithContext(ctx).First(&adminGroup, model.GroupIDAdmin).Error; err != nil { + t.Fatalf("查询 admin 组失败: %v", err) + } + if err := db.WithContext(ctx).First(&userGroup, model.GroupIDUser).Error; err != nil { + t.Fatalf("查询 user 组失败: %v", err) + } + if adminGroup.Description != "Administrator group" || userGroup.Description != "Regular user group" { + t.Errorf("内置组描述 = %q / %q", adminGroup.Description, userGroup.Description) + } + + // 手工修改过的值不应被覆盖。 + if err := db.WithContext(ctx).Model(&model.User{}). + Where("username = ?", "admin").Update("nickname", "Boss").Error; err != nil { + t.Fatalf("自定义昵称失败: %v", err) + } + if err := translate.Up(db.WithContext(ctx)); err != nil { + t.Fatalf("再次执行 v5 迁移失败: %v", err) + } + if err := db.WithContext(ctx).Where("username = ?", "admin").First(&admin).Error; err != nil { + t.Fatalf("查询管理员失败: %v", err) + } + if admin.Nickname != "Boss" { + t.Errorf("自定义昵称被覆盖: %q", admin.Nickname) + } +} diff --git a/internal/database/migrate.go b/internal/database/migrate.go index 88e59df..23bf9f1 100644 --- a/internal/database/migrate.go +++ b/internal/database/migrate.go @@ -40,8 +40,8 @@ var migrations = []Migration{ name string desc string }{ - {id: model.GroupIDAdmin, name: "admin", desc: "管理员组"}, - {id: model.GroupIDUser, name: "user", desc: "普通用户组"}, + {id: model.GroupIDAdmin, name: "admin", desc: "Administrator group"}, + {id: model.GroupIDUser, name: "user", desc: "Regular user group"}, } for _, seed := range seeds { if err := tx.Exec( @@ -66,6 +66,23 @@ var migrations = []Migration{ Name: "seed_admin_user", Up: seedAdminUser, }, + { + Version: 5, + Name: "translate_builtin_data", + Up: func(tx *gorm.DB) error { + statements := []string{ + "UPDATE user_groups SET description = 'Administrator group' WHERE id = 0 AND description = '管理员组'", + "UPDATE user_groups SET description = 'Regular user group' WHERE id = 1 AND description = '普通用户组'", + "UPDATE users SET nickname = 'Administrator' WHERE username = 'admin' AND nickname = '管理员'", + } + for _, statement := range statements { + if err := tx.Exec(statement).Error; err != nil { + return err + } + } + return nil + }, + }, } // schemaMigration 记录已应用的迁移版本。 diff --git a/internal/database/seed.go b/internal/database/seed.go index c239721..418ffde 100644 --- a/internal/database/seed.go +++ b/internal/database/seed.go @@ -49,7 +49,7 @@ func seedAdminUser(tx *gorm.DB) error { Username: adminUsername, Email: adminEmail, PasswordHash: string(hash), - Nickname: "管理员", + Nickname: "Administrator", Status: 1, } if err := tx.Create(&user).Error; err != nil { diff --git a/internal/httpx/httpx.go b/internal/httpx/httpx.go index 2971f0c..28f6f5c 100644 --- a/internal/httpx/httpx.go +++ b/internal/httpx/httpx.go @@ -18,7 +18,7 @@ const ( // ErrorResponse 统一错误响应。 type ErrorResponse struct { - Error string `json:"error" example:"记录不存在"` + Error string `json:"error" example:"record not found"` } // ParsePagination 解析 page/page_size,非法值回落默认。 @@ -38,7 +38,7 @@ func ParsePagination(c *gin.Context) (int, int) { func ParseID(c *gin.Context) (uint, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { - c.JSON(http.StatusBadRequest, ErrorResponse{Error: "id 无效"}) + c.JSON(http.StatusBadRequest, ErrorResponse{Error: "invalid id"}) return 0, false } return uint(id), true @@ -47,19 +47,19 @@ func ParseID(c *gin.Context) (uint, bool) { // RespondDBError 记录数据库错误并返回 500。 func RespondDBError(c *gin.Context, err error) { slog.ErrorContext(c.Request.Context(), "数据库操作失败", "err", err, "path", c.Request.URL.Path) - c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "服务器内部错误"}) + c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"}) } // RespondServerError 记录业务错误并返回 500。 func RespondServerError(c *gin.Context, err error, msg string) { slog.ErrorContext(c.Request.Context(), msg, "err", err, "path", c.Request.URL.Path) - c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "服务器内部错误"}) + c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"}) } // RespondGetError 查询类错误:记录不存在返回 404,其余按数据库错误处理。 func RespondGetError(c *gin.Context, err error) { if errors.Is(err, gorm.ErrRecordNotFound) { - c.JSON(http.StatusNotFound, ErrorResponse{Error: "记录不存在"}) + c.JSON(http.StatusNotFound, ErrorResponse{Error: "record not found"}) return } RespondDBError(c, err) @@ -76,5 +76,5 @@ func RespondDuplicateOrDBError(c *gin.Context, err error, duplicateMsg string) { // RespondUnauthorized 中止请求并返回 401。 func RespondUnauthorized(c *gin.Context) { - c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse{Error: "未登录或登录已过期"}) + c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse{Error: "unauthorized or session expired"}) } diff --git a/internal/note/note.go b/internal/note/note.go index c7a1b7a..2f64c7d 100644 --- a/internal/note/note.go +++ b/internal/note/note.go @@ -13,8 +13,8 @@ import ( // Request 创建/更新便签请求。 type Request struct { - Title string `json:"title" binding:"required,max=200" example:"购物清单"` - Content string `json:"content" example:"牛奶、鸡蛋"` + Title string `json:"title" binding:"required,max=200" example:"Shopping list"` + Content string `json:"content" example:"Milk, eggs"` } // ListResponse 便签分页列表响应。 @@ -26,16 +26,16 @@ type ListResponse struct { } // @Summary List notes -// @Description 分页查询便签列表,按 id 倒序返回。page 从 1 开始;page_size 取值 1-100,默认 20。 +// @Description List notes ordered by id DESC. page starts at 1; page_size is 1-100, default 20. // @Tags notes // @Produce json -// @Param page query int false "页码,默认 1" example(1) -// @Param page_size query int false "每页数量,默认 20,最大 100" example(20) +// @Param page query int false "Page number, default 1" example(1) +// @Param page_size query int false "Page size, default 20, max 100" example(20) // @Success 200 {object} note.ListResponse // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "account disabled" // @Router /notes [get] func List(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -68,23 +68,23 @@ func List(db *gorm.DB) gin.HandlerFunc { } // @Summary Create a note -// @Description 创建便签。title 必填且最长 200 字符,content 可选。 +// @Description Create a note. title is required (max 200 chars); content is optional. // @Tags notes // @Accept json // @Produce json -// @Param note body note.Request true "便签内容" +// @Param note body note.Request true "Note payload" // @Success 201 {object} model.Note -// @Failure 400 {object} httpx.ErrorResponse "参数无效" +// @Failure 400 {object} httpx.ErrorResponse "invalid request" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "account disabled" // @Router /notes [post] func Create(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var req Request if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()}) + c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: " + err.Error()}) return } @@ -98,17 +98,17 @@ func Create(db *gorm.DB) gin.HandlerFunc { } // @Summary Get a note -// @Description 按 id 查询单个便签。 +// @Description Get a note by id. // @Tags notes // @Produce json -// @Param id path int true "便签 ID" example(1) +// @Param id path int true "Note ID" example(1) // @Success 200 {object} model.Note -// @Failure 400 {object} httpx.ErrorResponse "id 无效" -// @Failure 404 {object} httpx.ErrorResponse "记录不存在" +// @Failure 400 {object} httpx.ErrorResponse "invalid id" +// @Failure 404 {object} httpx.ErrorResponse "record not found" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "account disabled" // @Router /notes/{id} [get] func Get(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -127,19 +127,19 @@ func Get(db *gorm.DB) gin.HandlerFunc { } // @Summary Update a note -// @Description 全量更新便签的 title 与 content,字段校验规则同创建。 +// @Description Update title and content; validation is the same as create. // @Tags notes // @Accept json // @Produce json -// @Param id path int true "便签 ID" example(1) -// @Param note body note.Request true "便签内容" +// @Param id path int true "Note ID" example(1) +// @Param note body note.Request true "Note payload" // @Success 200 {object} model.Note -// @Failure 400 {object} httpx.ErrorResponse "参数无效或 id 无效" -// @Failure 404 {object} httpx.ErrorResponse "记录不存在" +// @Failure 400 {object} httpx.ErrorResponse "invalid request or id" +// @Failure 404 {object} httpx.ErrorResponse "record not found" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "account disabled" // @Router /notes/{id} [put] func Update(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -150,7 +150,7 @@ func Update(db *gorm.DB) gin.HandlerFunc { var req Request if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()}) + c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: " + err.Error()}) return } @@ -172,17 +172,17 @@ func Update(db *gorm.DB) gin.HandlerFunc { } // @Summary Delete a note -// @Description 按 id 删除便签,成功时返回 204 且无响应体。 +// @Description Delete a note by id; returns 204 with no body on success. // @Tags notes // @Produce json -// @Param id path int true "便签 ID" example(1) -// @Success 204 "删除成功" -// @Failure 400 {object} httpx.ErrorResponse "id 无效" -// @Failure 404 {object} httpx.ErrorResponse "记录不存在" +// @Param id path int true "Note ID" example(1) +// @Success 204 "Deleted" +// @Failure 400 {object} httpx.ErrorResponse "invalid id" +// @Failure 404 {object} httpx.ErrorResponse "record not found" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "account disabled" // @Router /notes/{id} [delete] func Delete(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -197,7 +197,7 @@ func Delete(db *gorm.DB) gin.HandlerFunc { return } if result.RowsAffected == 0 { - c.JSON(http.StatusNotFound, httpx.ErrorResponse{Error: "记录不存在"}) + c.JSON(http.StatusNotFound, httpx.ErrorResponse{Error: "record not found"}) return } c.Status(http.StatusNoContent) diff --git a/internal/note/note_test.go b/internal/note/note_test.go index b19c94d..712afc1 100644 --- a/internal/note/note_test.go +++ b/internal/note/note_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "rill/internal/model" @@ -80,6 +81,9 @@ func TestNoteCRUD(t *testing.T) { if w.Code != http.StatusNotFound { t.Errorf("删除后详情状态码 = %d, 期望 %d", w.Code, http.StatusNotFound) } + if !strings.Contains(w.Body.String(), "record not found") { + t.Errorf("错误文案应为英文: %s", w.Body.String()) + } } func TestNoteValidation(t *testing.T) { diff --git a/internal/user/user.go b/internal/user/user.go index 3391e1f..8e39deb 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -17,7 +17,7 @@ import ( const groupMembersTable = "user_group_members" // ErrGroupsNotFound 用户组不存在。 -var ErrGroupsNotFound = errors.New("用户组不存在") +var ErrGroupsNotFound = errors.New("user group not found") // CreateRequest 创建用户请求。 type CreateRequest struct { @@ -48,16 +48,16 @@ type ListResponse struct { } // @Summary List users -// @Description 分页查询用户列表(含所属用户组),按 id 倒序返回。page 从 1 开始;page_size 取值 1-100,默认 20。 +// @Description List users with their groups, ordered by id DESC. page starts at 1; page_size is 1-100, default 20. // @Tags users // @Produce json -// @Param page query int false "页码,默认 1" example(1) -// @Param page_size query int false "每页数量,默认 20,最大 100" example(20) +// @Param page query int false "Page number, default 1" example(1) +// @Param page_size query int false "Page size, default 20, max 100" example(20) // @Success 200 {object} user.ListResponse // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /users [get] func List(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -94,24 +94,24 @@ func List(db *gorm.DB) gin.HandlerFunc { } // @Summary Create a user -// @Description 创建用户并关联用户组。username、email 唯一,password 长度 6-72;不传 group_ids 时默认加入普通用户组(id 1)。 +// @Description Create a user and assign groups. username and email are unique; password is 6-72 chars; defaults to the regular user group (id 1) when group_ids is omitted. // @Tags users // @Accept json // @Produce json -// @Param user body user.CreateRequest true "用户信息" +// @Param user body user.CreateRequest true "User payload" // @Success 201 {object} model.User -// @Failure 400 {object} httpx.ErrorResponse "参数无效或用户组不存在" -// @Failure 409 {object} httpx.ErrorResponse "用户名或邮箱已存在" +// @Failure 400 {object} httpx.ErrorResponse "invalid request or user group not found" +// @Failure 409 {object} httpx.ErrorResponse "username or email already exists" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /users [post] func Create(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var req CreateRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()}) + c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: " + err.Error()}) return } @@ -156,7 +156,7 @@ func Create(db *gorm.DB) gin.HandlerFunc { return ReplaceGroups(tx, user.ID, groupIDs) }) if err != nil { - httpx.RespondDuplicateOrDBError(c, err, "用户名或邮箱已存在") + httpx.RespondDuplicateOrDBError(c, err, "username or email already exists") return } user.Groups = groups @@ -165,17 +165,17 @@ func Create(db *gorm.DB) gin.HandlerFunc { } // @Summary Get a user -// @Description 按 id 查询用户(含所属用户组)。 +// @Description Get a user by id, including groups. // @Tags users // @Produce json -// @Param id path int true "用户 ID" example(1) +// @Param id path int true "User ID" example(1) // @Success 200 {object} model.User -// @Failure 400 {object} httpx.ErrorResponse "id 无效" -// @Failure 404 {object} httpx.ErrorResponse "记录不存在" +// @Failure 400 {object} httpx.ErrorResponse "invalid id" +// @Failure 404 {object} httpx.ErrorResponse "record not found" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /users/{id} [get] func Get(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -202,19 +202,19 @@ func Get(db *gorm.DB) gin.HandlerFunc { } // @Summary Update a user -// @Description 更新用户信息,仅更新请求中提供的字段。传 group_ids 会整体替换用户组;password 非空时重置密码。 +// @Description Update user fields. group_ids replaces all groups; a non-empty password resets the password. // @Tags users // @Accept json // @Produce json -// @Param id path int true "用户 ID" example(1) -// @Param user body user.UpdateRequest true "待更新字段" +// @Param id path int true "User ID" example(1) +// @Param user body user.UpdateRequest true "Fields to update" // @Success 200 {object} model.User -// @Failure 400 {object} httpx.ErrorResponse "参数无效、id 无效或用户组不存在" -// @Failure 404 {object} httpx.ErrorResponse "记录不存在" +// @Failure 400 {object} httpx.ErrorResponse "invalid request, id, or user group not found" +// @Failure 404 {object} httpx.ErrorResponse "record not found" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /users/{id} [put] func Update(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -225,7 +225,7 @@ func Update(db *gorm.DB) gin.HandlerFunc { var req UpdateRequest if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()}) + c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: " + err.Error()}) return } @@ -294,17 +294,17 @@ func Update(db *gorm.DB) gin.HandlerFunc { } // @Summary Delete a user -// @Description 按 id 删除用户及其用户组成员关系,成功时返回 204 且无响应体。 +// @Description Delete a user and their group memberships; returns 204 with no body on success. // @Tags users // @Produce json -// @Param id path int true "用户 ID" example(1) -// @Success 204 "删除成功" -// @Failure 400 {object} httpx.ErrorResponse "id 无效" -// @Failure 404 {object} httpx.ErrorResponse "记录不存在" +// @Param id path int true "User ID" example(1) +// @Success 204 "Deleted" +// @Failure 400 {object} httpx.ErrorResponse "invalid id" +// @Failure 404 {object} httpx.ErrorResponse "record not found" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /users/{id} [delete] func Delete(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { diff --git a/internal/usergroup/user_group.go b/internal/usergroup/user_group.go index 2499f2b..1fdfbb0 100644 --- a/internal/usergroup/user_group.go +++ b/internal/usergroup/user_group.go @@ -14,8 +14,8 @@ import ( // Request 创建/更新用户组请求。 type Request struct { - Name string `json:"name" binding:"required,max=50" example:"运营组"` - Description string `json:"description" binding:"max=255" example:"负责日常运营"` + Name string `json:"name" binding:"required,max=50" example:"Operations"` + Description string `json:"description" binding:"max=255" example:"Handles daily operations"` } // ListResponse 用户组分页列表响应。 @@ -27,16 +27,16 @@ type ListResponse struct { } // @Summary List user groups -// @Description 分页查询用户组列表,按 id 升序返回。page 从 1 开始;page_size 取值 1-100,默认 20。 +// @Description List user groups ordered by id ASC. page starts at 1; page_size is 1-100, default 20. // @Tags user-groups // @Produce json -// @Param page query int false "页码,默认 1" example(1) -// @Param page_size query int false "每页数量,默认 20,最大 100" example(20) +// @Param page query int false "Page number, default 1" example(1) +// @Param page_size query int false "Page size, default 20, max 100" example(20) // @Success 200 {object} usergroup.ListResponse // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /user-groups [get] func List(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -69,24 +69,24 @@ func List(db *gorm.DB) gin.HandlerFunc { } // @Summary Create a user group -// @Description 创建用户组。name 必填且唯一(最长 50 字符),description 可选(最长 255 字符);id 由服务端分配。 +// @Description Create a user group. name is required and unique (max 50 chars); description is optional (max 255 chars); id is assigned by the server. // @Tags user-groups // @Accept json // @Produce json -// @Param group body usergroup.Request true "用户组信息" +// @Param group body usergroup.Request true "User group payload" // @Success 201 {object} model.UserGroup -// @Failure 400 {object} httpx.ErrorResponse "参数无效" -// @Failure 409 {object} httpx.ErrorResponse "用户组名称已存在" +// @Failure 400 {object} httpx.ErrorResponse "invalid request" +// @Failure 409 {object} httpx.ErrorResponse "user group name already exists" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /user-groups [post] func Create(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { var req Request if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()}) + c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: " + err.Error()}) return } @@ -105,7 +105,7 @@ func Create(db *gorm.DB) gin.HandlerFunc { return tx.Create(&group).Error }) if err != nil { - httpx.RespondDuplicateOrDBError(c, err, "用户组名称已存在") + httpx.RespondDuplicateOrDBError(c, err, "user group name already exists") return } c.JSON(http.StatusCreated, group) @@ -113,17 +113,17 @@ func Create(db *gorm.DB) gin.HandlerFunc { } // @Summary Get a user group -// @Description 按 id 查询单个用户组,id 0 为内置 admin 组。 +// @Description Get a user group by id; id 0 is the built-in admin group. // @Tags user-groups // @Produce json -// @Param id path int true "用户组 ID" example(1) +// @Param id path int true "User group ID" example(1) // @Success 200 {object} model.UserGroup -// @Failure 400 {object} httpx.ErrorResponse "id 无效" -// @Failure 404 {object} httpx.ErrorResponse "记录不存在" +// @Failure 400 {object} httpx.ErrorResponse "invalid id" +// @Failure 404 {object} httpx.ErrorResponse "record not found" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /user-groups/{id} [get] func Get(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -142,20 +142,20 @@ func Get(db *gorm.DB) gin.HandlerFunc { } // @Summary Update a user group -// @Description 更新用户组的 name 与 description,name 必填且唯一。 +// @Description Update a user group's name and description; name is required and unique. // @Tags user-groups // @Accept json // @Produce json -// @Param id path int true "用户组 ID" example(1) -// @Param group body usergroup.Request true "用户组信息" +// @Param id path int true "User group ID" example(1) +// @Param group body usergroup.Request true "User group payload" // @Success 200 {object} model.UserGroup -// @Failure 400 {object} httpx.ErrorResponse "参数无效或 id 无效" -// @Failure 404 {object} httpx.ErrorResponse "记录不存在" -// @Failure 409 {object} httpx.ErrorResponse "用户组名称已存在" +// @Failure 400 {object} httpx.ErrorResponse "invalid request or id" +// @Failure 404 {object} httpx.ErrorResponse "record not found" +// @Failure 409 {object} httpx.ErrorResponse "user group name already exists" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /user-groups/{id} [put] func Update(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -166,7 +166,7 @@ func Update(db *gorm.DB) gin.HandlerFunc { var req Request if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "参数无效: " + err.Error()}) + c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: " + err.Error()}) return } @@ -180,7 +180,7 @@ func Update(db *gorm.DB) gin.HandlerFunc { group.Name = req.Name group.Description = req.Description if err := db.WithContext(ctx).Save(&group).Error; err != nil { - httpx.RespondDuplicateOrDBError(c, err, "用户组名称已存在") + httpx.RespondDuplicateOrDBError(c, err, "user group name already exists") return } c.JSON(http.StatusOK, group) @@ -188,18 +188,18 @@ func Update(db *gorm.DB) gin.HandlerFunc { } // @Summary Delete a user group -// @Description 按 id 删除用户组,成功时返回 204 且无响应体。系统内置组或组内仍有用户时返回 409。 +// @Description Delete a user group by id; returns 204 with no body on success. Returns 409 for system groups or when the group still has members. // @Tags user-groups // @Produce json -// @Param id path int true "用户组 ID" example(2) -// @Success 204 "删除成功" -// @Failure 400 {object} httpx.ErrorResponse "id 无效" -// @Failure 404 {object} httpx.ErrorResponse "记录不存在" -// @Failure 409 {object} httpx.ErrorResponse "系统内置组不可删除或用户组内仍有用户" +// @Param id path int true "User group ID" example(2) +// @Success 204 "Deleted" +// @Failure 400 {object} httpx.ErrorResponse "invalid id" +// @Failure 404 {object} httpx.ErrorResponse "record not found" +// @Failure 409 {object} httpx.ErrorResponse "system group cannot be deleted or group still has members" // @Failure 500 {object} httpx.ErrorResponse // @Security BearerAuth -// @Failure 401 {object} httpx.ErrorResponse "未登录或登录已过期" -// @Failure 403 {object} httpx.ErrorResponse "需要管理员权限或账号已被禁用" +// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired" +// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled" // @Router /user-groups/{id} [delete] func Delete(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -215,7 +215,7 @@ func Delete(db *gorm.DB) gin.HandlerFunc { return } if group.IsSystem { - c.JSON(http.StatusConflict, httpx.ErrorResponse{Error: "系统内置组不可删除"}) + c.JSON(http.StatusConflict, httpx.ErrorResponse{Error: "system group cannot be deleted"}) return } @@ -227,7 +227,7 @@ func Delete(db *gorm.DB) gin.HandlerFunc { return } if members > 0 { - c.JSON(http.StatusConflict, httpx.ErrorResponse{Error: "用户组内仍有用户,无法删除"}) + c.JSON(http.StatusConflict, httpx.ErrorResponse{Error: "user group still has members"}) return } @@ -243,7 +243,7 @@ func Delete(db *gorm.DB) gin.HandlerFunc { func parseGroupID(c *gin.Context) (uint, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { - c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "id 无效"}) + c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid id"}) return 0, false } return uint(id), true diff --git a/internal/usergroup/user_group_test.go b/internal/usergroup/user_group_test.go index 0e05ee0..c9621a1 100644 --- a/internal/usergroup/user_group_test.go +++ b/internal/usergroup/user_group_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "rill/internal/model" @@ -104,6 +105,9 @@ func TestUserGroupProtected(t *testing.T) { if w.Code != http.StatusConflict { t.Errorf("删除内置组 %d 状态码 = %d, 期望 %d", id, w.Code, http.StatusConflict) } + if !strings.Contains(w.Body.String(), "system group cannot be deleted") { + t.Errorf("错误文案应为英文: %s", w.Body.String()) + } } w := testutil.Call(t, r, http.MethodPost, "/api/user-groups", map[string]string{"name": "admin"}) diff --git a/main.go b/main.go index b40ba3a..77bdf18 100644 --- a/main.go +++ b/main.go @@ -29,14 +29,14 @@ import ( // @title Rill API // @version 1.0 -// @description Rill 服务端 HTTP API 文档,所有接口以配置项 api.prefix(默认 /api)为前缀,请求与响应均为 JSON。 -// @description Swagger 页面:{prefix}/swagger/index.html;OpenAPI JSON:{prefix}/swagger/doc.json。 -// @description 除 health、swagger、auth 外,其余接口需要 Bearer JWT:先调用 /auth/login 获取 token,再在请求头携带 Authorization: Bearer {token}。 +// @description Rill server HTTP API documentation. All endpoints are prefixed with api.prefix (default /api); requests and responses are JSON. +// @description Swagger UI: {prefix}/swagger/index.html; OpenAPI JSON: {prefix}/swagger/doc.json. +// @description Except for health, swagger, and auth, all endpoints require a Bearer JWT: call /auth/login to get a token, then send Authorization: Bearer {token}. // @BasePath /api // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization -// @description Bearer JWT,格式:Bearer {token},通过 /auth/login 获取 +// @description Bearer JWT, format: Bearer {token}, obtained from /auth/login func main() { configPath := flag.String("c", "data/config.yaml", "配置文件路径(不存在时自动生成)") flag.Parse()