diff --git a/apis/favourite/api.go b/apis/favourite/api.go index f50601f..8cbadb2 100644 --- a/apis/favourite/api.go +++ b/apis/favourite/api.go @@ -4,11 +4,10 @@ import ( "github.com/opentreehole/go-common" "gorm.io/gorm" "gorm.io/plugin/dbresolver" - - . "treehole_next/models" - . "treehole_next/utils" + "treehole_next/utils" "github.com/gofiber/fiber/v2" + . "treehole_next/models" ) // ListFavorites @@ -17,8 +16,7 @@ import ( // @Tags Favorite // @Produce application/json // @Router /user/favorites [get] -// @Param object query ListModel false "query" -// @Success 200 {object} models.Map +// @Param object query ListFavoriteModel false "query" // @Success 200 {array} models.Hole func ListFavorites(c *fiber.Ctx) error { // get userID @@ -27,15 +25,25 @@ func ListFavorites(c *fiber.Ctx) error { return err } - var query ListModel + var query ListFavoriteModel err = common.ValidateQuery(c, &query) if err != nil { return err } + if query.FavoriteGroupID != nil { + if !IsFavoriteGroupExist(DB, userID, *query.FavoriteGroupID) { + return common.Forbidden("收藏夹不存在") + } + } if query.Plain { // get favorite ids - data, err := UserGetFavoriteData(DB, userID) + var data []int + if query.FavoriteGroupID == nil { + data, err = UserGetFavoriteData(DB, userID) + } else { + data, err = UserGetFavoriteDataByFavoriteGroup(DB, userID, *query.FavoriteGroupID) + } if err != nil { return err } @@ -54,13 +62,20 @@ func ListFavorites(c *fiber.Ctx) error { // get favorites holes := make(Holes, 0) - err = DB. - Joins("JOIN user_favorites ON user_favorites.hole_id = hole.id AND user_favorites.user_id = ?", userID). - Order(order).Find(&holes).Error + if query.FavoriteGroupID == nil { + err = DB. + Joins("JOIN user_favorites ON user_favorites.hole_id = hole.id AND user_favorites.user_id = ?", userID). + Order(order).Find(&holes).Error + } else { + err = DB. + Joins("JOIN user_favorites ON user_favorites.hole_id = hole.id AND user_favorites.user_id = ? AND user_favorites.favorite_group_id = ?", userID, *query.FavoriteGroupID). + Order(order).Find(&holes).Error + } + if err != nil { return err } - return Serialize(c, &holes) + return utils.Serialize(c, &holes) } } @@ -92,7 +107,7 @@ func AddFavorite(c *fiber.Ctx) error { err = DB.Clauses(dbresolver.Write).Transaction(func(tx *gorm.DB) error { // add favorite - err = AddUserFavourite(tx, userID, body.HoleID) + err = AddUserFavorite(tx, userID, body.HoleID, body.FavoriteGroupID) if err != nil { return err } @@ -135,7 +150,7 @@ func ModifyFavorite(c *fiber.Ctx) error { } // modify favorite - err = ModifyUserFavourite(DB, userID, body.HoleIDs) + err = ModifyUserFavorite(DB, userID, body.HoleIDs, body.FavoriteGroupID) if err != nil { return err } @@ -176,7 +191,7 @@ func DeleteFavorite(c *fiber.Ctx) error { } // delete favorite - err = DB.Delete(UserFavorite{UserID: userID, HoleID: body.HoleID}).Error + err = DeleteUserFavorite(DB, userID, body.HoleID, body.FavoriteGroupID) if err != nil { return err } @@ -192,3 +207,199 @@ func DeleteFavorite(c *fiber.Ctx) error { Data: data, }) } + +// ListFavoriteGroups +// +// @Summary List User's Favorite Groups +// @Tags Favorite +// @Produce application/json +// @Router /user/favorite_groups [get] +// @Param object query ListFavoriteGroupModel false "query" +// @Success 200 {array} models.FavoriteGroup +func ListFavoriteGroups(c *fiber.Ctx) error { + // get userID + userID, err := common.GetUserID(c) + if err != nil { + return err + } + + var query ListFavoriteGroupModel + err = common.ValidateQuery(c, &query) + if err != nil { + return err + } + + if query.Plain { + // get favoriteGroups + data, err := UserGetFavoriteGroups(DB, userID) + if err != nil { + return err + } + return c.JSON(Map{"data": data}) + } else { + // get order + var order string + switch query.Order { + case "id": + order = "id desc" + case "time_created": + order = "created_at desc, id desc" + case "time_updated": + order = "updated_at desc, id desc" + } + + // get favoriteGroups + var data FavoriteGroups + err = DB.Where("user_id = ? AND deleted = false", userID).Order(order).Find(&data).Error + if err != nil { + return err + } + return c.JSON(&data) + } +} + +// AddFavoriteGroup +// +// @Summary Add A Favorite Group +// @Tags Favorite +// @Accept application/json +// @Produce application/json +// @Router /user/favorite_groups [post] +// @Param json body AddFavoriteGroupModel true "json" +// @Success 201 {array} models.FavoriteGroup +func AddFavoriteGroup(c *fiber.Ctx) error { + // validate body + var body AddFavoriteGroupModel + err := common.ValidateBody(c, &body) + if err != nil { + return err + } + + // get userID + userID, err := common.GetUserID(c) + if err != nil { + return err + } + + // add favorite group + err = AddUserFavoriteGroup(DB, userID, body.Name) + if err != nil { + return err + } + + // create response + data, err := UserGetFavoriteGroups(DB, userID) + if err != nil { + return err + } + + return c.Status(201).JSON(&data) +} + +// ModifyFavoriteGroup +// +// @Summary Modify User's Favorite Group +// @Tags Favorite +// @Produce application/json +// @Router /user/favorite_groups [put] +// @Param json body ModifyFavoriteGroupModel true "json" +// @Success 200 {array} models.FavoriteGroup +// @Failure 404 {object} common.HttpError +func ModifyFavoriteGroup(c *fiber.Ctx) error { + // validate body + var body ModifyFavoriteGroupModel + err := common.ValidateBody(c, &body) + if err != nil { + return err + } + + // get userID + userID, err := common.GetUserID(c) + if err != nil { + return err + } + + // modify favorite group + err = ModifyUserFavoriteGroup(DB, userID, *body.FavoriteGroupID, body.Name) + if err != nil { + return err + } + + // create response + data, err := UserGetFavoriteGroups(DB, userID) + if err != nil { + return err + } + + return c.JSON(&data) +} + +// DeleteFavoriteGroup +// +// @Summary Delete A Favorite Group +// @Tags Favorite +// @Produce application/json +// @Router /user/favorite_groups [delete] +// @Param json body DeleteModel true "json" +// @Success 204 +// @Failure 404 {object} common.HttpError +func DeleteFavoriteGroup(c *fiber.Ctx) error { + // validate body + var body DeleteFavoriteGroupModel + err := common.ValidateBody(c, &body) + if err != nil { + return err + } + + // get userID + userID, err := common.GetUserID(c) + if err != nil { + return err + } + + // delete favorite group + err = DeleteUserFavoriteGroup(DB, userID, *body.FavoriteGroupID) + if err != nil { + return err + } + + return c.Status(204).JSON(nil) +} + +// MoveFavorite +// +// @Summary Move User's Favorite +// @Tags Favorite +// @Produce application/json +// @Router /user/favorites/move [put] +// @Param json body MoveModel true "json" +// @Success 200 {array} models.Hole +// @Failure 404 {object} Response +func MoveFavorite(c *fiber.Ctx) error { + // validate body + var body MoveModel + err := common.ValidateBody(c, &body) + if err != nil { + return err + } + + // get userID + userID, err := common.GetUserID(c) + if err != nil { + return err + } + + // move favorite + err = MoveUserFavorite(DB, userID, body.HoleIDs, *body.FromFavoriteGroupID, *body.ToFavoriteGroupID) + if err != nil { + return err + } + + // create response + data, err := UserGetFavoriteData(DB, userID) + if err != nil { + return err + } + + return c.JSON(&data) +} diff --git a/apis/favourite/routes.go b/apis/favourite/routes.go index eda07af..c0a94b6 100644 --- a/apis/favourite/routes.go +++ b/apis/favourite/routes.go @@ -7,4 +7,9 @@ func RegisterRoutes(app fiber.Router) { app.Post("/user/favorites", AddFavorite) app.Put("/user/favorites", ModifyFavorite) app.Delete("/user/favorites", DeleteFavorite) + app.Get("/user/favorite_groups", ListFavoriteGroups) + app.Post("/user/favorite_groups", AddFavoriteGroup) + app.Put("/user/favorite_groups", ModifyFavoriteGroup) + app.Delete("/user/favorite_groups", DeleteFavoriteGroup) + app.Put("/user/favorites/move", MoveFavorite) } diff --git a/apis/favourite/schemas.go b/apis/favourite/schemas.go index 90ee0c9..c0922c3 100644 --- a/apis/favourite/schemas.go +++ b/apis/favourite/schemas.go @@ -5,19 +5,47 @@ type Response struct { Data []int `json:"data"` } -type ListModel struct { - Order string `json:"order" query:"order" validate:"omitempty,oneof=id time_created hole_time_updated" default:"time_created"` - Plain bool `json:"plain" default:"false" query:"plain"` +type ListFavoriteModel struct { + Order string `json:"order" query:"order" validate:"omitempty,oneof=id time_created hole_time_updated" default:"time_created"` + Plain bool `json:"plain" default:"false" query:"plain"` + FavoriteGroupID *int `json:"favorite_group_id" query:"favorite_group_id"` } type AddModel struct { - HoleID int `json:"hole_id"` + HoleID int `json:"hole_id"` + FavoriteGroupID int `json:"favorite_group_id" default:"0"` } type ModifyModel struct { - HoleIDs []int `json:"hole_ids"` + HoleIDs []int `json:"hole_ids"` + FavoriteGroupID int `json:"favorite_group_id" default:"0"` } type DeleteModel struct { - HoleID int `json:"hole_id"` + HoleID int `json:"hole_id"` + FavoriteGroupID int `json:"favorite_group_id" default:"0"` +} + +type AddFavoriteGroupModel struct { + Name string `json:"name" validate:"required,max=64"` +} + +type ModifyFavoriteGroupModel struct { + Name string `json:"name" validate:"required,max=64"` + FavoriteGroupID *int `json:"favorite_group_id" validate:"required"` +} + +type DeleteFavoriteGroupModel struct { + FavoriteGroupID *int `json:"favorite_group_id" validate:"required"` +} + +type MoveModel struct { + HoleIDs []int `json:"hole_ids"` + FromFavoriteGroupID *int `json:"from_favorite_group_id" default:"0" validate:"required"` + ToFavoriteGroupID *int `json:"to_favorite_group_id" validate:"required"` +} + +type ListFavoriteGroupModel struct { + Order string `json:"order" query:"order" validate:"omitempty,oneof=id time_created time_updated" default:"time_created"` + Plain bool `json:"plain" default:"false" query:"plain"` } diff --git a/docs/docs.go b/docs/docs.go index 31f643c..bbe8532 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1885,6 +1885,149 @@ const docTemplate = `{ } } }, + "/user/favorite_groups": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "List User's Favorite Groups", + "parameters": [ + { + "enum": [ + "id", + "time_created", + "time_updated" + ], + "type": "string", + "default": "time_created", + "name": "order", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "name": "plain", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.FavoriteGroup" + } + } + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "Modify User's Favorite Group", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/favourite.ModifyFavoriteGroupModel" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.FavoriteGroup" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/common.HttpError" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "Add A Favorite Group", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/favourite.AddFavoriteGroupModel" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.FavoriteGroup" + } + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "Delete A Favorite Group", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/favourite.DeleteModel" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/common.HttpError" + } + } + } + } + }, "/user/favorites": { "get": { "produces": [ @@ -1895,6 +2038,11 @@ const docTemplate = `{ ], "summary": "List User's Favorites", "parameters": [ + { + "type": "integer", + "name": "favorite_group_id", + "in": "query" + }, { "enum": [ "id", @@ -2031,6 +2179,45 @@ const docTemplate = `{ } } }, + "/user/favorites/move": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "Move User's Favorite", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/favourite.MoveModel" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Hole" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/favourite.Response" + } + } + } + } + }, "/users/me": { "get": { "produces": [ @@ -2356,12 +2543,19 @@ const docTemplate = `{ "field": { "type": "string" }, - "tag": { + "message": { "type": "string" }, - "value": { + "param": { "type": "string" - } + }, + "struct_field": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "value": {} } }, "common.HttpError": { @@ -2419,9 +2613,25 @@ const docTemplate = `{ } } }, + "favourite.AddFavoriteGroupModel": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 64 + } + } + }, "favourite.AddModel": { "type": "object", "properties": { + "favorite_group_id": { + "type": "integer", + "default": 0 + }, "hole_id": { "type": "integer" } @@ -2430,19 +2640,65 @@ const docTemplate = `{ "favourite.DeleteModel": { "type": "object", "properties": { + "favorite_group_id": { + "type": "integer", + "default": 0 + }, "hole_id": { "type": "integer" } } }, + "favourite.ModifyFavoriteGroupModel": { + "type": "object", + "required": [ + "favorite_group_id", + "name" + ], + "properties": { + "favorite_group_id": { + "type": "integer" + }, + "name": { + "type": "string", + "maxLength": 64 + } + } + }, "favourite.ModifyModel": { "type": "object", "properties": { + "favorite_group_id": { + "type": "integer", + "default": 0 + }, + "hole_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "favourite.MoveModel": { + "type": "object", + "required": [ + "from_favorite_group_id", + "to_favorite_group_id" + ], + "properties": { + "from_favorite_group_id": { + "type": "integer", + "default": 0 + }, "hole_ids": { "type": "array", "items": { "type": "integer" } + }, + "to_favorite_group_id": { + "type": "integer" } } }, @@ -2764,6 +3020,33 @@ const docTemplate = `{ } } }, + "models.FavoriteGroup": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "deleted": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string", + "default": "默认" + }, + "time_created": { + "type": "string" + }, + "time_updated": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, "models.Floor": { "type": "object", "properties": { @@ -3177,6 +3460,9 @@ const docTemplate = `{ "default_special_tag": { "type": "string" }, + "favorite_group_count": { + "type": "integer" + }, "has_answered_questions": { "type": "boolean" }, diff --git a/docs/swagger.json b/docs/swagger.json index 51ea899..27cb6f2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1878,6 +1878,149 @@ } } }, + "/user/favorite_groups": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "List User's Favorite Groups", + "parameters": [ + { + "enum": [ + "id", + "time_created", + "time_updated" + ], + "type": "string", + "default": "time_created", + "name": "order", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "name": "plain", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.FavoriteGroup" + } + } + } + } + }, + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "Modify User's Favorite Group", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/favourite.ModifyFavoriteGroupModel" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.FavoriteGroup" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/common.HttpError" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "Add A Favorite Group", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/favourite.AddFavoriteGroupModel" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.FavoriteGroup" + } + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "Delete A Favorite Group", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/favourite.DeleteModel" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/common.HttpError" + } + } + } + } + }, "/user/favorites": { "get": { "produces": [ @@ -1888,6 +2031,11 @@ ], "summary": "List User's Favorites", "parameters": [ + { + "type": "integer", + "name": "favorite_group_id", + "in": "query" + }, { "enum": [ "id", @@ -2024,6 +2172,45 @@ } } }, + "/user/favorites/move": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "Favorite" + ], + "summary": "Move User's Favorite", + "parameters": [ + { + "description": "json", + "name": "json", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/favourite.MoveModel" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Hole" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/favourite.Response" + } + } + } + } + }, "/users/me": { "get": { "produces": [ @@ -2349,12 +2536,19 @@ "field": { "type": "string" }, - "tag": { + "message": { "type": "string" }, - "value": { + "param": { "type": "string" - } + }, + "struct_field": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "value": {} } }, "common.HttpError": { @@ -2412,9 +2606,25 @@ } } }, + "favourite.AddFavoriteGroupModel": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 64 + } + } + }, "favourite.AddModel": { "type": "object", "properties": { + "favorite_group_id": { + "type": "integer", + "default": 0 + }, "hole_id": { "type": "integer" } @@ -2423,19 +2633,65 @@ "favourite.DeleteModel": { "type": "object", "properties": { + "favorite_group_id": { + "type": "integer", + "default": 0 + }, "hole_id": { "type": "integer" } } }, + "favourite.ModifyFavoriteGroupModel": { + "type": "object", + "required": [ + "favorite_group_id", + "name" + ], + "properties": { + "favorite_group_id": { + "type": "integer" + }, + "name": { + "type": "string", + "maxLength": 64 + } + } + }, "favourite.ModifyModel": { "type": "object", "properties": { + "favorite_group_id": { + "type": "integer", + "default": 0 + }, + "hole_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "favourite.MoveModel": { + "type": "object", + "required": [ + "from_favorite_group_id", + "to_favorite_group_id" + ], + "properties": { + "from_favorite_group_id": { + "type": "integer", + "default": 0 + }, "hole_ids": { "type": "array", "items": { "type": "integer" } + }, + "to_favorite_group_id": { + "type": "integer" } } }, @@ -2757,6 +3013,33 @@ } } }, + "models.FavoriteGroup": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "deleted": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string", + "default": "默认" + }, + "time_created": { + "type": "string" + }, + "time_updated": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, "models.Floor": { "type": "object", "properties": { @@ -3170,6 +3453,9 @@ "default_special_tag": { "type": "string" }, + "favorite_group_count": { + "type": "integer" + }, "has_answered_questions": { "type": "boolean" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2c32ae6..0ff3eec 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -4,10 +4,15 @@ definitions: properties: field: type: string - tag: + message: + type: string + param: type: string - value: + struct_field: + type: string + tag: type: string + value: {} type: object common.HttpError: properties: @@ -47,23 +52,66 @@ definitions: type: integer type: array type: object + favourite.AddFavoriteGroupModel: + properties: + name: + maxLength: 64 + type: string + required: + - name + type: object favourite.AddModel: properties: + favorite_group_id: + default: 0 + type: integer hole_id: type: integer type: object favourite.DeleteModel: properties: + favorite_group_id: + default: 0 + type: integer hole_id: type: integer type: object + favourite.ModifyFavoriteGroupModel: + properties: + favorite_group_id: + type: integer + name: + maxLength: 64 + type: string + required: + - favorite_group_id + - name + type: object favourite.ModifyModel: properties: + favorite_group_id: + default: 0 + type: integer hole_ids: items: type: integer type: array type: object + favourite.MoveModel: + properties: + from_favorite_group_id: + default: 0 + type: integer + hole_ids: + items: + type: integer + type: array + to_favorite_group_id: + type: integer + required: + - from_favorite_group_id + - to_favorite_group_id + type: object favourite.Response: properties: data: @@ -285,6 +333,24 @@ definitions: time_updated: type: string type: object + models.FavoriteGroup: + properties: + count: + type: integer + deleted: + type: boolean + id: + type: integer + name: + default: 默认 + type: string + time_created: + type: string + time_updated: + type: string + user_id: + type: integer + type: object models.Floor: properties: anonyname: @@ -575,6 +641,8 @@ definitions: $ref: '#/definitions/models.UserConfig' default_special_tag: type: string + favorite_group_count: + type: integer has_answered_questions: type: boolean id: @@ -1961,6 +2029,99 @@ paths: summary: List Holes By Tag tags: - Hole + /user/favorite_groups: + delete: + parameters: + - description: json + in: body + name: json + required: true + schema: + $ref: '#/definitions/favourite.DeleteModel' + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: Not Found + schema: + $ref: '#/definitions/common.HttpError' + summary: Delete A Favorite Group + tags: + - Favorite + get: + parameters: + - default: time_created + enum: + - id + - time_created + - time_updated + in: query + name: order + type: string + - default: false + in: query + name: plain + type: boolean + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.FavoriteGroup' + type: array + summary: List User's Favorite Groups + tags: + - Favorite + post: + consumes: + - application/json + parameters: + - description: json + in: body + name: json + required: true + schema: + $ref: '#/definitions/favourite.AddFavoriteGroupModel' + produces: + - application/json + responses: + "201": + description: Created + schema: + items: + $ref: '#/definitions/models.FavoriteGroup' + type: array + summary: Add A Favorite Group + tags: + - Favorite + put: + parameters: + - description: json + in: body + name: json + required: true + schema: + $ref: '#/definitions/favourite.ModifyFavoriteGroupModel' + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.FavoriteGroup' + type: array + "404": + description: Not Found + schema: + $ref: '#/definitions/common.HttpError' + summary: Modify User's Favorite Group + tags: + - Favorite /user/favorites: delete: parameters: @@ -1986,6 +2147,9 @@ paths: - Favorite get: parameters: + - in: query + name: favorite_group_id + type: integer - default: time_created enum: - id @@ -2056,6 +2220,31 @@ paths: summary: Modify User's Favorites tags: - Favorite + /user/favorites/move: + put: + parameters: + - description: json + in: body + name: json + required: true + schema: + $ref: '#/definitions/favourite.MoveModel' + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Hole' + type: array + "404": + description: Not Found + schema: + $ref: '#/definitions/favourite.Response' + summary: Move User's Favorite + tags: + - Favorite /users/{id}/punishments: get: parameters: diff --git a/models/favorite_group.go b/models/favorite_group.go new file mode 100644 index 0000000..298274a --- /dev/null +++ b/models/favorite_group.go @@ -0,0 +1,100 @@ +package models + +import ( + "errors" + "github.com/opentreehole/go-common" + "gorm.io/gorm" + "gorm.io/plugin/dbresolver" + "time" +) + +type FavoriteGroup struct { + ID int `json:"id" gorm:"primaryKey"` + UserID int `json:"user_id" gorm:"primaryKey"` + Name string `json:"name" gorm:"not null;size:64" default:"默认"` + CreatedAt time.Time `json:"time_created"` + UpdatedAt time.Time `json:"time_updated"` + Deleted bool `json:"deleted" gorm:"default:false"` + Count int `json:"count" gorm:"default:0"` +} + +const MaxGroupPerUser = 10 + +type FavoriteGroups []FavoriteGroup + +func (FavoriteGroup) TableName() string { + return "favorite_groups" +} + +func UserGetFavoriteGroups(tx *gorm.DB, userID int) (favoriteGroups FavoriteGroups, err error) { + err = tx.Where("user_id = ? and deleted = false", userID).Find(&favoriteGroups).Error + return +} + +func DeleteUserFavoriteGroup(tx *gorm.DB, userID int, groupID int) (err error) { + if groupID == 0 { + return common.Forbidden("默认收藏夹不可删除") + } + err = tx.Clauses(dbresolver.Write).Where("user_id = ? AND id = ?", userID, groupID).Updates(FavoriteGroup{Deleted: true}).Error + if err != nil { + return err + } + err = tx.Model(&UserFavorite{}).Where("user_id = ? AND favorite_group_id = ?", userID, groupID).Delete(&UserFavorite{}).Error + if err != nil { + return err + } + return tx.Model(&User{}).Where("id = ?", userID).Update("favorite_group_count", gorm.Expr("favorite_group_count - 1")).Error +} + +func CreateDefaultFavoriteGroup(tx *gorm.DB, userID int) (err error) { + return tx.Clauses(dbresolver.Write).Transaction(func(tx *gorm.DB) error { + err = tx.Create(&FavoriteGroup{ + UserID: userID, + Name: "默认收藏夹", + ID: 0, + CreatedAt: time.Now(), + }).Error + if err != nil { + return err + } + return tx.Model(&User{}).Where("id = ?", userID).Update("favorite_group_count", gorm.Expr("favorite_group_count + 1")).Error + }) + +} + +func AddUserFavoriteGroup(tx *gorm.DB, userID int, name string) (err error) { + return tx.Clauses(dbresolver.Write).Transaction(func(tx *gorm.DB) error { + var groupID int + err = tx.Model(&FavoriteGroup{}).Select("IFNULL(MAX(id), 0) AS max_id").Where("user_id = ? and deleted = false", userID). + Take(&groupID).Error + groupID++ + if err != nil { + return err + } + if groupID >= MaxGroupPerUser { + err = tx.Model(&FavoriteGroup{}).Where("user_id = ? and deleted = true", userID).Order("id").Limit(1).Take(&groupID).Error + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return common.Forbidden("收藏夹数量已达上限") + } + if err != nil { + return err + } + + err = tx.Create(&FavoriteGroup{ + UserID: userID, + Name: name, + ID: groupID, + CreatedAt: time.Now(), + }).Error + if err != nil { + return err + } + return tx.Model(&User{}).Where("id = ?", userID).Update("favorite_group_count", gorm.Expr("favorite_group_count + 1")).Error + }) +} + +func ModifyUserFavoriteGroup(tx *gorm.DB, userID int, groupID int, name string) (err error) { + return tx.Clauses(dbresolver.Write).Where("user_id = ? AND id = ?", userID, groupID). + Updates(FavoriteGroup{Name: name, UpdatedAt: time.Now()}).Error +} diff --git a/models/hole.go b/models/hole.go index 158d5f1..00add8f 100644 --- a/models/hole.go +++ b/models/hole.go @@ -83,6 +83,12 @@ func (hole *Hole) CacheName() string { type Holes []*Hole +func IsHolesExist(tx *gorm.DB, holeID []int) bool { + var num int64 + tx.Model(&Hole{}).Where("id in ?", holeID).Count(&num) + return num == int64(len(holeID)) +} + /************** get hole methods *******************/ diff --git a/models/init.go b/models/init.go index 056cddf..f688d27 100644 --- a/models/init.go +++ b/models/init.go @@ -117,11 +117,6 @@ func InitDB() { DB = DB.Debug() } - err = DB.SetupJoinTable(&User{}, "UserFavoriteHoles", &UserFavorite{}) - if err != nil { - log.Fatal().Err(err).Send() - } - err = DB.SetupJoinTable(&User{}, "UserLikedFloors", &FloorLike{}) if err != nil { log.Fatal().Err(err).Send() @@ -155,6 +150,8 @@ func InitDB() { &Message{}, &FloorHistory{}, &AdminLog{}, + &UserFavorite{}, + &FavoriteGroup{}, ) if err != nil { log.Fatal().Err(err).Send() diff --git a/models/user.go b/models/user.go index 88d1d1a..e4cffe4 100644 --- a/models/user.go +++ b/models/user.go @@ -31,10 +31,9 @@ type User struct { SpecialTags []string `json:"special_tags" gorm:"serializer:json;not null;default:\"[]\""` - /// association fields, should add foreign key + FavoriteGroupCount int `json:"favorite_group_count" gorm:"not null;default:0"` - // favorite holes of the user - UserFavoriteHoles Holes `json:"-" gorm:"many2many:user_favorite;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` + /// association fields, should add foreign key // holes owned by the user UserHoles Holes `json:"-" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` @@ -193,6 +192,13 @@ func (user *User) LoadUserByID(userID int) error { } } + if user.FavoriteGroupCount == 0 { + err = CreateDefaultFavoriteGroup(tx, userID) + if err != nil { + return err + } + } + // check permission modified := false for divisionID := range user.BanDivision { diff --git a/models/user_favorite.go b/models/user_favorite.go index 19c72f1..13443cf 100644 --- a/models/user_favorite.go +++ b/models/user_favorite.go @@ -1,6 +1,7 @@ package models import ( + "github.com/opentreehole/go-common" "time" "gorm.io/gorm" @@ -11,9 +12,10 @@ import ( ) type UserFavorite struct { - UserID int `json:"user_id" gorm:"primaryKey"` - HoleID int `json:"hole_id" gorm:"primaryKey"` - CreatedAt time.Time `json:"time_created"` + UserID int `json:"user_id" gorm:"primaryKey"` + FavoriteGroupID int `json:"favorite_group_id" gorm:"primaryKey"` + HoleID int `json:"hole_id" gorm:"primaryKey"` + CreatedAt time.Time `json:"time_created"` } type UserFavorites []UserFavorite @@ -22,13 +24,28 @@ func (UserFavorite) TableName() string { return "user_favorites" } -func ModifyUserFavourite(tx *gorm.DB, userID int, holeIDs []int) error { +func IsFavoriteGroupExist(tx *gorm.DB, userID int, favoriteGroupID int) bool { + var num int64 + tx.Model(&FavoriteGroup{}).Where("user_id = ? AND id = ? AND deleted = false", userID, favoriteGroupID).Count(&num) + return num > 0 +} + +// ModifyUserFavorite only take effect in the same favorite_group +func ModifyUserFavorite(tx *gorm.DB, userID int, holeIDs []int, favoriteGroupID int) error { if len(holeIDs) == 0 { return nil } + if !IsFavoriteGroupExist(tx, userID, favoriteGroupID) { + return common.Forbidden("收藏夹不存在") + } + if !IsHolesExist(tx, holeIDs) { + return common.Forbidden("帖子不存在") + } return tx.Clauses(dbresolver.Write).Transaction(func(tx *gorm.DB) error { var oldHoleIDs []int - err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Model(&UserFavorite{}).Select("hole_id").Scan(&oldHoleIDs).Error + err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&UserFavorite{}).Where("user_id = ? AND favorite_group_id = ?", userID, favoriteGroupID). + Pluck("hole_id", &oldHoleIDs).Error if err != nil { return err } @@ -47,7 +64,7 @@ func ModifyUserFavourite(tx *gorm.DB, userID int, holeIDs []int) error { if len(removingHoleIDs) > 0 { deleteUserFavorite := make(UserFavorites, 0) for _, holeID := range removingHoleIDs { - deleteUserFavorite = append(deleteUserFavorite, UserFavorite{UserID: userID, HoleID: holeID}) + deleteUserFavorite = append(deleteUserFavorite, UserFavorite{UserID: userID, HoleID: holeID, FavoriteGroupID: favoriteGroupID}) } err = tx.Delete(&deleteUserFavorite).Error if err != nil { @@ -69,27 +86,122 @@ func ModifyUserFavourite(tx *gorm.DB, userID int, holeIDs []int) error { if len(newHoleIDs) > 0 { insertUserFavorite := make(UserFavorites, 0) for _, holeID := range newHoleIDs { - insertUserFavorite = append(insertUserFavorite, UserFavorite{UserID: userID, HoleID: holeID}) + insertUserFavorite = append(insertUserFavorite, UserFavorite{UserID: userID, HoleID: holeID, FavoriteGroupID: favoriteGroupID}) } err = tx.Create(&insertUserFavorite).Error if err != nil { return err } } - return nil + return tx.Model(&FavoriteGroup{}).Where("user_id = ? AND id = ?", userID, favoriteGroupID).Update("number", len(holeIDs)).Error }) } -func AddUserFavourite(tx *gorm.DB, userID int, holeID int) error { - return tx.Clauses(clause.OnConflict{ +func AddUserFavorite(tx *gorm.DB, userID int, holeID int, favoriteGroupID int) error { + if !IsFavoriteGroupExist(tx, userID, favoriteGroupID) { + return common.Forbidden("收藏夹不存在") + } + if !IsHolesExist(tx, []int{holeID}) { + return common.Forbidden("帖子不存在") + } + var err = tx.Clauses(clause.OnConflict{ DoUpdates: clause.Assignments(Map{"created_at": time.Now()}), }).Create(&UserFavorite{ - UserID: userID, - HoleID: holeID}).Error + UserID: userID, + HoleID: holeID, + FavoriteGroupID: favoriteGroupID, + }).Error + if err != nil { + return err + } + return tx.Clauses(dbresolver.Write).Model(&FavoriteGroup{}). + Where("user_id = ? AND id = ?", userID, favoriteGroupID).Update("number", gorm.Expr("number + 1")).Error } +// UserGetFavoriteData get all favorite data of a user func UserGetFavoriteData(tx *gorm.DB, userID int) ([]int, error) { data := make([]int, 0, 10) - err := tx.Clauses(dbresolver.Write).Raw("SELECT hole_id FROM user_favorites WHERE user_id = ?", userID).Scan(&data).Error + err := tx.Clauses(dbresolver.Write).Model(&UserFavorite{}).Where("user_id = ?", userID).Distinct(). + Pluck("hole_id", &data).Error return data, err } + +// UserGetFavoriteDataByFavoriteGroup get favorite data in specific favorite group +func UserGetFavoriteDataByFavoriteGroup(tx *gorm.DB, userID int, favoriteGroupID int) ([]int, error) { + if !IsFavoriteGroupExist(tx, userID, favoriteGroupID) { + return nil, common.Forbidden("收藏夹不存在") + } + data := make([]int, 0, 10) + err := tx.Clauses(dbresolver.Write).Model(&UserFavorite{}). + Where("user_id = ? AND favorite_group_id = ?", userID, favoriteGroupID).Pluck("hole_id", &data).Error + return data, err +} + +// DeleteUserFavorite delete user favorite +// if user favorite hole only once, delete the hole +// otherwise, delete the favorite in the specific favorite group +func DeleteUserFavorite(tx *gorm.DB, userID int, holeID int, favoriteGroupID int) error { + if !IsFavoriteGroupExist(tx, userID, favoriteGroupID) { + return common.Forbidden("收藏夹不存在") + } + if !IsHolesExist(tx, []int{holeID}) { + return common.Forbidden("帖子不存在") + } + return tx.Clauses(dbresolver.Write).Transaction(func(tx *gorm.DB) error { + err := tx.Delete(&UserFavorite{UserID: userID, HoleID: holeID, FavoriteGroupID: favoriteGroupID}).Error + if err != nil { + return err + } + return tx.Clauses(dbresolver.Write).Model(&FavoriteGroup{}).Where("user_id = ? AND id = ?", userID, favoriteGroupID).Update("number", gorm.Expr("number - 1")).Error + }) +} + +// MoveUserFavorite move holes that are really in the fromFavoriteGroup +func MoveUserFavorite(tx *gorm.DB, userID int, holeIDs []int, fromFavoriteGroupID int, toFavoriteGroupID int) error { + if fromFavoriteGroupID == toFavoriteGroupID { + return nil + } + if len(holeIDs) == 0 { + return nil + } + if !IsFavoriteGroupExist(tx, userID, fromFavoriteGroupID) || !IsFavoriteGroupExist(tx, userID, toFavoriteGroupID) { + return common.Forbidden("收藏夹不存在") + } + if !IsHolesExist(tx, holeIDs) { + return common.Forbidden("帖子不存在") + } + return tx.Clauses(dbresolver.Write).Transaction(func(tx *gorm.DB) error { + var oldHoleIDs []int + err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&UserFavorite{}).Where("user_id = ? AND favorite_group_id = ?", userID, fromFavoriteGroupID). + Pluck("hole_id", &oldHoleIDs).Error + if err != nil { + return err + } + + // move user_favorite that in holeIDs + var removingHoleIDMapping = make(map[int]bool) + var removingHoleIDs []int + for _, holeID := range oldHoleIDs { + removingHoleIDMapping[holeID] = true + } + for _, holeID := range holeIDs { + if removingHoleIDMapping[holeID] { + removingHoleIDs = append(removingHoleIDs, holeID) + } + } + if len(removingHoleIDs) > 0 { + err = tx.Table("user_favorites"). + Where("user_id = ? AND favorite_group_id = ? AND hole_id IN ?", userID, fromFavoriteGroupID, removingHoleIDs). + Updates(map[string]interface{}{"favorite_group_id": toFavoriteGroupID}).Error + if err != nil { + return err + } + } + err = tx.Model(&FavoriteGroup{}).Where("user_id = ? AND id = ?", userID, fromFavoriteGroupID).Update("number", gorm.Expr("number - ?", len(removingHoleIDs))).Error + if err != nil { + return err + } + return tx.Model(&FavoriteGroup{}).Where("user_id = ? AND id = ?", userID, toFavoriteGroupID).Update("number", gorm.Expr("number + ?", len(removingHoleIDs))).Error + }) +} diff --git a/utils/utils.go b/utils/utils.go index fea853f..5594117 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "golang.org/x/exp/slices" "strconv" "github.com/gofiber/fiber/v2" @@ -49,6 +50,16 @@ func Min[T constraints.Ordered](x T, y T) T { } } +func Intersect[T comparable](x []T, y []T) []T { + var result = make([]T, 0) + for i := range x { + if slices.Contains(y, x[i]) { + result = append(result, x[i]) + } + } + return result +} + func StripContent(content string, contentMaxSize int) string { return string([]rune(content)[:Min(len([]rune(content)), contentMaxSize)]) }