diff --git a/docs/docs.go b/docs/docs.go index 8a8665a..eeb5494 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -105,6 +105,209 @@ const docTemplate = `{ } } }, + "/admin/app/keyword": { + "post": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "添加意图关键字", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.意图关键字" + ], + "summary": "添加意图关键字", + "parameters": [ + { + "description": "请求参数", + "name": "RequestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/keyword.createKeywordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/keyword.createKeywordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/admin/app/keyword/{id}": { + "put": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "修改意图关键字", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.意图关键字" + ], + "summary": "修改意图关键字", + "parameters": [ + { + "type": "string", + "description": "编号ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "请求参数", + "name": "RequestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/keyword.modifyKeywordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/keyword.modifyKeywordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + }, + "delete": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "删除意图关键字", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.意图关键字" + ], + "summary": "删除意图关键字", + "parameters": [ + { + "type": "string", + "description": "编号ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/keyword.deleteKeywordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/admin/app/keywords": { + "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "获取意图关键字列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.意图关键字" + ], + "summary": "获取意图关键字列表", + "parameters": [ + { + "type": "integer", + "description": "小程序ID", + "name": "app_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "意图关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "当前页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "每页返回的数据量,最多 100 条", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/keyword.keywordPageListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/admin/app/{id}": { "put": { "security": [ @@ -485,6 +688,126 @@ const docTemplate = `{ } } }, + "keyword.createKeywordRequest": { + "type": "object", + "required": [ + "app_id", + "keyword" + ], + "properties": { + "app_id": { + "description": "小程序ID", + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + } + } + }, + "keyword.createKeywordResponse": { + "type": "object", + "properties": { + "id": { + "description": "关键字ID", + "type": "integer" + }, + "message": { + "description": "提示信息", + "type": "string" + } + } + }, + "keyword.deleteKeywordResponse": { + "type": "object", + "properties": { + "message": { + "description": "提示信息", + "type": "string" + } + } + }, + "keyword.keywordListData": { + "type": "object", + "properties": { + "app_id": { + "description": "小程序ID", + "type": "string" + }, + "created_at": { + "description": "创建时间", + "type": "string" + }, + "created_user": { + "description": "创建人", + "type": "string" + }, + "id": { + "description": "关键字编号", + "type": "integer" + }, + "keyword": { + "description": "意图关键字", + "type": "string" + }, + "material_type_count": { + "description": "素材类型数量", + "type": "string" + }, + "updated_at": { + "description": "更新时间", + "type": "string" + }, + "updated_user": { + "description": "更新人", + "type": "string" + } + } + }, + "keyword.keywordPageListResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/keyword.keywordListData" + } + }, + "page": { + "description": "当前页码", + "type": "integer" + }, + "page_size": { + "description": "每页返回的数据量", + "type": "integer" + }, + "total": { + "description": "符合查询条件的总记录数", + "type": "integer" + } + } + }, + "keyword.modifyKeywordRequest": { + "type": "object", + "required": [ + "keyword" + ], + "properties": { + "keyword": { + "description": "意图关键字", + "type": "string" + } + } + }, + "keyword.modifyKeywordResponse": { + "type": "object", + "properties": { + "message": { + "description": "提示信息", + "type": "string" + } + } + }, "upload.uploadImageResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index eefdf21..cd0dd69 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -97,6 +97,209 @@ } } }, + "/admin/app/keyword": { + "post": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "添加意图关键字", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.意图关键字" + ], + "summary": "添加意图关键字", + "parameters": [ + { + "description": "请求参数", + "name": "RequestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/keyword.createKeywordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/keyword.createKeywordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/admin/app/keyword/{id}": { + "put": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "修改意图关键字", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.意图关键字" + ], + "summary": "修改意图关键字", + "parameters": [ + { + "type": "string", + "description": "编号ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "请求参数", + "name": "RequestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/keyword.modifyKeywordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/keyword.modifyKeywordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + }, + "delete": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "删除意图关键字", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.意图关键字" + ], + "summary": "删除意图关键字", + "parameters": [ + { + "type": "string", + "description": "编号ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/keyword.deleteKeywordResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, + "/admin/app/keywords": { + "get": { + "security": [ + { + "LoginVerifyToken": [] + } + ], + "description": "获取意图关键字列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端.意图关键字" + ], + "summary": "获取意图关键字列表", + "parameters": [ + { + "type": "integer", + "description": "小程序ID", + "name": "app_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "意图关键字", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "当前页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "每页返回的数据量,最多 100 条", + "name": "page_size", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/keyword.keywordPageListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/code.Failure" + } + } + } + } + }, "/admin/app/{id}": { "put": { "security": [ @@ -477,6 +680,126 @@ } } }, + "keyword.createKeywordRequest": { + "type": "object", + "required": [ + "app_id", + "keyword" + ], + "properties": { + "app_id": { + "description": "小程序ID", + "type": "string" + }, + "keyword": { + "description": "关键字", + "type": "string" + } + } + }, + "keyword.createKeywordResponse": { + "type": "object", + "properties": { + "id": { + "description": "关键字ID", + "type": "integer" + }, + "message": { + "description": "提示信息", + "type": "string" + } + } + }, + "keyword.deleteKeywordResponse": { + "type": "object", + "properties": { + "message": { + "description": "提示信息", + "type": "string" + } + } + }, + "keyword.keywordListData": { + "type": "object", + "properties": { + "app_id": { + "description": "小程序ID", + "type": "string" + }, + "created_at": { + "description": "创建时间", + "type": "string" + }, + "created_user": { + "description": "创建人", + "type": "string" + }, + "id": { + "description": "关键字编号", + "type": "integer" + }, + "keyword": { + "description": "意图关键字", + "type": "string" + }, + "material_type_count": { + "description": "素材类型数量", + "type": "string" + }, + "updated_at": { + "description": "更新时间", + "type": "string" + }, + "updated_user": { + "description": "更新人", + "type": "string" + } + } + }, + "keyword.keywordPageListResponse": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/keyword.keywordListData" + } + }, + "page": { + "description": "当前页码", + "type": "integer" + }, + "page_size": { + "description": "每页返回的数据量", + "type": "integer" + }, + "total": { + "description": "符合查询条件的总记录数", + "type": "integer" + } + } + }, + "keyword.modifyKeywordRequest": { + "type": "object", + "required": [ + "keyword" + ], + "properties": { + "keyword": { + "description": "意图关键字", + "type": "string" + } + } + }, + "keyword.modifyKeywordResponse": { + "type": "object", + "properties": { + "message": { + "description": "提示信息", + "type": "string" + } + } + }, "upload.uploadImageResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 97fceb7..4405446 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -129,6 +129,90 @@ definitions: description: 描述信息 type: string type: object + keyword.createKeywordRequest: + properties: + app_id: + description: 小程序ID + type: string + keyword: + description: 关键字 + type: string + required: + - app_id + - keyword + type: object + keyword.createKeywordResponse: + properties: + id: + description: 关键字ID + type: integer + message: + description: 提示信息 + type: string + type: object + keyword.deleteKeywordResponse: + properties: + message: + description: 提示信息 + type: string + type: object + keyword.keywordListData: + properties: + app_id: + description: 小程序ID + type: string + created_at: + description: 创建时间 + type: string + created_user: + description: 创建人 + type: string + id: + description: 关键字编号 + type: integer + keyword: + description: 意图关键字 + type: string + material_type_count: + description: 素材类型数量 + type: string + updated_at: + description: 更新时间 + type: string + updated_user: + description: 更新人 + type: string + type: object + keyword.keywordPageListResponse: + properties: + list: + items: + $ref: '#/definitions/keyword.keywordListData' + type: array + page: + description: 当前页码 + type: integer + page_size: + description: 每页返回的数据量 + type: integer + total: + description: 符合查询条件的总记录数 + type: integer + type: object + keyword.modifyKeywordRequest: + properties: + keyword: + description: 意图关键字 + type: string + required: + - keyword + type: object + keyword.modifyKeywordResponse: + properties: + message: + description: 提示信息 + type: string + type: object upload.uploadImageResponse: properties: preview_image_url: @@ -232,6 +316,136 @@ paths: summary: 删除小程序 tags: - 管理端.小程序 + /admin/app/keyword: + post: + consumes: + - application/json + description: 添加意图关键字 + parameters: + - description: 请求参数 + in: body + name: RequestBody + required: true + schema: + $ref: '#/definitions/keyword.createKeywordRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/keyword.createKeywordResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] + summary: 添加意图关键字 + tags: + - 管理端.意图关键字 + /admin/app/keyword/{id}: + delete: + consumes: + - application/json + description: 删除意图关键字 + parameters: + - description: 编号ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/keyword.deleteKeywordResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] + summary: 删除意图关键字 + tags: + - 管理端.意图关键字 + put: + consumes: + - application/json + description: 修改意图关键字 + parameters: + - description: 编号ID + in: path + name: id + required: true + type: string + - description: 请求参数 + in: body + name: RequestBody + required: true + schema: + $ref: '#/definitions/keyword.modifyKeywordRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/keyword.modifyKeywordResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] + summary: 修改意图关键字 + tags: + - 管理端.意图关键字 + /admin/app/keywords: + get: + consumes: + - application/json + description: 获取意图关键字列表 + parameters: + - description: 小程序ID + in: query + name: app_id + required: true + type: integer + - description: 意图关键字 + in: query + name: keyword + type: string + - default: 1 + description: 当前页码 + in: query + name: page + required: true + type: integer + - default: 20 + description: 每页返回的数据量,最多 100 条 + in: query + name: page_size + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/keyword.keywordPageListResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/code.Failure' + security: + - LoginVerifyToken: [] + summary: 获取意图关键字列表 + tags: + - 管理端.意图关键字 /admin/apps: get: consumes: diff --git a/internal/api/app/func_list.go b/internal/api/app/func_list.go index fb2f9dd..7568e83 100755 --- a/internal/api/app/func_list.go +++ b/internal/api/app/func_list.go @@ -6,6 +6,7 @@ import ( "mini-chat/internal/code" "mini-chat/internal/pkg/core" + "mini-chat/internal/pkg/timeutil" "mini-chat/internal/pkg/validation" "gorm.io/gorm" @@ -128,8 +129,8 @@ func (h *handler) PageList() core.HandlerFunc { Name: v.Name, Description: v.Description, Avatar: v.Avatar, - CreatedAt: v.CreatedAt.Format("2006-01-02 15:04:05"), - UpdatedAt: v.UpdatedAt.Format("2006-01-02 15:04:05"), + CreatedAt: timeutil.FriendlyTime(v.CreatedAt), + UpdatedAt: timeutil.FriendlyTime(v.CreatedAt), } } diff --git a/internal/api/keyword/kenword.go b/internal/api/keyword/kenword.go new file mode 100644 index 0000000..2a67213 --- /dev/null +++ b/internal/api/keyword/kenword.go @@ -0,0 +1,21 @@ +package keyword + +import ( + "mini-chat/internal/pkg/logger" + "mini-chat/internal/repository/mysql" + "mini-chat/internal/repository/mysql/dao" +) + +type handler struct { + logger logger.CustomLogger + writeDB *dao.Query + readDB *dao.Query +} + +func New(logger logger.CustomLogger, db mysql.Repo) *handler { + return &handler{ + logger: logger, + writeDB: dao.Use(db.GetDbW()), + readDB: dao.Use(db.GetDbR()), + } +} diff --git a/internal/api/keyword/keyword_create.go b/internal/api/keyword/keyword_create.go new file mode 100644 index 0000000..8269364 --- /dev/null +++ b/internal/api/keyword/keyword_create.go @@ -0,0 +1,82 @@ +package keyword + +import ( + "fmt" + "net/http" + "strings" + "time" + + "mini-chat/internal/code" + "mini-chat/internal/pkg/core" + "mini-chat/internal/pkg/validation" + "mini-chat/internal/repository/mysql/model" +) + +type createKeywordRequest struct { + AppID string `json:"app_id" binding:"required"` // 小程序ID + Keyword string `json:"keyword" binding:"required"` // 关键字 +} + +type createKeywordResponse struct { + Message string `json:"message"` // 提示信息 + ID int32 `json:"id"` // 关键字ID +} + +// CreateKeyword 添加意图关键字 +// @Summary 添加意图关键字 +// @Description 添加意图关键字 +// @Tags 管理端.意图关键字 +// @Accept json +// @Produce json +// @Param RequestBody body createKeywordRequest true "请求参数" +// @Success 200 {object} createKeywordResponse +// @Failure 400 {object} code.Failure +// @Router /admin/app/keyword [post] +// @Security LoginVerifyToken +func (h *handler) CreateKeyword() core.HandlerFunc { + return func(ctx core.Context) { + req := new(createKeywordRequest) + res := new(createKeywordResponse) + if err := ctx.ShouldBindJSON(req); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ParamBindError, + validation.Error(err)), + ) + return + } + + req.Keyword = strings.TrimSpace(req.Keyword) + + // 验证关键字是否已存在 + if _, err := h.readDB.AppKeyword.WithContext(ctx.RequestContext()). + Where(h.readDB.AppKeyword.AppID.Eq(req.AppID)). + Where(h.readDB.AppKeyword.Keyword.Eq(req.Keyword)). + First(); err == nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.CreateKeywordError, + fmt.Sprintf("%s: 该关键字(%s)已存在", code.Text(code.CreateKeywordError), req.Keyword)), + ) + return + } + + createData := new(model.AppKeyword) + createData.AppID = req.AppID + createData.Keyword = req.Keyword + createData.CreatedUser = ctx.SessionUserInfo().UserName + createData.CreatedAt = time.Now() + + if err := h.writeDB.AppKeyword.WithContext(ctx.RequestContext()).Create(createData); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.CreateKeywordError, + fmt.Sprintf("%s: %s", code.Text(code.CreateKeywordError), err.Error()), + )) + } + + res.Message = "操作成功" + res.ID = createData.ID + ctx.Payload(res) + } +} diff --git a/internal/api/keyword/keyword_delete.go b/internal/api/keyword/keyword_delete.go new file mode 100644 index 0000000..8a3a61c --- /dev/null +++ b/internal/api/keyword/keyword_delete.go @@ -0,0 +1,86 @@ +package keyword + +import ( + "fmt" + "net/http" + "strconv" + + "mini-chat/internal/code" + "mini-chat/internal/pkg/core" + + "gorm.io/gorm" +) + +type deleteKeywordResponse struct { + Message string `json:"message"` // 提示信息 +} + +// DeleteKeyword 删除意图关键字 +// @Summary 删除意图关键字 +// @Description 删除意图关键字 +// @Tags 管理端.意图关键字 +// @Accept json +// @Produce json +// @Param id path string true "编号ID" +// @Success 200 {object} deleteKeywordResponse +// @Failure 400 {object} code.Failure +// @Router /admin/app/keyword/{id} [delete] +// @Security LoginVerifyToken +func (h *handler) DeleteKeyword() core.HandlerFunc { + return func(ctx core.Context) { + ID, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ParamBindError, + "未传递编号ID。"), + ) + return + } + + info, err := h.readDB.AppKeyword.WithContext(ctx.RequestContext()). + Where(h.readDB.AppKeyword.ID.Eq(int32(ID))). + First() + if err != nil && err != gorm.ErrRecordNotFound { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.DeleteKeywordError, + fmt.Sprintf("%s: %s", code.Text(code.DeleteKeywordError), err.Error())), + ) + return + } + + if info == nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.DeleteKeywordError, + fmt.Sprintf("%s: 编号(%d)不存在。", code.Text(code.DeleteKeywordError), ID)), + ) + return + } + + if _, err := h.writeDB.AppKeyword.WithContext(ctx.RequestContext()). + Where(h.writeDB.AppKeyword.ID.Eq(int32(ID))). + Delete(); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.DeleteKeywordError, + fmt.Sprintf("%s: %s", code.Text(code.DeleteKeywordError), err.Error())), + ) + } + + if _, err := h.writeDB.AppKeywordReply.WithContext(ctx.RequestContext()). + Where(h.writeDB.AppKeywordReply.KeywordID.Eq(int32(ID))). + Delete(); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.DeleteKeywordError, + fmt.Sprintf("%s: %s", code.Text(code.DeleteKeywordError), err.Error())), + ) + } + + res := new(deleteKeywordResponse) + res.Message = "操作成功" + ctx.Payload(res) + } +} diff --git a/internal/api/keyword/keyword_list.go b/internal/api/keyword/keyword_list.go new file mode 100644 index 0000000..d5e39e3 --- /dev/null +++ b/internal/api/keyword/keyword_list.go @@ -0,0 +1,212 @@ +package keyword + +import ( + "fmt" + "net/http" + "strings" + + "mini-chat/internal/code" + "mini-chat/internal/pkg/core" + "mini-chat/internal/pkg/timeutil" + "mini-chat/internal/pkg/validation" + + "gorm.io/gorm" +) + +type keywordPageListRequest struct { + AppID string `form:"app_id" binding:"required"` // 小程序ID + Keyword string `form:"keyword"` // 意图关键字 + Page int `form:"page"` // 当前页码,默认为第一页 + PageSize int `form:"page_size"` // 每页返回的数据量 +} + +type keywordListData struct { + ID int32 `json:"id"` // 关键字编号 + AppID string `json:"app_id"` // 小程序ID + Keyword string `json:"keyword"` // 意图关键字 + CreatedUser string `json:"created_user"` // 创建人 + CreatedAt string `json:"created_at"` // 创建时间 + UpdatedUser string `json:"updated_user"` // 更新人 + UpdatedAt string `json:"updated_at"` // 更新时间 + MaterialTypeCount string `json:"material_type_count"` // 素材类型数量 +} + +type keywordPageListResponse struct { + Page int `json:"page"` // 当前页码 + PageSize int `json:"page_size"` // 每页返回的数据量 + Total int64 `json:"total"` // 符合查询条件的总记录数 + List []keywordListData `json:"list"` +} + +// KeywordPageList 获取意图关键字列表 +// @Summary 获取意图关键字列表 +// @Description 获取意图关键字列表 +// @Tags 管理端.意图关键字 +// @Accept json +// @Produce json +// @Param app_id query int true "小程序ID" +// @Param keyword query string false "意图关键字" +// @Param page query int true "当前页码" default(1) +// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20) +// @Success 200 {object} keywordPageListResponse +// @Failure 400 {object} code.Failure +// @Router /admin/app/keywords [get] +// @Security LoginVerifyToken +func (h *handler) KeywordPageList() core.HandlerFunc { + return func(ctx core.Context) { + req := new(keywordPageListRequest) + res := new(keywordPageListResponse) + if err := ctx.ShouldBindForm(req); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ParamBindError, + validation.Error(err)), + ) + return + } + + if req.Page == 0 { + req.Page = 1 + } + + if req.PageSize == 0 { + req.PageSize = 20 + } + + if req.PageSize > 100 { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListKeywordError, + fmt.Sprintf("%s: 一次最多只能查询 100 条", code.Text(code.ListKeywordError)), + )) + return + } + + query := h.readDB.AppKeyword.WithContext(ctx.RequestContext()) + query = query.Where(h.readDB.AppKeyword.AppID.Eq(req.AppID)) + + if req.Keyword != "" { + query = query.Where(h.readDB.AppKeyword.Keyword.Like(fmt.Sprintf("%%%s%%", req.Keyword))) + } + + listQueryDB := query.Session(&gorm.Session{}) + countQueryDB := query.Session(&gorm.Session{}) + + resultData, err := listQueryDB. + Order(h.readDB.AppKeyword.ID.Desc()). + Limit(req.PageSize). + Offset((req.Page - 1) * req.PageSize). + Find() + if err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListKeywordError, + fmt.Sprintf("%s:%s", code.Text(code.ListKeywordError), err.Error())), + ) + return + } + + count, err := countQueryDB.Count() + if err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListKeywordError, + fmt.Sprintf("%s:%s", code.Text(code.ListKeywordError), err.Error())), + ) + return + } + + res.Page = req.Page + res.PageSize = req.PageSize + res.Total = count + res.List = make([]keywordListData, len(resultData)) + + var keywordIDs []int32 + + for _, v := range resultData { + keywordIDs = append(keywordIDs, v.ID) + } + + keywordIDMaterialTypeCountMap, err := h.contactMaterialTypeCount(keywordIDs) + if err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ListKeywordError, + fmt.Sprintf("%s:%s", code.Text(code.ListKeywordError), err.Error())), + ) + return + } + + for k, v := range resultData { + res.List[k] = keywordListData{ + ID: v.ID, + AppID: v.AppID, + Keyword: v.Keyword, + CreatedUser: v.CreatedUser, + CreatedAt: timeutil.FriendlyTime(v.CreatedAt), + UpdatedUser: v.UpdatedUser, + UpdatedAt: timeutil.FriendlyTime(v.UpdatedAt), + MaterialTypeCount: keywordIDMaterialTypeCountMap[v.ID], + } + } + + ctx.Payload(res) + } +} + +func (h *handler) contactMaterialTypeCount(keywordIDs []int32) (map[int32]string, error) { + var results []struct { + KeywordID int32 + Type int + Count int `db:"count"` + } + + if err := h.readDB.AppKeywordReply. + Select(h.readDB.AppKeywordReply.KeywordID, h.readDB.AppKeywordReply.Type, h.readDB.AppKeywordReply.ID.Count().As("count")). + Where(h.readDB.AppKeywordReply.KeywordID.In(keywordIDs...)). + Group(h.readDB.AppKeywordReply.KeywordID, h.readDB.AppKeywordReply.Type). + Scan(&results); err != nil { + return nil, err + } + + keywordIDMap := make(map[int32]string) + + typeMap := map[int]string{ + 1: "文本", + 2: "图片", + 3: "语音条", + 4: "视频", + 5: "小程序", + 6: "地理位置", + 7: "链接", + 8: "GIF图", + 9: "名片", + 10: "文件", + 11: "转人工", + } + + for _, result := range results { + keywordID := result.KeywordID + typeDesc := fmt.Sprintf("%d条%s", result.Count, typeMap[result.Type]) + + if currentString, exists := keywordIDMap[keywordID]; exists { + keywordIDMap[keywordID] = currentString + "、" + typeDesc + "、" + } else { + keywordIDMap[keywordID] = "1 组话术(共" + typeDesc + } + } + + for keywordID, resultString := range keywordIDMap { + // 查找最后一个逗号的位置 + commaIndex := strings.LastIndex(resultString, "、") + + // 如果找到逗号,替换为右括号 + if commaIndex != -1 { + keywordIDMap[keywordID] = resultString[:commaIndex] + ")" + } else { + keywordIDMap[keywordID] += ")" + } + } + + return keywordIDMap, nil +} diff --git a/internal/api/keyword/keyword_modify.go b/internal/api/keyword/keyword_modify.go new file mode 100644 index 0000000..64cd6dc --- /dev/null +++ b/internal/api/keyword/keyword_modify.go @@ -0,0 +1,117 @@ +package keyword + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "mini-chat/internal/code" + "mini-chat/internal/pkg/core" + "mini-chat/internal/pkg/validation" + + "gorm.io/gorm" +) + +type modifyKeywordRequest struct { + Keyword string `json:"keyword" binding:"required"` // 意图关键字 +} + +type modifyKeywordResponse struct { + Message string `json:"message"` // 提示信息 +} + +// ModifyKeyword 修改意图关键字 +// @Summary 修改意图关键字 +// @Description 修改意图关键字 +// @Tags 管理端.意图关键字 +// @Accept json +// @Produce json +// @Param id path string true "编号ID" +// @Param RequestBody body modifyKeywordRequest true "请求参数" +// @Success 200 {object} modifyKeywordResponse +// @Failure 400 {object} code.Failure +// @Router /admin/app/keyword/{id} [put] +// @Security LoginVerifyToken +func (h *handler) ModifyKeyword() core.HandlerFunc { + return func(ctx core.Context) { + req := new(modifyKeywordRequest) + res := new(modifyKeywordResponse) + if err := ctx.ShouldBindJSON(req); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ParamBindError, + validation.Error(err)), + ) + return + } + + ID, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ParamBindError, + "未传递编号ID。"), + ) + return + } + + info, err := h.readDB.AppKeyword.WithContext(ctx.RequestContext()). + Where(h.readDB.AppKeyword.ID.Eq(int32(ID))). + First() + if err != nil && err != gorm.ErrRecordNotFound { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ModifyKeywordError, + fmt.Sprintf("%s: %s", code.Text(code.ModifyKeywordError), err.Error())), + ) + return + } + + if info == nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ModifyKeywordError, + fmt.Sprintf("%s: 编号(%d)不存在。", code.Text(code.ModifyKeywordError), ID)), + ) + return + } + + req.Keyword = strings.TrimSpace(req.Keyword) + + if info.Keyword != req.Keyword { + // 验证关键字是否已存在 + if _, err := h.readDB.AppKeyword.WithContext(ctx.RequestContext()). + Where(h.readDB.AppKeyword.AppID.Eq(info.AppID)). + Where(h.readDB.AppKeyword.Keyword.Eq(req.Keyword)). + First(); err == nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ModifyKeywordError, + fmt.Sprintf("%s: 该关键字(%s)已存在", code.Text(code.ModifyKeywordError), req.Keyword)), + ) + return + } + } + + updateData := map[string]interface{}{ + "keyword": req.Keyword, + "updated_user": ctx.SessionUserInfo().UserName, + "updated_at": time.Now(), + } + + if _, err := h.writeDB.AppKeyword.WithContext(ctx.RequestContext()). + Where(h.writeDB.AppKeyword.ID.Eq(int32(ID))). + Updates(updateData); err != nil { + ctx.AbortWithError(core.Error( + http.StatusBadRequest, + code.ModifyKeywordError, + fmt.Sprintf("%s: %s", code.Text(code.ModifyKeywordError), err.Error())), + ) + } + + res.Message = "操作成功" + ctx.Payload(res) + } +} diff --git a/internal/code/code.go b/internal/code/code.go index df25c71..e199ac3 100644 --- a/internal/code/code.go +++ b/internal/code/code.go @@ -26,6 +26,11 @@ const ( DeleteAppError = 20202 ListAppError = 20203 ModifyAppError = 20204 + + CreateKeywordError = 20301 + ListKeywordError = 20302 + ModifyKeywordError = 20303 + DeleteKeywordError = 20304 ) func Text(code int) string { diff --git a/internal/code/zh-cn.go b/internal/code/zh-cn.go index 29cac1e..06675fe 100644 --- a/internal/code/zh-cn.go +++ b/internal/code/zh-cn.go @@ -12,4 +12,9 @@ var zhCNText = map[int]string{ DeleteAppError: "删除小程序失败", ListAppError: "获取小程序列表失败", ModifyAppError: "修改小程序失败", + + CreateKeywordError: "创建关键字失败", + DeleteKeywordError: "删除关键字失败", + ListKeywordError: "获取关键字列表失败", + ModifyKeywordError: "修改关键字失败", } diff --git a/internal/pkg/core/core.go b/internal/pkg/core/core.go index 673a518..7ba0b1e 100644 --- a/internal/pkg/core/core.go +++ b/internal/pkg/core/core.go @@ -3,7 +3,6 @@ package core import ( "encoding/json" "fmt" - "mini-chat/internal/pkg/cors" "net/http" "net/url" "runtime/debug" @@ -12,6 +11,7 @@ import ( "mini-chat/configs" _ "mini-chat/docs" "mini-chat/internal/code" + "mini-chat/internal/pkg/cors" "mini-chat/internal/pkg/env" "mini-chat/internal/pkg/errors" "mini-chat/internal/pkg/logger" diff --git a/internal/router/router.go b/internal/router/router.go index 5be21dd..5b289e1 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -4,6 +4,7 @@ import ( "mini-chat/internal/alert" "mini-chat/internal/api/admin" "mini-chat/internal/api/app" + "mini-chat/internal/api/keyword" "mini-chat/internal/api/upload" "mini-chat/internal/cron" "mini-chat/internal/dblogger" @@ -43,6 +44,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo, cron cron.Server) (co adminHandler := admin.New(logger, db) appHandler := app.New(logger, db) uploadHandler := upload.New(logger, db) + keywordHandler := keyword.New(logger, db) // 管理端非认证接口路由组 adminNonAuthApiRouter := mux.Group("/admin") @@ -62,6 +64,11 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo, cron cron.Server) (co adminAuthApiRouter.POST("/app/delete", appHandler.DeleteApp()) // 删除小程序 adminAuthApiRouter.PUT("/app/:id", appHandler.ModifyApp()) // 修改小程序 adminAuthApiRouter.GET("/apps", appHandler.PageList()) // 小程序列表 + + adminAuthApiRouter.POST("/app/keyword", keywordHandler.CreateKeyword()) // 添加意图关键字 + adminAuthApiRouter.PUT("/app/keyword/:id", keywordHandler.ModifyKeyword()) // 修改意图关键字 + adminAuthApiRouter.DELETE("/app/keyword/:id", keywordHandler.DeleteKeyword()) // 删除意图关键字 + adminAuthApiRouter.GET("/app/keywords", keywordHandler.KeywordPageList()) // 获取意图关键字列表 } return mux, nil