diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go index 624055e0..487e6816 100644 --- a/docs/swagger/docs.go +++ b/docs/swagger/docs.go @@ -3525,9 +3525,9 @@ const docTemplate = `{ } } }, - "/votes/{type}/{target_id}": { + "/votes/{target_name}/{target_id}/{choice}": { "post": { - "description": "Vote for a specific comment or page", + "description": "Create a new vote for a specific comment or page", "consumes": [ "application/json" ], @@ -3537,36 +3537,45 @@ const docTemplate = `{ "tags": [ "Vote" ], - "summary": "Vote", - "operationId": "Vote", + "summary": "Create Vote", + "operationId": "CreateVote", "parameters": [ { "enum": [ - "comment_up", - "comment_down", - "page_up", - "page_down" + "comment", + "page" ], "type": "string", - "description": "The type of vote target", - "name": "type", + "description": "The name of vote target", + "name": "target_name", "in": "path", "required": true }, { "type": "integer", - "description": "Target comment or page ID you want to vote for", + "description": "The target comment or page ID", "name": "target_id", "in": "path", "required": true }, + { + "enum": [ + "up", + "down" + ], + "type": "string", + "description": "The vote choice", + "name": "choice", + "in": "path", + "required": true + }, { "description": "The vote data", "name": "vote", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.ParamsVote" + "$ref": "#/definitions/handler.ParamsVoteCreate" } } ], @@ -4430,7 +4439,7 @@ const docTemplate = `{ } } }, - "handler.ParamsVote": { + "handler.ParamsVoteCreate": { "type": "object", "properties": { "email": { @@ -5405,12 +5414,20 @@ const docTemplate = `{ "type": "object", "required": [ "down", + "is_down", + "is_up", "up" ], "properties": { "down": { "type": "integer" }, + "is_down": { + "type": "boolean" + }, + "is_up": { + "type": "boolean" + }, "up": { "type": "integer" } diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index 58941f6a..16c7a16f 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -3518,9 +3518,9 @@ } } }, - "/votes/{type}/{target_id}": { + "/votes/{target_name}/{target_id}/{choice}": { "post": { - "description": "Vote for a specific comment or page", + "description": "Create a new vote for a specific comment or page", "consumes": [ "application/json" ], @@ -3530,36 +3530,45 @@ "tags": [ "Vote" ], - "summary": "Vote", - "operationId": "Vote", + "summary": "Create Vote", + "operationId": "CreateVote", "parameters": [ { "enum": [ - "comment_up", - "comment_down", - "page_up", - "page_down" + "comment", + "page" ], "type": "string", - "description": "The type of vote target", - "name": "type", + "description": "The name of vote target", + "name": "target_name", "in": "path", "required": true }, { "type": "integer", - "description": "Target comment or page ID you want to vote for", + "description": "The target comment or page ID", "name": "target_id", "in": "path", "required": true }, + { + "enum": [ + "up", + "down" + ], + "type": "string", + "description": "The vote choice", + "name": "choice", + "in": "path", + "required": true + }, { "description": "The vote data", "name": "vote", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.ParamsVote" + "$ref": "#/definitions/handler.ParamsVoteCreate" } } ], @@ -4423,7 +4432,7 @@ } } }, - "handler.ParamsVote": { + "handler.ParamsVoteCreate": { "type": "object", "properties": { "email": { @@ -5398,12 +5407,20 @@ "type": "object", "required": [ "down", + "is_down", + "is_up", "up" ], "properties": { "down": { "type": "integer" }, + "is_down": { + "type": "boolean" + }, + "is_up": { + "type": "boolean" + }, "up": { "type": "integer" } diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index f1180203..5ed14bfe 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -574,7 +574,7 @@ definitions: - name - receive_email type: object - handler.ParamsVote: + handler.ParamsVoteCreate: properties: email: description: The user email @@ -1255,10 +1255,16 @@ definitions: properties: down: type: integer + is_down: + type: boolean + is_up: + type: boolean up: type: integer required: - down + - is_down + - is_up - up type: object info: @@ -3278,34 +3284,40 @@ paths: summary: Get Version Info tags: - System - /votes/{type}/{target_id}: + /votes/{target_name}/{target_id}/{choice}: post: consumes: - application/json - description: Vote for a specific comment or page - operationId: Vote + description: Create a new vote for a specific comment or page + operationId: CreateVote parameters: - - description: The type of vote target + - description: The name of vote target enum: - - comment_up - - comment_down - - page_up - - page_down + - comment + - page in: path - name: type + name: target_name required: true type: string - - description: Target comment or page ID you want to vote for + - description: The target comment or page ID in: path name: target_id required: true type: integer + - description: The vote choice + enum: + - up + - down + in: path + name: choice + required: true + type: string - description: The vote data in: body name: vote required: true schema: - $ref: '#/definitions/handler.ParamsVote' + $ref: '#/definitions/handler.ParamsVoteCreate' produces: - application/json responses: @@ -3340,7 +3352,7 @@ paths: msg: type: string type: object - summary: Vote + summary: Create Vote tags: - Vote /votes/sync: diff --git a/internal/dao/query_find.go b/internal/dao/query_find.go index 1e995236..fa552814 100644 --- a/internal/dao/query_find.go +++ b/internal/dao/query_find.go @@ -222,11 +222,11 @@ func (dao *Dao) GetVoteNum(targetID uint, voteType string) int { return int(num) } -func (dao *Dao) GetVoteNumUpDown(targetID uint, voteTo string) (int, int) { +func (dao *Dao) GetVoteNumUpDown(targetName string, targetID uint) (int, int) { var up int64 var down int64 - dao.DB().Model(&entity.Vote{}).Where("target_id = ? AND type = ?", targetID, voteTo+"_up").Count(&up) - dao.DB().Model(&entity.Vote{}).Where("target_id = ? AND type = ?", targetID, voteTo+"_down").Count(&down) + dao.DB().Model(&entity.Vote{}).Where("target_id = ? AND type = ?", targetID, targetName+"_up").Count(&up) + dao.DB().Model(&entity.Vote{}).Where("target_id = ? AND type = ?", targetID, targetName+"_down").Count(&down) return int(up), int(down) } diff --git a/server/handler/vote.go b/server/handler/vote.go index c44cab69..db959ced 100644 --- a/server/handler/vote.go +++ b/server/handler/vote.go @@ -4,140 +4,182 @@ import ( "strings" "github.com/artalkjs/artalk/v2/internal/core" + "github.com/artalkjs/artalk/v2/internal/dao" "github.com/artalkjs/artalk/v2/internal/entity" "github.com/artalkjs/artalk/v2/internal/i18n" "github.com/artalkjs/artalk/v2/server/common" "github.com/gofiber/fiber/v2" ) -type ParamsVote struct { - Name string `json:"name" validate:"optional"` // The username - Email string `json:"email" validate:"optional"` // The user email +type ResponseVote struct { + Up int `json:"up"` + Down int `json:"down"` + IsUp bool `json:"is_up"` + IsDown bool `json:"is_down"` } -type ResponseVote struct { - Up int `json:"up"` - Down int `json:"down"` +type ParamsVoteCreate struct { + Name string `json:"name" validate:"optional"` // The username + Email string `json:"email" validate:"optional"` // The user email } -// @Id Vote -// @Summary Vote -// @Description Vote for a specific comment or page +// @Id CreateVote +// @Summary Create Vote +// @Description Create a new vote for a specific comment or page // @Tags Vote -// @Param type path string true "The type of vote target" Enums(comment_up, comment_down, page_up, page_down) -// @Param target_id path int true "Target comment or page ID you want to vote for" -// @Param vote body ParamsVote true "The vote data" +// @Param target_name path string true "The name of vote target" Enums(comment, page) +// @Param target_id path int true "The target comment or page ID" +// @Param choice path string true "The vote choice" Enums(up, down) +// @Param vote body ParamsVoteCreate true "The vote data" // @Accept json // @Produce json // @Success 200 {object} ResponseVote // @Failure 403 {object} Map{msg=string} // @Failure 404 {object} Map{msg=string} // @Failure 500 {object} Map{msg=string} -// @Router /votes/{type}/{target_id} [post] -func Vote(app *core.App, router fiber.Router) { - router.Post("/votes/:type/:target_id", common.LimiterGuard(app, func(c *fiber.Ctx) error { - rawType := c.Params("type") +// @Router /votes/{target_name}/{target_id}/{choice} [post] +func VoteCreate(app *core.App, router fiber.Router) { + router.Post("/votes/:target_name/:target_id/:choice", common.LimiterGuard(app, func(c *fiber.Ctx) error { + targetName := c.Params("target_name") targetID, _ := c.ParamsInt("target_id") + choice := c.Params("choice") + ip := c.IP() - var p ParamsVote + var p ParamsVoteCreate if isOK, resp := common.ParamsDecode(c, &p); !isOK { return resp } - // find user - var user entity.User - if p.Name != "" && p.Email != "" { - var err error - user, err = app.Dao().FindCreateUser(p.Name, p.Email, "") - if err != nil { - return common.RespError(c, 500, "Failed to create user") - } - } - - ip := c.IP() - - // check type - isVoteComment := strings.HasPrefix(rawType, "comment_") - isVotePage := strings.HasPrefix(rawType, "page_") - isUp := strings.HasSuffix(rawType, "_up") - isDown := strings.HasSuffix(rawType, "_down") - voteTo := strings.TrimSuffix(strings.TrimSuffix(rawType, "_up"), "_down") - voteType := strings.TrimPrefix(strings.TrimPrefix(rawType, "comment_"), "page_") - - if !isUp && !isDown { - return common.RespError(c, 404, "unknown type") + if choice != "up" && choice != "down" { + return common.RespError(c, 404, "unknown vote choice") } - var comment entity.Comment - var page entity.Page + // Find the target model + var ( + comment entity.Comment + page entity.Page + user entity.User + ) - switch { - case isVoteComment: + switch targetName { + case "comment": comment = app.Dao().FindComment(uint(targetID)) if comment.IsEmpty() { return common.RespError(c, 404, i18n.T("{{name}} not found", Map{"name": i18n.T("Comment")})) } - case isVotePage: + case "page": page = app.Dao().FindPageByID(uint(targetID)) if page.IsEmpty() { return common.RespError(c, 404, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")})) } default: - return common.RespError(c, 404, "unknown type") + return common.RespError(c, 404, "unknown vote target name") } - // sync target model field value - save := func(up int, down int) { - switch { - case isVoteComment: + // Find user + if p.Name != "" && p.Email != "" { + var err error + user, err = app.Dao().FindCreateUser(p.Name, p.Email, "") + if err != nil { + return common.RespError(c, 500, "Failed to create user") + } + } + + // Sync target model field value + sync := func() (int, int) { + up, down := app.Dao().GetVoteNumUpDown(targetName, uint(targetID)) + + switch targetName { + case "comment": comment.VoteUp = up comment.VoteDown = down app.Dao().UpdateComment(&comment) - case isVotePage: + case "page": page.VoteUp = up page.VoteDown = down app.Dao().UpdatePage(&page) } - } - createNew := func(t string) error { - // create new vote record - _, err := app.Dao().NewVote(uint(targetID), entity.VoteType(t), user.ID, string(c.Request().Header.UserAgent()), ip) + return up, down + } - return err + // Create new vote record + create := func(choice string) error { + return createVote(app.Dao(), createNewVoteParams{ + ip: ip, + ua: string(c.Request().Header.UserAgent()), + userID: user.ID, + targetName: targetName, + targetID: uint(targetID), + choice: choice, + }) } - // un-vote - var availableVotes []entity.Vote - app.Dao().DB().Where("target_id = ? AND type LIKE ? AND ip = ?", uint(targetID), voteTo+"%", ip).Find(&availableVotes) - if len(availableVotes) > 0 { - for _, v := range availableVotes { + exitsVotes := getExistsVotesByIP(app.Dao(), ip, targetName, uint(targetID)) + if len(exitsVotes) == 0 { + // vote + create(choice) + } else { + exitsChoice := getVoteChoice(string(exitsVotes[0].Type)) + + // un-vote all if already exists + for _, v := range exitsVotes { app.Dao().DB().Unscoped().Delete(&v) } - avaVoteType := strings.TrimPrefix(strings.TrimPrefix(string(availableVotes[0].Type), "comment_"), "page_") - if voteType != avaVoteType { - createNew(rawType) + if choice != exitsChoice { + // vote opposite choice + create(choice) + } else { + // if choice is same then only un-vote + // reset choice to initial state + choice = "" } - - up, down := app.Dao().GetVoteNumUpDown(uint(targetID), voteTo) - save(up, down) - - return common.RespData(c, ResponseVote{ - Up: up, - Down: down, - }) } - createNew(rawType) - // sync - up, down := app.Dao().GetVoteNumUpDown(uint(targetID), voteTo) - save(up, down) + up, down := sync() return common.RespData(c, ResponseVote{ - Up: up, - Down: down, + Up: up, + Down: down, + IsUp: choice == "up", + IsDown: choice == "down", }) })) } + +// VoteChoice is `up` or `down` +func getVoteChoice(voteType string) string { + choice := strings.TrimPrefix(strings.TrimPrefix(voteType, "comment_"), "page_") + if choice != "up" && choice != "down" { + return "" + } + return choice +} + +// VoteTarget is `comment` or `page` +func getVoteTargetName(voteType string) string { + return strings.TrimSuffix(strings.TrimSuffix(voteType, "_up"), "_down") +} + +func getExistsVotesByIP(dao *dao.Dao, ip string, targetName string, targetID uint) []entity.Vote { + var existsVotes []entity.Vote + dao.DB().Where("type LIKE ? AND target_id = ? AND ip = ?", targetName+"%", uint(targetID), ip).Find(&existsVotes) + return existsVotes +} + +type createNewVoteParams struct { + ip string + ua string + userID uint + targetName string + targetID uint + choice string +} + +// Create new vote record +func createVote(dao *dao.Dao, opts createNewVoteParams) error { + _, err := dao.NewVote(opts.targetID, entity.VoteType(opts.targetName+"_"+opts.choice), opts.userID, opts.ua, opts.ip) + return err +} diff --git a/server/server.go b/server/server.go index 68e9fa35..7100d6e8 100644 --- a/server/server.go +++ b/server/server.go @@ -71,7 +71,7 @@ func Serve(app *core.App) (*fiber.App, error) { h.CommentCreate(app, api) h.CommentList(app, api) h.CommentGet(app, api) - h.Vote(app, api) + h.VoteCreate(app, api) h.PagePV(app, api) h.Stat(app, api) h.NotifyList(app, api) diff --git a/ui/artalk/src/api/v2.ts b/ui/artalk/src/api/v2.ts index 503c182e..4e837b64 100644 --- a/ui/artalk/src/api/v2.ts +++ b/ui/artalk/src/api/v2.ts @@ -298,7 +298,7 @@ export interface HandlerParamsUserUpdate { receive_email: boolean } -export interface HandlerParamsVote { +export interface HandlerParamsVoteCreate { /** The user email */ email?: string /** The username */ @@ -591,6 +591,8 @@ export interface HandlerResponseUserUpdate { export interface HandlerResponseVote { down: number + is_down: boolean + is_up: boolean up: number } @@ -2533,12 +2535,12 @@ export class Api extends HttpClient extends HttpClient this.request< @@ -2565,7 +2568,7 @@ export class Api extends HttpClient({ - path: `/votes/${type}/${targetId}`, + path: `/votes/${targetName}/${targetId}/${choice}`, method: 'POST', body: vote, type: ContentType.Json, diff --git a/ui/artalk/src/comment/actions.ts b/ui/artalk/src/comment/actions.ts index 455928ee..3876bf6c 100644 --- a/ui/artalk/src/comment/actions.ts +++ b/ui/artalk/src/comment/actions.ts @@ -20,12 +20,12 @@ export default class CommentActions { } /** 投票操作 */ - public vote(type: 'up' | 'down') { + public vote(choice: 'up' | 'down') { const actionBtn = - type === 'up' ? this.comment.getRender().voteBtnUp : this.comment.getRender().voteBtnDown + choice === 'up' ? this.comment.getRender().voteBtnUp : this.comment.getRender().voteBtnDown this.getApi() - .votes.vote(`comment_${type}`, this.data.id, { + .votes.createVote('comment', this.data.id, choice, { ...this.getApi().getUserFields(), }) .then((res) => {