From 4692420e5ff366f7d7e0d5d14a2afab1527cbd0b Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 25 Jul 2024 16:40:04 +0800 Subject: [PATCH 01/38] fix(update): fix update to v1.3.6 --- internal/migrations/migrations.go | 1 + internal/migrations/v21.go | 32 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 internal/migrations/v21.go diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index 8a259684d..463f68ed8 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -96,6 +96,7 @@ var migrations = []Migration{ NewMigration("v1.2.1", "add password login control", addPasswordLoginControl, true), NewMigration("v1.2.5", "add notification plugin and theme config", addNotificationPluginAndThemeConfig, true), NewMigration("v1.3.0", "add review", addReview, false), + NewMigration("v1.3.6", "add hot score to question table", addQuestionHotScore, true), } func GetMigrations() []Migration { diff --git a/internal/migrations/v21.go b/internal/migrations/v21.go new file mode 100644 index 000000000..880852f8e --- /dev/null +++ b/internal/migrations/v21.go @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "xorm.io/xorm" +) + +func addQuestionHotScore(ctx context.Context, x *xorm.Engine) error { + type Question struct { + HotScore int `xorm:"not null default 0 INT(11) hot_score"` + } + return x.Context(ctx).Sync(new(Question)) +} From 46fba3e2bd7dd4269611a41bb5c542962c76ff7c Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Tue, 30 Jul 2024 10:47:12 +0800 Subject: [PATCH 02/38] docs(docs): remove the images about installation --- docs/img/install-database.png | Bin 13214 -> 0 bytes docs/img/install-site-info.png | Bin 43093 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/img/install-database.png delete mode 100644 docs/img/install-site-info.png diff --git a/docs/img/install-database.png b/docs/img/install-database.png deleted file mode 100644 index 09fbf36a18bb039cfaec997dd76fc6e4b9c12599..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13214 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 diff --git a/docs/img/install-site-info.png b/docs/img/install-site-info.png deleted file mode 100644 index b8166caf426bca8f51a475b8c23da10617761ed1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43093 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 DY$>gM From 969592589cf85940c41a2077e5ebe4d22d68569c Mon Sep 17 00:00:00 2001 From: kumfo Date: Wed, 31 Jul 2024 10:32:10 +0800 Subject: [PATCH 03/38] feat(badge): init entity --- internal/base/constant/object_type.go | 23 +++++++++----- internal/entity/badge.go | 43 +++++++++++++++++++++++++++ internal/entity/badge_award.go | 39 ++++++++++++++++++++++++ internal/entity/badge_group.go | 35 ++++++++++++++++++++++ 4 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 internal/entity/badge.go create mode 100644 internal/entity/badge_award.go create mode 100644 internal/entity/badge_group.go diff --git a/internal/base/constant/object_type.go b/internal/base/constant/object_type.go index b3e3883df..eef952c44 100644 --- a/internal/base/constant/object_type.go +++ b/internal/base/constant/object_type.go @@ -27,6 +27,9 @@ const ( CollectionObjectType = "collection" CommentObjectType = "comment" ReportObjectType = "report" + BadgeObjectType = "badge" + BadgeAwardObjectType = "badge_award" + BadgeGroupObjectType = "badge_group" ) var ( @@ -38,15 +41,21 @@ var ( CollectionObjectType: 6, CommentObjectType: 7, ReportObjectType: 8, + BadgeObjectType: 9, + BadgeAwardObjectType: 10, + BadgeGroupObjectType: 11, } ObjectTypeNumberMapping = map[int]string{ - 1: QuestionObjectType, - 2: AnswerObjectType, - 3: TagObjectType, - 4: UserObjectType, - 6: CollectionObjectType, - 7: CommentObjectType, - 8: ReportObjectType, + 1: QuestionObjectType, + 2: AnswerObjectType, + 3: TagObjectType, + 4: UserObjectType, + 6: CollectionObjectType, + 7: CommentObjectType, + 8: ReportObjectType, + 9: BadgeObjectType, + 10: BadgeAwardObjectType, + 11: BadgeGroupObjectType, } ) diff --git a/internal/entity/badge.go b/internal/entity/badge.go new file mode 100644 index 000000000..d310ef799 --- /dev/null +++ b/internal/entity/badge.go @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// Badge badge +type Badge struct { + ID string `json:"id" xorm:"id"` + CreatedAt time.Time `json:"created_at" xorm:"created_at"` + UpdatedAt time.Time `json:"updated_at" xorm:"updated_at"` + Name string `json:"name" xorm:"name"` + AwardTotal int64 `json:"award_total" xorm:"award_total"` + Description string `json:"description" xorm:"description"` + Status int8 `json:"status" xorm:"status"` + BadgeGroupId int64 `json:"badge_group_id" xorm:"badge_group_id"` + Single int8 `json:"single" xorm:"single"` + Collect string `json:"collect" xorm:"collect"` + Handler string `json:"handler" xorm:"handler"` + Param string `json:"param" xorm:"param"` +} + +// TableName badge table name +func (*Badge) TableName() string { + return "badge" +} diff --git a/internal/entity/badge_award.go b/internal/entity/badge_award.go new file mode 100644 index 000000000..fe1dab96e --- /dev/null +++ b/internal/entity/badge_award.go @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// BadgeAward badge_award +type BadgeAward struct { + ID string `json:"id" xorm:"id"` + CreatedAt time.Time `json:"created_at" xorm:"created_at"` + UpdatedAt time.Time `json:"updated_at" xorm:"updated_at"` + UserId int64 `json:"user_id" xorm:"user_id"` + BadgeId int64 `json:"badge_id" xorm:"badge_id"` + ObjectId int64 `json:"object_id" xorm:"object_id"` + BadgeGroupId int8 `json:"badge_group_id" xorm:"badge_group_id"` + IsBadgeDeleted int8 `json:"is_badge_deleted" xorm:"is_badge_deleted"` +} + +// TableName badge_award table name +func (*BadgeAward) TableName() string { + return "badge_award" +} diff --git a/internal/entity/badge_group.go b/internal/entity/badge_group.go new file mode 100644 index 000000000..145352996 --- /dev/null +++ b/internal/entity/badge_group.go @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +// BadgeGroup badge_group +type BadgeGroup struct { + ID string `json:"id" xorm:"id"` + Name string `json:"name" xorm:"name"` + CreatedAt time.Time `json:"created_at" xorm:"created_at"` + UpdatedAt time.Time `json:"updated_at" xorm:"updated_at"` +} + +// TableName badge_group table name +func (*BadgeGroup) TableName() string { + return "badge_group" +} From b70229492031fcf117e25aa5a50e5dc8a1a6dcd5 Mon Sep 17 00:00:00 2001 From: kumfo Date: Wed, 31 Jul 2024 14:00:18 +0800 Subject: [PATCH 04/38] feat(badge): update entity --- internal/entity/badge.go | 27 ++++++++++++++++----------- internal/entity/badge_award.go | 14 +++++++------- internal/entity/badge_group.go | 8 ++++---- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/internal/entity/badge.go b/internal/entity/badge.go index d310ef799..0b296f611 100644 --- a/internal/entity/badge.go +++ b/internal/entity/badge.go @@ -21,20 +21,25 @@ package entity import "time" +const ( + BadgeStatusAvailable = 1 + BadgeStatusDeleted = 10 +) + // Badge badge type Badge struct { ID string `json:"id" xorm:"id"` - CreatedAt time.Time `json:"created_at" xorm:"created_at"` - UpdatedAt time.Time `json:"updated_at" xorm:"updated_at"` - Name string `json:"name" xorm:"name"` - AwardTotal int64 `json:"award_total" xorm:"award_total"` - Description string `json:"description" xorm:"description"` - Status int8 `json:"status" xorm:"status"` - BadgeGroupId int64 `json:"badge_group_id" xorm:"badge_group_id"` - Single int8 `json:"single" xorm:"single"` - Collect string `json:"collect" xorm:"collect"` - Handler string `json:"handler" xorm:"handler"` - Param string `json:"param" xorm:"param"` + CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` + Name string `json:"name" xorm:"not null default '' VARCHAR(256) name"` + AwardCount int64 `json:"award_count" xorm:"not null default 0 INT(11) award_count"` + Description string `json:"description" xorm:"not null default '' MEDIUMTEXT description"` + Status int8 `json:"status" xorm:"not null default 1 INT(11) status"` + BadgeGroupId int64 `json:"badge_group_id" xorm:"not null default 0 BIGINT(20) badge_group_id"` + Single int8 `json:"single" xorm:"not null default 1 TINYINT(4) single"` + Collect string `json:"collect" xorm:"not null default '' VARCHAR(64) collect"` + Handler string `json:"handler" xorm:"not null default '' VARCHAR(64) handler"` + Param string `json:"param" xorm:"not null default '' VARCHAR(128) param"` } // TableName badge table name diff --git a/internal/entity/badge_award.go b/internal/entity/badge_award.go index fe1dab96e..01100b1f2 100644 --- a/internal/entity/badge_award.go +++ b/internal/entity/badge_award.go @@ -24,13 +24,13 @@ import "time" // BadgeAward badge_award type BadgeAward struct { ID string `json:"id" xorm:"id"` - CreatedAt time.Time `json:"created_at" xorm:"created_at"` - UpdatedAt time.Time `json:"updated_at" xorm:"updated_at"` - UserId int64 `json:"user_id" xorm:"user_id"` - BadgeId int64 `json:"badge_id" xorm:"badge_id"` - ObjectId int64 `json:"object_id" xorm:"object_id"` - BadgeGroupId int8 `json:"badge_group_id" xorm:"badge_group_id"` - IsBadgeDeleted int8 `json:"is_badge_deleted" xorm:"is_badge_deleted"` + CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` + UserId int64 `json:"user_id" xorm:"not null index BIGINT(20) user_id"` + BadgeId int64 `json:"badge_id" xorm:"not null index BIGINT(20) badge_id"` + ObjectId int64 `json:"object_id" xorm:"not null index BIGINT(20) object_id"` + BadgeGroupId int8 `json:"badge_group_id" xorm:"not null index BIGINT(20) badge_group_id"` + IsBadgeDeleted int8 `json:"is_badge_deleted" xorm:"not null index TINYINT(1) s_badge_deleted"` } // TableName badge_award table name diff --git a/internal/entity/badge_group.go b/internal/entity/badge_group.go index 145352996..9953d067d 100644 --- a/internal/entity/badge_group.go +++ b/internal/entity/badge_group.go @@ -23,10 +23,10 @@ import "time" // BadgeGroup badge_group type BadgeGroup struct { - ID string `json:"id" xorm:"id"` - Name string `json:"name" xorm:"name"` - CreatedAt time.Time `json:"created_at" xorm:"created_at"` - UpdatedAt time.Time `json:"updated_at" xorm:"updated_at"` + ID string `json:"id" xorm:"not null pk autoincr BIGINT(20) id"` + Name string `json:"name" xorm:"not null default '' VARCHAR(256) name"` + CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` } // TableName badge_group table name From a4830a4119838d95d012a66662b2145e2a4a0097 Mon Sep 17 00:00:00 2001 From: Sonui Date: Wed, 31 Jul 2024 12:49:00 +0800 Subject: [PATCH 05/38] fix: Update Test_emailRepo_VerifyCode to handle additional parameters This commit updates the `Test_emailRepo_VerifyCode` function in the `email_repo_test.go` file. It adds support for additional parameters in the `SetCode` function call, specifically the `user_id` and `skip_validation_latest_code` parameters. This change ensures that the `VerifyCode` function can properly handle the updated code format. --- internal/repo/repo_test/email_repo_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/repo/repo_test/email_repo_test.go b/internal/repo/repo_test/email_repo_test.go index f9f433723..62d1ccb85 100644 --- a/internal/repo/repo_test/email_repo_test.go +++ b/internal/repo/repo_test/email_repo_test.go @@ -30,8 +30,8 @@ import ( func Test_emailRepo_VerifyCode(t *testing.T) { emailRepo := export.NewEmailRepo(testDataSource) - code, content := "1111", "test" - err := emailRepo.SetCode(context.TODO(), code, content, time.Minute) + code, content := "1111", "{\"source_type\":\"\",\"e_mail\":\"\",\"user_id\":\"1\",\"skip_validation_latest_code\":false}" + err := emailRepo.SetCode(context.TODO(), "1", code, content, time.Minute) assert.NoError(t, err) verifyContent, err := emailRepo.VerifyCode(context.TODO(), code) From 95f20c9827af4222b50b42e8b57cf1070dac0d3f Mon Sep 17 00:00:00 2001 From: kumfo Date: Wed, 31 Jul 2024 14:28:44 +0800 Subject: [PATCH 06/38] feat(badge): update entity --- internal/entity/badge.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/entity/badge.go b/internal/entity/badge.go index 0b296f611..7a375af7d 100644 --- a/internal/entity/badge.go +++ b/internal/entity/badge.go @@ -24,6 +24,13 @@ import "time" const ( BadgeStatusAvailable = 1 BadgeStatusDeleted = 10 + + BadgeLevelBronze = 1 + BadgeLevelSilver = 2 + BadgeLevelGold = 3 + + BadgeSingleAward = 1 + BadgeMultiAward = 2 ) // Badge badge @@ -32,10 +39,12 @@ type Badge struct { CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` Name string `json:"name" xorm:"not null default '' VARCHAR(256) name"` + Icon string `json:"icon" xorm:"not null default '' VARCHAR(1024) icon"` AwardCount int64 `json:"award_count" xorm:"not null default 0 INT(11) award_count"` Description string `json:"description" xorm:"not null default '' MEDIUMTEXT description"` Status int8 `json:"status" xorm:"not null default 1 INT(11) status"` BadgeGroupId int64 `json:"badge_group_id" xorm:"not null default 0 BIGINT(20) badge_group_id"` + Level int64 `json:"level" xorm:"not null default 1 TINYINT(4) level"` Single int8 `json:"single" xorm:"not null default 1 TINYINT(4) single"` Collect string `json:"collect" xorm:"not null default '' VARCHAR(64) collect"` Handler string `json:"handler" xorm:"not null default '' VARCHAR(64) handler"` From dc56fce18a89e0fa2f1dfe89df65ee1c0aea5be4 Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 1 Aug 2024 11:19:21 +0800 Subject: [PATCH 07/38] feat(badge): init version update --- i18n/en_US.yaml | 249 ++++++++++++++++++++++++++++++++++++- internal/migrations/v22.go | 184 +++++++++++++++++++++++++++ 2 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 internal/migrations/v22.go diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index a123d70d7..bfe53305c 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -421,7 +421,7 @@ backend: tags_title: other: Tags no_description: - other: The tag has no description. + other: The tag has no description. notification: action: update_question: @@ -526,6 +526,253 @@ backend: reaction: tooltip: other: "{{ .Names }} and {{ .Count }} more..." + badge: + badges: + autobiographer: + name: + other: Autobiographer + desc: + other: Filled out [profile] information. + certified: + name: + other: Certified + desc: + other: Completed our new user tutorial. + editor: + name: + other: Editor + desc: + other: First post edit. + first_flag: + name: + other: First Flag + desc: + other: First flagged a post. + first_upvote: + name: + other: First Upvote + desc: + other: First up voted a post. + first_link: + name: + other: First Link + desc: + other: First dirst added a link to another post. + first_reaction: + name: + other: First Reaction + desc: + other: First reacted to the post. + first_share: + name: + other: First Share + desc: + other: First shared a post. + scholar: + name: + other: Scholar + desc: + other: Asked a question and accepted an answer. + commentator: + name: + other: Commentator + desc: + other: Leave 5 comments. + new_user_of_the_month: + name: + other: New User of the Month + desc: + other: Outstanding contributions in their first month. + read_guidelines: + name: + other: Read Guidelines + desc: + other: Read the [community guidelines]. + reader: + name: + other: Reader + desc: + other: Read every answers in a topic with more than 10 answers. + welcome: + name: + other: Welcome + desc: + other: Received a up vote. + nice_share: + name: + other: Nice Share + desc: + other: Shared a post with 25 unique visitors. + good_share: + name: + other: Good Share + desc: + other: Shared a post with 300 unique visitors. + great_share: + name: + other: Great Share + desc: + other: Shared a post with 1000 unique visitors. + out_of_love: + name: + other: Out of Love + desc: + other: Used 50 up votes in a day. + higher_love: + name: + other: Higher Love + desc: + other: Used 50 up votes in a day 5 times. + crazy_in_love: + name: + other: Crazy in Love + desc: + other: Used 50 up votes in a day 20 times. + promoter: + name: + other: Promoter + desc: + other: Invited a user. + campaigner: + name: + other: Campaigner + desc: + other: Invited 3 basic users. + champion: + name: + other: Champion + desc: + other: Invited 5 members. + thank_you: + name: + other: Thank You + desc: + other: Has 20 up voted posts and gave 10 up votes. + gives_back: + name: + other: Gives Back + desc: + other: Has 100 up voted posts and gave 100 up votes. + empathetic: + name: + other: Empathetic + desc: + other: Has 500 up voted posts and gave 1000 up votes. + enthusiast: + name: + other: Enthusiast + desc: + other: Visited 10 consecutive days. + aficionado: + name: + other: Aficionado + desc: + other: Visited 100 consecutive days. + devotee: + name: + other: Devotee + desc: + other: Visited 365 consecutive days. + anniversary: + name: + other: Anniversary + desc: + other: Active member for a year, posted at least once. + appreciated: + name: + other: Appreciated + desc: + other: Received 1 up vote on 20 posts. + respected: + name: + other: Respected + desc: + other: Received 2 up votes on 100 posts. + admired: + name: + other: Admired + desc: + other: Received 5 up votes on 300 posts. + solved: + name: + other: Solved + desc: + other: Have an answer be accepted. + guidance_counsellor: + name: + other: Guidance Counsellor + desc: + other: Have 10 answers be accepted. + know_it_all: + name: + other: Know-it-All + desc: + other: Have 50 answers be accepted. + solution_institution: + name: + other: Solution Institution + desc: + other: Have 150 answers be accepted. + nice_answer: + name: + other: Nice Answer + desc: + other: Answer score of 10 or more. + good_answer: + name: + other: Good Answer + desc: + other: Answer score of 25 or more. + great_answer: + name: + other: Great Answer + desc: + other: Answer score of 50 or more. + nice_question: + name: + other: Nice Question + desc: + other: Question score of 10 or more. + good_question: + name: + other: Good Question + desc: + other: Question score of 25 or more. + great_question: + name: + other: Great Question + desc: + other: Question score of 50 or more. + popular_question: + name: + other: Popular Question + desc: + other: Question with 500 views. + notable_question: + name: + other: Notable Question + desc: + other: Question with 1,000 views. + famous_question: + name: + other: Famous Question + desc: + other: Question with 5,000 views. + popular_link: + name: + other: Popular Link + desc: + other: Posted an external link with 50 clicks. + hot_link: + name: + other: Hot Link + desc: + other: Posted an external link with 300 clicks. + famous_link: + name: + other: Famous Link + desc: + other: Posted an external link with 100 clicks. # The following fields are used for interface presentation(Front-end) ui: diff --git a/internal/migrations/v22.go b/internal/migrations/v22.go new file mode 100644 index 000000000..20b1eb3c4 --- /dev/null +++ b/internal/migrations/v22.go @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package migrations + +import ( + "context" + "github.com/apache/incubator-answer/internal/entity" + "time" + "xorm.io/xorm" +) + +var ( + defaultBadgeGroupTable = []*entity.BadgeGroup{ + {ID: "1", Name: "Getting Started"}, + {ID: "2", Name: "Community"}, + {ID: "3", Name: "Posting"}, + } + + defaultBadgeTable = []*entity.Badge{ + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.badges.autobiographer.name", + Icon: "", + AwardCount: 0, + Description: "badge.badges.autobiographer.desc", + Status: entity.BadgeStatusAvailable, + BadgeGroupId: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.badges.editor.name", + Icon: "", + AwardCount: 0, + Description: "badge.badges.editor.desc", + Status: entity.BadgeStatusAvailable, + BadgeGroupId: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Collect: "question", + Handler: "FirstQuestion", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.badges.first_flag.name", + Icon: "", + AwardCount: 0, + Description: "badge.badges.first_flag.desc", + Status: entity.BadgeStatusAvailable, + BadgeGroupId: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.badges.first_upvote.name", + Icon: "", + AwardCount: 0, + Description: "badge.badges.first_upvote.desc", + Status: entity.BadgeStatusAvailable, + BadgeGroupId: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.badges.first_reaction.name", + Icon: "", + AwardCount: 0, + Description: "badge.badges.first_reaction.desc", + Status: entity.BadgeStatusAvailable, + BadgeGroupId: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.badges.first_share.name", + Icon: "", + AwardCount: 0, + Description: "badge.badges.first_share.desc", + Status: entity.BadgeStatusAvailable, + BadgeGroupId: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.badges.scholar.name", + Icon: "", + AwardCount: 0, + Description: "badge.badges.scholar.desc", + Status: entity.BadgeStatusAvailable, + BadgeGroupId: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.badges.solved.name", + Icon: "", + AwardCount: 0, + Description: "badge.badges.solved.desc", + Status: entity.BadgeStatusAvailable, + BadgeGroupId: 2, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Collect: "", + Handler: "", + Param: "", + }, + } +) + +func addBadges(ctx context.Context, x *xorm.Engine) (err error) { + // create table + err = x.Context(ctx).Sync(new(entity.Badge)) + if err != nil { + return + } + + err = x.Context(ctx).Sync(new(entity.BadgeGroup)) + if err != nil { + return + } + + err = x.Context(ctx).Sync(new(entity.BadgeAward)) + if err != nil { + return + } + + // insert default data + _, err = x.Context(ctx).Insert(defaultBadgeGroupTable) + if err != nil { + return + } + _, err = x.Context(ctx).Insert(defaultBadgeTable) + return +} From 37981c139b04a70d29a459ab3737a0df32bfc9f8 Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 1 Aug 2024 15:44:11 +0800 Subject: [PATCH 08/38] feat(badge): init badge update data --- i18n/en_US.yaml | 2 +- internal/base/constant/object_type.go | 3 - internal/entity/badge.go | 7 +- internal/entity/badge_award.go | 2 +- internal/entity/badge_group.go | 2 +- internal/migrations/init.go | 21 ++++ internal/migrations/init_data.go | 3 + internal/migrations/migrations.go | 1 + internal/migrations/v22.go | 152 +++++++++++++++++++++----- 9 files changed, 159 insertions(+), 34 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index bfe53305c..5cd065733 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -527,7 +527,7 @@ backend: tooltip: other: "{{ .Names }} and {{ .Count }} more..." badge: - badges: + default_badges: autobiographer: name: other: Autobiographer diff --git a/internal/base/constant/object_type.go b/internal/base/constant/object_type.go index eef952c44..e4ac3d20c 100644 --- a/internal/base/constant/object_type.go +++ b/internal/base/constant/object_type.go @@ -29,7 +29,6 @@ const ( ReportObjectType = "report" BadgeObjectType = "badge" BadgeAwardObjectType = "badge_award" - BadgeGroupObjectType = "badge_group" ) var ( @@ -43,7 +42,6 @@ var ( ReportObjectType: 8, BadgeObjectType: 9, BadgeAwardObjectType: 10, - BadgeGroupObjectType: 11, } ObjectTypeNumberMapping = map[int]string{ @@ -56,6 +54,5 @@ var ( 8: ReportObjectType, 9: BadgeObjectType, 10: BadgeAwardObjectType, - 11: BadgeGroupObjectType, } ) diff --git a/internal/entity/badge.go b/internal/entity/badge.go index 7a375af7d..976bb47d0 100644 --- a/internal/entity/badge.go +++ b/internal/entity/badge.go @@ -22,8 +22,9 @@ package entity import "time" const ( - BadgeStatusAvailable = 1 - BadgeStatusDeleted = 10 + BadgeStatusActive = 1 + BadgeStatusDeleted = 10 + BadgeStatusInactive = 11 BadgeLevelBronze = 1 BadgeLevelSilver = 2 @@ -52,6 +53,6 @@ type Badge struct { } // TableName badge table name -func (*Badge) TableName() string { +func (Badge) TableName() string { return "badge" } diff --git a/internal/entity/badge_award.go b/internal/entity/badge_award.go index 01100b1f2..235d369c2 100644 --- a/internal/entity/badge_award.go +++ b/internal/entity/badge_award.go @@ -34,6 +34,6 @@ type BadgeAward struct { } // TableName badge_award table name -func (*BadgeAward) TableName() string { +func (BadgeAward) TableName() string { return "badge_award" } diff --git a/internal/entity/badge_group.go b/internal/entity/badge_group.go index 9953d067d..3be4d8209 100644 --- a/internal/entity/badge_group.go +++ b/internal/entity/badge_group.go @@ -30,6 +30,6 @@ type BadgeGroup struct { } // TableName badge_group table name -func (*BadgeGroup) TableName() string { +func (BadgeGroup) TableName() string { return "badge_group" } diff --git a/internal/migrations/init.go b/internal/migrations/init.go index a56216aab..40e9a91a4 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -65,6 +65,7 @@ func (m *Mentor) InitDB() error { m.do("init version table", m.initVersionTable) m.do("init admin user", m.initAdminUser) m.do("init config", m.initConfig) + m.do("init badge", m.initBadge) m.do("init default privileges config", m.initDefaultRankPrivileges) m.do("init role", m.initRole) m.do("init power", m.initPower) @@ -126,6 +127,26 @@ func (m *Mentor) initConfig() { _, m.err = m.engine.Context(m.ctx).Insert(defaultConfigTable) } +// initBadge init badge's table and data +func (m *Mentor) initBadge() { + uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine}) + _, m.err = m.engine.Context(m.ctx).Insert(defaultBadgeGroupTable) + if m.err != nil { + return + } + + for _, badge := range defaultBadgeTable { + badge.ID, m.err = uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Badge{}.TableName()) + if m.err != nil { + return + } + _, m.err = m.engine.Context(m.ctx).Insert(badge) + if m.err != nil { + return + } + } +} + func (m *Mentor) initDefaultRankPrivileges() { chooseOption := schema.DefaultPrivilegeOptions.Choose(schema.PrivilegeLevel2) for _, privilege := range chooseOption.Privileges { diff --git a/internal/migrations/init_data.go b/internal/migrations/init_data.go index adbe71753..f9a09f52a 100644 --- a/internal/migrations/init_data.go +++ b/internal/migrations/init_data.go @@ -69,6 +69,9 @@ var ( &entity.UserNotificationConfig{}, &entity.PluginUserConfig{}, &entity.Review{}, + &entity.Badge{}, + &entity.BadgeGroup{}, + &entity.BadgeAward{}, } roles = []*entity.Role{ diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index 463f68ed8..a32e851a7 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -97,6 +97,7 @@ var migrations = []Migration{ NewMigration("v1.2.5", "add notification plugin and theme config", addNotificationPluginAndThemeConfig, true), NewMigration("v1.3.0", "add review", addReview, false), NewMigration("v1.3.6", "add hot score to question table", addQuestionHotScore, true), + NewMigration("v1.4.0", "add badge/badge_group/badge_award table", addBadges, true), } func GetMigrations() []Migration { diff --git a/internal/migrations/v22.go b/internal/migrations/v22.go index 20b1eb3c4..d55a57d9d 100644 --- a/internal/migrations/v22.go +++ b/internal/migrations/v22.go @@ -21,7 +21,9 @@ package migrations import ( "context" + "github.com/apache/incubator-answer/internal/base/data" "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/repo/unique" "time" "xorm.io/xorm" ) @@ -37,11 +39,11 @@ var ( { CreatedAt: time.Now(), UpdatedAt: time.Now(), - Name: "badge.badges.autobiographer.name", + Name: "badge.default_badges.autobiographer.name", Icon: "", AwardCount: 0, - Description: "badge.badges.autobiographer.desc", - Status: entity.BadgeStatusAvailable, + Description: "badge.default_badges.autobiographer.desc", + Status: entity.BadgeStatusActive, BadgeGroupId: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, @@ -52,11 +54,11 @@ var ( { CreatedAt: time.Now(), UpdatedAt: time.Now(), - Name: "badge.badges.editor.name", + Name: "badge.default_badges.editor.name", Icon: "", AwardCount: 0, - Description: "badge.badges.editor.desc", - Status: entity.BadgeStatusAvailable, + Description: "badge.default_badges.editor.desc", + Status: entity.BadgeStatusActive, BadgeGroupId: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, @@ -67,11 +69,11 @@ var ( { CreatedAt: time.Now(), UpdatedAt: time.Now(), - Name: "badge.badges.first_flag.name", + Name: "badge.default_badges.first_flag.name", Icon: "", AwardCount: 0, - Description: "badge.badges.first_flag.desc", - Status: entity.BadgeStatusAvailable, + Description: "badge.default_badges.first_flag.desc", + Status: entity.BadgeStatusActive, BadgeGroupId: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, @@ -82,11 +84,11 @@ var ( { CreatedAt: time.Now(), UpdatedAt: time.Now(), - Name: "badge.badges.first_upvote.name", + Name: "badge.default_badges.first_upvote.name", Icon: "", AwardCount: 0, - Description: "badge.badges.first_upvote.desc", - Status: entity.BadgeStatusAvailable, + Description: "badge.default_badges.first_upvote.desc", + Status: entity.BadgeStatusActive, BadgeGroupId: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, @@ -97,11 +99,11 @@ var ( { CreatedAt: time.Now(), UpdatedAt: time.Now(), - Name: "badge.badges.first_reaction.name", + Name: "badge.default_badges.first_reaction.name", Icon: "", AwardCount: 0, - Description: "badge.badges.first_reaction.desc", - Status: entity.BadgeStatusAvailable, + Description: "badge.default_badges.first_reaction.desc", + Status: entity.BadgeStatusActive, BadgeGroupId: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, @@ -112,11 +114,11 @@ var ( { CreatedAt: time.Now(), UpdatedAt: time.Now(), - Name: "badge.badges.first_share.name", + Name: "badge.default_badges.first_share.name", Icon: "", AwardCount: 0, - Description: "badge.badges.first_share.desc", - Status: entity.BadgeStatusAvailable, + Description: "badge.default_badges.first_share.desc", + Status: entity.BadgeStatusActive, BadgeGroupId: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, @@ -127,11 +129,11 @@ var ( { CreatedAt: time.Now(), UpdatedAt: time.Now(), - Name: "badge.badges.scholar.name", + Name: "badge.default_badges.scholar.name", Icon: "", AwardCount: 0, - Description: "badge.badges.scholar.desc", - Status: entity.BadgeStatusAvailable, + Description: "badge.default_badges.scholar.desc", + Status: entity.BadgeStatusActive, BadgeGroupId: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, @@ -142,11 +144,11 @@ var ( { CreatedAt: time.Now(), UpdatedAt: time.Now(), - Name: "badge.badges.solved.name", + Name: "badge.default_badges.solved.name", Icon: "", AwardCount: 0, - Description: "badge.badges.solved.desc", - Status: entity.BadgeStatusAvailable, + Description: "badge.default_badges.solved.desc", + Status: entity.BadgeStatusActive, BadgeGroupId: 2, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, @@ -154,10 +156,101 @@ var ( Handler: "", Param: "", }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.default_badges.nice_answer.name", + Icon: "", + AwardCount: 0, + Description: "badge.default_badges.nice_answer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupId: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeMultiAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.default_badges.good_answer.name", + Icon: "", + AwardCount: 0, + Description: "badge.default_badges.good_answer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupId: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeMultiAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.default_badges.great_answer.name", + Icon: "", + AwardCount: 0, + Description: "badge.default_badges.great_answer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupId: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeMultiAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.default_badges.nice_question.name", + Icon: "", + AwardCount: 0, + Description: "badge.default_badges.nice_question.desc", + Status: entity.BadgeStatusActive, + BadgeGroupId: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeMultiAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.default_badges.good_question.name", + Icon: "", + AwardCount: 0, + Description: "badge.default_badges.good_question.desc", + Status: entity.BadgeStatusActive, + BadgeGroupId: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Collect: "", + Handler: "", + Param: "", + }, + { + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "badge.default_badges.great_question.name", + Icon: "", + AwardCount: 0, + Description: "badge.default_badges.great_question.desc", + Status: entity.BadgeStatusActive, + BadgeGroupId: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeMultiAward, + Collect: "", + Handler: "", + Param: "", + }, } ) func addBadges(ctx context.Context, x *xorm.Engine) (err error) { + uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: x}) // create table err = x.Context(ctx).Sync(new(entity.Badge)) if err != nil { @@ -179,6 +272,15 @@ func addBadges(ctx context.Context, x *xorm.Engine) (err error) { if err != nil { return } - _, err = x.Context(ctx).Insert(defaultBadgeTable) + for _, badge := range defaultBadgeTable { + badge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, entity.Badge{}.TableName()) + if err != nil { + return + } + _, err = x.Context(ctx).Insert(badge) + if err != nil { + return + } + } return } From ecbfd61bac6573fc4bd56515eb9e10adb11dd11d Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 1 Aug 2024 16:14:07 +0800 Subject: [PATCH 09/38] feat(badge): init badge update data --- internal/migrations/v22.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/migrations/v22.go b/internal/migrations/v22.go index d55a57d9d..b00166717 100644 --- a/internal/migrations/v22.go +++ b/internal/migrations/v22.go @@ -180,7 +180,7 @@ var ( Description: "badge.default_badges.good_answer.desc", Status: entity.BadgeStatusActive, BadgeGroupId: 3, - Level: entity.BadgeLevelBronze, + Level: entity.BadgeLevelSilver, Single: entity.BadgeMultiAward, Collect: "", Handler: "", @@ -195,7 +195,7 @@ var ( Description: "badge.default_badges.great_answer.desc", Status: entity.BadgeStatusActive, BadgeGroupId: 3, - Level: entity.BadgeLevelBronze, + Level: entity.BadgeLevelGold, Single: entity.BadgeMultiAward, Collect: "", Handler: "", @@ -225,7 +225,7 @@ var ( Description: "badge.default_badges.good_question.desc", Status: entity.BadgeStatusActive, BadgeGroupId: 3, - Level: entity.BadgeLevelBronze, + Level: entity.BadgeLevelSilver, Single: entity.BadgeSingleAward, Collect: "", Handler: "", @@ -240,7 +240,7 @@ var ( Description: "badge.default_badges.great_question.desc", Status: entity.BadgeStatusActive, BadgeGroupId: 3, - Level: entity.BadgeLevelBronze, + Level: entity.BadgeLevelGold, Single: entity.BadgeMultiAward, Collect: "", Handler: "", From 4a07fdb4a8b180bd28684e54c66a3a7caf51bfcc Mon Sep 17 00:00:00 2001 From: Sonui Date: Thu, 1 Aug 2024 23:53:55 +0800 Subject: [PATCH 10/38] fix: set last answer ID to zero if no answers --- internal/service/question_common/question.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index fce3761ea..b9f2fd539 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -145,6 +145,15 @@ func (qs *QuestionCommon) UpdateAnswerCount(ctx context.Context, questionID stri if err != nil { return err } + if count == 0 { + err = qs.questionRepo.UpdateLastAnswer(ctx, &entity.Question{ + ID: questionID, + LastAnswerID: "0", + }) + if err != nil { + return err + } + } return qs.questionRepo.UpdateAnswerCount(ctx, questionID, int(count)) } From 80f139ef9ea7bd4da6376e1d7f9c70b603292c53 Mon Sep 17 00:00:00 2001 From: Sonui Date: Fri, 2 Aug 2024 00:01:01 +0800 Subject: [PATCH 11/38] typo: GetCss swagger annotation content --- internal/controller_admin/siteinfo_controller.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/controller_admin/siteinfo_controller.go b/internal/controller_admin/siteinfo_controller.go index 623ab5f79..6429d6719 100644 --- a/internal/controller_admin/siteinfo_controller.go +++ b/internal/controller_admin/siteinfo_controller.go @@ -188,12 +188,12 @@ func (sc *SiteInfoController) GetRobots(ctx *gin.Context) { ctx.String(http.StatusOK, resp.Robots) } -// GetRobots get site robots information -// @Summary get site robots information -// @Description get site robots information +// GetCss get site custom CSS +// @Summary get site custom CSS +// @Description get site custom CSS // @Tags site -// @Produce json -// @Success 200 {string} txt "" +// @Produce text/css +// @Success 200 {string} css "" // @Router /custom.css [get] func (sc *SiteInfoController) GetCss(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx) From 5383b9058101974b666332388ed4f658b5f712d5 Mon Sep 17 00:00:00 2001 From: Sonui Date: Fri, 2 Aug 2024 09:33:48 +0800 Subject: [PATCH 12/38] perf: optimize search for unanswered questions using answer_count --- internal/repo/question/question_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 5a683af73..83a841573 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -385,7 +385,7 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, case "score": session.OrderBy("question.pin desc,question.vote_count DESC, question.view_count DESC") case "unanswered": - session.Where("question.last_answer_id = 0") + session.Where("question.answer_count = 0") session.OrderBy("question.pin desc,question.created_at DESC") } From d055507c800ac2a877a8329a366dbbda124712fa Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 5 Aug 2024 14:29:22 +0800 Subject: [PATCH 13/38] feat: Add badges to admin --- i18n/en_US.yaml | 16 ++- ui/src/common/constants.ts | 3 + ui/src/common/interface.ts | 2 + .../Admin/Badges/components/Action/index.tsx | 51 ++++++++ ui/src/pages/Admin/Badges/index.tsx | 122 ++++++++++++++++++ ui/src/pages/Admin/index.tsx | 1 + ui/src/router/routes.ts | 4 + 7 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 ui/src/pages/Admin/Badges/components/Action/index.tsx create mode 100644 ui/src/pages/Admin/Badges/index.tsx diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 5cd065733..f50465c2b 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1692,6 +1692,7 @@ ui: questions: Questions answers: Answers users: Users + badges: Badges flags: Flags settings: Settings general: General @@ -2117,7 +2118,20 @@ ui: msg: should_be_number: the input should be number number_larger_1: number should be equal or larger than 1 - + badges: + action: Action + active: Active + all: All + awards: Awards + deactivate: Deactivate + filter: + placeholder: Filter by name, badge:id + group: Group + inactive: Inactive + name: Name + show_logs: Show logs + status: Status + title: Badges form: optional: (optional) empty: cannot be empty diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index c846d4ddc..d52f2b877 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -93,6 +93,9 @@ export const ADMIN_NAV_MENUS = [ { name: 'users', }, + { + name: 'badges', + }, { name: 'customize', children: [ diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index ff7c09027..41dc0fef0 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -328,6 +328,8 @@ export type UserFilterBy = | 'suspended' | 'deleted'; +export type BadgeFilterBy = 'all' | 'active' | 'inactive'; + export type InstalledPluginsFilterBy = | 'all' | 'active' diff --git a/ui/src/pages/Admin/Badges/components/Action/index.tsx b/ui/src/pages/Admin/Badges/components/Action/index.tsx new file mode 100644 index 000000000..77601cbb4 --- /dev/null +++ b/ui/src/pages/Admin/Badges/components/Action/index.tsx @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Dropdown } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import { Icon } from '@/components'; + +interface Props { + badgeData; +} + +const UserOperation = ({ badgeData }: Props) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.badges' }); + + console.log(badgeData); + + return ( + + + + + + + {t('active')} + {t('deactivate')} + + {t('show_logs')} + + + + ); +}; + +export default UserOperation; diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx new file mode 100644 index 000000000..288540123 --- /dev/null +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FC } from 'react'; +import { Form, Table, Stack } from 'react-bootstrap'; +import { useSearchParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +import { QueryGroup } from '@/components'; +import * as Type from '@/common/interface'; + +import Action from './components/Action'; + +const BadgeFilterKeys: Type.BadgeFilterBy[] = ['all', 'active', 'inactive']; + +// const bgMap = { +// normal: 'text-bg-success', +// suspended: 'text-bg-danger', +// deleted: 'text-bg-danger', +// inactive: 'text-bg-secondary', +// }; + +const Users: FC = () => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.badges' }); + + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); + const curFilter = urlSearchParams.get('filter') || BadgeFilterKeys[0]; + const curQuery = urlSearchParams.get('query') || ''; + + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; + + return ( + <> +

{t('title')}

+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
{t('name')}{t('group')}{t('awards')}{t('status')}{t('action')}
+ badge +
+
Nice Question
+
Question score of 10 or more.
+
+
Community Badges200Active
+ {/* {Number(data?.count) <= 0 && !isLoading && } */} + {/*
+ +
*/} + + ); +}; + +export default Users; diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx index b527088f9..5da0780d2 100644 --- a/ui/src/pages/Admin/index.tsx +++ b/ui/src/pages/Admin/index.tsx @@ -37,6 +37,7 @@ const g10Paths = [ 'questions', 'answers', 'users', + 'badges', 'flags', 'installed-plugins', ]; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 5c420e369..bd4a0c796 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -400,6 +400,10 @@ const routes: RouteNode[] = [ path: ':slug_name', page: 'pages/Admin/Plugins/Config', }, + { + path: 'badges', + page: 'pages/Admin/Badges', + }, ], }, { From 027caa650e55da2e85999d80dbeb5ed54b9d1510 Mon Sep 17 00:00:00 2001 From: EkkoKo <65719025+EkkoKo@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:58:06 +0300 Subject: [PATCH 14/38] Makefile: Wraped with quotes the go location in go variable --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9cda2f5ed..9dc8c983e 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ DOCKER_CMD=docker GO_ENV=CGO_ENABLED=0 GO111MODULE=on Revision=$(shell git rev-parse --short HEAD 2>/dev/null || echo "") GO_FLAGS=-ldflags="-X github.com/apache/incubator-answer/cmd.Version=$(VERSION) -X 'github.com/apache/incubator-answer/cmd.Revision=$(Revision)' -X 'github.com/apache/incubator-answer/cmd.Time=`date +%s`' -extldflags -static" -GO=$(GO_ENV) $(shell which go) +GO=$(GO_ENV) "$(shell which go)" build: generate @$(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC) From f0b585974709d4adc4364d533419c5fa45a83cd4 Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Mon, 5 Aug 2024 17:03:23 +0800 Subject: [PATCH 15/38] feat: Add Open Search support --- internal/controller/template_controller.go | 8 +++++ .../controller/template_render/question.go | 22 ++++++++++++++ internal/router/template_router.go | 2 ++ ui/template/header.html | 1 + ui/template/opensearch.xml | 29 +++++++++++++++++++ 5 files changed, 62 insertions(+) create mode 100644 ui/template/opensearch.xml diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index a7d69ebc8..09f5bffcb 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -562,6 +562,14 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI ctx.HTML(code, tpl, data) } +func (tc *TemplateController) OpenSearch(ctx *gin.Context) { + if tc.checkPrivateMode(ctx) { + tc.Page404(ctx) + return + } + tc.templateRenderController.OpenSearch(ctx) +} + func (tc *TemplateController) Sitemap(ctx *gin.Context) { if tc.checkPrivateMode(ctx) { tc.Page404(ctx) diff --git a/internal/controller/template_render/question.go b/internal/controller/template_render/question.go index a65c3083d..83d08dc72 100644 --- a/internal/controller/template_render/question.go +++ b/internal/controller/template_render/question.go @@ -89,6 +89,28 @@ func (t *TemplateRenderController) Sitemap(ctx *gin.Context) { ) } +func (t *TemplateRenderController) OpenSearch(ctx *gin.Context) { + general, err := t.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Error("get site general failed:", err) + return + } + + favicon := "favicon.ico" + branding, err := t.siteInfoService.GetSiteBranding(ctx) + if err == nil { + favicon = branding.Favicon + } + + ctx.Header("Content-Type", "application/xml") + ctx.HTML( + http.StatusOK, "opensearch.xml", gin.H{ + "general": general, + "favicon": favicon, + }, + ) +} + func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error { general, err := t.siteInfoService.GetSiteGeneral(ctx) if err != nil { diff --git a/internal/router/template_router.go b/internal/router/template_router.go index 01e8b3073..195030f9a 100644 --- a/internal/router/template_router.go +++ b/internal/router/template_router.go @@ -60,6 +60,8 @@ func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup, baseURLPath seoNoAuth.GET("/404", a.templateController.Page404) + seoNoAuth.GET("/opensearch.xml", a.templateController.OpenSearch) + seo := r.Group(baseURLPath) seo.Use(a.authUserMiddleware.CheckPrivateMode()) seo.GET("/", a.templateController.Index) diff --git a/ui/template/header.html b/ui/template/header.html index 6eff73402..653f9a28a 100644 --- a/ui/template/header.html +++ b/ui/template/header.html @@ -33,6 +33,7 @@ + + + {{$.general.Name}} + {{if $.general.Description}} + {{$.general.Description}} + {{end}} + UTF-8 + {{$.favicon}} + + From 264f927433f8153cea3566ae7c981e2947d6e1ab Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Tue, 6 Aug 2024 15:39:55 +0800 Subject: [PATCH 16/38] fix: branding favicon might be empty --- internal/controller/template_render/question.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/controller/template_render/question.go b/internal/controller/template_render/question.go index 83d08dc72..b97536188 100644 --- a/internal/controller/template_render/question.go +++ b/internal/controller/template_render/question.go @@ -96,9 +96,9 @@ func (t *TemplateRenderController) OpenSearch(ctx *gin.Context) { return } - favicon := "favicon.ico" + favicon := general.SiteUrl + "/favicon.ico" branding, err := t.siteInfoService.GetSiteBranding(ctx) - if err == nil { + if err == nil && len(branding.Favicon) > 0 { favicon = branding.Favicon } From ea43edc864e718eb22e49e4e6576b958ab071d85 Mon Sep 17 00:00:00 2001 From: kumfo Date: Wed, 7 Aug 2024 16:35:22 +0800 Subject: [PATCH 17/38] feat(badge): define func and add badge's list --- cmd/wire_gen.go | 11 +- docs/docs.go | 137 ++++++++++++++++- docs/swagger.json | 139 +++++++++++++++++- docs/swagger.yaml | 89 ++++++++++- internal/controller/badge_controller.go | 52 +++++++ internal/controller/controller.go | 1 + internal/entity/badge.go | 58 -------- .../{badge_award.go => badge_award_entity.go} | 10 ++ internal/entity/badge_entity.go | 60 ++++++++ .../{badge_group.go => badge_group_entity.go} | 0 internal/repo/badge/badge_repo.go | 76 ++++++++++ internal/repo/badge_award/badge_award_repo.go | 106 +++++++++++++ internal/repo/badge_group/badge_group_repo.go | 50 +++++++ internal/repo/provider.go | 6 + internal/router/answer_api_router.go | 6 + internal/schema/badge.go | 34 +++++ internal/service/badge/badge_service.go | 112 ++++++++++++++ .../badge_award/badge_award_service.go | 64 ++++++++ .../badge_group/badge_group_service.go | 40 +++++ internal/service/provider.go | 6 + 20 files changed, 975 insertions(+), 82 deletions(-) create mode 100644 internal/controller/badge_controller.go delete mode 100644 internal/entity/badge.go rename internal/entity/{badge_award.go => badge_award_entity.go} (88%) create mode 100644 internal/entity/badge_entity.go rename internal/entity/{badge_group.go => badge_group_entity.go} (100%) create mode 100644 internal/repo/badge/badge_repo.go create mode 100644 internal/repo/badge_award/badge_award_repo.go create mode 100644 internal/repo/badge_group/badge_group_repo.go create mode 100644 internal/schema/badge.go create mode 100644 internal/service/badge/badge_service.go create mode 100644 internal/service/badge_award/badge_award_service.go create mode 100644 internal/service/badge_group/badge_group_service.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 4928a122b..c20aa2cfd 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -40,6 +40,9 @@ import ( "github.com/apache/incubator-answer/internal/repo/activity_common" "github.com/apache/incubator-answer/internal/repo/answer" "github.com/apache/incubator-answer/internal/repo/auth" + "github.com/apache/incubator-answer/internal/repo/badge" + "github.com/apache/incubator-answer/internal/repo/badge_award" + "github.com/apache/incubator-answer/internal/repo/badge_group" "github.com/apache/incubator-answer/internal/repo/captcha" "github.com/apache/incubator-answer/internal/repo/collection" "github.com/apache/incubator-answer/internal/repo/comment" @@ -71,6 +74,7 @@ import ( "github.com/apache/incubator-answer/internal/service/activity_queue" "github.com/apache/incubator-answer/internal/service/answer_common" auth2 "github.com/apache/incubator-answer/internal/service/auth" + badge2 "github.com/apache/incubator-answer/internal/service/badge" collection2 "github.com/apache/incubator-answer/internal/service/collection" "github.com/apache/incubator-answer/internal/service/collection_common" comment2 "github.com/apache/incubator-answer/internal/service/comment" @@ -253,7 +257,12 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reviewController := controller.NewReviewController(reviewService, rankService, captchaService) metaService := meta2.NewMetaService(metaCommonService, userCommon, answerRepo, questionRepo) metaController := controller.NewMetaController(metaService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController) + badgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo) + badgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo) + badgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo) + badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo) + badgeController := controller.NewBadgeController(badgeService) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService) authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService) diff --git a/docs/docs.go b/docs/docs.go index cf1264d14..6fc76693f 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2239,6 +2239,49 @@ const docTemplate = `{ } } }, + "/answer/api/v1/badges": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "list all badges group by group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "list all badges group by group", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetBadgeListResp" + } + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/collection/switch": { "post": { "security": [ @@ -4410,7 +4453,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetTagBasicResp" } } } @@ -5380,7 +5423,7 @@ const docTemplate = `{ }, "/answer/api/v1/tags": { "get": { - "description": "get tags list", + "description": "get tags list by slug name", "produces": [ "application/json" ], @@ -5404,7 +5447,22 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetTagBasicResp" + } + } + } + } + ] } } } @@ -7327,6 +7385,26 @@ const docTemplate = `{ } } }, + "schema.BadgeListInfo": { + "type": "object", + "properties": { + "award_count": { + "type": "integer" + }, + "earned": { + "type": "boolean" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "schema.CloseQuestionReq": { "type": "object", "required": [ @@ -7578,6 +7656,20 @@ const docTemplate = `{ } } }, + "schema.GetBadgeListResp": { + "type": "object", + "properties": { + "badges": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.BadgeListInfo" + } + }, + "group_name": { + "type": "string" + } + } + }, "schema.GetCommentPersonalWithPageResp": { "type": "object", "properties": { @@ -8242,6 +8334,23 @@ const docTemplate = `{ } } }, + "schema.GetTagBasicResp": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "type": "string" + } + } + }, "schema.GetTagPageResp": { "type": "object", "properties": { @@ -9819,7 +9928,7 @@ const docTemplate = `{ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9828,7 +9937,7 @@ const docTemplate = `{ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9842,7 +9951,7 @@ const docTemplate = `{ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9851,7 +9960,7 @@ const docTemplate = `{ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9859,6 +9968,20 @@ const docTemplate = `{ } } }, + "schema.SiteWriteTag": { + "type": "object", + "required": [ + "slug_name" + ], + "properties": { + "display_name": { + "type": "string" + }, + "slug_name": { + "type": "string" + } + } + }, "schema.TagItem": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 1e93d2b08..45936363f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2209,6 +2209,49 @@ } } }, + "/answer/api/v1/badges": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "list all badges group by group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "list all badges group by group", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetBadgeListResp" + } + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/collection/switch": { "post": { "security": [ @@ -4380,7 +4423,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetTagBasicResp" } } } @@ -5350,7 +5393,7 @@ }, "/answer/api/v1/tags": { "get": { - "description": "get tags list", + "description": "get tags list by slug name", "produces": [ "application/json" ], @@ -5374,7 +5417,22 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/handler.RespBody" + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetTagBasicResp" + } + } + } + } + ] } } } @@ -7297,6 +7355,26 @@ } } }, + "schema.BadgeListInfo": { + "type": "object", + "properties": { + "award_count": { + "type": "integer" + }, + "earned": { + "type": "boolean" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "schema.CloseQuestionReq": { "type": "object", "required": [ @@ -7548,6 +7626,20 @@ } } }, + "schema.GetBadgeListResp": { + "type": "object", + "properties": { + "badges": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.BadgeListInfo" + } + }, + "group_name": { + "type": "string" + } + } + }, "schema.GetCommentPersonalWithPageResp": { "type": "object", "properties": { @@ -8212,6 +8304,23 @@ } } }, + "schema.GetTagBasicResp": { + "type": "object", + "properties": { + "display_name": { + "type": "string" + }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, + "slug_name": { + "type": "string" + } + } + }, "schema.GetTagPageResp": { "type": "object", "properties": { @@ -8809,7 +8918,7 @@ "enum": [ "newest", "active", - "frequent", + "hot", "score", "unanswered" ] @@ -9789,7 +9898,7 @@ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9798,7 +9907,7 @@ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9812,7 +9921,7 @@ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9821,7 +9930,7 @@ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9829,6 +9938,20 @@ } } }, + "schema.SiteWriteTag": { + "type": "object", + "required": [ + "slug_name" + ], + "properties": { + "display_name": { + "type": "string" + }, + "slug_name": { + "type": "string" + } + } + }, "schema.TagItem": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 93f8116af..c91c987e6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -342,6 +342,19 @@ definitions: maxLength: 100 type: string type: object + schema.BadgeListInfo: + properties: + award_count: + type: integer + earned: + type: boolean + icon: + type: string + id: + type: string + name: + type: string + type: object schema.CloseQuestionReq: properties: close_msg: @@ -513,6 +526,15 @@ definitions: description: if user is followed object will be true,otherwise false type: boolean type: object + schema.GetBadgeListResp: + properties: + badges: + items: + $ref: '#/definitions/schema.BadgeListInfo' + type: array + group_name: + type: string + type: object schema.GetCommentPersonalWithPageResp: properties: answer_id: @@ -983,6 +1005,17 @@ definitions: terms_of_service_parsed_text: type: string type: object + schema.GetTagBasicResp: + properties: + display_name: + type: string + recommend: + type: boolean + reserved: + type: boolean + slug_name: + type: string + type: object schema.GetTagPageResp: properties: created_at: @@ -1401,7 +1434,7 @@ definitions: enum: - newest - active - - frequent + - hot - score - unanswered type: string @@ -2071,13 +2104,13 @@ definitions: properties: recommend_tags: items: - type: string + $ref: '#/definitions/schema.SiteWriteTag' type: array required_tag: type: boolean reserved_tags: items: - type: string + $ref: '#/definitions/schema.SiteWriteTag' type: array restrict_answer: type: boolean @@ -2086,17 +2119,26 @@ definitions: properties: recommend_tags: items: - type: string + $ref: '#/definitions/schema.SiteWriteTag' type: array required_tag: type: boolean reserved_tags: items: - type: string + $ref: '#/definitions/schema.SiteWriteTag' type: array restrict_answer: type: boolean type: object + schema.SiteWriteTag: + properties: + display_name: + type: string + slug_name: + type: string + required: + - slug_name + type: object schema.TagItem: properties: display_name: @@ -4063,6 +4105,30 @@ paths: summary: recover answer tags: - Answer + /answer/api/v1/badges: + get: + consumes: + - application/json + description: list all badges group by group + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetBadgeListResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: list all badges group by group + tags: + - api-badge /answer/api/v1/collection/switch: post: consumes: @@ -5382,7 +5448,7 @@ paths: - properties: data: items: - $ref: '#/definitions/schema.GetTagResp' + $ref: '#/definitions/schema.GetTagBasicResp' type: array type: object security: @@ -5965,7 +6031,7 @@ paths: - Tag /answer/api/v1/tags: get: - description: get tags list + description: get tags list by slug name parameters: - collectionFormat: csv description: string collection @@ -5980,7 +6046,14 @@ paths: "200": description: OK schema: - $ref: '#/definitions/handler.RespBody' + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetTagBasicResp' + type: array + type: object summary: get tags list tags: - Tag diff --git a/internal/controller/badge_controller.go b/internal/controller/badge_controller.go new file mode 100644 index 000000000..ebc534ced --- /dev/null +++ b/internal/controller/badge_controller.go @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller + +import ( + "github.com/apache/incubator-answer/internal/base/handler" + "github.com/apache/incubator-answer/internal/base/middleware" + "github.com/apache/incubator-answer/internal/service/badge" + "github.com/gin-gonic/gin" +) + +type BadgeController struct { + badgeService *badge.BadgeService +} + +func NewBadgeController(badgeService *badge.BadgeService) *BadgeController { + return &BadgeController{ + badgeService: badgeService, + } +} + +// GetBadgeList list all badges +// @Summary list all badges group by group +// @Description list all badges group by group +// @Tags api-badge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListResp} +// @Router /answer/api/v1/badges [get] +func (b *BadgeController) GetBadgeList(ctx *gin.Context) { + userID := middleware.GetLoginUserIDFromContext(ctx) + resp, err := b.badgeService.ListByGroup(ctx, userID) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 9eb64c585..8fad918a1 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -51,4 +51,5 @@ var ProviderSetController = wire.NewSet( NewCaptchaController, NewMetaController, NewEmbedController, + NewBadgeController, ) diff --git a/internal/entity/badge.go b/internal/entity/badge.go deleted file mode 100644 index 976bb47d0..000000000 --- a/internal/entity/badge.go +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package entity - -import "time" - -const ( - BadgeStatusActive = 1 - BadgeStatusDeleted = 10 - BadgeStatusInactive = 11 - - BadgeLevelBronze = 1 - BadgeLevelSilver = 2 - BadgeLevelGold = 3 - - BadgeSingleAward = 1 - BadgeMultiAward = 2 -) - -// Badge badge -type Badge struct { - ID string `json:"id" xorm:"id"` - CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` - UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` - Name string `json:"name" xorm:"not null default '' VARCHAR(256) name"` - Icon string `json:"icon" xorm:"not null default '' VARCHAR(1024) icon"` - AwardCount int64 `json:"award_count" xorm:"not null default 0 INT(11) award_count"` - Description string `json:"description" xorm:"not null default '' MEDIUMTEXT description"` - Status int8 `json:"status" xorm:"not null default 1 INT(11) status"` - BadgeGroupId int64 `json:"badge_group_id" xorm:"not null default 0 BIGINT(20) badge_group_id"` - Level int64 `json:"level" xorm:"not null default 1 TINYINT(4) level"` - Single int8 `json:"single" xorm:"not null default 1 TINYINT(4) single"` - Collect string `json:"collect" xorm:"not null default '' VARCHAR(64) collect"` - Handler string `json:"handler" xorm:"not null default '' VARCHAR(64) handler"` - Param string `json:"param" xorm:"not null default '' VARCHAR(128) param"` -} - -// TableName badge table name -func (Badge) TableName() string { - return "badge" -} diff --git a/internal/entity/badge_award.go b/internal/entity/badge_award_entity.go similarity index 88% rename from internal/entity/badge_award.go rename to internal/entity/badge_award_entity.go index 235d369c2..a852f6bd3 100644 --- a/internal/entity/badge_award.go +++ b/internal/entity/badge_award_entity.go @@ -37,3 +37,13 @@ type BadgeAward struct { func (BadgeAward) TableName() string { return "badge_award" } + +type BadgeEarnedCount struct { + BadgeID string `xorm:"badge_id"` + EarnedCount int `xorm:"earned_count"` +} + +// TableName badge_award table name +func (BadgeEarnedCount) TableName() string { + return "badge_award" +} diff --git a/internal/entity/badge_entity.go b/internal/entity/badge_entity.go new file mode 100644 index 000000000..da6cd4dd9 --- /dev/null +++ b/internal/entity/badge_entity.go @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package entity + +import "time" + +type BadgeLevel int + +const ( + BadgeStatusActive = 1 + BadgeStatusDeleted = 10 + BadgeStatusInactive = 11 + + BadgeLevelBronze BadgeLevel = 1 + BadgeLevelSilver BadgeLevel = 2 + BadgeLevelGold BadgeLevel = 3 + + BadgeSingleAward = 1 + BadgeMultiAward = 2 +) + +// Badge badge +type Badge struct { + ID string `json:"id" xorm:"id"` + CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` + Name string `json:"name" xorm:"not null default '' VARCHAR(256) name"` + Icon string `json:"icon" xorm:"not null default '' VARCHAR(1024) icon"` + AwardCount int `json:"award_count" xorm:"not null default 0 INT(11) award_count"` + Description string `json:"description" xorm:"not null default '' MEDIUMTEXT description"` + Status int8 `json:"status" xorm:"not null default 1 INT(11) status"` + BadgeGroupId int64 `json:"badge_group_id" xorm:"not null default 0 BIGINT(20) badge_group_id"` + Level BadgeLevel `json:"level" xorm:"not null default 1 TINYINT(4) level"` + Single int8 `json:"single" xorm:"not null default 1 TINYINT(4) single"` + Collect string `json:"collect" xorm:"not null default '' VARCHAR(64) collect"` + Handler string `json:"handler" xorm:"not null default '' VARCHAR(64) handler"` + Param string `json:"param" xorm:"not null default '' VARCHAR(128) param"` +} + +// TableName badge table name +func (Badge) TableName() string { + return "badge" +} diff --git a/internal/entity/badge_group.go b/internal/entity/badge_group_entity.go similarity index 100% rename from internal/entity/badge_group.go rename to internal/entity/badge_group_entity.go diff --git a/internal/repo/badge/badge_repo.go b/internal/repo/badge/badge_repo.go new file mode 100644 index 000000000..b4cd72f1c --- /dev/null +++ b/internal/repo/badge/badge_repo.go @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/service/badge" + "github.com/apache/incubator-answer/internal/service/unique" +) + +type badgeRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo +} + +// NewBadgeRepo creates a new badge repository +func NewBadgeRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeRepo { + return &badgeRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + } +} + +// ListByLevel returns a list of badges by level +func (r *badgeRepo) ListByLevel(ctx context.Context, level entity.BadgeLevel) (badges []*entity.Badge, err error) { + badges = make([]*entity.Badge, 0) + err = r.data.DB.Context(ctx).Where("level = ?", level).Find(&badges) + return +} + +// ListByGroup returns a list of badges by group +func (r *badgeRepo) ListByGroup(ctx context.Context, groupID int64) (badges []*entity.Badge, err error) { + badges = make([]*entity.Badge, 0) + err = r.data.DB.Context(ctx).Where("group_id = ?", groupID).Find(&badges) + return +} + +// ListByLevelAndGroup returns a list of badges by level and group +func (r *badgeRepo) ListByLevelAndGroup(ctx context.Context, level entity.BadgeLevel, groupID int64) (badges []*entity.Badge, err error) { + badges = make([]*entity.Badge, 0) + err = r.data.DB.Context(ctx).Where("level = ? AND group_id = ?", level, groupID).Find(&badges) + return +} + +// ListActivated returns a list of activated badges +func (r *badgeRepo) ListActivated(ctx context.Context) (badges []*entity.Badge, err error) { + badges = make([]*entity.Badge, 0) + err = r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusActive).Find(&badges) + return +} + +// ListInactivated returns a list of inactivated badges +func (r *badgeRepo) ListInactivated(ctx context.Context) (badges []*entity.Badge, err error) { + badges = make([]*entity.Badge, 0) + err = r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusInactive).Find(&badges) + return +} diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go new file mode 100644 index 000000000..b6f0e95eb --- /dev/null +++ b/internal/repo/badge_award/badge_award_repo.go @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge_award + +import ( + "context" + "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/service/badge_award" + "github.com/apache/incubator-answer/internal/service/unique" + "time" +) + +type badgeAwardRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo +} + +func NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge_award.BadgeAwardRepo { + return &badgeAwardRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + } +} + +func (r *badgeAwardRepo) Award(ctx context.Context, badgeID string, userID string, objectID string, force bool, createdAt time.Time) { + return +} +func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID string, userID string, objectID string) (isAward bool) { + return +} +func (r *badgeAwardRepo) CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) { + return +} +func (r *badgeAwardRepo) CountByUserId(ctx context.Context, userID string) (awardCount int64) { + return +} +func (r *badgeAwardRepo) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) { + return +} +func (r *badgeAwardRepo) CountByObjectId(ctx context.Context, objectID string) (awardCount int64) { + return +} +func (r *badgeAwardRepo) CountByObjectIdAndBadgeId(ctx context.Context, objectID string, badgeID string) (awardCount int64) { + return +} +func (r *badgeAwardRepo) CountBadgesByUserIdAndObjectId(ctx context.Context, userID string, objectID string, badgeID string) (awardCount int64) { + return +} +func (r *badgeAwardRepo) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) { + err = r.data.DB.Context(ctx).Select("badge_id, count(`id`) AS earned_count").Where("user_id = ?", userID).GroupBy("badge_id").Find(&earnedCounts) + return +} +func (r *badgeAwardRepo) ListAllByUserId(ctx context.Context, userID string) (badgeAwards []*entity.BadgeAward) { + return +} +func (r *badgeAwardRepo) ListPagedByBadgeId(ctx context.Context, badgeID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { + return +} +func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { + return +} +func (r *badgeAwardRepo) ListPagedByObjectId(ctx context.Context, badgeID string, objectID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { + return +} +func (r *badgeAwardRepo) ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, objectID string, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { + return +} +func (r *badgeAwardRepo) ListTagPagedByBadgeId(ctx context.Context, badgeIDs []int64, page int64, pageSize int64, filterUserID int64) (badgeAwards []*entity.BadgeAward, total int64) { + return +} +func (r *badgeAwardRepo) ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []int64, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { + return +} +func (r *badgeAwardRepo) ListPagedLatest(ctx context.Context, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { + return +} +func (r *badgeAwardRepo) ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int64) (badgeAwards []*entity.BadgeAward, total int64) { + return +} +func (r *badgeAwardRepo) ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int64, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { + return +} +func (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward) { + return +} +func (r *badgeAwardRepo) GetByUserIdAndBadgeIdAndObjectId(ctx context.Context, userID string, badgeID string, objectID string) (badgeAward *entity.BadgeAward) { + return +} diff --git a/internal/repo/badge_group/badge_group_repo.go b/internal/repo/badge_group/badge_group_repo.go new file mode 100644 index 000000000..7dfe90fc3 --- /dev/null +++ b/internal/repo/badge_group/badge_group_repo.go @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge_group + +import ( + "context" + "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/service/badge_group" + "github.com/apache/incubator-answer/internal/service/unique" +) + +type badgeGroupRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo +} + +func NewBadgeGroupRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge_group.BadgeGroupRepo { + return &badgeGroupRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + } +} + +func (r *badgeGroupRepo) ListGroups(ctx context.Context) (groups []*entity.BadgeGroup, err error) { + groups = make([]*entity.BadgeGroup, 0) + err = r.data.DB.Context(ctx).Find(&groups) + return +} + +func (r *badgeGroupRepo) AddGroup(ctx context.Context, group *entity.BadgeGroup) (err error) { + return +} diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 3a517120e..ee309e027 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -25,6 +25,9 @@ import ( "github.com/apache/incubator-answer/internal/repo/activity_common" "github.com/apache/incubator-answer/internal/repo/answer" "github.com/apache/incubator-answer/internal/repo/auth" + "github.com/apache/incubator-answer/internal/repo/badge" + "github.com/apache/incubator-answer/internal/repo/badge_award" + "github.com/apache/incubator-answer/internal/repo/badge_group" "github.com/apache/incubator-answer/internal/repo/captcha" "github.com/apache/incubator-answer/internal/repo/collection" "github.com/apache/incubator-answer/internal/repo/comment" @@ -100,4 +103,7 @@ var ProviderSetRepo = wire.NewSet( limit.NewRateLimitRepo, plugin_config.NewPluginUserConfigRepo, review.NewReviewRepo, + badge.NewBadgeRepo, + badge_group.NewBadgeGroupRepo, + badge_award.NewBadgeAwardRepo, ) diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 328541868..dadf2a853 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -55,6 +55,7 @@ type AnswerAPIRouter struct { userPluginController *controller.UserPluginController reviewController *controller.ReviewController metaController *controller.MetaController + badgeController *controller.BadgeController } func NewAnswerAPIRouter( @@ -86,6 +87,7 @@ func NewAnswerAPIRouter( userPluginController *controller.UserPluginController, reviewController *controller.ReviewController, metaController *controller.MetaController, + badgeController *controller.BadgeController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ langController: langController, @@ -116,6 +118,7 @@ func NewAnswerAPIRouter( userPluginController: userPluginController, reviewController: reviewController, metaController: metaController, + badgeController: badgeController, } } @@ -187,6 +190,9 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // reaction r.GET("/meta/reaction", a.metaController.GetReaction) + + // badges + r.GET("/badges", a.badgeController.GetBadgeList) } func (a *AnswerAPIRouter) RegisterAuthUserWithAnyStatusAnswerAPIRouter(r *gin.RouterGroup) { diff --git a/internal/schema/badge.go b/internal/schema/badge.go new file mode 100644 index 000000000..c52a235b4 --- /dev/null +++ b/internal/schema/badge.go @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +// BadgeListInfo get badge list response +type BadgeListInfo struct { + ID string `json:"id" ` + Name string `json:"name" ` + Icon string `json:"icon" ` + AwardCount int `json:"award_count" ` + Earned bool `json:"earned" ` +} + +type GetBadgeListResp struct { + Badges []*BadgeListInfo `json:"badges" ` + GroupName string `json:"group_name" ` +} diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go new file mode 100644 index 000000000..1109b4447 --- /dev/null +++ b/internal/service/badge/badge_service.go @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/incubator-answer/internal/base/handler" + "github.com/apache/incubator-answer/internal/base/translator" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/internal/service/badge_award" + "github.com/apache/incubator-answer/internal/service/badge_group" + "github.com/apache/incubator-answer/pkg/converter" +) + +type BadgeRepo interface { + ListByLevel(ctx context.Context, level entity.BadgeLevel) ([]*entity.Badge, error) + ListByGroup(ctx context.Context, groupID int64) ([]*entity.Badge, error) + ListByLevelAndGroup(ctx context.Context, level entity.BadgeLevel, groupID int64) ([]*entity.Badge, error) + ListActivated(ctx context.Context) ([]*entity.Badge, error) + ListInactivated(ctx context.Context) ([]*entity.Badge, error) +} + +type BadgeService struct { + badgeRepo BadgeRepo + badgeGroupRepo badge_group.BadgeGroupRepo + badgeAwardRepo badge_award.BadgeAwardRepo +} + +func NewBadgeService( + badgeRepo BadgeRepo, + badgeGroupRepo badge_group.BadgeGroupRepo, + badgeAwardRepo badge_award.BadgeAwardRepo) *BadgeService { + return &BadgeService{ + badgeRepo: badgeRepo, + badgeGroupRepo: badgeGroupRepo, + badgeAwardRepo: badgeAwardRepo, + } +} + +func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []*schema.GetBadgeListResp, err error) { + var ( + groups []*entity.BadgeGroup + badges []*entity.Badge + earnedCounts []*entity.BadgeEarnedCount + + groupMap = make(map[int64]string, 0) + badgesMap = make(map[int64][]*schema.BadgeListInfo, 0) + ) + resp = make([]*schema.GetBadgeListResp, 0) + + groups, err = b.badgeGroupRepo.ListGroups(ctx) + if err != nil { + return + } + badges, err = b.badgeRepo.ListActivated(ctx) + if err != nil { + return + } + earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, userID) + + for _, group := range groups { + groupMap[converter.StringToInt64(group.ID)] = group.Name + } + + for _, badge := range badges { + // check is earned + earned := false + if len(earnedCounts) > 0 { + for _, earnedCount := range earnedCounts { + if badge.ID == earnedCount.BadgeID { + earned = true + break + } + } + } + + badgesMap[badge.BadgeGroupId] = append(badgesMap[badge.BadgeGroupId], &schema.BadgeListInfo{ + ID: badge.ID, + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Icon: badge.Icon, + AwardCount: badge.AwardCount, + Earned: earned, + }) + } + + for _, group := range groups { + resp = append(resp, &schema.GetBadgeListResp{ + GroupName: group.Name, + Badges: badgesMap[converter.StringToInt64(group.ID)], + }) + } + + return +} diff --git a/internal/service/badge_award/badge_award_service.go b/internal/service/badge_award/badge_award_service.go new file mode 100644 index 000000000..8f84453ee --- /dev/null +++ b/internal/service/badge_award/badge_award_service.go @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge_award + +import ( + "context" + "github.com/apache/incubator-answer/internal/entity" + "time" +) + +type BadgeAwardRepo interface { + Award(ctx context.Context, badgeID string, userID string, objectID string, force bool, createdAt time.Time) + CheckIsAward(ctx context.Context, badgeID string, userID string, objectID string) bool + + CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) + CountByUserId(ctx context.Context, userID string) (awardCount int64) + CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) + CountByObjectId(ctx context.Context, objectID string) (awardCount int64) + CountByObjectIdAndBadgeId(ctx context.Context, objectID string, badgeID string) int64 + CountBadgesByUserIdAndObjectId(ctx context.Context, userID string, objectID string, badgeID string) (awardCount int64) + + SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) + + ListAllByUserId(ctx context.Context, userID string) (badgeAwards []*entity.BadgeAward) + ListPagedByBadgeId(ctx context.Context, badgeID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) + ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) + ListPagedByObjectId(ctx context.Context, badgeID string, objectID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) + ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, objectID string, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) + ListTagPagedByBadgeId(ctx context.Context, badgeIDs []int64, page int64, pageSize int64, filterUserID int64) (badgeAwards []*entity.BadgeAward, total int64) + ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []int64, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) + ListPagedLatest(ctx context.Context, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) + ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int64) (badgeAwards []*entity.BadgeAward, total int64) + ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int64, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) + + GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward) + GetByUserIdAndBadgeIdAndObjectId(ctx context.Context, userID string, badgeID string, objectID string) (badgeAward *entity.BadgeAward) +} + +type BadgeAwardService struct { + badgeAwardRepo BadgeAwardRepo +} + +func NewBadgeAwardService(badgeAwardRepo BadgeAwardRepo) *BadgeAwardService { + return &BadgeAwardService{ + badgeAwardRepo: badgeAwardRepo, + } +} diff --git a/internal/service/badge_group/badge_group_service.go b/internal/service/badge_group/badge_group_service.go new file mode 100644 index 000000000..c78f3e4f6 --- /dev/null +++ b/internal/service/badge_group/badge_group_service.go @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge_group + +import ( + "context" + "github.com/apache/incubator-answer/internal/entity" +) + +type BadgeGroupRepo interface { + ListGroups(ctx context.Context) (groups []*entity.BadgeGroup, err error) + AddGroup(ctx context.Context, group *entity.BadgeGroup) (err error) +} + +type BadgeGroupService struct { + badgeGroupRepo BadgeGroupRepo +} + +func NewBadgeGroupService(badgeGroupRepo BadgeGroupRepo) *BadgeGroupService { + return &BadgeGroupService{ + badgeGroupRepo: badgeGroupRepo, + } +} diff --git a/internal/service/provider.go b/internal/service/provider.go index e82d93167..38832923a 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -26,6 +26,9 @@ import ( "github.com/apache/incubator-answer/internal/service/activity_queue" answercommon "github.com/apache/incubator-answer/internal/service/answer_common" "github.com/apache/incubator-answer/internal/service/auth" + "github.com/apache/incubator-answer/internal/service/badge" + "github.com/apache/incubator-answer/internal/service/badge_award" + "github.com/apache/incubator-answer/internal/service/badge_group" "github.com/apache/incubator-answer/internal/service/collection" collectioncommon "github.com/apache/incubator-answer/internal/service/collection_common" "github.com/apache/incubator-answer/internal/service/comment" @@ -117,4 +120,7 @@ var ProviderSetService = wire.NewSet( notice_queue.NewNewQuestionNotificationQueueService, review.NewReviewService, meta.NewMetaService, + badge.NewBadgeService, + badge_award.NewBadgeAwardService, + badge_group.NewBadgeGroupService, ) From 23b6b11f9ea5f78c33902525d74509eca635992e Mon Sep 17 00:00:00 2001 From: kumfo Date: Wed, 7 Aug 2024 17:13:32 +0800 Subject: [PATCH 18/38] feat(badge): badge init add icon --- internal/migrations/v22.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/migrations/v22.go b/internal/migrations/v22.go index b00166717..de15cea6d 100644 --- a/internal/migrations/v22.go +++ b/internal/migrations/v22.go @@ -40,7 +40,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.autobiographer.name", - Icon: "", + Icon: "person-badge-fill", AwardCount: 0, Description: "badge.default_badges.autobiographer.desc", Status: entity.BadgeStatusActive, @@ -55,7 +55,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.editor.name", - Icon: "", + Icon: "pencil-fill", AwardCount: 0, Description: "badge.default_badges.editor.desc", Status: entity.BadgeStatusActive, @@ -70,7 +70,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.first_flag.name", - Icon: "", + Icon: "flag-fill", AwardCount: 0, Description: "badge.default_badges.first_flag.desc", Status: entity.BadgeStatusActive, @@ -85,7 +85,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.first_upvote.name", - Icon: "", + Icon: "hand-thumbs-up-fill", AwardCount: 0, Description: "badge.default_badges.first_upvote.desc", Status: entity.BadgeStatusActive, @@ -100,7 +100,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.first_reaction.name", - Icon: "", + Icon: "emoji-smile-fill", AwardCount: 0, Description: "badge.default_badges.first_reaction.desc", Status: entity.BadgeStatusActive, @@ -115,7 +115,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.first_share.name", - Icon: "", + Icon: "share-fill", AwardCount: 0, Description: "badge.default_badges.first_share.desc", Status: entity.BadgeStatusActive, @@ -130,7 +130,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.scholar.name", - Icon: "", + Icon: "check-circle-fill", AwardCount: 0, Description: "badge.default_badges.scholar.desc", Status: entity.BadgeStatusActive, @@ -145,7 +145,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.solved.name", - Icon: "", + Icon: "check-square-fill", AwardCount: 0, Description: "badge.default_badges.solved.desc", Status: entity.BadgeStatusActive, @@ -160,7 +160,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.nice_answer.name", - Icon: "", + Icon: "chat-square-text-fill", AwardCount: 0, Description: "badge.default_badges.nice_answer.desc", Status: entity.BadgeStatusActive, @@ -175,7 +175,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.good_answer.name", - Icon: "", + Icon: "chat-square-text-fill", AwardCount: 0, Description: "badge.default_badges.good_answer.desc", Status: entity.BadgeStatusActive, @@ -190,7 +190,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.great_answer.name", - Icon: "", + Icon: "chat-square-text-fill", AwardCount: 0, Description: "badge.default_badges.great_answer.desc", Status: entity.BadgeStatusActive, @@ -205,7 +205,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.nice_question.name", - Icon: "", + Icon: "question-circle-fill", AwardCount: 0, Description: "badge.default_badges.nice_question.desc", Status: entity.BadgeStatusActive, @@ -220,7 +220,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.good_question.name", - Icon: "", + Icon: "question-circle-fill", AwardCount: 0, Description: "badge.default_badges.good_question.desc", Status: entity.BadgeStatusActive, @@ -235,7 +235,7 @@ var ( CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "badge.default_badges.great_question.name", - Icon: "", + Icon: "question-circle-fill", AwardCount: 0, Description: "badge.default_badges.great_question.desc", Status: entity.BadgeStatusActive, From 07e4f549b13216f76a69a897fb73d2a8fcd55de4 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Thu, 8 Aug 2024 16:19:47 +0800 Subject: [PATCH 19/38] feat(badge): add event for the badge --- cmd/wire_gen.go | 18 ++-- internal/base/constant/event.go | 75 ++++++++++++++++ internal/controller/template_controller.go | 16 ++++ internal/repo/badge/badge_event_handler.go | 57 ++++++++++++ internal/repo/badge/badge_rule.go | 95 ++++++++++++++++++++ internal/repo/badge/event_rule_mapping.go | 47 ++++++++++ internal/repo/badge/rule.go | 43 +++++++++ internal/schema/event_schema.go | 70 +++++++++++++++ internal/service/comment/comment_service.go | 19 +++- internal/service/content/answer_service.go | 13 +++ internal/service/content/question_service.go | 10 +++ internal/service/content/user_service.go | 8 ++ internal/service/content/vote_service.go | 31 +++++++ internal/service/event_queue/event_queue.go | 69 ++++++++++++++ internal/service/meta/meta_service.go | 22 ++++- internal/service/provider.go | 2 + internal/service/report/report_service.go | 30 ++++++- 17 files changed, 611 insertions(+), 14 deletions(-) create mode 100644 internal/base/constant/event.go create mode 100644 internal/repo/badge/badge_event_handler.go create mode 100644 internal/repo/badge/badge_rule.go create mode 100644 internal/repo/badge/event_rule_mapping.go create mode 100644 internal/repo/badge/rule.go create mode 100644 internal/schema/event_schema.go create mode 100644 internal/service/event_queue/event_queue.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 4928a122b..c6cdc021f 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -78,6 +78,7 @@ import ( config2 "github.com/apache/incubator-answer/internal/service/config" "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/dashboard" + "github.com/apache/incubator-answer/internal/service/event_queue" export2 "github.com/apache/incubator-answer/internal/service/export" "github.com/apache/incubator-answer/internal/service/follow" meta2 "github.com/apache/incubator-answer/internal/service/meta" @@ -172,7 +173,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, metaRepo := meta.NewMetaRepo(dataData) metaCommonService := metacommon.NewMetaCommonService(metaRepo) questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaCommonService, configService, activityQueueService, revisionRepo, dataData) - userService := content.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService, questionCommon) + eventQueueService := event_queue.NewEventQueueService() + userService := content.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService, questionCommon, eventQueueService) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) userController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService, userNotificationConfigService) @@ -181,7 +183,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService) notificationQueueService := notice_queue.NewNotificationQueueService() externalNotificationQueueService := notice_queue.NewNewQuestionNotificationQueueService() - commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, externalNotificationQueueService, activityQueueService) + commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, externalNotificationQueueService, activityQueueService, eventQueueService) rolePowerRelRepo := role.NewRolePowerRelRepo(dataData) rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService) rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService) @@ -194,13 +196,13 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService, userExternalLoginRepo, siteInfoCommonService) reviewRepo := review.NewReviewRepo(dataData) reviewService := review2.NewReviewService(reviewRepo, objService, userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, externalNotificationQueueService, tagCommonService, questionCommon, notificationQueueService, siteInfoCommonService) - questionService := content.NewQuestionService(questionRepo, answerRepo, tagCommonService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService, reviewService, configService) - answerService := content.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService, reviewService) + questionService := content.NewQuestionService(questionRepo, answerRepo, tagCommonService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService, reviewService, configService, eventQueueService) + answerService := content.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService, reviewService, eventQueueService) reportHandle := report_handle.NewReportHandle(questionService, answerService, commentService) - reportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService) + reportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, eventQueueService) reportController := controller.NewReportController(reportService, rankService, captchaService) contentVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService) - voteService := content.NewVoteService(contentVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService) + voteService := content.NewVoteService(contentVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService, eventQueueService) voteController := controller.NewVoteController(voteService, rankService, captchaService) tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService) tagController := controller.NewTagController(tagService, tagCommonService, rankService) @@ -251,7 +253,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, permissionController := controller.NewPermissionController(rankService) userPluginController := controller.NewUserPluginController(pluginCommonService) reviewController := controller.NewReviewController(reviewService, rankService, captchaService) - metaService := meta2.NewMetaService(metaCommonService, userCommon, answerRepo, questionRepo) + metaService := meta2.NewMetaService(metaCommonService, userCommon, answerRepo, questionRepo, eventQueueService) metaController := controller.NewMetaController(metaService) answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) @@ -260,7 +262,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService) shortIDMiddleware := middleware.NewShortIDMiddleware(siteInfoCommonService) templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, siteInfoCommonService, questionRepo) - templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService) + templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService, eventQueueService, userService) templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController, authUserMiddleware) connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService) userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService) diff --git a/internal/base/constant/event.go b/internal/base/constant/event.go new file mode 100644 index 000000000..f7fd8412a --- /dev/null +++ b/internal/base/constant/event.go @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package constant + +// EventType event type. It is used to define the type of event. Such as object.action +type EventType string + +// event object +const ( + eventQuestion = "question" + eventAnswer = "answer" + eventComment = "comment" + eventUser = "user" +) + +// event action +const ( + eventCreate = "create" + eventUpdate = "update" + eventDelete = "delete" + eventVote = "vote" + eventAccept = "accept" // only question have the accept event + eventShare = "share" // the object share link has been clicked + eventFlag = "flag" + eventReact = "react" +) + +const ( + EventUserUpdate EventType = eventUser + "." + eventUpdate + EventUserShare EventType = eventUser + "." + eventShare +) + +const ( + EventQuestionCreate EventType = eventQuestion + "." + eventCreate + EventQuestionUpdate EventType = eventQuestion + "." + eventUpdate + EventQuestionDelete EventType = eventQuestion + "." + eventDelete + EventQuestionVote EventType = eventQuestion + "." + eventVote + EventQuestionAccept EventType = eventQuestion + "." + eventAccept + EventQuestionFlag EventType = eventQuestion + "." + eventFlag + EventQuestionReact EventType = eventQuestion + "." + eventReact +) + +const ( + EventAnswerCreate EventType = eventAnswer + "." + eventCreate + EventAnswerUpdate EventType = eventAnswer + "." + eventUpdate + EventAnswerDelete EventType = eventAnswer + "." + eventDelete + EventAnswerVote EventType = eventAnswer + "." + eventVote + EventAnswerFlag EventType = eventAnswer + "." + eventFlag + EventAnswerReact EventType = eventAnswer + "." + eventReact +) + +const ( + EventCommentCreate EventType = eventComment + "." + eventCreate + EventCommentUpdate EventType = eventComment + "." + eventUpdate + EventCommentDelete EventType = eventComment + "." + eventDelete + EventCommentVote EventType = eventComment + "." + eventVote + EventCommentFlag EventType = eventComment + "." + eventFlag +) diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index 09f5bffcb..a786f646d 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -22,6 +22,8 @@ package controller import ( "encoding/json" "fmt" + "github.com/apache/incubator-answer/internal/service/content" + "github.com/apache/incubator-answer/internal/service/event_queue" "github.com/apache/incubator-answer/plugin" "html/template" "net/http" @@ -54,12 +56,16 @@ type TemplateController struct { cssPath string templateRenderController *templaterender.TemplateRenderController siteInfoService siteinfo_common.SiteInfoCommonService + eventQueueService event_queue.EventQueueService + userService *content.UserService } // NewTemplateController new controller func NewTemplateController( templateRenderController *templaterender.TemplateRenderController, siteInfoService siteinfo_common.SiteInfoCommonService, + eventQueueService event_queue.EventQueueService, + userService *content.UserService, ) *TemplateController { script, css := GetStyle() return &TemplateController{ @@ -67,6 +73,8 @@ func NewTemplateController( cssPath: css, templateRenderController: templateRenderController, siteInfoService: siteInfoService, + eventQueueService: eventQueueService, + userService: userService, } } func GetStyle() (script []string, css string) { @@ -271,6 +279,7 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) { id := ctx.Param("id") title := ctx.Param("title") answerid := ctx.Param("answerid") + shareUsername := ctx.Query("share") if checker.IsQuestionsIgnorePath(id) { // if id == "ask" { file, err := ui.Build.ReadFile("build/index.html") @@ -291,6 +300,13 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) { tc.Page404(ctx) return } + if len(shareUsername) > 0 { + userInfo, err := tc.userService.GetOtherUserInfoByUsername( + ctx, &schema.GetOtherUserInfoByUsernameReq{Username: shareUsername}) + if err == nil { + tc.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserShare, userInfo.ID)) + } + } encodeTitle := htmltext.UrlTitle(detail.Title) if encodeTitle == title { correctTitle = true diff --git a/internal/repo/badge/badge_event_handler.go b/internal/repo/badge/badge_event_handler.go new file mode 100644 index 000000000..242778ecc --- /dev/null +++ b/internal/repo/badge/badge_event_handler.go @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/incubator-answer/internal/service/event_queue" + "github.com/segmentfault/pacman/log" + + "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/schema" +) + +type BadgeEventService struct { + data *data.Data + eventQueueService event_queue.EventQueueService +} + +func NewBadgeEventService( + data *data.Data, + eventQueueService event_queue.EventQueueService, +) *BadgeEventService { + n := &BadgeEventService{ + data: data, + eventQueueService: eventQueueService, + } + eventQueueService.RegisterHandler(n.Handler) + return n +} + +func (ns *BadgeEventService) Handler(ctx context.Context, msg *schema.EventMsg) error { + log.Debugf("received badge event %+v", msg) + // TODO: Check if badge already exists + + // TODO: Check rule + + // TODO: Distribute badge + + return nil +} diff --git a/internal/repo/badge/badge_rule.go b/internal/repo/badge/badge_rule.go new file mode 100644 index 000000000..3ae0b7b44 --- /dev/null +++ b/internal/repo/badge/badge_rule.go @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/base/reason" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/service/unique" + "github.com/segmentfault/pacman/errors" +) + +// BadgeRuleRepo collection repository +type BadgeRuleRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo +} + +// FilledPersonalProfile filled personal profile +func (br *BadgeRuleRepo) FilledPersonalProfile(ctx context.Context, userID string) (reach bool, err error) { + bean := &entity.User{ID: userID} + exist, err := br.data.DB.Context(ctx).Get(bean) + if err != nil { + return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return false, nil + } + if len(bean.Bio) > 0 { + return true, nil + } + return false, nil +} + +// FirstPostEdit first post edit +func (br *BadgeRuleRepo) FirstPostEdit(ctx context.Context, userID string, objectID string) { + +} + +// FirstFlaggedPost first flagged post. +func (br *BadgeRuleRepo) FirstFlaggedPost(ctx context.Context, userID string, reportID string) { +} + +// FirstVotedPost first voted post +func (br *BadgeRuleRepo) FirstVotedPost(ctx context.Context) { + +} + +// FirstReactedPost first reacted post +func (br *BadgeRuleRepo) FirstReactedPost(ctx context.Context) { + +} + +// FirstSharedPost first shared post +func (br *BadgeRuleRepo) FirstSharedPost(ctx context.Context) { + +} + +// AskQuestionAcceptAnswer ask question accept answer +func (br *BadgeRuleRepo) AskQuestionAcceptAnswer(ctx context.Context) { + +} + +// AnswerAccepted answer accepted +func (br *BadgeRuleRepo) AnswerAccepted(ctx context.Context) { + +} + +// ReachAnswerScore reach answer score +func (br *BadgeRuleRepo) ReachAnswerScore(ctx context.Context) { + +} + +// ReachQuestionScore reach question score +func (br *BadgeRuleRepo) ReachQuestionScore(ctx context.Context) { + +} diff --git a/internal/repo/badge/event_rule_mapping.go b/internal/repo/badge/event_rule_mapping.go new file mode 100644 index 000000000..8f01fdef9 --- /dev/null +++ b/internal/repo/badge/event_rule_mapping.go @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import "github.com/apache/incubator-answer/internal/base/constant" + +var ( + EventRuleMapping = map[constant.EventType][]string{ + constant.EventUserUpdate: {FilledPersonalProfile}, + constant.EventUserShare: {FirstSharedPost}, + constant.EventQuestionCreate: {""}, + constant.EventQuestionUpdate: {FirstPostEdit}, + constant.EventQuestionDelete: {""}, + constant.EventQuestionVote: {FirstVotedPost, ReachQuestionScore}, + constant.EventQuestionAccept: {AskQuestionAcceptAnswer, AnswerAccepted}, + constant.EventQuestionFlag: {FirstFlaggedPost}, + constant.EventQuestionReact: {FirstReactedPost}, + constant.EventAnswerCreate: {""}, + constant.EventAnswerUpdate: {FirstPostEdit}, + constant.EventAnswerDelete: {""}, + constant.EventAnswerVote: {FirstVotedPost, ReachAnswerScore}, + constant.EventAnswerFlag: {FirstFlaggedPost}, + constant.EventAnswerReact: {FirstReactedPost}, + constant.EventCommentCreate: {""}, + constant.EventCommentUpdate: {""}, + constant.EventCommentDelete: {""}, + constant.EventCommentVote: {FirstVotedPost}, + constant.EventCommentFlag: {FirstFlaggedPost}, + } +) diff --git a/internal/repo/badge/rule.go b/internal/repo/badge/rule.go new file mode 100644 index 000000000..635b30dae --- /dev/null +++ b/internal/repo/badge/rule.go @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +const ( + // FilledPersonalProfile filled personal profile + FilledPersonalProfile = "filled_personal_profile" + // FirstPostEdit first post edit + FirstPostEdit = "first_post_edit" + // FirstFlaggedPost first flagged post. + FirstFlaggedPost = "first_flagged_post" + // FirstVotedPost first voted post + FirstVotedPost = "first_voted_post" + // FirstReactedPost first reacted post + FirstReactedPost = "first_reacted_post" + // FirstSharedPost first shared post + FirstSharedPost = "first_shared_post" + // AskQuestionAcceptAnswer ask question accept answer + AskQuestionAcceptAnswer = "ask_question_accept_answer" + // AnswerAccepted answer accepted + AnswerAccepted = "answer_accepted" + // ReachAnswerScore reach answer score + ReachAnswerScore = "reach_answer_score" + // ReachQuestionScore reach question score + ReachQuestionScore = "reach_question_score" +) diff --git a/internal/schema/event_schema.go b/internal/schema/event_schema.go new file mode 100644 index 000000000..01f80fbbe --- /dev/null +++ b/internal/schema/event_schema.go @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import "github.com/apache/incubator-answer/internal/base/constant" + +// EventMsg event message +type EventMsg struct { + EventType constant.EventType + UserID string + + QuestionID string + QuestionUserID string + + AnswerID string + AnswerUserID string + + CommentID string + CommentUserID string + + ExtraInfo map[string]string +} + +func NewEvent(e constant.EventType, userID string) *EventMsg { + return &EventMsg{ + UserID: userID, + EventType: e, + ExtraInfo: make(map[string]string), + } +} + +func (e *EventMsg) QID(questionID, userID string) *EventMsg { + e.QuestionID = questionID + e.QuestionUserID = userID + return e +} + +func (e *EventMsg) AID(answerID, userID string) *EventMsg { + e.AnswerID = answerID + e.AnswerUserID = userID + return e +} + +func (e *EventMsg) CID(comment, userID string) *EventMsg { + e.CommentID = comment + e.CommentUserID = userID + return e +} + +func (e *EventMsg) AddExtra(key, value string) *EventMsg { + e.ExtraInfo[key] = value + return e +} diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index 9f2c45f1a..75d30f9da 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -21,6 +21,7 @@ package comment import ( "context" + "github.com/apache/incubator-answer/internal/service/event_queue" "time" "github.com/apache/incubator-answer/internal/base/constant" @@ -86,6 +87,7 @@ type CommentService struct { notificationQueueService notice_queue.NotificationQueueService externalNotificationQueueService notice_queue.ExternalNotificationQueueService activityQueueService activity_queue.ActivityQueueService + eventQueueService event_queue.EventQueueService } // NewCommentService new comment service @@ -100,6 +102,7 @@ func NewCommentService( notificationQueueService notice_queue.NotificationQueueService, externalNotificationQueueService notice_queue.ExternalNotificationQueueService, activityQueueService activity_queue.ActivityQueueService, + eventQueueService event_queue.EventQueueService, ) *CommentService { return &CommentService{ commentRepo: commentRepo, @@ -112,6 +115,7 @@ func NewCommentService( notificationQueueService: notificationQueueService, externalNotificationQueueService: externalNotificationQueueService, activityQueueService: activityQueueService, + eventQueueService: eventQueueService, } } @@ -184,13 +188,19 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment OriginalObjectID: req.ObjectID, ActivityTypeKey: constant.ActQuestionCommented, } + var event *schema.EventMsg switch objInfo.ObjectType { case constant.QuestionObjectType: activityMsg.ActivityTypeKey = constant.ActQuestionCommented + event = schema.NewEvent(constant.EventCommentCreate, req.UserID). + CID(comment.ID, comment.UserID).QID(objInfo.QuestionID, objInfo.ObjectCreatorUserID) case constant.AnswerObjectType: activityMsg.ActivityTypeKey = constant.ActAnswerCommented + event = schema.NewEvent(constant.EventCommentCreate, req.UserID). + CID(comment.ID, comment.UserID).AID(objInfo.AnswerID, objInfo.ObjectCreatorUserID) } cs.activityQueueService.Send(ctx, activityMsg) + cs.eventQueueService.Send(ctx, event) return resp, nil } @@ -241,7 +251,12 @@ func (cs *CommentService) addCommentNotification( // RemoveComment delete comment func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) { - return cs.commentRepo.RemoveComment(ctx, req.CommentID) + err = cs.commentRepo.RemoveComment(ctx, req.CommentID) + if err != nil { + return err + } + cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentDelete, req.UserID).CID(req.CommentID, req.UserID)) + return nil } // UpdateComment update comment @@ -273,6 +288,8 @@ func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateC OriginalText: req.OriginalText, ParsedText: req.ParsedText, } + cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentUpdate, req.UserID). + CID(old.ID, old.UserID)) return resp, nil } diff --git a/internal/service/content/answer_service.go b/internal/service/content/answer_service.go index f8feda8af..5674a79b5 100644 --- a/internal/service/content/answer_service.go +++ b/internal/service/content/answer_service.go @@ -22,6 +22,7 @@ package content import ( "context" "encoding/json" + "github.com/apache/incubator-answer/internal/service/event_queue" "time" "github.com/apache/incubator-answer/internal/base/constant" @@ -67,6 +68,7 @@ type AnswerService struct { externalNotificationQueueService notice_queue.ExternalNotificationQueueService activityQueueService activity_queue.ActivityQueueService reviewService *review.ReviewService + eventQueueService event_queue.EventQueueService } func NewAnswerService( @@ -86,6 +88,7 @@ func NewAnswerService( externalNotificationQueueService notice_queue.ExternalNotificationQueueService, activityQueueService activity_queue.ActivityQueueService, reviewService *review.ReviewService, + eventQueueService event_queue.EventQueueService, ) *AnswerService { return &AnswerService{ answerRepo: answerRepo, @@ -104,6 +107,7 @@ func NewAnswerService( externalNotificationQueueService: externalNotificationQueueService, activityQueueService: activityQueueService, reviewService: reviewService, + eventQueueService: eventQueueService, } } @@ -175,6 +179,8 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns OriginalObjectID: answerInfo.ID, ActivityTypeKey: constant.ActAnswerDeleted, }) + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerDelete, req.UserID). + AID(answerInfo.ID, answerInfo.UserID)) return } @@ -295,6 +301,8 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionAnswered, }) + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerCreate, req.UserID). + AID(insertData.ID, insertData.UserID)) return insertData.ID, nil } @@ -383,6 +391,8 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq ActivityTypeKey: constant.ActAnswerEdited, RevisionID: revisionID, }) + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerUpdate, req.UserID). + AID(insertData.ID, insertData.UserID)) } return insertData.ID, nil @@ -436,6 +446,9 @@ func (as *AnswerService) AcceptAnswer(ctx context.Context, req *schema.AcceptAns oldAnswerInfo.ID = uid.DeShortID(oldAnswerInfo.ID) } + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID). + QID(questionInfo.ID, questionInfo.UserID).AID(req.AnswerID, req.UserID)) + as.updateAnswerRank(ctx, req.UserID, questionInfo, acceptedAnswerInfo, oldAnswerInfo) return nil } diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go index 6d8b37ba9..dfb778ea1 100644 --- a/internal/service/content/question_service.go +++ b/internal/service/content/question_service.go @@ -23,6 +23,7 @@ import ( "encoding/json" "fmt" answercommon "github.com/apache/incubator-answer/internal/service/answer_common" + "github.com/apache/incubator-answer/internal/service/event_queue" "strings" "time" @@ -84,6 +85,7 @@ type QuestionService struct { newQuestionNotificationService *notification.ExternalNotificationService reviewService *review.ReviewService configService *config.ConfigService + eventQueueService event_queue.EventQueueService } func NewQuestionService( @@ -106,6 +108,7 @@ func NewQuestionService( newQuestionNotificationService *notification.ExternalNotificationService, reviewService *review.ReviewService, configService *config.ConfigService, + eventQueueService event_queue.EventQueueService, ) *QuestionService { return &QuestionService{ questionRepo: questionRepo, @@ -127,6 +130,7 @@ func NewQuestionService( newQuestionNotificationService: newQuestionNotificationService, reviewService: reviewService, configService: configService, + eventQueueService: eventQueueService, } } @@ -385,6 +389,8 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question qs.externalNotificationQueueService.Send(ctx, schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, tags)) } + qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionCreate, req.UserID). + QID(question.ID, question.UserID)) questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) return @@ -546,6 +552,8 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov OriginalObjectID: questionInfo.ID, ActivityTypeKey: constant.ActQuestionDeleted, }) + qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionDelete, req.UserID). + QID(questionInfo.ID, questionInfo.UserID)) return nil } @@ -937,6 +945,8 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest RevisionID: revisionID, OriginalObjectID: question.ID, }) + qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionUpdate, req.UserID). + QID(question.ID, question.UserID)) } questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) diff --git a/internal/service/content/user_service.go b/internal/service/content/user_service.go index 11f3bb63b..2a6122ae7 100644 --- a/internal/service/content/user_service.go +++ b/internal/service/content/user_service.go @@ -23,6 +23,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/apache/incubator-answer/internal/service/event_queue" "time" "github.com/apache/incubator-answer/internal/base/constant" @@ -65,6 +66,7 @@ type UserService struct { userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo userNotificationConfigService *user_notification_config.UserNotificationConfigService questionService *questioncommon.QuestionCommon + eventQueueService event_queue.EventQueueService } func NewUserService(userRepo usercommon.UserRepo, @@ -79,6 +81,7 @@ func NewUserService(userRepo usercommon.UserRepo, userNotificationConfigRepo user_notification_config.UserNotificationConfigRepo, userNotificationConfigService *user_notification_config.UserNotificationConfigService, questionService *questioncommon.QuestionCommon, + eventQueueService event_queue.EventQueueService, ) *UserService { return &UserService{ userCommonService: userCommonService, @@ -93,6 +96,7 @@ func NewUserService(userRepo usercommon.UserRepo, userNotificationConfigRepo: userNotificationConfigRepo, userNotificationConfigService: userNotificationConfigService, questionService: questionService, + eventQueueService: eventQueueService, } } @@ -352,6 +356,10 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq cond := us.formatUserInfoForUpdateInfo(oldUserInfo, req, siteUsers) err = us.userRepo.UpdateInfo(ctx, cond) + if err != nil { + return nil, err + } + us.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserUpdate, req.UserID)) return nil, err } diff --git a/internal/service/content/vote_service.go b/internal/service/content/vote_service.go index bfb403c9f..c83011a9c 100644 --- a/internal/service/content/vote_service.go +++ b/internal/service/content/vote_service.go @@ -21,6 +21,8 @@ package content import ( "context" + "fmt" + "github.com/apache/incubator-answer/internal/service/event_queue" "strings" "github.com/apache/incubator-answer/internal/service/activity_common" @@ -62,6 +64,7 @@ type VoteService struct { commentCommonRepo comment_common.CommentCommonRepo objectService *object_info.ObjService activityRepo activity_common.ActivityRepo + eventQueueService event_queue.EventQueueService } func NewVoteService( @@ -71,6 +74,7 @@ func NewVoteService( answerRepo answercommon.AnswerRepo, commentCommonRepo comment_common.CommentCommonRepo, objectService *object_info.ObjService, + eventQueueService event_queue.EventQueueService, ) *VoteService { return &VoteService{ voteRepo: voteRepo, @@ -79,6 +83,7 @@ func NewVoteService( answerRepo: answerRepo, commentCommonRepo: commentCommonRepo, objectService: objectService, + eventQueueService: eventQueueService, } } @@ -112,6 +117,9 @@ func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *s return nil, err } err = vs.voteRepo.Vote(ctx, voteUpOperationInfo) + if err != nil { + return nil, err + } } if err != nil { return nil, err @@ -125,6 +133,7 @@ func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *s resp.Votes = resp.UpVotes - resp.DownVotes if !req.IsCancel { resp.VoteStatus = constant.ActVoteUp + vs.sendEvent(ctx, req, objectInfo, resp) } return resp, nil } @@ -173,6 +182,7 @@ func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp resp.Votes = resp.UpVotes - resp.DownVotes if !req.IsCancel { resp.VoteStatus = constant.ActVoteDown + vs.sendEvent(ctx, req, objectInfo, resp) } return resp, nil } @@ -289,3 +299,24 @@ func (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperati } return activities } + +func (vs *VoteService) sendEvent(ctx context.Context, + req *schema.VoteReq, objectInfo *schema.SimpleObjectInfo, resp *schema.VoteResp) { + var event *schema.EventMsg + switch objectInfo.ObjectType { + case constant.QuestionObjectType: + event = schema.NewEvent(constant.EventQuestionVote, req.UserID). + QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID) + case constant.AnswerObjectType: + event = schema.NewEvent(constant.EventAnswerVote, req.UserID). + AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID) + case constant.CommentObjectType: + event = schema.NewEvent(constant.EventCommentVote, req.UserID). + CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID) + default: + return + } + event.AddExtra("vote_up_amount", fmt.Sprintf("%d", resp.UpVotes)) + event.AddExtra("vote_down_amount", fmt.Sprintf("%d", resp.DownVotes)) + vs.eventQueueService.Send(ctx, event) +} diff --git a/internal/service/event_queue/event_queue.go b/internal/service/event_queue/event_queue.go new file mode 100644 index 000000000..b89a3ccc4 --- /dev/null +++ b/internal/service/event_queue/event_queue.go @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package event_queue + +import ( + "context" + + "github.com/apache/incubator-answer/internal/schema" + "github.com/segmentfault/pacman/log" +) + +type EventQueueService interface { + Send(ctx context.Context, msg *schema.EventMsg) + RegisterHandler(handler func(ctx context.Context, msg *schema.EventMsg) error) +} + +type eventQueueService struct { + Queue chan *schema.EventMsg + Handler func(ctx context.Context, msg *schema.EventMsg) error +} + +func (ns *eventQueueService) Send(ctx context.Context, msg *schema.EventMsg) { + ns.Queue <- msg +} + +func (ns *eventQueueService) RegisterHandler( + handler func(ctx context.Context, msg *schema.EventMsg) error) { + ns.Handler = handler +} + +func (ns *eventQueueService) working() { + go func() { + for msg := range ns.Queue { + log.Debugf("received badge %+v", msg) + if ns.Handler == nil { + log.Warnf("no handler for badge") + continue + } + if err := ns.Handler(context.Background(), msg); err != nil { + log.Error(err) + } + } + }() +} + +// NewEventQueueService create a new badge queue service +func NewEventQueueService() EventQueueService { + ns := &eventQueueService{} + ns.Queue = make(chan *schema.EventMsg, 128) + ns.working() + return ns +} diff --git a/internal/service/meta/meta_service.go b/internal/service/meta/meta_service.go index 1026b1733..778c6ca66 100644 --- a/internal/service/meta/meta_service.go +++ b/internal/service/meta/meta_service.go @@ -23,6 +23,7 @@ import ( "context" "encoding/json" "errors" + "github.com/apache/incubator-answer/internal/service/event_queue" "strconv" "strings" @@ -46,14 +47,22 @@ type MetaService struct { userCommon *usercommon.UserCommon questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo + eventQueueService event_queue.EventQueueService } -func NewMetaService(metaCommonService *metacommon.MetaCommonService, userCommon *usercommon.UserCommon, answerRepo answercommon.AnswerRepo, questionRepo questioncommon.QuestionRepo) *MetaService { +func NewMetaService( + metaCommonService *metacommon.MetaCommonService, + userCommon *usercommon.UserCommon, + answerRepo answercommon.AnswerRepo, + questionRepo questioncommon.QuestionRepo, + eventQueueService event_queue.EventQueueService, +) *MetaService { return &MetaService{ metaCommonService: metaCommonService, questionRepo: questionRepo, userCommon: userCommon, answerRepo: answerRepo, + eventQueueService: eventQueueService, } } @@ -86,22 +95,27 @@ func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.Upda if err != nil { return nil, err } + var event *schema.EventMsg if objectType == constant.AnswerObjectType { - _, exist, err := ms.answerRepo.GetAnswer(ctx, req.ObjectID) + answerInfo, exist, err := ms.answerRepo.GetAnswer(ctx, req.ObjectID) if err != nil { return nil, err } if !exist { return nil, myErrors.BadRequest(reason.AnswerNotFound) } + event = schema.NewEvent(constant.EventAnswerReact, req.UserID). + AID(answerInfo.ID, answerInfo.UserID) } else if objectType == constant.QuestionObjectType { - _, exist, err := ms.questionRepo.GetQuestion(ctx, req.ObjectID) + questionInfo, exist, err := ms.questionRepo.GetQuestion(ctx, req.ObjectID) if err != nil { return nil, err } if !exist { return nil, myErrors.BadRequest(reason.QuestionNotFound) } + event = schema.NewEvent(constant.EventQuestionReact, req.UserID). + QID(questionInfo.ID, questionInfo.UserID) } else { return nil, myErrors.BadRequest(reason.ObjectNotFound) } @@ -138,7 +152,7 @@ func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.Upda if err != nil { return nil, err } - + ms.eventQueueService.Send(ctx, event) return resp, nil } diff --git a/internal/service/provider.go b/internal/service/provider.go index e82d93167..a22a6ea91 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -33,6 +33,7 @@ import ( "github.com/apache/incubator-answer/internal/service/config" "github.com/apache/incubator-answer/internal/service/content" "github.com/apache/incubator-answer/internal/service/dashboard" + "github.com/apache/incubator-answer/internal/service/event_queue" "github.com/apache/incubator-answer/internal/service/export" "github.com/apache/incubator-answer/internal/service/follow" "github.com/apache/incubator-answer/internal/service/meta" @@ -117,4 +118,5 @@ var ProviderSetService = wire.NewSet( notice_queue.NewNewQuestionNotificationQueueService, review.NewReviewService, meta.NewMetaService, + event_queue.NewEventQueueService, ) diff --git a/internal/service/report/report_service.go b/internal/service/report/report_service.go index 7f060a39f..218423e13 100644 --- a/internal/service/report/report_service.go +++ b/internal/service/report/report_service.go @@ -21,6 +21,7 @@ package report import ( "encoding/json" + "github.com/apache/incubator-answer/internal/service/event_queue" "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/handler" @@ -55,6 +56,7 @@ type ReportService struct { commentCommonRepo comment_common.CommentCommonRepo reportHandle *report_handle.ReportHandle configService *config.ConfigService + eventQueueService event_queue.EventQueueService } // NewReportService new report service @@ -67,6 +69,7 @@ func NewReportService( commentCommonRepo comment_common.CommentCommonRepo, reportHandle *report_handle.ReportHandle, configService *config.ConfigService, + eventQueueService event_queue.EventQueueService, ) *ReportService { return &ReportService{ reportRepo: reportRepo, @@ -77,6 +80,7 @@ func NewReportService( commentCommonRepo: commentCommonRepo, reportHandle: reportHandle, configService: configService, + eventQueueService: eventQueueService, } } @@ -112,7 +116,12 @@ func (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq Content: req.Content, Status: entity.ReportStatusPending, } - return rs.reportRepo.AddReport(ctx, report) + err = rs.reportRepo.AddReport(ctx, report) + if err != nil { + return err + } + rs.sendEvent(ctx, report, objInfo) + return nil } // GetUnreviewedReportPostPage get unreviewed report post page @@ -218,3 +227,22 @@ func (rs *ReportService) ReviewReport(ctx context.Context, req *schema.ReviewRep return rs.reportRepo.UpdateStatus(ctx, report.ID, entity.ReportStatusCompleted) } + +func (rs *ReportService) sendEvent(ctx context.Context, + report *entity.Report, objectInfo *schema.SimpleObjectInfo) { + var event *schema.EventMsg + switch objectInfo.ObjectType { + case constant.QuestionObjectType: + event = schema.NewEvent(constant.EventQuestionFlag, report.UserID). + QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID) + case constant.AnswerObjectType: + event = schema.NewEvent(constant.EventAnswerFlag, report.UserID). + AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID) + case constant.CommentObjectType: + event = schema.NewEvent(constant.EventCommentFlag, report.UserID). + CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID) + default: + return + } + rs.eventQueueService.Send(ctx, event) +} From 797ea9b9187847fde3dad778fd8c1715b0b5cd33 Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 8 Aug 2024 16:41:17 +0800 Subject: [PATCH 20/38] feat(badge): badge info && badge award list --- cmd/wire_gen.go | 4 +- docs/docs.go | 156 ++++++++++++++++++ docs/swagger.json | 156 ++++++++++++++++++ docs/swagger.yaml | 96 +++++++++++ i18n/en_US.yaml | 3 + internal/base/reason/reason.go | 1 + internal/controller/badge_controller.go | 61 ++++++- internal/entity/badge_award_entity.go | 8 +- internal/repo/badge/badge_repo.go | 6 + internal/repo/badge_award/badge_award_repo.go | 33 +++- internal/router/answer_api_router.go | 2 + internal/schema/badge.go | 34 ---- internal/schema/badge_schema.go | 70 ++++++++ internal/service/badge/badge_service.go | 52 +++++- .../badge_award/badge_award_service.go | 102 ++++++++++-- 15 files changed, 718 insertions(+), 66 deletions(-) delete mode 100644 internal/schema/badge.go create mode 100644 internal/schema/badge_schema.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index c20aa2cfd..30b3b65ce 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -75,6 +75,7 @@ import ( "github.com/apache/incubator-answer/internal/service/answer_common" auth2 "github.com/apache/incubator-answer/internal/service/auth" badge2 "github.com/apache/incubator-answer/internal/service/badge" + badge_award2 "github.com/apache/incubator-answer/internal/service/badge_award" collection2 "github.com/apache/incubator-answer/internal/service/collection" "github.com/apache/incubator-answer/internal/service/collection_common" comment2 "github.com/apache/incubator-answer/internal/service/comment" @@ -261,7 +262,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, badgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo) badgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo) badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo) - badgeController := controller.NewBadgeController(badgeService) + badgeAwardService := badge_award2.NewBadgeAwardService(badgeAwardRepo, userCommon, objService, questionRepo, answerRepo) + badgeController := controller.NewBadgeController(badgeService, badgeAwardService) answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService) diff --git a/docs/docs.go b/docs/docs.go index 6fc76693f..bda5a17b6 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2239,6 +2239,117 @@ const docTemplate = `{ } } }, + "/answer/api/v1/badge": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get badge info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "get badge info", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetBadgeInfoResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/badge/awards/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get badge award list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "get badge award list", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "badge id", + "name": "badge_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetBadgeInfoResp" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/badges": { "get": { "security": [ @@ -6942,6 +7053,19 @@ const docTemplate = `{ } } }, + "entity.BadgeLevel": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BadgeLevelBronze", + "BadgeLevelSilver", + "BadgeLevelGold" + ] + }, "handler.RespBody": { "type": "object", "properties": { @@ -7400,6 +7524,9 @@ const docTemplate = `{ "id": { "type": "string" }, + "level": { + "$ref": "#/definitions/entity.BadgeLevel" + }, "name": { "type": "string" } @@ -7656,6 +7783,35 @@ const docTemplate = `{ } } }, + "schema.GetBadgeInfoResp": { + "type": "object", + "properties": { + "award_count": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "earned_count": { + "type": "integer" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_single": { + "type": "boolean" + }, + "level": { + "$ref": "#/definitions/entity.BadgeLevel" + }, + "name": { + "type": "string" + } + } + }, "schema.GetBadgeListResp": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 45936363f..5ea50e6bd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2209,6 +2209,117 @@ } } }, + "/answer/api/v1/badge": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get badge info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "get badge info", + "parameters": [ + { + "type": "string", + "default": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetBadgeInfoResp" + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/badge/awards/page": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get badge award list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "get badge award list", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "badge id", + "name": "badge_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/schema.GetBadgeInfoResp" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/badges": { "get": { "security": [ @@ -6912,6 +7023,19 @@ } } }, + "entity.BadgeLevel": { + "type": "integer", + "enum": [ + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "BadgeLevelBronze", + "BadgeLevelSilver", + "BadgeLevelGold" + ] + }, "handler.RespBody": { "type": "object", "properties": { @@ -7370,6 +7494,9 @@ "id": { "type": "string" }, + "level": { + "$ref": "#/definitions/entity.BadgeLevel" + }, "name": { "type": "string" } @@ -7626,6 +7753,35 @@ } } }, + "schema.GetBadgeInfoResp": { + "type": "object", + "properties": { + "award_count": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "earned_count": { + "type": "integer" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_single": { + "type": "boolean" + }, + "level": { + "$ref": "#/definitions/entity.BadgeLevel" + }, + "name": { + "type": "string" + } + } + }, "schema.GetBadgeListResp": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c91c987e6..ca11b20f3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -32,6 +32,16 @@ definitions: minimum: 1 type: integer type: object + entity.BadgeLevel: + enum: + - 1 + - 2 + - 3 + type: integer + x-enum-varnames: + - BadgeLevelBronze + - BadgeLevelSilver + - BadgeLevelGold handler.RespBody: properties: code: @@ -352,6 +362,8 @@ definitions: type: string id: type: string + level: + $ref: '#/definitions/entity.BadgeLevel' name: type: string type: object @@ -526,6 +538,25 @@ definitions: description: if user is followed object will be true,otherwise false type: boolean type: object + schema.GetBadgeInfoResp: + properties: + award_count: + type: integer + description: + type: string + earned_count: + type: integer + icon: + type: string + id: + type: string + is_single: + type: boolean + level: + $ref: '#/definitions/entity.BadgeLevel' + name: + type: string + type: object schema.GetBadgeListResp: properties: badges: @@ -4105,6 +4136,71 @@ paths: summary: recover answer tags: - Answer + /answer/api/v1/badge: + get: + consumes: + - application/json + description: get badge info + parameters: + - default: string + description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetBadgeInfoResp' + type: object + security: + - ApiKeyAuth: [] + summary: get badge info + tags: + - api-badge + /answer/api/v1/badge/awards/page: + get: + consumes: + - application/json + description: get badge award list + parameters: + - description: page + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: badge id + in: query + name: badge_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/schema.GetBadgeInfoResp' + type: object + security: + - ApiKeyAuth: [] + summary: get badge award list + tags: + - api-badge /answer/api/v1/badges: get: consumes: diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index f50465c2b..0d7f2ecf3 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -312,6 +312,9 @@ backend: site_info: config_not_found: other: Site config not found. + badge: + object_not_found: + other: Badge object not found reason: spam: name: diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 66ef1bed0..a61686900 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -106,6 +106,7 @@ const ( AddBulkUsersAmountError = "error.user.add_bulk_users_amount_error" InvalidURLError = "error.common.invalid_url" MetaObjectNotFound = "error.meta.object_not_found" + BadgeObjectNotFound = "error.badge.object_not_found" ) // user external login reasons diff --git a/internal/controller/badge_controller.go b/internal/controller/badge_controller.go index ebc534ced..c26d9205a 100644 --- a/internal/controller/badge_controller.go +++ b/internal/controller/badge_controller.go @@ -22,17 +22,25 @@ package controller import ( "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/middleware" + "github.com/apache/incubator-answer/internal/base/pager" + "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/badge" + "github.com/apache/incubator-answer/internal/service/badge_award" + "github.com/apache/incubator-answer/pkg/uid" "github.com/gin-gonic/gin" ) type BadgeController struct { - badgeService *badge.BadgeService + badgeService *badge.BadgeService + badgeAwardService *badge_award.BadgeAwardService } -func NewBadgeController(badgeService *badge.BadgeService) *BadgeController { +func NewBadgeController( + badgeService *badge.BadgeService, + badgeAwardService *badge_award.BadgeAwardService) *BadgeController { return &BadgeController{ - badgeService: badgeService, + badgeService: badgeService, + badgeAwardService: badgeAwardService, } } @@ -50,3 +58,50 @@ func (b *BadgeController) GetBadgeList(ctx *gin.Context) { resp, err := b.badgeService.ListByGroup(ctx, userID) handler.HandleResponse(ctx, err, resp) } + +// GetBadgeInfo get badge info +// @Summary get badge info +// @Description get badge info +// @Tags api-badge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param id query string true "id" default(string) +// @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp} +// @Router /answer/api/v1/badge [get] +func (b *BadgeController) GetBadgeInfo(ctx *gin.Context) { + id := ctx.Query("id") + id = uid.DeShortID(id) + + userID := middleware.GetLoginUserIDFromContext(ctx) + resp, err := b.badgeService.GetBadgeInfo(ctx, id, userID) + handler.HandleResponse(ctx, err, resp) +} + +// GetBadgeAwardList get badge award list +// @Summary get badge award list +// @Description get badge award list +// @Tags api-badge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param page query int false "page" +// @Param page_size query int false "page size" +// @Param badge_id query string true "badge id" +// @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp} +// @Router /answer/api/v1/badge/awards/page [get] +func (b *BadgeController) GetBadgeAwardList(ctx *gin.Context) { + req := &schema.GetBadgeAwardWithPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.BadgeID = uid.DeShortID(req.BadgeID) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + + resp, total, err := b.badgeAwardService.GetBadgeAwardList(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) +} diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index a852f6bd3..a26fd8de4 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -26,10 +26,10 @@ type BadgeAward struct { ID string `json:"id" xorm:"id"` CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` - UserId int64 `json:"user_id" xorm:"not null index BIGINT(20) user_id"` - BadgeId int64 `json:"badge_id" xorm:"not null index BIGINT(20) badge_id"` - ObjectId int64 `json:"object_id" xorm:"not null index BIGINT(20) object_id"` - BadgeGroupId int8 `json:"badge_group_id" xorm:"not null index BIGINT(20) badge_group_id"` + UserID string `json:"user_id" xorm:"not null index BIGINT(20) user_id"` + BadgeID string `json:"badge_id" xorm:"not null index BIGINT(20) badge_id"` + ObjectID string `json:"object_id" xorm:"not null index BIGINT(20) object_id"` + BadgeGroupID int8 `json:"badge_group_id" xorm:"not null index BIGINT(20) badge_group_id"` IsBadgeDeleted int8 `json:"is_badge_deleted" xorm:"not null index TINYINT(1) s_badge_deleted"` } diff --git a/internal/repo/badge/badge_repo.go b/internal/repo/badge/badge_repo.go index b4cd72f1c..7651f95b5 100644 --- a/internal/repo/badge/badge_repo.go +++ b/internal/repo/badge/badge_repo.go @@ -40,6 +40,12 @@ func NewBadgeRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.Badge } } +func (r badgeRepo) GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) { + badge = &entity.Badge{} + exists, err = r.data.DB.Context(ctx).Where("id = ?", id).Get(badge) + return +} + // ListByLevel returns a list of badges by level func (r *badgeRepo) ListByLevel(ctx context.Context, level entity.BadgeLevel) (badges []*entity.Badge, err error) { badges = make([]*entity.Badge, 0) diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index b6f0e95eb..0a4603478 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -22,9 +22,12 @@ package badge_award import ( "context" "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/base/pager" + "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/service/badge_award" "github.com/apache/incubator-answer/internal/service/unique" + "github.com/segmentfault/pacman/errors" "time" ) @@ -53,6 +56,10 @@ func (r *badgeAwardRepo) CountByUserId(ctx context.Context, userID string) (awar return } func (r *badgeAwardRepo) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) { + awardCount, err := r.data.DB.Context(ctx).Where("user_id = ? AND badge_id = ?", userID, badgeID).Count(&entity.BadgeAward{}) + if err != nil { + return 0 + } return } func (r *badgeAwardRepo) CountByObjectId(ctx context.Context, objectID string) (awardCount int64) { @@ -71,31 +78,39 @@ func (r *badgeAwardRepo) SumUserEarnedGroupByBadgeID(ctx context.Context, userID func (r *badgeAwardRepo) ListAllByUserId(ctx context.Context, userID string) (badgeAwards []*entity.BadgeAward) { return } -func (r *badgeAwardRepo) ListPagedByBadgeId(ctx context.Context, badgeID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { + +// ListPagedByBadgeId list badge awards by badge id +func (r *badgeAwardRepo) ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) { + session := r.data.DB.Context(ctx) + session.Where("badge_id = ?", badgeID) + total, err = pager.Help(page, pageSize, &badgeAwardList, &entity.Question{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } -func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { +func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) ListPagedByObjectId(ctx context.Context, badgeID string, objectID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { +func (r *badgeAwardRepo) ListPagedByObjectId(ctx context.Context, badgeID string, objectID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, objectID string, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { +func (r *badgeAwardRepo) ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, objectID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) ListTagPagedByBadgeId(ctx context.Context, badgeIDs []int64, page int64, pageSize int64, filterUserID int64) (badgeAwards []*entity.BadgeAward, total int64) { +func (r *badgeAwardRepo) ListTagPagedByBadgeId(ctx context.Context, badgeIDs []string, page int, pageSize int, filterUserID string) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []int64, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { +func (r *badgeAwardRepo) ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) ListPagedLatest(ctx context.Context, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { +func (r *badgeAwardRepo) ListPagedLatest(ctx context.Context, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int64) (badgeAwards []*entity.BadgeAward, total int64) { +func (r *badgeAwardRepo) ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int64, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) { +func (r *badgeAwardRepo) ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } func (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward) { diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index dadf2a853..c7d4c38ea 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -192,6 +192,8 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/meta/reaction", a.metaController.GetReaction) // badges + r.GET("/badge", a.badgeController.GetBadgeInfo) + r.GET("/badge/awards/page", a.badgeController.GetBadgeAwardList) r.GET("/badges", a.badgeController.GetBadgeList) } diff --git a/internal/schema/badge.go b/internal/schema/badge.go deleted file mode 100644 index c52a235b4..000000000 --- a/internal/schema/badge.go +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package schema - -// BadgeListInfo get badge list response -type BadgeListInfo struct { - ID string `json:"id" ` - Name string `json:"name" ` - Icon string `json:"icon" ` - AwardCount int `json:"award_count" ` - Earned bool `json:"earned" ` -} - -type GetBadgeListResp struct { - Badges []*BadgeListInfo `json:"badges" ` - GroupName string `json:"group_name" ` -} diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go new file mode 100644 index 000000000..06d92ecc4 --- /dev/null +++ b/internal/schema/badge_schema.go @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package schema + +import "github.com/apache/incubator-answer/internal/entity" + +// BadgeListInfo get badge list response +type BadgeListInfo struct { + ID string `json:"id" ` + Name string `json:"name" ` + Icon string `json:"icon" ` + AwardCount int `json:"award_count" ` + Earned bool `json:"earned" ` + Level entity.BadgeLevel `json:"level" ` +} + +type GetBadgeListResp struct { + Badges []*BadgeListInfo `json:"badges" ` + GroupName string `json:"group_name" ` +} + +type GetBadgeInfoResp struct { + ID string `json:"id" ` + Name string `json:"name" ` + Description string `json:"description" ` + Icon string `json:"icon" ` + AwardCount int `json:"award_count" ` + EarnedCount int64 `json:"earned_count" ` + IsSingle bool `json:"is_single" ` + Level entity.BadgeLevel `json:"level" ` +} + +type GetBadgeAwardWithPageReq struct { + // page + Page int `validate:"omitempty,min=1" form:"page"` + // page size + PageSize int `validate:"omitempty,min=1" form:"page_size"` + // badge id + BadgeID string `validate:"required" form:"badge_id"` + // user id + UserID string `json:"-"` +} + +type GetBadgeAwardWithPageResp struct { + CreatedAt int64 `json:"created_at"` + ObjectID string `json:"object_id"` + QuestionID string `json:"question_id"` + AnswerID string `json:"answer_id"` + CommentID string `json:"comment_id"` + ObjectType string `json:"object_type" enums:"question,answer,comment"` + UrlTitle string `json:"url_title"` + AuthorUserInfo UserBasicInfo `json:"author_user_info"` +} diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index 1109b4447..94ba5b649 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -22,15 +22,20 @@ package badge import ( "context" "github.com/apache/incubator-answer/internal/base/handler" + "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/base/translator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/badge_award" "github.com/apache/incubator-answer/internal/service/badge_group" "github.com/apache/incubator-answer/pkg/converter" + "github.com/apache/incubator-answer/pkg/uid" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" ) type BadgeRepo interface { + GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) ListByLevel(ctx context.Context, level entity.BadgeLevel) ([]*entity.Badge, error) ListByGroup(ctx context.Context, groupID int64) ([]*entity.Badge, error) ListByLevelAndGroup(ctx context.Context, level entity.BadgeLevel, groupID int64) ([]*entity.Badge, error) @@ -55,6 +60,7 @@ func NewBadgeService( } } +// ListByGroup list all badges group by group func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []*schema.GetBadgeListResp, err error) { var ( groups []*entity.BadgeGroup @@ -74,7 +80,13 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* if err != nil { return } - earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, userID) + + if len(userID) > 0 { + earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, userID) + if err != nil { + return + } + } for _, group := range groups { groupMap[converter.StringToInt64(group.ID)] = group.Name @@ -93,11 +105,12 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* } badgesMap[badge.BadgeGroupId] = append(badgesMap[badge.BadgeGroupId], &schema.BadgeListInfo{ - ID: badge.ID, + ID: uid.EnShortID(badge.ID), Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), Icon: badge.Icon, AwardCount: badge.AwardCount, Earned: earned, + Level: badge.Level, }) } @@ -110,3 +123,38 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* return } + +// GetBadgeInfo get badge info +func (b *BadgeService) GetBadgeInfo(ctx *gin.Context, id string, userID string) (info *schema.GetBadgeInfoResp, err error) { + var ( + badge *entity.Badge + earnedTotal int64 = 0 + exists = false + ) + + badge, exists, err = b.badgeRepo.GetByID(ctx, id) + if err != nil { + return + } + + if !exists || badge.Status == entity.BadgeStatusInactive { + err = errors.BadRequest(reason.BadgeObjectNotFound) + return + } + + if len(userID) > 0 { + earnedTotal = b.badgeAwardRepo.CountByUserIdAndBadgeId(ctx, userID, badge.ID) + } + + info = &schema.GetBadgeInfoResp{ + ID: uid.EnShortID(badge.ID), + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Description: translator.Tr(handler.GetLangByCtx(ctx), badge.Description), + Icon: badge.Icon, + AwardCount: badge.AwardCount, + EarnedCount: earnedTotal, + IsSingle: badge.Single == entity.BadgeSingleAward, + Level: badge.Level, + } + return +} diff --git a/internal/service/badge_award/badge_award_service.go b/internal/service/badge_award/badge_award_service.go index 8f84453ee..b8e96545d 100644 --- a/internal/service/badge_award/badge_award_service.go +++ b/internal/service/badge_award/badge_award_service.go @@ -21,7 +21,17 @@ package badge_award import ( "context" + "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/schema" + answercommon "github.com/apache/incubator-answer/internal/service/answer_common" + "github.com/apache/incubator-answer/internal/service/object_info" + questioncommon "github.com/apache/incubator-answer/internal/service/question_common" + usercommon "github.com/apache/incubator-answer/internal/service/user_common" + "github.com/apache/incubator-answer/pkg/htmltext" + "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" "time" ) @@ -33,32 +43,98 @@ type BadgeAwardRepo interface { CountByUserId(ctx context.Context, userID string) (awardCount int64) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) CountByObjectId(ctx context.Context, objectID string) (awardCount int64) - CountByObjectIdAndBadgeId(ctx context.Context, objectID string, badgeID string) int64 + CountByObjectIdAndBadgeId(ctx context.Context, objectID string, badgeID string) (awardCount int64) CountBadgesByUserIdAndObjectId(ctx context.Context, userID string, objectID string, badgeID string) (awardCount int64) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) ListAllByUserId(ctx context.Context, userID string) (badgeAwards []*entity.BadgeAward) - ListPagedByBadgeId(ctx context.Context, badgeID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) - ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) - ListPagedByObjectId(ctx context.Context, badgeID string, objectID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) - ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, objectID string, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) - ListTagPagedByBadgeId(ctx context.Context, badgeIDs []int64, page int64, pageSize int64, filterUserID int64) (badgeAwards []*entity.BadgeAward, total int64) - ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []int64, userID string, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) - ListPagedLatest(ctx context.Context, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) - ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int64) (badgeAwards []*entity.BadgeAward, total int64) - ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int64, page int64, pageSize int64) (badgeAwards []*entity.BadgeAward, total int64) + ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) + ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListPagedByObjectId(ctx context.Context, badgeID string, objectID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, objectID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListTagPagedByBadgeId(ctx context.Context, badgeIDs []string, page int, pageSize int, filterUserID string) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListPagedLatest(ctx context.Context, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward) GetByUserIdAndBadgeIdAndObjectId(ctx context.Context, userID string, badgeID string, objectID string) (badgeAward *entity.BadgeAward) } type BadgeAwardService struct { - badgeAwardRepo BadgeAwardRepo + badgeAwardRepo BadgeAwardRepo + userCommon *usercommon.UserCommon + objectInfoService *object_info.ObjService + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo } -func NewBadgeAwardService(badgeAwardRepo BadgeAwardRepo) *BadgeAwardService { +func NewBadgeAwardService( + badgeAwardRepo BadgeAwardRepo, + userCommon *usercommon.UserCommon, + objectInfoService *object_info.ObjService, + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, +) *BadgeAwardService { return &BadgeAwardService{ - badgeAwardRepo: badgeAwardRepo, + badgeAwardRepo: badgeAwardRepo, + userCommon: userCommon, + objectInfoService: objectInfoService, + questionRepo: questionRepo, + answerRepo: answerRepo, } } + +// GetBadgeAwardList get badge award list +func (b *BadgeAwardService) GetBadgeAwardList( + ctx context.Context, req *schema.GetBadgeAwardWithPageReq, +) (resp []*schema.GetBadgeAwardWithPageResp, total int64, err error) { + var ( + badgeAwardList []*entity.BadgeAward + ) + + badgeAwardList, total, err = b.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize) + if err != nil { + return + } + + resp = make([]*schema.GetBadgeAwardWithPageResp, 0, len(badgeAwardList)) + + for i, badgeAward := range badgeAwardList { + objInfo, e := b.objectInfoService.GetInfo(ctx, badgeAward.ObjectID) + if e != nil { + err = e + return + } + if objInfo.IsDeleted() { + err = errors.BadRequest(reason.NewObjectAlreadyDeleted) + return + } + + row := &schema.GetBadgeAwardWithPageResp{ + CreatedAt: badgeAward.CreatedAt.Unix(), + ObjectID: badgeAward.ObjectID, + QuestionID: objInfo.QuestionID, + AnswerID: objInfo.AnswerID, + CommentID: objInfo.CommentID, + ObjectType: objInfo.ObjectType, + UrlTitle: htmltext.UrlTitle(objInfo.Title), + AuthorUserInfo: schema.UserBasicInfo{}, + } + + // get user info + userInfo, exists, e := b.userCommon.GetUserBasicInfoByID(ctx, badgeAward.UserID) + if e != nil { + log.Errorf("user not found by id: %s, err: %v", badgeAward.UserID, e) + } + if exists { + _ = copier.Copy(&row.AuthorUserInfo, userInfo) + } + + resp[i] = row + } + + return +} From 9b0de85ed4f5d9b77506b4698cc87850baf89691 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Thu, 8 Aug 2024 18:09:27 +0800 Subject: [PATCH 21/38] feat(badge): add badge event handler --- cmd/wire_gen.go | 4 +- internal/repo/badge/badge_event_rule.go | 216 ++++++++++++++++++ internal/repo/badge/badge_repo.go | 31 ++- internal/repo/badge/badge_rule.go | 95 -------- internal/repo/badge/event_rule_mapping.go | 47 ---- internal/repo/badge/rule.go | 43 ---- internal/repo/provider.go | 1 + internal/schema/event_schema.go | 10 + .../badge/badge_event_handler.go | 34 ++- internal/service/badge/badge_service.go | 19 +- internal/service/provider.go | 1 + 11 files changed, 304 insertions(+), 197 deletions(-) create mode 100644 internal/repo/badge/badge_event_rule.go delete mode 100644 internal/repo/badge/badge_rule.go delete mode 100644 internal/repo/badge/event_rule_mapping.go delete mode 100644 internal/repo/badge/rule.go rename internal/{repo => service}/badge/badge_event_handler.go (60%) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 24cdb1630..f49ef20e2 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -263,7 +263,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, badgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo) badgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo) badgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo) - badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo) + eventRuleRepo := badge.NewEventRuleRepo(dataData) + badgeEventService := badge2.NewBadgeEventService(dataData, eventQueueService, badgeRepo, eventRuleRepo) + badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService) badgeAwardService := badge_award2.NewBadgeAwardService(badgeAwardRepo, userCommon, objService, questionRepo, answerRepo) badgeController := controller.NewBadgeController(badgeService, badgeAwardService) answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController) diff --git a/internal/repo/badge/badge_event_rule.go b/internal/repo/badge/badge_event_rule.go new file mode 100644 index 000000000..8f9407202 --- /dev/null +++ b/internal/repo/badge/badge_event_rule.go @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package badge + +import ( + "context" + "github.com/apache/incubator-answer/internal/base/constant" + "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/base/reason" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/internal/service/badge" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// eventRuleRepo event rule repo +type eventRuleRepo struct { + data *data.Data + EventRuleMapping map[constant.EventType][]badge.EventRuleHandler +} + +// NewEventRuleRepo creates a new badge repository +func NewEventRuleRepo(data *data.Data) badge.EventRuleRepo { + b := &eventRuleRepo{ + data: data, + } + b.EventRuleMapping = map[constant.EventType][]badge.EventRuleHandler{ + constant.EventUserUpdate: {b.FirstUpdateUserProfile}, + constant.EventUserShare: {b.FirstSharedPost}, + constant.EventQuestionCreate: nil, + constant.EventQuestionUpdate: {b.FirstPostEdit}, + constant.EventQuestionDelete: nil, + constant.EventQuestionVote: {b.FirstVotedPost, b.ReachQuestionVote}, + constant.EventQuestionAccept: {b.FirstAcceptAnswer, b.ReachAnswerAcceptedAmount}, + constant.EventQuestionFlag: {b.FirstFlaggedPost}, + constant.EventQuestionReact: {b.FirstReactedPost}, + constant.EventAnswerCreate: nil, + constant.EventAnswerUpdate: {b.FirstPostEdit}, + constant.EventAnswerDelete: nil, + constant.EventAnswerVote: {b.FirstVotedPost, b.ReachAnswerVote}, + constant.EventAnswerFlag: {b.FirstFlaggedPost}, + constant.EventAnswerReact: {b.FirstReactedPost}, + constant.EventCommentCreate: nil, + constant.EventCommentUpdate: nil, + constant.EventCommentDelete: nil, + constant.EventCommentVote: {b.FirstVotedPost}, + constant.EventCommentFlag: {b.FirstFlaggedPost}, + } + return b +} + +// HandleEventWithRule handle event with rule +func (br *eventRuleRepo) HandleEventWithRule(ctx context.Context, msg *schema.EventMsg) ( + awards []*entity.BadgeAward) { + handlers := br.EventRuleMapping[msg.EventType] + for _, h := range handlers { + t, err := h(ctx, msg) + if err != nil { + log.Errorf("error handling badge event %+v: %v", msg, err) + } else { + awards = append(awards, t...) + } + } + return awards +} + +// FirstUpdateUserProfile first update user profile +func (br *eventRuleRepo) FirstUpdateUserProfile(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "FirstUpdateUserProfile") + if b == nil { + return nil, nil + } + bean := &entity.User{ID: event.UserID} + exist, err := br.data.DB.Context(ctx).Get(bean) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + return nil, nil + } + if len(bean.Bio) > 0 { + return append(awards, br.createBadgeAward(event.UserID, b.ID, "")), nil + } + return nil, nil +} + +// FirstPostEdit first post edit +func (br *eventRuleRepo) FirstPostEdit(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "FirstPostEdit") + if b == nil { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil +} + +// FirstFlaggedPost first flagged post. +func (br *eventRuleRepo) FirstFlaggedPost(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "FirstFlaggedPost") + if b == nil { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil +} + +// FirstVotedPost first voted post +func (br *eventRuleRepo) FirstVotedPost(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "FirstVotedPost") + if b == nil { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil +} + +// FirstReactedPost first reacted post +func (br *eventRuleRepo) FirstReactedPost(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "FirstReactedPost") + if b == nil { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil +} + +// FirstSharedPost first shared post +func (br *eventRuleRepo) FirstSharedPost(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "FirstSharedPost") + if b == nil { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil +} + +// FirstAcceptAnswer user first accept answer +func (br *eventRuleRepo) FirstAcceptAnswer(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "FirstAcceptAnswer") + if b == nil { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil +} + +// ReachAnswerAcceptedAmount reach answer accepted amount +func (br *eventRuleRepo) ReachAnswerAcceptedAmount(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "ReachAnswerAcceptedAmount") + if b == nil { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil +} + +// ReachAnswerVote reach answer vote +func (br *eventRuleRepo) ReachAnswerVote(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "ReachAnswerVote") + if b == nil { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil +} + +// ReachQuestionVote reach question vote +func (br *eventRuleRepo) ReachQuestionVote(ctx context.Context, + event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { + b := br.getBadgeByHandler(ctx, "ReachQuestionVote") + if b == nil { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil +} + +func (br *eventRuleRepo) getBadgeByHandler(ctx context.Context, handler string) (b *entity.Badge) { + b = &entity.Badge{Handler: handler} + exist, err := br.data.DB.Context(ctx).Get(b) + if err != nil { + log.Errorf("error getting badge by handler %s: %v", handler, err) + return nil + } + if !exist { + log.Errorf("badge not found by handler %s", handler) + return nil + } + return b +} + +func (br *eventRuleRepo) createBadgeAward(userID, badgeID, objectID string) (awards *entity.BadgeAward) { + return &entity.BadgeAward{ + UserID: userID, + BadgeID: badgeID, + ObjectID: objectID, + } +} diff --git a/internal/repo/badge/badge_repo.go b/internal/repo/badge/badge_repo.go index 7651f95b5..5030f0da1 100644 --- a/internal/repo/badge/badge_repo.go +++ b/internal/repo/badge/badge_repo.go @@ -22,9 +22,11 @@ package badge import ( "context" "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/service/badge" "github.com/apache/incubator-answer/internal/service/unique" + "github.com/segmentfault/pacman/errors" ) type badgeRepo struct { @@ -40,9 +42,21 @@ func NewBadgeRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.Badge } } -func (r badgeRepo) GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) { +func (r *badgeRepo) GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) { badge = &entity.Badge{} exists, err = r.data.DB.Context(ctx).Where("id = ?", id).Get(badge) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + +func (r *badgeRepo) GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error) { + badges = make([]*entity.Badge, 0) + err = r.data.DB.Context(ctx).In("id", ids).Find(&badges) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } @@ -50,6 +64,9 @@ func (r badgeRepo) GetByID(ctx context.Context, id string) (badge *entity.Badge, func (r *badgeRepo) ListByLevel(ctx context.Context, level entity.BadgeLevel) (badges []*entity.Badge, err error) { badges = make([]*entity.Badge, 0) err = r.data.DB.Context(ctx).Where("level = ?", level).Find(&badges) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } @@ -57,6 +74,9 @@ func (r *badgeRepo) ListByLevel(ctx context.Context, level entity.BadgeLevel) (b func (r *badgeRepo) ListByGroup(ctx context.Context, groupID int64) (badges []*entity.Badge, err error) { badges = make([]*entity.Badge, 0) err = r.data.DB.Context(ctx).Where("group_id = ?", groupID).Find(&badges) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } @@ -64,6 +84,9 @@ func (r *badgeRepo) ListByGroup(ctx context.Context, groupID int64) (badges []*e func (r *badgeRepo) ListByLevelAndGroup(ctx context.Context, level entity.BadgeLevel, groupID int64) (badges []*entity.Badge, err error) { badges = make([]*entity.Badge, 0) err = r.data.DB.Context(ctx).Where("level = ? AND group_id = ?", level, groupID).Find(&badges) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } @@ -71,6 +94,9 @@ func (r *badgeRepo) ListByLevelAndGroup(ctx context.Context, level entity.BadgeL func (r *badgeRepo) ListActivated(ctx context.Context) (badges []*entity.Badge, err error) { badges = make([]*entity.Badge, 0) err = r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusActive).Find(&badges) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } @@ -78,5 +104,8 @@ func (r *badgeRepo) ListActivated(ctx context.Context) (badges []*entity.Badge, func (r *badgeRepo) ListInactivated(ctx context.Context) (badges []*entity.Badge, err error) { badges = make([]*entity.Badge, 0) err = r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusInactive).Find(&badges) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } diff --git a/internal/repo/badge/badge_rule.go b/internal/repo/badge/badge_rule.go deleted file mode 100644 index 3ae0b7b44..000000000 --- a/internal/repo/badge/badge_rule.go +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package badge - -import ( - "context" - "github.com/apache/incubator-answer/internal/base/data" - "github.com/apache/incubator-answer/internal/base/reason" - "github.com/apache/incubator-answer/internal/entity" - "github.com/apache/incubator-answer/internal/service/unique" - "github.com/segmentfault/pacman/errors" -) - -// BadgeRuleRepo collection repository -type BadgeRuleRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo -} - -// FilledPersonalProfile filled personal profile -func (br *BadgeRuleRepo) FilledPersonalProfile(ctx context.Context, userID string) (reach bool, err error) { - bean := &entity.User{ID: userID} - exist, err := br.data.DB.Context(ctx).Get(bean) - if err != nil { - return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - if !exist { - return false, nil - } - if len(bean.Bio) > 0 { - return true, nil - } - return false, nil -} - -// FirstPostEdit first post edit -func (br *BadgeRuleRepo) FirstPostEdit(ctx context.Context, userID string, objectID string) { - -} - -// FirstFlaggedPost first flagged post. -func (br *BadgeRuleRepo) FirstFlaggedPost(ctx context.Context, userID string, reportID string) { -} - -// FirstVotedPost first voted post -func (br *BadgeRuleRepo) FirstVotedPost(ctx context.Context) { - -} - -// FirstReactedPost first reacted post -func (br *BadgeRuleRepo) FirstReactedPost(ctx context.Context) { - -} - -// FirstSharedPost first shared post -func (br *BadgeRuleRepo) FirstSharedPost(ctx context.Context) { - -} - -// AskQuestionAcceptAnswer ask question accept answer -func (br *BadgeRuleRepo) AskQuestionAcceptAnswer(ctx context.Context) { - -} - -// AnswerAccepted answer accepted -func (br *BadgeRuleRepo) AnswerAccepted(ctx context.Context) { - -} - -// ReachAnswerScore reach answer score -func (br *BadgeRuleRepo) ReachAnswerScore(ctx context.Context) { - -} - -// ReachQuestionScore reach question score -func (br *BadgeRuleRepo) ReachQuestionScore(ctx context.Context) { - -} diff --git a/internal/repo/badge/event_rule_mapping.go b/internal/repo/badge/event_rule_mapping.go deleted file mode 100644 index 8f01fdef9..000000000 --- a/internal/repo/badge/event_rule_mapping.go +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package badge - -import "github.com/apache/incubator-answer/internal/base/constant" - -var ( - EventRuleMapping = map[constant.EventType][]string{ - constant.EventUserUpdate: {FilledPersonalProfile}, - constant.EventUserShare: {FirstSharedPost}, - constant.EventQuestionCreate: {""}, - constant.EventQuestionUpdate: {FirstPostEdit}, - constant.EventQuestionDelete: {""}, - constant.EventQuestionVote: {FirstVotedPost, ReachQuestionScore}, - constant.EventQuestionAccept: {AskQuestionAcceptAnswer, AnswerAccepted}, - constant.EventQuestionFlag: {FirstFlaggedPost}, - constant.EventQuestionReact: {FirstReactedPost}, - constant.EventAnswerCreate: {""}, - constant.EventAnswerUpdate: {FirstPostEdit}, - constant.EventAnswerDelete: {""}, - constant.EventAnswerVote: {FirstVotedPost, ReachAnswerScore}, - constant.EventAnswerFlag: {FirstFlaggedPost}, - constant.EventAnswerReact: {FirstReactedPost}, - constant.EventCommentCreate: {""}, - constant.EventCommentUpdate: {""}, - constant.EventCommentDelete: {""}, - constant.EventCommentVote: {FirstVotedPost}, - constant.EventCommentFlag: {FirstFlaggedPost}, - } -) diff --git a/internal/repo/badge/rule.go b/internal/repo/badge/rule.go deleted file mode 100644 index 635b30dae..000000000 --- a/internal/repo/badge/rule.go +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package badge - -const ( - // FilledPersonalProfile filled personal profile - FilledPersonalProfile = "filled_personal_profile" - // FirstPostEdit first post edit - FirstPostEdit = "first_post_edit" - // FirstFlaggedPost first flagged post. - FirstFlaggedPost = "first_flagged_post" - // FirstVotedPost first voted post - FirstVotedPost = "first_voted_post" - // FirstReactedPost first reacted post - FirstReactedPost = "first_reacted_post" - // FirstSharedPost first shared post - FirstSharedPost = "first_shared_post" - // AskQuestionAcceptAnswer ask question accept answer - AskQuestionAcceptAnswer = "ask_question_accept_answer" - // AnswerAccepted answer accepted - AnswerAccepted = "answer_accepted" - // ReachAnswerScore reach answer score - ReachAnswerScore = "reach_answer_score" - // ReachQuestionScore reach question score - ReachQuestionScore = "reach_question_score" -) diff --git a/internal/repo/provider.go b/internal/repo/provider.go index ee309e027..7f222a425 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -104,6 +104,7 @@ var ProviderSetRepo = wire.NewSet( plugin_config.NewPluginUserConfigRepo, review.NewReviewRepo, badge.NewBadgeRepo, + badge.NewEventRuleRepo, badge_group.NewBadgeGroupRepo, badge_award.NewBadgeAwardRepo, ) diff --git a/internal/schema/event_schema.go b/internal/schema/event_schema.go index 01f80fbbe..17be96272 100644 --- a/internal/schema/event_schema.go +++ b/internal/schema/event_schema.go @@ -68,3 +68,13 @@ func (e *EventMsg) AddExtra(key, value string) *EventMsg { e.ExtraInfo[key] = value return e } + +func (e *EventMsg) GetObjectID() string { + if len(e.CommentID) > 0 { + return e.CommentID + } + if len(e.AnswerID) > 0 { + return e.AnswerID + } + return e.QuestionID +} diff --git a/internal/repo/badge/badge_event_handler.go b/internal/service/badge/badge_event_handler.go similarity index 60% rename from internal/repo/badge/badge_event_handler.go rename to internal/service/badge/badge_event_handler.go index 242778ecc..587ee77d2 100644 --- a/internal/repo/badge/badge_event_handler.go +++ b/internal/service/badge/badge_event_handler.go @@ -21,6 +21,8 @@ package badge import ( "context" + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/service/badge_award" "github.com/apache/incubator-answer/internal/service/event_queue" "github.com/segmentfault/pacman/log" @@ -31,15 +33,28 @@ import ( type BadgeEventService struct { data *data.Data eventQueueService event_queue.EventQueueService + badgeAwardRepo badge_award.BadgeAwardRepo + badgeRepo BadgeRepo + eventRuleRepo EventRuleRepo +} + +type EventRuleHandler func(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) + +type EventRuleRepo interface { + HandleEventWithRule(ctx context.Context, msg *schema.EventMsg) (awards []*entity.BadgeAward) } func NewBadgeEventService( data *data.Data, eventQueueService event_queue.EventQueueService, + badgeRepo BadgeRepo, + eventRuleRepo EventRuleRepo, ) *BadgeEventService { n := &BadgeEventService{ data: data, eventQueueService: eventQueueService, + badgeRepo: badgeRepo, + eventRuleRepo: eventRuleRepo, } eventQueueService.RegisterHandler(n.Handler) return n @@ -47,11 +62,24 @@ func NewBadgeEventService( func (ns *BadgeEventService) Handler(ctx context.Context, msg *schema.EventMsg) error { log.Debugf("received badge event %+v", msg) - // TODO: Check if badge already exists - // TODO: Check rule + awards := ns.eventRuleRepo.HandleEventWithRule(ctx, msg) + if len(awards) == 0 { + return nil + } - // TODO: Distribute badge + badgeIDs := make([]string, 0) + for _, award := range awards { + badgeIDs = append(badgeIDs, award.BadgeID) + } + + badges, err := ns.badgeRepo.GetByIDs(ctx, badgeIDs) + if err != nil { + log.Errorf("error getting badges %+v: %v", badgeIDs, err) + return err + } + // TODO: award badges to user + log.Debugf("awarding badges %+v to user", badges) return nil } diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index 94ba5b649..16087f9c0 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -36,6 +36,7 @@ import ( type BadgeRepo interface { GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) + GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error) ListByLevel(ctx context.Context, level entity.BadgeLevel) ([]*entity.Badge, error) ListByGroup(ctx context.Context, groupID int64) ([]*entity.Badge, error) ListByLevelAndGroup(ctx context.Context, level entity.BadgeLevel, groupID int64) ([]*entity.Badge, error) @@ -44,19 +45,23 @@ type BadgeRepo interface { } type BadgeService struct { - badgeRepo BadgeRepo - badgeGroupRepo badge_group.BadgeGroupRepo - badgeAwardRepo badge_award.BadgeAwardRepo + badgeRepo BadgeRepo + badgeGroupRepo badge_group.BadgeGroupRepo + badgeAwardRepo badge_award.BadgeAwardRepo + badgeEventService *BadgeEventService } func NewBadgeService( badgeRepo BadgeRepo, badgeGroupRepo badge_group.BadgeGroupRepo, - badgeAwardRepo badge_award.BadgeAwardRepo) *BadgeService { + badgeAwardRepo badge_award.BadgeAwardRepo, + badgeEventService *BadgeEventService, +) *BadgeService { return &BadgeService{ - badgeRepo: badgeRepo, - badgeGroupRepo: badgeGroupRepo, - badgeAwardRepo: badgeAwardRepo, + badgeRepo: badgeRepo, + badgeGroupRepo: badgeGroupRepo, + badgeAwardRepo: badgeAwardRepo, + badgeEventService: badgeEventService, } } diff --git a/internal/service/provider.go b/internal/service/provider.go index bf1528b8b..7a6b1340e 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -123,6 +123,7 @@ var ProviderSetService = wire.NewSet( meta.NewMetaService, event_queue.NewEventQueueService, badge.NewBadgeService, + badge.NewBadgeEventService, badge_award.NewBadgeAwardService, badge_group.NewBadgeGroupService, ) From 49ba98c6fc1c130ade00af9fddd62bc76aa471e0 Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 8 Aug 2024 18:16:08 +0800 Subject: [PATCH 22/38] feat(badge): change badge_award entity --- internal/entity/badge_award_entity.go | 2 +- .../badge_award/badge_award_service.go | 36 ++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index a26fd8de4..26369394b 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -28,7 +28,7 @@ type BadgeAward struct { UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` UserID string `json:"user_id" xorm:"not null index BIGINT(20) user_id"` BadgeID string `json:"badge_id" xorm:"not null index BIGINT(20) badge_id"` - ObjectID string `json:"object_id" xorm:"not null index BIGINT(20) object_id"` + AwardKey string `json:"award_key" xorm:"not null index VARCHAR(64) award_key"` BadgeGroupID int8 `json:"badge_group_id" xorm:"not null index BIGINT(20) badge_group_id"` IsBadgeDeleted int8 `json:"is_badge_deleted" xorm:"not null index TINYINT(1) s_badge_deleted"` } diff --git a/internal/service/badge_award/badge_award_service.go b/internal/service/badge_award/badge_award_service.go index b8e96545d..7eb218fda 100644 --- a/internal/service/badge_award/badge_award_service.go +++ b/internal/service/badge_award/badge_award_service.go @@ -21,16 +21,13 @@ package badge_award import ( "context" - "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" answercommon "github.com/apache/incubator-answer/internal/service/answer_common" "github.com/apache/incubator-answer/internal/service/object_info" questioncommon "github.com/apache/incubator-answer/internal/service/question_common" usercommon "github.com/apache/incubator-answer/internal/service/user_common" - "github.com/apache/incubator-answer/pkg/htmltext" "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "time" ) @@ -103,24 +100,29 @@ func (b *BadgeAwardService) GetBadgeAwardList( resp = make([]*schema.GetBadgeAwardWithPageResp, 0, len(badgeAwardList)) for i, badgeAward := range badgeAwardList { - objInfo, e := b.objectInfoService.GetInfo(ctx, badgeAward.ObjectID) - if e != nil { - err = e - return - } - if objInfo.IsDeleted() { - err = errors.BadRequest(reason.NewObjectAlreadyDeleted) - return + var ( + objectID, questionID, answerID, commentID, objectType, urlTitle string + ) + + // if exist object info + objInfo, e := b.objectInfoService.GetInfo(ctx, badgeAward.AwardKey) + if e == nil && !objInfo.IsDeleted() { + objectID = objInfo.ObjectID + questionID = objInfo.QuestionID + answerID = objInfo.AnswerID + commentID = objInfo.CommentID + objectType = objInfo.ObjectType + urlTitle = objInfo.Title } row := &schema.GetBadgeAwardWithPageResp{ CreatedAt: badgeAward.CreatedAt.Unix(), - ObjectID: badgeAward.ObjectID, - QuestionID: objInfo.QuestionID, - AnswerID: objInfo.AnswerID, - CommentID: objInfo.CommentID, - ObjectType: objInfo.ObjectType, - UrlTitle: htmltext.UrlTitle(objInfo.Title), + ObjectID: objectID, + QuestionID: questionID, + AnswerID: answerID, + CommentID: commentID, + ObjectType: objectType, + UrlTitle: urlTitle, AuthorUserInfo: schema.UserBasicInfo{}, } From 122ac657f14f4806956ed0e70bc518d175efa57b Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 8 Aug 2024 18:17:41 +0800 Subject: [PATCH 23/38] feat(badge): change badge_award check --- internal/repo/badge_award/badge_award_repo.go | 4 ++-- internal/service/badge_award/badge_award_service.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 0a4603478..85d3ceac2 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -43,10 +43,10 @@ func NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge_ } } -func (r *badgeAwardRepo) Award(ctx context.Context, badgeID string, userID string, objectID string, force bool, createdAt time.Time) { +func (r *badgeAwardRepo) Award(ctx context.Context, badgeID string, userID string, awardKey string, force bool, createdAt time.Time) { return } -func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID string, userID string, objectID string) (isAward bool) { +func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string) (isAward bool) { return } func (r *badgeAwardRepo) CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) { diff --git a/internal/service/badge_award/badge_award_service.go b/internal/service/badge_award/badge_award_service.go index 7eb218fda..d94258b45 100644 --- a/internal/service/badge_award/badge_award_service.go +++ b/internal/service/badge_award/badge_award_service.go @@ -33,8 +33,8 @@ import ( ) type BadgeAwardRepo interface { - Award(ctx context.Context, badgeID string, userID string, objectID string, force bool, createdAt time.Time) - CheckIsAward(ctx context.Context, badgeID string, userID string, objectID string) bool + Award(ctx context.Context, badgeID string, userID string, awardKey string, force bool, createdAt time.Time) + CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string) bool CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) CountByUserId(ctx context.Context, userID string) (awardCount int64) From 203062dd985e0f4909cfde3cd7a69a0965b86ff4 Mon Sep 17 00:00:00 2001 From: kumfo Date: Fri, 9 Aug 2024 10:33:03 +0800 Subject: [PATCH 24/38] feat(badge): badge award --- internal/entity/badge_award_entity.go | 2 +- internal/entity/badge_entity.go | 2 +- internal/migrations/v22.go | 28 +++---- internal/repo/badge/badge_repo.go | 6 ++ internal/repo/badge_award/badge_award_repo.go | 55 ++++++++++--- internal/service/badge/badge_service.go | 5 +- .../badge_award/badge_award_service.go | 77 +++++++++++++++---- 7 files changed, 129 insertions(+), 46 deletions(-) diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index 26369394b..310c85a7d 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -29,7 +29,7 @@ type BadgeAward struct { UserID string `json:"user_id" xorm:"not null index BIGINT(20) user_id"` BadgeID string `json:"badge_id" xorm:"not null index BIGINT(20) badge_id"` AwardKey string `json:"award_key" xorm:"not null index VARCHAR(64) award_key"` - BadgeGroupID int8 `json:"badge_group_id" xorm:"not null index BIGINT(20) badge_group_id"` + BadgeGroupID int64 `json:"badge_group_id" xorm:"not null index BIGINT(20) badge_group_id"` IsBadgeDeleted int8 `json:"is_badge_deleted" xorm:"not null index TINYINT(1) s_badge_deleted"` } diff --git a/internal/entity/badge_entity.go b/internal/entity/badge_entity.go index da6cd4dd9..d2c426fa8 100644 --- a/internal/entity/badge_entity.go +++ b/internal/entity/badge_entity.go @@ -46,7 +46,7 @@ type Badge struct { AwardCount int `json:"award_count" xorm:"not null default 0 INT(11) award_count"` Description string `json:"description" xorm:"not null default '' MEDIUMTEXT description"` Status int8 `json:"status" xorm:"not null default 1 INT(11) status"` - BadgeGroupId int64 `json:"badge_group_id" xorm:"not null default 0 BIGINT(20) badge_group_id"` + BadgeGroupID int64 `json:"badge_group_id" xorm:"not null default 0 BIGINT(20) badge_group_id"` Level BadgeLevel `json:"level" xorm:"not null default 1 TINYINT(4) level"` Single int8 `json:"single" xorm:"not null default 1 TINYINT(4) single"` Collect string `json:"collect" xorm:"not null default '' VARCHAR(64) collect"` diff --git a/internal/migrations/v22.go b/internal/migrations/v22.go index de15cea6d..19ec1ce0f 100644 --- a/internal/migrations/v22.go +++ b/internal/migrations/v22.go @@ -44,7 +44,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.autobiographer.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 1, + BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Collect: "", @@ -59,7 +59,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.editor.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 1, + BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Collect: "question", @@ -74,7 +74,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.first_flag.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 1, + BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Collect: "", @@ -89,7 +89,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.first_upvote.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 1, + BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Collect: "", @@ -104,7 +104,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.first_reaction.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 1, + BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Collect: "", @@ -119,7 +119,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.first_share.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 1, + BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Collect: "", @@ -134,7 +134,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.scholar.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 1, + BadgeGroupID: 1, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Collect: "", @@ -149,7 +149,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.solved.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 2, + BadgeGroupID: 2, Level: entity.BadgeLevelBronze, Single: entity.BadgeSingleAward, Collect: "", @@ -164,7 +164,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.nice_answer.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 3, + BadgeGroupID: 3, Level: entity.BadgeLevelBronze, Single: entity.BadgeMultiAward, Collect: "", @@ -179,7 +179,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.good_answer.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 3, + BadgeGroupID: 3, Level: entity.BadgeLevelSilver, Single: entity.BadgeMultiAward, Collect: "", @@ -194,7 +194,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.great_answer.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 3, + BadgeGroupID: 3, Level: entity.BadgeLevelGold, Single: entity.BadgeMultiAward, Collect: "", @@ -209,7 +209,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.nice_question.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 3, + BadgeGroupID: 3, Level: entity.BadgeLevelBronze, Single: entity.BadgeMultiAward, Collect: "", @@ -224,7 +224,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.good_question.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 3, + BadgeGroupID: 3, Level: entity.BadgeLevelSilver, Single: entity.BadgeSingleAward, Collect: "", @@ -239,7 +239,7 @@ var ( AwardCount: 0, Description: "badge.default_badges.great_question.desc", Status: entity.BadgeStatusActive, - BadgeGroupId: 3, + BadgeGroupID: 3, Level: entity.BadgeLevelGold, Single: entity.BadgeMultiAward, Collect: "", diff --git a/internal/repo/badge/badge_repo.go b/internal/repo/badge/badge_repo.go index 5030f0da1..d4d2f9922 100644 --- a/internal/repo/badge/badge_repo.go +++ b/internal/repo/badge/badge_repo.go @@ -109,3 +109,9 @@ func (r *badgeRepo) ListInactivated(ctx context.Context) (badges []*entity.Badge } return } + +// UpdateAwardCount updates the award count of a badge +func (r *badgeRepo) UpdateAwardCount(ctx context.Context, id string, count int64) (err error) { + _, err = r.data.DB.Context(ctx).Where("id = ?", id).Incr("award_count", count).Update(&entity.Badge{}) + return +} diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 85d3ceac2..92807d1df 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -28,7 +28,6 @@ import ( "github.com/apache/incubator-answer/internal/service/badge_award" "github.com/apache/incubator-answer/internal/service/unique" "github.com/segmentfault/pacman/errors" - "time" ) type badgeAwardRepo struct { @@ -43,10 +42,36 @@ func NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge_ } } -func (r *badgeAwardRepo) Award(ctx context.Context, badgeID string, userID string, awardKey string, force bool, createdAt time.Time) { - return -} -func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string) (isAward bool) { +func (r *badgeAwardRepo) Add(ctx context.Context, badgeAward *entity.BadgeAward) (err error) { + badgeAward.ID, err = r.uniqueIDRepo.GenUniqueIDStr(ctx, entity.BadgeAward{}.TableName()) + if err != nil { + return + } + _, err = r.data.DB.Context(ctx).Insert(badgeAward) + return +} +func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string, singleOrMulti int8) (isAward bool) { + isAward = false + if singleOrMulti == entity.BadgeSingleAward { + _, exists, err := r.GetByUserIdAndBadgeId(ctx, userID, badgeID) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + if exists { + return true + } + } else { + _, exists, err := r.GetByUserIdAndBadgeIdAndObjectId(ctx, userID, badgeID, awardKey) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return + } + if exists { + return true + } + } + return } func (r *badgeAwardRepo) CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) { @@ -62,13 +87,13 @@ func (r *badgeAwardRepo) CountByUserIdAndBadgeId(ctx context.Context, userID str } return } -func (r *badgeAwardRepo) CountByObjectId(ctx context.Context, objectID string) (awardCount int64) { +func (r *badgeAwardRepo) CountByObjectId(ctx context.Context, awardKey string) (awardCount int64) { return } -func (r *badgeAwardRepo) CountByObjectIdAndBadgeId(ctx context.Context, objectID string, badgeID string) (awardCount int64) { +func (r *badgeAwardRepo) CountByObjectIdAndBadgeId(ctx context.Context, awardKey string, badgeID string) (awardCount int64) { return } -func (r *badgeAwardRepo) CountBadgesByUserIdAndObjectId(ctx context.Context, userID string, objectID string, badgeID string) (awardCount int64) { +func (r *badgeAwardRepo) CountBadgesByUserIdAndObjectId(ctx context.Context, userID string, awardKey string, badgeID string) (awardCount int64) { return } func (r *badgeAwardRepo) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) { @@ -92,10 +117,10 @@ func (r *badgeAwardRepo) ListPagedByBadgeId(ctx context.Context, badgeID string, func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) ListPagedByObjectId(ctx context.Context, badgeID string, objectID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { +func (r *badgeAwardRepo) ListPagedByObjectId(ctx context.Context, badgeID string, awardKey string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, objectID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { +func (r *badgeAwardRepo) ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, awardKey string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } func (r *badgeAwardRepo) ListTagPagedByBadgeId(ctx context.Context, badgeIDs []string, page int, pageSize int, filterUserID string) (badgeAwards []*entity.BadgeAward, total int64, err error) { @@ -113,9 +138,15 @@ func (r *badgeAwardRepo) ListNewestEarnedByLevel(ctx context.Context, userID str func (r *badgeAwardRepo) ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } -func (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward) { + +// GetByUserIdAndBadgeId get badge award by user id and badge id +func (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward, exists bool, err error) { + exists, err = r.data.DB.Context(ctx).Where("user_id = ? AND badge_id = ? AND is_badge_deleted = 0", userID, badgeID).Get(&badgeAward) return } -func (r *badgeAwardRepo) GetByUserIdAndBadgeIdAndObjectId(ctx context.Context, userID string, badgeID string, objectID string) (badgeAward *entity.BadgeAward) { + +// GetByUserIdAndBadgeIdAndObjectId get badge award by user id, badge id and object id +func (r *badgeAwardRepo) GetByUserIdAndBadgeIdAndObjectId(ctx context.Context, userID string, badgeID string, awardKey string) (badgeAward *entity.BadgeAward, exists bool, err error) { + exists, err = r.data.DB.Context(ctx).Where("user_id = ? AND badge_id = ? AND award_key = ? AND is_badge_deleted = 0", userID, badgeID, awardKey).Get(&badgeAward) return } diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index 16087f9c0..faf97b11a 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -37,11 +37,14 @@ import ( type BadgeRepo interface { GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error) + ListByLevel(ctx context.Context, level entity.BadgeLevel) ([]*entity.Badge, error) ListByGroup(ctx context.Context, groupID int64) ([]*entity.Badge, error) ListByLevelAndGroup(ctx context.Context, level entity.BadgeLevel, groupID int64) ([]*entity.Badge, error) ListActivated(ctx context.Context) ([]*entity.Badge, error) ListInactivated(ctx context.Context) ([]*entity.Badge, error) + + UpdateAwardCount(ctx context.Context, id string, count int64) error } type BadgeService struct { @@ -109,7 +112,7 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* } } - badgesMap[badge.BadgeGroupId] = append(badgesMap[badge.BadgeGroupId], &schema.BadgeListInfo{ + badgesMap[badge.BadgeGroupID] = append(badgesMap[badge.BadgeGroupID], &schema.BadgeListInfo{ ID: uid.EnShortID(badge.ID), Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), Icon: badge.Icon, diff --git a/internal/service/badge_award/badge_award_service.go b/internal/service/badge_award/badge_award_service.go index d94258b45..2c7c5ffac 100644 --- a/internal/service/badge_award/badge_award_service.go +++ b/internal/service/badge_award/badge_award_service.go @@ -21,66 +21,64 @@ package badge_award import ( "context" + "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - answercommon "github.com/apache/incubator-answer/internal/service/answer_common" + "github.com/apache/incubator-answer/internal/service/badge" "github.com/apache/incubator-answer/internal/service/object_info" - questioncommon "github.com/apache/incubator-answer/internal/service/question_common" usercommon "github.com/apache/incubator-answer/internal/service/user_common" "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "time" ) type BadgeAwardRepo interface { - Award(ctx context.Context, badgeID string, userID string, awardKey string, force bool, createdAt time.Time) - CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string) bool + Add(ctx context.Context, badgeAward *entity.BadgeAward) (err error) + CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string, singleOrMulti int8) bool CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) CountByUserId(ctx context.Context, userID string) (awardCount int64) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) - CountByObjectId(ctx context.Context, objectID string) (awardCount int64) - CountByObjectIdAndBadgeId(ctx context.Context, objectID string, badgeID string) (awardCount int64) - CountBadgesByUserIdAndObjectId(ctx context.Context, userID string, objectID string, badgeID string) (awardCount int64) + CountByObjectId(ctx context.Context, awardKey string) (awardCount int64) + CountByObjectIdAndBadgeId(ctx context.Context, awardKey string, badgeID string) (awardCount int64) + CountBadgesByUserIdAndObjectId(ctx context.Context, userID string, awardKey string, badgeID string) (awardCount int64) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) ListAllByUserId(ctx context.Context, userID string) (badgeAwards []*entity.BadgeAward) ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) - ListPagedByObjectId(ctx context.Context, badgeID string, objectID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) - ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, objectID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListPagedByObjectId(ctx context.Context, badgeID string, awardKey string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, awardKey string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) ListTagPagedByBadgeId(ctx context.Context, badgeIDs []string, page int, pageSize int, filterUserID string) (badgeAwards []*entity.BadgeAward, total int64, err error) ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) ListPagedLatest(ctx context.Context, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int) (badgeAwards []*entity.BadgeAward, total int64, err error) ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) - GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward) - GetByUserIdAndBadgeIdAndObjectId(ctx context.Context, userID string, badgeID string, objectID string) (badgeAward *entity.BadgeAward) + GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward, exists bool, err error) + GetByUserIdAndBadgeIdAndObjectId(ctx context.Context, userID string, badgeID string, awardKey string) (badgeAward *entity.BadgeAward, exists bool, err error) } type BadgeAwardService struct { badgeAwardRepo BadgeAwardRepo + badgeRepo badge.BadgeRepo userCommon *usercommon.UserCommon objectInfoService *object_info.ObjService - questionRepo questioncommon.QuestionRepo - answerRepo answercommon.AnswerRepo } func NewBadgeAwardService( badgeAwardRepo BadgeAwardRepo, + badgeRepo badge.BadgeRepo, userCommon *usercommon.UserCommon, objectInfoService *object_info.ObjService, - questionRepo questioncommon.QuestionRepo, - answerRepo answercommon.AnswerRepo, ) *BadgeAwardService { return &BadgeAwardService{ badgeAwardRepo: badgeAwardRepo, + badgeRepo: badgeRepo, userCommon: userCommon, objectInfoService: objectInfoService, - questionRepo: questionRepo, - answerRepo: answerRepo, } } @@ -140,3 +138,48 @@ func (b *BadgeAwardService) GetBadgeAwardList( return } + +// Award award badge +func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string, force bool, createdAt time.Time) (err error) { + var ( + badgeData *entity.Badge + exists, awarded bool + ) + + badgeData, exists, err = b.badgeRepo.GetByID(ctx, badgeID) + if err != nil { + return + } + + if !exists || badgeData.Status == entity.BadgeStatusInactive { + err = errors.BadRequest(reason.BadgeObjectNotFound) + return + } + + awarded = b.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single) + if !force && awarded { + return + } + + if createdAt.IsZero() { + createdAt = time.Now() + } + + err = b.badgeAwardRepo.Add(ctx, &entity.BadgeAward{ + CreatedAt: createdAt, + UpdatedAt: createdAt, + UserID: userID, + BadgeID: badgeID, + AwardKey: awardKey, + BadgeGroupID: badgeData.BadgeGroupID, + IsBadgeDeleted: 0, + }) + if err != nil { + return + } + + // increment badge award count + err = b.badgeRepo.UpdateAwardCount(ctx, badgeID, 1) + + return +} From c2531f9b07243dce326cbf5cf89d9fba23ade976 Mon Sep 17 00:00:00 2001 From: kumfo Date: Fri, 9 Aug 2024 11:33:24 +0800 Subject: [PATCH 25/38] chore(badge): change project code struct --- cmd/wire_gen.go | 3 +-- docs/docs.go | 6 +++--- docs/swagger.json | 6 +++--- docs/swagger.yaml | 6 +++--- internal/controller/badge_controller.go | 5 ++--- internal/repo/badge/badge_event_rule.go | 2 +- internal/repo/badge_award/badge_award_repo.go | 4 ++-- internal/repo/badge_group/badge_group_repo.go | 4 ++-- .../{badge_award => badge}/badge_award_service.go | 11 +++++------ internal/service/badge/badge_event_handler.go | 3 +-- .../{badge_group => badge}/badge_group_service.go | 2 +- internal/service/badge/badge_service.go | 12 +++++------- internal/service/provider.go | 6 ++---- 13 files changed, 31 insertions(+), 39 deletions(-) rename internal/service/{badge_award => badge}/badge_award_service.go (96%) rename internal/service/{badge_group => badge}/badge_group_service.go (98%) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index f49ef20e2..309af9356 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -75,7 +75,6 @@ import ( "github.com/apache/incubator-answer/internal/service/answer_common" auth2 "github.com/apache/incubator-answer/internal/service/auth" badge2 "github.com/apache/incubator-answer/internal/service/badge" - badge_award2 "github.com/apache/incubator-answer/internal/service/badge_award" collection2 "github.com/apache/incubator-answer/internal/service/collection" "github.com/apache/incubator-answer/internal/service/collection_common" comment2 "github.com/apache/incubator-answer/internal/service/comment" @@ -266,7 +265,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, eventRuleRepo := badge.NewEventRuleRepo(dataData) badgeEventService := badge2.NewBadgeEventService(dataData, eventQueueService, badgeRepo, eventRuleRepo) badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService) - badgeAwardService := badge_award2.NewBadgeAwardService(badgeAwardRepo, userCommon, objService, questionRepo, answerRepo) + badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService) badgeController := controller.NewBadgeController(badgeService, badgeAwardService) answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) diff --git a/docs/docs.go b/docs/docs.go index bda5a17b6..e19465904 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -6740,14 +6740,14 @@ const docTemplate = `{ }, "/custom.css": { "get": { - "description": "get site robots information", + "description": "get site custom CSS", "produces": [ - "application/json" + "text/css" ], "tags": [ "site" ], - "summary": "get site robots information", + "summary": "get site custom CSS", "responses": { "200": { "description": "OK", diff --git a/docs/swagger.json b/docs/swagger.json index 5ea50e6bd..0a3bf6c7d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -6710,14 +6710,14 @@ }, "/custom.css": { "get": { - "description": "get site robots information", + "description": "get site custom CSS", "produces": [ - "application/json" + "text/css" ], "tags": [ "site" ], - "summary": "get site robots information", + "summary": "get site custom CSS", "responses": { "200": { "description": "OK", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ca11b20f3..777445b3f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -6846,15 +6846,15 @@ paths: - Activity /custom.css: get: - description: get site robots information + description: get site custom CSS produces: - - application/json + - text/css responses: "200": description: OK schema: type: string - summary: get site robots information + summary: get site custom CSS tags: - site /installation/base-info: diff --git a/internal/controller/badge_controller.go b/internal/controller/badge_controller.go index c26d9205a..d7ecc3fab 100644 --- a/internal/controller/badge_controller.go +++ b/internal/controller/badge_controller.go @@ -25,19 +25,18 @@ import ( "github.com/apache/incubator-answer/internal/base/pager" "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/badge" - "github.com/apache/incubator-answer/internal/service/badge_award" "github.com/apache/incubator-answer/pkg/uid" "github.com/gin-gonic/gin" ) type BadgeController struct { badgeService *badge.BadgeService - badgeAwardService *badge_award.BadgeAwardService + badgeAwardService *badge.BadgeAwardService } func NewBadgeController( badgeService *badge.BadgeService, - badgeAwardService *badge_award.BadgeAwardService) *BadgeController { + badgeAwardService *badge.BadgeAwardService) *BadgeController { return &BadgeController{ badgeService: badgeService, badgeAwardService: badgeAwardService, diff --git a/internal/repo/badge/badge_event_rule.go b/internal/repo/badge/badge_event_rule.go index 8f9407202..d6c0d8263 100644 --- a/internal/repo/badge/badge_event_rule.go +++ b/internal/repo/badge/badge_event_rule.go @@ -211,6 +211,6 @@ func (br *eventRuleRepo) createBadgeAward(userID, badgeID, objectID string) (awa return &entity.BadgeAward{ UserID: userID, BadgeID: badgeID, - ObjectID: objectID, + AwardKey: objectID, } } diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 92807d1df..8860a5b13 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -25,7 +25,7 @@ import ( "github.com/apache/incubator-answer/internal/base/pager" "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" - "github.com/apache/incubator-answer/internal/service/badge_award" + "github.com/apache/incubator-answer/internal/service/badge" "github.com/apache/incubator-answer/internal/service/unique" "github.com/segmentfault/pacman/errors" ) @@ -35,7 +35,7 @@ type badgeAwardRepo struct { uniqueIDRepo unique.UniqueIDRepo } -func NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge_award.BadgeAwardRepo { +func NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeAwardRepo { return &badgeAwardRepo{ data: data, uniqueIDRepo: uniqueIDRepo, diff --git a/internal/repo/badge_group/badge_group_repo.go b/internal/repo/badge_group/badge_group_repo.go index 7dfe90fc3..63f438b9e 100644 --- a/internal/repo/badge_group/badge_group_repo.go +++ b/internal/repo/badge_group/badge_group_repo.go @@ -23,7 +23,7 @@ import ( "context" "github.com/apache/incubator-answer/internal/base/data" "github.com/apache/incubator-answer/internal/entity" - "github.com/apache/incubator-answer/internal/service/badge_group" + "github.com/apache/incubator-answer/internal/service/badge" "github.com/apache/incubator-answer/internal/service/unique" ) @@ -32,7 +32,7 @@ type badgeGroupRepo struct { uniqueIDRepo unique.UniqueIDRepo } -func NewBadgeGroupRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge_group.BadgeGroupRepo { +func NewBadgeGroupRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeGroupRepo { return &badgeGroupRepo{ data: data, uniqueIDRepo: uniqueIDRepo, diff --git a/internal/service/badge_award/badge_award_service.go b/internal/service/badge/badge_award_service.go similarity index 96% rename from internal/service/badge_award/badge_award_service.go rename to internal/service/badge/badge_award_service.go index 2c7c5ffac..229b09a56 100644 --- a/internal/service/badge_award/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -17,14 +17,13 @@ * under the License. */ -package badge_award +package badge import ( "context" "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service/badge" "github.com/apache/incubator-answer/internal/service/object_info" usercommon "github.com/apache/incubator-answer/internal/service/user_common" "github.com/jinzhu/copier" @@ -62,15 +61,15 @@ type BadgeAwardRepo interface { } type BadgeAwardService struct { - badgeAwardRepo BadgeAwardRepo - badgeRepo badge.BadgeRepo - userCommon *usercommon.UserCommon + badgeAwardRepo BadgeAwardRepo + badgeRepo BadgeRepo + userCommon *usercommon.UserCommon objectInfoService *object_info.ObjService } func NewBadgeAwardService( badgeAwardRepo BadgeAwardRepo, - badgeRepo badge.BadgeRepo, + badgeRepo BadgeRepo, userCommon *usercommon.UserCommon, objectInfoService *object_info.ObjService, ) *BadgeAwardService { diff --git a/internal/service/badge/badge_event_handler.go b/internal/service/badge/badge_event_handler.go index 587ee77d2..d6758baf4 100644 --- a/internal/service/badge/badge_event_handler.go +++ b/internal/service/badge/badge_event_handler.go @@ -22,7 +22,6 @@ package badge import ( "context" "github.com/apache/incubator-answer/internal/entity" - "github.com/apache/incubator-answer/internal/service/badge_award" "github.com/apache/incubator-answer/internal/service/event_queue" "github.com/segmentfault/pacman/log" @@ -33,7 +32,7 @@ import ( type BadgeEventService struct { data *data.Data eventQueueService event_queue.EventQueueService - badgeAwardRepo badge_award.BadgeAwardRepo + badgeAwardRepo BadgeAwardRepo badgeRepo BadgeRepo eventRuleRepo EventRuleRepo } diff --git a/internal/service/badge_group/badge_group_service.go b/internal/service/badge/badge_group_service.go similarity index 98% rename from internal/service/badge_group/badge_group_service.go rename to internal/service/badge/badge_group_service.go index c78f3e4f6..16dd74ee4 100644 --- a/internal/service/badge_group/badge_group_service.go +++ b/internal/service/badge/badge_group_service.go @@ -17,7 +17,7 @@ * under the License. */ -package badge_group +package badge import ( "context" diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index faf97b11a..366dd3386 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -26,8 +26,6 @@ import ( "github.com/apache/incubator-answer/internal/base/translator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" - "github.com/apache/incubator-answer/internal/service/badge_award" - "github.com/apache/incubator-answer/internal/service/badge_group" "github.com/apache/incubator-answer/pkg/converter" "github.com/apache/incubator-answer/pkg/uid" "github.com/gin-gonic/gin" @@ -48,16 +46,16 @@ type BadgeRepo interface { } type BadgeService struct { - badgeRepo BadgeRepo - badgeGroupRepo badge_group.BadgeGroupRepo - badgeAwardRepo badge_award.BadgeAwardRepo + badgeRepo BadgeRepo + badgeGroupRepo BadgeGroupRepo + badgeAwardRepo BadgeAwardRepo badgeEventService *BadgeEventService } func NewBadgeService( badgeRepo BadgeRepo, - badgeGroupRepo badge_group.BadgeGroupRepo, - badgeAwardRepo badge_award.BadgeAwardRepo, + badgeGroupRepo BadgeGroupRepo, + badgeAwardRepo BadgeAwardRepo, badgeEventService *BadgeEventService, ) *BadgeService { return &BadgeService{ diff --git a/internal/service/provider.go b/internal/service/provider.go index 7a6b1340e..12e0db797 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -27,8 +27,6 @@ import ( answercommon "github.com/apache/incubator-answer/internal/service/answer_common" "github.com/apache/incubator-answer/internal/service/auth" "github.com/apache/incubator-answer/internal/service/badge" - "github.com/apache/incubator-answer/internal/service/badge_award" - "github.com/apache/incubator-answer/internal/service/badge_group" "github.com/apache/incubator-answer/internal/service/collection" collectioncommon "github.com/apache/incubator-answer/internal/service/collection_common" "github.com/apache/incubator-answer/internal/service/comment" @@ -124,6 +122,6 @@ var ProviderSetService = wire.NewSet( event_queue.NewEventQueueService, badge.NewBadgeService, badge.NewBadgeEventService, - badge_award.NewBadgeAwardService, - badge_group.NewBadgeGroupService, + badge.NewBadgeAwardService, + badge.NewBadgeGroupService, ) From 5293f4f3eaf397ad67bca83bfc5806aac62e1cc3 Mon Sep 17 00:00:00 2001 From: kumfo Date: Fri, 9 Aug 2024 15:00:46 +0800 Subject: [PATCH 26/38] feat(badge): user's badge award list --- docs/docs.go | 72 +++++++++++++++++++ docs/swagger.json | 72 +++++++++++++++++++ docs/swagger.yaml | 43 +++++++++++ internal/controller/badge_controller.go | 25 +++++++ internal/entity/badge_award_entity.go | 2 +- internal/router/answer_api_router.go | 1 + internal/schema/badge_schema.go | 12 ++++ internal/service/badge/badge_award_service.go | 69 +++++++++++++++++- internal/service/badge/badge_service.go | 8 +-- 9 files changed, 296 insertions(+), 8 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index e19465904..79e23a854 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2350,6 +2350,58 @@ const docTemplate = `{ } } }, + "/answer/api/v1/badge/user/awards": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user badge award list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "get user badge award list", + "parameters": [ + { + "type": "integer", + "description": "user id", + "name": "user_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" + } + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/badges": { "get": { "security": [ @@ -8734,6 +8786,26 @@ const docTemplate = `{ } } }, + "schema.GetUserBadgeAwardListResp": { + "type": "object", + "properties": { + "earned_count": { + "type": "integer" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "string" + }, + "level": { + "$ref": "#/definitions/entity.BadgeLevel" + }, + "name": { + "type": "string" + } + } + }, "schema.GetUserNotificationConfigResp": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 0a3bf6c7d..07aabfeac 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2320,6 +2320,58 @@ } } }, + "/answer/api/v1/badge/user/awards": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user badge award list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "get user badge award list", + "parameters": [ + { + "type": "integer", + "description": "user id", + "name": "user_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" + } + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/badges": { "get": { "security": [ @@ -8704,6 +8756,26 @@ } } }, + "schema.GetUserBadgeAwardListResp": { + "type": "object", + "properties": { + "earned_count": { + "type": "integer" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "string" + }, + "level": { + "$ref": "#/definitions/entity.BadgeLevel" + }, + "name": { + "type": "string" + } + } + }, "schema.GetUserNotificationConfigResp": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 777445b3f..c8c5a5605 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1203,6 +1203,19 @@ definitions: activation_url: type: string type: object + schema.GetUserBadgeAwardListResp: + properties: + earned_count: + type: integer + icon: + type: string + id: + type: string + level: + $ref: '#/definitions/entity.BadgeLevel' + name: + type: string + type: object schema.GetUserNotificationConfigResp: properties: all_new_question: @@ -4201,6 +4214,36 @@ paths: summary: get badge award list tags: - api-badge + /answer/api/v1/badge/user/awards: + get: + consumes: + - application/json + description: get user badge award list + parameters: + - description: user id + in: query + name: user_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetUserBadgeAwardListResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: get user badge award list + tags: + - api-badge /answer/api/v1/badges: get: consumes: diff --git a/internal/controller/badge_controller.go b/internal/controller/badge_controller.go index d7ecc3fab..7eb42da01 100644 --- a/internal/controller/badge_controller.go +++ b/internal/controller/badge_controller.go @@ -104,3 +104,28 @@ func (b *BadgeController) GetBadgeAwardList(ctx *gin.Context) { } handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) } + +// GetBadgeAwardListByUsername get user badge award list +// @Summary get user badge award list +// @Description get user badge award list +// @Tags api-badge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param user_id query int true "user id" +// @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp} +// @Router /answer/api/v1/badge/user/awards [get] +func (b *BadgeController) GetBadgeAwardListByUsername(ctx *gin.Context) { + req := &schema.GetUserBadgeAwardListReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, total, err := b.badgeAwardService.GetUserBadgeAwardList(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) +} diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index 310c85a7d..17649734c 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -40,7 +40,7 @@ func (BadgeAward) TableName() string { type BadgeEarnedCount struct { BadgeID string `xorm:"badge_id"` - EarnedCount int `xorm:"earned_count"` + EarnedCount int64 `xorm:"earned_count"` } // TableName badge_award table name diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index c7d4c38ea..63dec5fad 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -194,6 +194,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // badges r.GET("/badge", a.badgeController.GetBadgeInfo) r.GET("/badge/awards/page", a.badgeController.GetBadgeAwardList) + r.GET("/badge/user/awards", a.badgeController.GetBadgeAwardListByUsername) r.GET("/badges", a.badgeController.GetBadgeList) } diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go index 06d92ecc4..0555a99bb 100644 --- a/internal/schema/badge_schema.go +++ b/internal/schema/badge_schema.go @@ -68,3 +68,15 @@ type GetBadgeAwardWithPageResp struct { UrlTitle string `json:"url_title"` AuthorUserInfo UserBasicInfo `json:"author_user_info"` } + +type GetUserBadgeAwardListReq struct { + Username string `validate:"omitempty,gt=0,lte=100" form:"username"` + UserID string `json:"-"` +} +type GetUserBadgeAwardListResp struct { + ID string `json:"id" ` + Name string `json:"name" ` + Icon string `json:"icon" ` + EarnedCount int64 `json:"earned_count" ` + Level entity.BadgeLevel `json:"level" ` +} diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index 229b09a56..c13c56586 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -21,11 +21,14 @@ package badge import ( "context" + "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/reason" + "github.com/apache/incubator-answer/internal/base/translator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/object_info" usercommon "github.com/apache/incubator-answer/internal/service/user_common" + "github.com/gin-gonic/gin" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" @@ -61,9 +64,9 @@ type BadgeAwardRepo interface { } type BadgeAwardService struct { - badgeAwardRepo BadgeAwardRepo - badgeRepo BadgeRepo - userCommon *usercommon.UserCommon + badgeAwardRepo BadgeAwardRepo + badgeRepo BadgeRepo + userCommon *usercommon.UserCommon objectInfoService *object_info.ObjService } @@ -182,3 +185,63 @@ func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID st return } + +// GetUserBadgeAwardList get user badge award list +func (b *BadgeAwardService) GetUserBadgeAwardList( + ctx *gin.Context, + req *schema.GetUserBadgeAwardListReq, +) ( + resp []*schema.GetUserBadgeAwardListResp, + total int64, + err error, +) { + var ( + earnedCounts []*entity.BadgeEarnedCount + userInfo *schema.UserBasicInfo + exist bool + ) + + // validate user exists or not + if len(req.Username) > 0 { + userInfo, exist, err = b.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) + if err != nil { + return + } + if !exist { + err = errors.BadRequest(reason.UserNotFound) + return + } + req.UserID = userInfo.ID + } + if len(req.UserID) == 0 { + err = errors.BadRequest(reason.UserNotFound) + return + } + + earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID) + if err != nil { + return + } + total = int64(len(earnedCounts)) + resp = make([]*schema.GetUserBadgeAwardListResp, 0, total) + + for i, earnedCount := range earnedCounts { + badge, exists, e := b.badgeRepo.GetByID(ctx, earnedCount.BadgeID) + if e != nil { + err = e + return + } + if !exists { + continue + } + resp[i] = &schema.GetUserBadgeAwardListResp{ + ID: badge.ID, + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Icon: badge.Icon, + EarnedCount: earnedCount.EarnedCount, + Level: badge.Level, + } + } + + return +} diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index 366dd3386..32a54a8d9 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -46,9 +46,9 @@ type BadgeRepo interface { } type BadgeService struct { - badgeRepo BadgeRepo - badgeGroupRepo BadgeGroupRepo - badgeAwardRepo BadgeAwardRepo + badgeRepo BadgeRepo + badgeGroupRepo BadgeGroupRepo + badgeAwardRepo BadgeAwardRepo badgeEventService *BadgeEventService } @@ -103,7 +103,7 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* earned := false if len(earnedCounts) > 0 { for _, earnedCount := range earnedCounts { - if badge.ID == earnedCount.BadgeID { + if badge.ID == earnedCount.BadgeID && earnedCount.EarnedCount > 0 { earned = true break } From 61db21e0d376a22a3b1e32a32a3d1578eb1106d4 Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 12 Aug 2024 15:33:04 +0800 Subject: [PATCH 27/38] feat(badge): fixed user's badge award list and add recent user badges --- docs/docs.go | 103 ++++++++++++++++- docs/swagger.json | 103 ++++++++++++++++- docs/swagger.yaml | 70 +++++++++++- internal/controller/badge_controller.go | 35 +++++- internal/entity/badge_award_entity.go | 21 +++- internal/repo/badge_award/badge_award_repo.go | 25 +++- internal/router/answer_api_router.go | 3 +- internal/schema/badge_schema.go | 95 +++++++++++----- internal/service/badge/badge_award_service.go | 107 +++++++++++++++--- 9 files changed, 489 insertions(+), 73 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 79e23a854..ecc621692 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2326,6 +2326,12 @@ const docTemplate = `{ "name": "badge_id", "in": "query", "required": true + }, + { + "type": "string", + "description": "only list the award by username", + "name": "username", + "in": "query" } ], "responses": { @@ -2370,9 +2376,61 @@ const docTemplate = `{ "summary": "get user badge award list", "parameters": [ { - "type": "integer", - "description": "user id", - "name": "user_id", + "type": "string", + "description": "user name", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/badge/user/awards/recent": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user badge award list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "get user badge award list", + "parameters": [ + { + "type": "string", + "description": "user name", + "name": "username", "in": "query", "required": true } @@ -7565,21 +7623,31 @@ const docTemplate = `{ "type": "object", "properties": { "award_count": { + "description": "badge award count", "type": "integer" }, "earned": { + "description": "badge earned count", "type": "boolean" }, "icon": { + "description": "badge icon", "type": "string" }, "id": { + "description": "badge id", "type": "string" }, "level": { - "$ref": "#/definitions/entity.BadgeLevel" + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] }, "name": { + "description": "badge name", "type": "string" } } @@ -7839,27 +7907,39 @@ const docTemplate = `{ "type": "object", "properties": { "award_count": { + "description": "badge award count", "type": "integer" }, "description": { + "description": "badge description", "type": "string" }, "earned_count": { + "description": "badge earned count", "type": "integer" }, "icon": { + "description": "badge icon", "type": "string" }, "id": { + "description": "badge id", "type": "string" }, "is_single": { + "description": "badge is single or multiple", "type": "boolean" }, "level": { - "$ref": "#/definitions/entity.BadgeLevel" + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] }, "name": { + "description": "badge name", "type": "string" } } @@ -7868,12 +7948,14 @@ const docTemplate = `{ "type": "object", "properties": { "badges": { + "description": "badge list info", "type": "array", "items": { "$ref": "#/definitions/schema.BadgeListInfo" } }, "group_name": { + "description": "badge group name", "type": "string" } } @@ -8790,18 +8872,27 @@ const docTemplate = `{ "type": "object", "properties": { "earned_count": { + "description": "badge award count", "type": "integer" }, "icon": { + "description": "badge icon", "type": "string" }, "id": { + "description": "badge id", "type": "string" }, "level": { - "$ref": "#/definitions/entity.BadgeLevel" + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] }, "name": { + "description": "badge name", "type": "string" } } diff --git a/docs/swagger.json b/docs/swagger.json index 07aabfeac..1412f179d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2296,6 +2296,12 @@ "name": "badge_id", "in": "query", "required": true + }, + { + "type": "string", + "description": "only list the award by username", + "name": "username", + "in": "query" } ], "responses": { @@ -2340,9 +2346,61 @@ "summary": "get user badge award list", "parameters": [ { - "type": "integer", - "description": "user id", - "name": "user_id", + "type": "string", + "description": "user name", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetUserBadgeAwardListResp" + } + } + } + } + ] + } + } + } + } + }, + "/answer/api/v1/badge/user/awards/recent": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "get user badge award list", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-badge" + ], + "summary": "get user badge award list", + "parameters": [ + { + "type": "string", + "description": "user name", + "name": "username", "in": "query", "required": true } @@ -7535,21 +7593,31 @@ "type": "object", "properties": { "award_count": { + "description": "badge award count", "type": "integer" }, "earned": { + "description": "badge earned count", "type": "boolean" }, "icon": { + "description": "badge icon", "type": "string" }, "id": { + "description": "badge id", "type": "string" }, "level": { - "$ref": "#/definitions/entity.BadgeLevel" + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] }, "name": { + "description": "badge name", "type": "string" } } @@ -7809,27 +7877,39 @@ "type": "object", "properties": { "award_count": { + "description": "badge award count", "type": "integer" }, "description": { + "description": "badge description", "type": "string" }, "earned_count": { + "description": "badge earned count", "type": "integer" }, "icon": { + "description": "badge icon", "type": "string" }, "id": { + "description": "badge id", "type": "string" }, "is_single": { + "description": "badge is single or multiple", "type": "boolean" }, "level": { - "$ref": "#/definitions/entity.BadgeLevel" + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] }, "name": { + "description": "badge name", "type": "string" } } @@ -7838,12 +7918,14 @@ "type": "object", "properties": { "badges": { + "description": "badge list info", "type": "array", "items": { "$ref": "#/definitions/schema.BadgeListInfo" } }, "group_name": { + "description": "badge group name", "type": "string" } } @@ -8760,18 +8842,27 @@ "type": "object", "properties": { "earned_count": { + "description": "badge award count", "type": "integer" }, "icon": { + "description": "badge icon", "type": "string" }, "id": { + "description": "badge id", "type": "string" }, "level": { - "$ref": "#/definitions/entity.BadgeLevel" + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] }, "name": { + "description": "badge name", "type": "string" } } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c8c5a5605..e2fc66828 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -355,16 +355,23 @@ definitions: schema.BadgeListInfo: properties: award_count: + description: badge award count type: integer earned: + description: badge earned count type: boolean icon: + description: badge icon type: string id: + description: badge id type: string level: - $ref: '#/definitions/entity.BadgeLevel' + allOf: + - $ref: '#/definitions/entity.BadgeLevel' + description: badge level name: + description: badge name type: string type: object schema.CloseQuestionReq: @@ -541,29 +548,40 @@ definitions: schema.GetBadgeInfoResp: properties: award_count: + description: badge award count type: integer description: + description: badge description type: string earned_count: + description: badge earned count type: integer icon: + description: badge icon type: string id: + description: badge id type: string is_single: + description: badge is single or multiple type: boolean level: - $ref: '#/definitions/entity.BadgeLevel' + allOf: + - $ref: '#/definitions/entity.BadgeLevel' + description: badge level name: + description: badge name type: string type: object schema.GetBadgeListResp: properties: badges: + description: badge list info items: $ref: '#/definitions/schema.BadgeListInfo' type: array group_name: + description: badge group name type: string type: object schema.GetCommentPersonalWithPageResp: @@ -1206,14 +1224,20 @@ definitions: schema.GetUserBadgeAwardListResp: properties: earned_count: + description: badge award count type: integer icon: + description: badge icon type: string id: + description: badge id type: string level: - $ref: '#/definitions/entity.BadgeLevel' + allOf: + - $ref: '#/definitions/entity.BadgeLevel' + description: badge level name: + description: badge name type: string type: object schema.GetUserNotificationConfigResp: @@ -4197,6 +4221,10 @@ paths: name: badge_id required: true type: string + - description: only list the award by username + in: query + name: username + type: string produces: - application/json responses: @@ -4220,11 +4248,41 @@ paths: - application/json description: get user badge award list parameters: - - description: user id + - description: user name in: query - name: user_id + name: username required: true - type: integer + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetUserBadgeAwardListResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: get user badge award list + tags: + - api-badge + /answer/api/v1/badge/user/awards/recent: + get: + consumes: + - application/json + description: get user badge award list + parameters: + - description: user name + in: query + name: username + required: true + type: string produces: - application/json responses: diff --git a/internal/controller/badge_controller.go b/internal/controller/badge_controller.go index 7eb42da01..65b594072 100644 --- a/internal/controller/badge_controller.go +++ b/internal/controller/badge_controller.go @@ -87,6 +87,7 @@ func (b *BadgeController) GetBadgeInfo(ctx *gin.Context) { // @Param page query int false "page" // @Param page_size query int false "page size" // @Param badge_id query string true "badge id" +// @Param username query string false "only list the award by username" // @Success 200 {object} handler.RespBody{data=schema.GetBadgeInfoResp} // @Router /answer/api/v1/badge/awards/page [get] func (b *BadgeController) GetBadgeAwardList(ctx *gin.Context) { @@ -95,7 +96,6 @@ func (b *BadgeController) GetBadgeAwardList(ctx *gin.Context) { return } req.BadgeID = uid.DeShortID(req.BadgeID) - req.UserID = middleware.GetLoginUserIDFromContext(ctx) resp, total, err := b.badgeAwardService.GetBadgeAwardList(ctx, req) if err != nil { @@ -105,17 +105,17 @@ func (b *BadgeController) GetBadgeAwardList(ctx *gin.Context) { handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) } -// GetBadgeAwardListByUsername get user badge award list +// GetAllBadgeAwardListByUsername get user badge award list // @Summary get user badge award list // @Description get user badge award list // @Tags api-badge // @Accept json // @Produce json // @Security ApiKeyAuth -// @Param user_id query int true "user id" +// @Param username query string true "user name" // @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp} // @Router /answer/api/v1/badge/user/awards [get] -func (b *BadgeController) GetBadgeAwardListByUsername(ctx *gin.Context) { +func (b *BadgeController) GetAllBadgeAwardListByUsername(ctx *gin.Context) { req := &schema.GetUserBadgeAwardListReq{} if handler.BindAndCheck(ctx, req) { return @@ -129,3 +129,30 @@ func (b *BadgeController) GetBadgeAwardListByUsername(ctx *gin.Context) { handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) } + +// GetRecentBadgeAwardListByUsername get user badge award list +// @Summary get user badge award list +// @Description get user badge award list +// @Tags api-badge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param username query string true "user name" +// @Success 200 {object} handler.RespBody{data=[]schema.GetUserBadgeAwardListResp} +// @Router /answer/api/v1/badge/user/awards/recent [get] +func (b *BadgeController) GetRecentBadgeAwardListByUsername(ctx *gin.Context) { + req := &schema.GetUserBadgeAwardListReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + req.Limit = 10 + + resp, total, err := b.badgeAwardService.GetUserRecentBadgeAwardList(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) +} diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index 17649734c..1d4216238 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -21,6 +21,11 @@ package entity import "time" +const ( + IsBadgeNotDeleted = 0 + IsBadgeDeleted = 1 +) + // BadgeAward badge_award type BadgeAward struct { ID string `json:"id" xorm:"id"` @@ -30,7 +35,7 @@ type BadgeAward struct { BadgeID string `json:"badge_id" xorm:"not null index BIGINT(20) badge_id"` AwardKey string `json:"award_key" xorm:"not null index VARCHAR(64) award_key"` BadgeGroupID int64 `json:"badge_group_id" xorm:"not null index BIGINT(20) badge_group_id"` - IsBadgeDeleted int8 `json:"is_badge_deleted" xorm:"not null index TINYINT(1) s_badge_deleted"` + IsBadgeDeleted int8 `json:"is_badge_deleted" xorm:"not null index TINYINT(1) is_badge_deleted"` } // TableName badge_award table name @@ -47,3 +52,17 @@ type BadgeEarnedCount struct { func (BadgeEarnedCount) TableName() string { return "badge_award" } + +type BadgeAwardRecent struct { + Created time.Time `xorm:"created"` + UserID string `xorm:"user_id"` + BadgeID string `xorm:"badge_id"` + AwardKey string `xorm:"award_key"` + EarnedCount int64 `xorm:"earned_count"` + IsBadgeDeleted int8 `xorm:"is_badge_deleted"` +} + +// TableName badge_award table name +func (BadgeAwardRecent) TableName() string { + return "badge_award" +} diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 8860a5b13..5a5d56cde 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -108,13 +108,21 @@ func (r *badgeAwardRepo) ListAllByUserId(ctx context.Context, userID string) (ba func (r *badgeAwardRepo) ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) { session := r.data.DB.Context(ctx) session.Where("badge_id = ?", badgeID) - total, err = pager.Help(page, pageSize, &badgeAwardList, &entity.Question{}, session) + total, err = pager.Help(page, pageSize, &badgeAwardList, &entity.BadgeAward{}, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } -func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { + +// ListPagedByBadgeIdAndUserId list badge awards by badge id and user id +func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) { + session := r.data.DB.Context(ctx) + session.Where("badge_id = ? AND user_id = ?", badgeID, userID) + total, err = pager.Help(page, pageSize, &badgeAwardList, &entity.Question{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } func (r *badgeAwardRepo) ListPagedByObjectId(ctx context.Context, badgeID string, awardKey string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { @@ -132,6 +140,19 @@ func (r *badgeAwardRepo) ListTagPagedByBadgeIdAndUserId(ctx context.Context, bad func (r *badgeAwardRepo) ListPagedLatest(ctx context.Context, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } + +// ListNewestEarned list newest earned badge awards +func (r *badgeAwardRepo) ListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error) { + badgeAwards = make([]*entity.BadgeAwardRecent, 0) + err = r.data.DB.Context(ctx). + Select("user_id, badge_id, max(created_at) created,count(*) earned_count"). + Where("user_id = ? AND is_badge_deleted = ? ", userID, entity.IsBadgeNotDeleted). + GroupBy("badge_id"). + OrderBy("created desc"). + Limit(limit).Find(&badgeAwards) + return +} + func (r *badgeAwardRepo) ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int) (badgeAwards []*entity.BadgeAward, total int64, err error) { return } diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 63dec5fad..f343d7d11 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -194,7 +194,8 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // badges r.GET("/badge", a.badgeController.GetBadgeInfo) r.GET("/badge/awards/page", a.badgeController.GetBadgeAwardList) - r.GET("/badge/user/awards", a.badgeController.GetBadgeAwardListByUsername) + r.GET("/badge/user/awards/recent", a.badgeController.GetRecentBadgeAwardListByUsername) + r.GET("/badge/user/awards", a.badgeController.GetAllBadgeAwardListByUsername) r.GET("/badges", a.badgeController.GetBadgeList) } diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go index 0555a99bb..cae2aa66c 100644 --- a/internal/schema/badge_schema.go +++ b/internal/schema/badge_schema.go @@ -23,28 +23,44 @@ import "github.com/apache/incubator-answer/internal/entity" // BadgeListInfo get badge list response type BadgeListInfo struct { - ID string `json:"id" ` - Name string `json:"name" ` - Icon string `json:"icon" ` - AwardCount int `json:"award_count" ` - Earned bool `json:"earned" ` - Level entity.BadgeLevel `json:"level" ` + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + AwardCount int `json:"award_count" ` + // badge earned count + Earned bool `json:"earned" ` + // badge level + Level entity.BadgeLevel `json:"level" ` } type GetBadgeListResp struct { - Badges []*BadgeListInfo `json:"badges" ` - GroupName string `json:"group_name" ` + // badge list info + Badges []*BadgeListInfo `json:"badges" ` + // badge group name + GroupName string `json:"group_name" ` } type GetBadgeInfoResp struct { - ID string `json:"id" ` - Name string `json:"name" ` - Description string `json:"description" ` - Icon string `json:"icon" ` - AwardCount int `json:"award_count" ` - EarnedCount int64 `json:"earned_count" ` - IsSingle bool `json:"is_single" ` - Level entity.BadgeLevel `json:"level" ` + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge description + Description string `json:"description" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + AwardCount int `json:"award_count" ` + // badge earned count + EarnedCount int64 `json:"earned_count" ` + // badge is single or multiple + IsSingle bool `json:"is_single" ` + // badge level + Level entity.BadgeLevel `json:"level" ` } type GetBadgeAwardWithPageReq struct { @@ -54,29 +70,48 @@ type GetBadgeAwardWithPageReq struct { PageSize int `validate:"omitempty,min=1" form:"page_size"` // badge id BadgeID string `validate:"required" form:"badge_id"` + // username + Username string `validate:"omitempty,gt=0,lte=100" form:"username"` // user id UserID string `json:"-"` } type GetBadgeAwardWithPageResp struct { - CreatedAt int64 `json:"created_at"` - ObjectID string `json:"object_id"` - QuestionID string `json:"question_id"` - AnswerID string `json:"answer_id"` - CommentID string `json:"comment_id"` - ObjectType string `json:"object_type" enums:"question,answer,comment"` - UrlTitle string `json:"url_title"` + // created time + CreatedAt int64 `json:"created_at"` + // object id + ObjectID string `json:"object_id"` + // question id + QuestionID string `json:"question_id"` + // answer id + AnswerID string `json:"answer_id"` + // comment id + CommentID string `json:"comment_id"` + // object type + ObjectType string `json:"object_type" enums:"question,answer,comment"` + // url title + UrlTitle string `json:"url_title"` + // author user info AuthorUserInfo UserBasicInfo `json:"author_user_info"` } type GetUserBadgeAwardListReq struct { - Username string `validate:"omitempty,gt=0,lte=100" form:"username"` - UserID string `json:"-"` + // username + Username string `validate:"required,gt=0,lte=100" form:"username"` + // user id + UserID string `json:"-"` + Limit int `json:"-"` } + type GetUserBadgeAwardListResp struct { - ID string `json:"id" ` - Name string `json:"name" ` - Icon string `json:"icon" ` - EarnedCount int64 `json:"earned_count" ` - Level entity.BadgeLevel `json:"level" ` + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + EarnedCount int64 `json:"earned_count" ` + // badge level + Level entity.BadgeLevel `json:"level" ` } diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index c13c56586..0ad299ba8 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -28,6 +28,7 @@ import ( "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/object_info" usercommon "github.com/apache/incubator-answer/internal/service/user_common" + "github.com/apache/incubator-answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" @@ -56,6 +57,7 @@ type BadgeAwardRepo interface { ListTagPagedByBadgeId(ctx context.Context, badgeIDs []string, page int, pageSize int, filterUserID string) (badgeAwards []*entity.BadgeAward, total int64, err error) ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) ListPagedLatest(ctx context.Context, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) + ListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error) ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int) (badgeAwards []*entity.BadgeAward, total int64, err error) ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) @@ -86,13 +88,20 @@ func NewBadgeAwardService( // GetBadgeAwardList get badge award list func (b *BadgeAwardService) GetBadgeAwardList( - ctx context.Context, req *schema.GetBadgeAwardWithPageReq, + ctx context.Context, + req *schema.GetBadgeAwardWithPageReq, ) (resp []*schema.GetBadgeAwardWithPageResp, total int64, err error) { var ( badgeAwardList []*entity.BadgeAward ) - badgeAwardList, total, err = b.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize) + req.UserID, err = b.validateUserByUsername(ctx, req.Username) + if err != nil { + badgeAwardList, total, err = b.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize) + } else { + badgeAwardList, total, err = b.badgeAwardRepo.ListPagedByBadgeIdAndUserId(ctx, req.BadgeID, req.UserID, req.Page, req.PageSize) + } + if err != nil { return } @@ -197,33 +206,66 @@ func (b *BadgeAwardService) GetUserBadgeAwardList( ) { var ( earnedCounts []*entity.BadgeEarnedCount - userInfo *schema.UserBasicInfo - exist bool ) - // validate user exists or not - if len(req.Username) > 0 { - userInfo, exist, err = b.userCommon.GetUserBasicInfoByUserName(ctx, req.Username) - if err != nil { + req.UserID, err = b.validateUserByUsername(ctx, req.Username) + if err != nil { + return + } + + earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID) + if err != nil { + return + } + total = int64(len(earnedCounts)) + resp = make([]*schema.GetUserBadgeAwardListResp, total) + + for i, earnedCount := range earnedCounts { + badge, exists, e := b.badgeRepo.GetByID(ctx, earnedCount.BadgeID) + if e != nil { + err = e return } - if !exist { - err = errors.BadRequest(reason.UserNotFound) - return + if !exists { + continue + } + resp[i] = &schema.GetUserBadgeAwardListResp{ + ID: uid.EnShortID(badge.ID), + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Icon: badge.Icon, + EarnedCount: earnedCount.EarnedCount, + Level: badge.Level, } - req.UserID = userInfo.ID } - if len(req.UserID) == 0 { - err = errors.BadRequest(reason.UserNotFound) + + return +} + +// GetUserRecentBadgeAwardList get user badge award list +func (b *BadgeAwardService) GetUserRecentBadgeAwardList( + ctx *gin.Context, + req *schema.GetUserBadgeAwardListReq, +) ( + resp []*schema.GetUserBadgeAwardListResp, + total int64, + err error, +) { + var ( + earnedCounts []*entity.BadgeAwardRecent + ) + + req.UserID, err = b.validateUserByUsername(ctx, req.Username) + if err != nil { return } - earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID) + earnedCounts, err = b.badgeAwardRepo.ListNewestEarned(ctx, req.UserID, req.Limit) if err != nil { return } + total = int64(len(earnedCounts)) - resp = make([]*schema.GetUserBadgeAwardListResp, 0, total) + resp = make([]*schema.GetUserBadgeAwardListResp, total) for i, earnedCount := range earnedCounts { badge, exists, e := b.badgeRepo.GetByID(ctx, earnedCount.BadgeID) @@ -235,7 +277,7 @@ func (b *BadgeAwardService) GetUserBadgeAwardList( continue } resp[i] = &schema.GetUserBadgeAwardListResp{ - ID: badge.ID, + ID: uid.EnShortID(badge.ID), Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), Icon: badge.Icon, EarnedCount: earnedCount.EarnedCount, @@ -245,3 +287,34 @@ func (b *BadgeAwardService) GetUserBadgeAwardList( return } + +// validate user + +type userReq struct { + UserID string + Username string +} + +func (b *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) { + var ( + userInfo *schema.UserBasicInfo + exist bool + ) + // validate user exists or not + if len(userName) > 0 { + userInfo, exist, err = b.userCommon.GetUserBasicInfoByUserName(ctx, userName) + if err != nil { + return + } + if !exist { + err = errors.BadRequest(reason.UserNotFound) + return + } + userID = userInfo.ID + } + if len(userID) == 0 { + err = errors.BadRequest(reason.UserNotFound) + return + } + return +} From 5c04bceca74847e7b0406d1a0a96b13b19d2f346 Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 12 Aug 2024 16:01:20 +0800 Subject: [PATCH 28/38] feat(badge): fixed badge detail --- internal/service/badge/badge_award_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index 0ad299ba8..96a9743f9 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -106,7 +106,7 @@ func (b *BadgeAwardService) GetBadgeAwardList( return } - resp = make([]*schema.GetBadgeAwardWithPageResp, 0, len(badgeAwardList)) + resp = make([]*schema.GetBadgeAwardWithPageResp, len(badgeAwardList)) for i, badgeAward := range badgeAwardList { var ( From 98d93b1f98b4c4d44492f1a2f4fd0b350fc6d2fb Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Mon, 12 Aug 2024 16:34:16 +0800 Subject: [PATCH 29/38] feat(badge): add badge checking rule --- internal/entity/badge_entity.go | 43 +++++++++------ internal/migrations/init.go | 2 +- internal/migrations/v22.go | 2 +- internal/repo/badge/badge_event_rule.go | 55 +++++++++++++++++-- internal/schema/event_schema.go | 7 +++ internal/service/badge/badge_event_handler.go | 2 - 6 files changed, 87 insertions(+), 24 deletions(-) diff --git a/internal/entity/badge_entity.go b/internal/entity/badge_entity.go index d2c426fa8..96babcb9b 100644 --- a/internal/entity/badge_entity.go +++ b/internal/entity/badge_entity.go @@ -19,7 +19,10 @@ package entity -import "time" +import ( + "github.com/tidwall/gjson" + "time" +) type BadgeLevel int @@ -38,23 +41,31 @@ const ( // Badge badge type Badge struct { - ID string `json:"id" xorm:"id"` - CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` - UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` - Name string `json:"name" xorm:"not null default '' VARCHAR(256) name"` - Icon string `json:"icon" xorm:"not null default '' VARCHAR(1024) icon"` - AwardCount int `json:"award_count" xorm:"not null default 0 INT(11) award_count"` - Description string `json:"description" xorm:"not null default '' MEDIUMTEXT description"` - Status int8 `json:"status" xorm:"not null default 1 INT(11) status"` - BadgeGroupID int64 `json:"badge_group_id" xorm:"not null default 0 BIGINT(20) badge_group_id"` - Level BadgeLevel `json:"level" xorm:"not null default 1 TINYINT(4) level"` - Single int8 `json:"single" xorm:"not null default 1 TINYINT(4) single"` - Collect string `json:"collect" xorm:"not null default '' VARCHAR(64) collect"` - Handler string `json:"handler" xorm:"not null default '' VARCHAR(64) handler"` - Param string `json:"param" xorm:"not null default '' VARCHAR(128) param"` + ID string `xorm:"not null pk autoincr BIGINT(20) id"` + CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` + Name string `xorm:"not null default '' VARCHAR(256) name"` + Icon string `xorm:"not null default '' VARCHAR(1024) icon"` + AwardCount int `xorm:"not null default 0 INT(11) award_count"` + Description string `xorm:"not null default '' MEDIUMTEXT description"` + Status int8 `xorm:"not null default 1 INT(11) status"` + BadgeGroupID int64 `xorm:"not null default 0 BIGINT(20) badge_group_id"` + Level BadgeLevel `xorm:"not null default 1 TINYINT(4) level"` + Single int8 `xorm:"not null default 1 TINYINT(4) single"` + Collect string `xorm:"not null default '' VARCHAR(64) collect"` + Handler string `xorm:"not null default '' VARCHAR(64) handler"` + Param string `xorm:"not null default '' TEXT param"` } // TableName badge table name -func (Badge) TableName() string { +func (b *Badge) TableName() string { return "badge" } + +func (b *Badge) GetIntParam(key string) int64 { + return gjson.Get(b.Param, key).Int() +} + +func (b *Badge) GetStringParam(key string) string { + return gjson.Get(b.Param, key).String() +} diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 40e9a91a4..0b4d1ce3c 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -136,7 +136,7 @@ func (m *Mentor) initBadge() { } for _, badge := range defaultBadgeTable { - badge.ID, m.err = uniqueIDRepo.GenUniqueIDStr(m.ctx, entity.Badge{}.TableName()) + badge.ID, m.err = uniqueIDRepo.GenUniqueIDStr(m.ctx, new(entity.Badge).TableName()) if m.err != nil { return } diff --git a/internal/migrations/v22.go b/internal/migrations/v22.go index 19ec1ce0f..f5793d4a1 100644 --- a/internal/migrations/v22.go +++ b/internal/migrations/v22.go @@ -273,7 +273,7 @@ func addBadges(ctx context.Context, x *xorm.Engine) (err error) { return } for _, badge := range defaultBadgeTable { - badge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, entity.Badge{}.TableName()) + badge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, new(entity.Badge).TableName()) if err != nil { return } diff --git a/internal/repo/badge/badge_event_rule.go b/internal/repo/badge/badge_event_rule.go index d6c0d8263..41d566864 100644 --- a/internal/repo/badge/badge_event_rule.go +++ b/internal/repo/badge/badge_event_rule.go @@ -29,6 +29,7 @@ import ( "github.com/apache/incubator-answer/internal/service/badge" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" + "strconv" ) // eventRuleRepo event rule repo @@ -170,6 +171,26 @@ func (br *eventRuleRepo) ReachAnswerAcceptedAmount(ctx context.Context, if b == nil { return nil, nil } + if len(event.AnswerUserID) == 0 { + return nil, nil + } + + // count user's accepted answer amount + amount, err := br.data.DB.Context(ctx).Count(&entity.Answer{ + UserID: event.AnswerUserID, + Accepted: schema.AnswerAcceptedEnable, + Status: entity.AnswerStatusAvailable, + }) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + // get badge requirement + requirement := b.GetIntParam("amount") + if requirement == 0 || amount < requirement { + return nil, nil + } + return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil } @@ -180,7 +201,20 @@ func (br *eventRuleRepo) ReachAnswerVote(ctx context.Context, if b == nil { return nil, nil } - return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil + + // get vote amount + amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount")) + if amount == 0 { + return nil, nil + } + + // get badge requirement + requirement := b.GetIntParam("amount") + if requirement == 0 || int64(amount) < requirement { + return nil, nil + } + + return append(awards, br.createBadgeAward(event.AnswerUserID, b.ID, event.AnswerID)), nil } // ReachQuestionVote reach question vote @@ -190,7 +224,20 @@ func (br *eventRuleRepo) ReachQuestionVote(ctx context.Context, if b == nil { return nil, nil } - return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil + + // get vote amount + amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount")) + if amount == 0 { + return nil, nil + } + + // get badge requirement + requirement := b.GetIntParam("amount") + if requirement == 0 || int64(amount) < requirement { + return nil, nil + } + + return append(awards, br.createBadgeAward(event.QuestionUserID, b.ID, event.QuestionID)), nil } func (br *eventRuleRepo) getBadgeByHandler(ctx context.Context, handler string) (b *entity.Badge) { @@ -207,10 +254,10 @@ func (br *eventRuleRepo) getBadgeByHandler(ctx context.Context, handler string) return b } -func (br *eventRuleRepo) createBadgeAward(userID, badgeID, objectID string) (awards *entity.BadgeAward) { +func (br *eventRuleRepo) createBadgeAward(userID, badgeID, awardKey string) (awards *entity.BadgeAward) { return &entity.BadgeAward{ UserID: userID, BadgeID: badgeID, - AwardKey: objectID, + AwardKey: awardKey, } } diff --git a/internal/schema/event_schema.go b/internal/schema/event_schema.go index 17be96272..fd8e06ddd 100644 --- a/internal/schema/event_schema.go +++ b/internal/schema/event_schema.go @@ -69,6 +69,13 @@ func (e *EventMsg) AddExtra(key, value string) *EventMsg { return e } +func (e *EventMsg) GetExtra(key string) string { + if v, ok := e.ExtraInfo[key]; ok { + return v + } + return "" +} + func (e *EventMsg) GetObjectID() string { if len(e.CommentID) > 0 { return e.CommentID diff --git a/internal/service/badge/badge_event_handler.go b/internal/service/badge/badge_event_handler.go index d6758baf4..09331197f 100644 --- a/internal/service/badge/badge_event_handler.go +++ b/internal/service/badge/badge_event_handler.go @@ -60,8 +60,6 @@ func NewBadgeEventService( } func (ns *BadgeEventService) Handler(ctx context.Context, msg *schema.EventMsg) error { - log.Debugf("received badge event %+v", msg) - awards := ns.eventRuleRepo.HandleEventWithRule(ctx, msg) if len(awards) == 0 { return nil From 290f63f2a14a542803e7190ccf0153324c1f1a77 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Tue, 13 Aug 2024 10:40:55 +0800 Subject: [PATCH 30/38] feat(badge): add comment for event schema --- internal/schema/event_schema.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/schema/event_schema.go b/internal/schema/event_schema.go index fd8e06ddd..a507f2699 100644 --- a/internal/schema/event_schema.go +++ b/internal/schema/event_schema.go @@ -38,6 +38,7 @@ type EventMsg struct { ExtraInfo map[string]string } +// NewEvent create a new event func NewEvent(e constant.EventType, userID string) *EventMsg { return &EventMsg{ UserID: userID, @@ -46,29 +47,34 @@ func NewEvent(e constant.EventType, userID string) *EventMsg { } } +// QID get question id func (e *EventMsg) QID(questionID, userID string) *EventMsg { e.QuestionID = questionID e.QuestionUserID = userID return e } +// AID get answer id func (e *EventMsg) AID(answerID, userID string) *EventMsg { e.AnswerID = answerID e.AnswerUserID = userID return e } +// CID get comment id func (e *EventMsg) CID(comment, userID string) *EventMsg { e.CommentID = comment e.CommentUserID = userID return e } +// AddExtra add extra info func (e *EventMsg) AddExtra(key, value string) *EventMsg { e.ExtraInfo[key] = value return e } +// GetExtra get extra info func (e *EventMsg) GetExtra(key string) string { if v, ok := e.ExtraInfo[key]; ok { return v @@ -76,6 +82,7 @@ func (e *EventMsg) GetExtra(key string) string { return "" } +// GetObjectID get object id func (e *EventMsg) GetObjectID() string { if len(e.CommentID) > 0 { return e.CommentID From bb40366f001fa183387a4d2a6d2676d866cb218e Mon Sep 17 00:00:00 2001 From: kumfo Date: Tue, 13 Aug 2024 16:12:34 +0800 Subject: [PATCH 31/38] feat(badge): badge manage and user center's badge list --- cmd/wire_gen.go | 3 +- docs/docs.go | 188 ++++++++++++++++++ docs/swagger.json | 188 ++++++++++++++++++ docs/swagger.yaml | 118 +++++++++++ i18n/en_US.yaml | 12 ++ internal/base/reason/reason.go | 1 + internal/controller_admin/badge_controller.go | 85 ++++++++ internal/controller_admin/controller.go | 1 + internal/entity/badge_award_entity.go | 2 +- internal/entity/badge_entity.go | 2 +- internal/migrations/v22.go | 6 +- internal/repo/badge/badge_repo.go | 62 +++++- internal/router/answer_api_router.go | 7 + internal/schema/badge_schema.go | 54 +++++ internal/service/badge/badge_service.go | 93 ++++++++- 15 files changed, 806 insertions(+), 16 deletions(-) create mode 100644 internal/controller_admin/badge_controller.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 309af9356..d4034b6b6 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -267,7 +267,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService) badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService) badgeController := controller.NewBadgeController(badgeService, badgeAwardService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController) + controller_adminBadgeController := controller_admin.NewBadgeController(badgeService) + answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController, controller_adminBadgeController) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService) authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService) diff --git a/docs/docs.go b/docs/docs.go index ecc621692..eaa7133a4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -154,6 +154,113 @@ const docTemplate = `{ } } }, + "/answer/admin/api/badge/status": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update badge status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AdminBadge" + ], + "summary": "update badge status", + "parameters": [ + { + "description": "UpdateBadgeStatusReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateBadgeStatusReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/badges": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "list all badges by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AdminBadge" + ], + "summary": "list all badges by page", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "enum": [ + "", + "active", + "inactive" + ], + "type": "string", + "description": "badge status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetBadgeListPagedResp" + } + } + } + } + ] + } + } + } + } + }, "/answer/admin/api/dashboard": { "get": { "security": [ @@ -7652,6 +7759,17 @@ const docTemplate = `{ } } }, + "schema.BadgeStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ], + "x-enum-varnames": [ + "BadgeStatusActive", + "BadgeStatusInactive" + ] + }, "schema.CloseQuestionReq": { "type": "object", "required": [ @@ -7944,6 +8062,55 @@ const docTemplate = `{ } } }, + "schema.GetBadgeListPagedResp": { + "type": "object", + "properties": { + "award_count": { + "description": "badge award count", + "type": "integer" + }, + "description": { + "description": "badge description", + "type": "string" + }, + "earned": { + "description": "badge earned count", + "type": "boolean" + }, + "group_name": { + "description": "badge group name", + "type": "string" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + }, + "status": { + "description": "badge status", + "allOf": [ + { + "$ref": "#/definitions/schema.BadgeStatus" + } + ] + } + } + }, "schema.GetBadgeListResp": { "type": "object", "properties": { @@ -10446,6 +10613,27 @@ const docTemplate = `{ } } }, + "schema.UpdateBadgeStatusReq": { + "type": "object", + "required": [ + "id", + "status" + ], + "properties": { + "id": { + "description": "badge id", + "type": "string" + }, + "status": { + "description": "badge status", + "allOf": [ + { + "$ref": "#/definitions/schema.BadgeStatus" + } + ] + } + } + }, "schema.UpdateCommentReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 1412f179d..116a3a6a1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -124,6 +124,113 @@ } } }, + "/answer/admin/api/badge/status": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "update badge status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AdminBadge" + ], + "summary": "update badge status", + "parameters": [ + { + "description": "UpdateBadgeStatusReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateBadgeStatusReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, + "/answer/admin/api/badges": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "list all badges by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "AdminBadge" + ], + "summary": "list all badges by page", + "parameters": [ + { + "type": "integer", + "description": "page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size", + "name": "page_size", + "in": "query" + }, + { + "enum": [ + "", + "active", + "inactive" + ], + "type": "string", + "description": "badge status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.GetBadgeListPagedResp" + } + } + } + } + ] + } + } + } + } + }, "/answer/admin/api/dashboard": { "get": { "security": [ @@ -7622,6 +7729,17 @@ } } }, + "schema.BadgeStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ], + "x-enum-varnames": [ + "BadgeStatusActive", + "BadgeStatusInactive" + ] + }, "schema.CloseQuestionReq": { "type": "object", "required": [ @@ -7914,6 +8032,55 @@ } } }, + "schema.GetBadgeListPagedResp": { + "type": "object", + "properties": { + "award_count": { + "description": "badge award count", + "type": "integer" + }, + "description": { + "description": "badge description", + "type": "string" + }, + "earned": { + "description": "badge earned count", + "type": "boolean" + }, + "group_name": { + "description": "badge group name", + "type": "string" + }, + "icon": { + "description": "badge icon", + "type": "string" + }, + "id": { + "description": "badge id", + "type": "string" + }, + "level": { + "description": "badge level", + "allOf": [ + { + "$ref": "#/definitions/entity.BadgeLevel" + } + ] + }, + "name": { + "description": "badge name", + "type": "string" + }, + "status": { + "description": "badge status", + "allOf": [ + { + "$ref": "#/definitions/schema.BadgeStatus" + } + ] + } + } + }, "schema.GetBadgeListResp": { "type": "object", "properties": { @@ -10416,6 +10583,27 @@ } } }, + "schema.UpdateBadgeStatusReq": { + "type": "object", + "required": [ + "id", + "status" + ], + "properties": { + "id": { + "description": "badge id", + "type": "string" + }, + "status": { + "description": "badge status", + "allOf": [ + { + "$ref": "#/definitions/schema.BadgeStatus" + } + ] + } + } + }, "schema.UpdateCommentReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e2fc66828..116b59325 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -374,6 +374,14 @@ definitions: description: badge name type: string type: object + schema.BadgeStatus: + enum: + - active + - inactive + type: string + x-enum-varnames: + - BadgeStatusActive + - BadgeStatusInactive schema.CloseQuestionReq: properties: close_msg: @@ -573,6 +581,38 @@ definitions: description: badge name type: string type: object + schema.GetBadgeListPagedResp: + properties: + award_count: + description: badge award count + type: integer + description: + description: badge description + type: string + earned: + description: badge earned count + type: boolean + group_name: + description: badge group name + type: string + icon: + description: badge icon + type: string + id: + description: badge id + type: string + level: + allOf: + - $ref: '#/definitions/entity.BadgeLevel' + description: badge level + name: + description: badge name + type: string + status: + allOf: + - $ref: '#/definitions/schema.BadgeStatus' + description: badge status + type: object schema.GetBadgeListResp: properties: badges: @@ -2307,6 +2347,19 @@ definitions: url_title: type: string type: object + schema.UpdateBadgeStatusReq: + properties: + id: + description: badge id + type: string + status: + allOf: + - $ref: '#/definitions/schema.BadgeStatus' + description: badge status + required: + - id + - status + type: object schema.UpdateCommentReq: properties: captcha_code: @@ -2924,6 +2977,71 @@ paths: summary: update answer status tags: - admin + /answer/admin/api/badge/status: + put: + consumes: + - application/json + description: update badge status + parameters: + - description: UpdateBadgeStatusReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateBadgeStatusReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: update badge status + tags: + - AdminBadge + /answer/admin/api/badges: + get: + consumes: + - application/json + description: list all badges by page + parameters: + - description: page + in: query + name: page + type: integer + - description: page size + in: query + name: page_size + type: integer + - description: badge status + enum: + - "" + - active + - inactive + in: query + name: status + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + items: + $ref: '#/definitions/schema.GetBadgeListPagedResp' + type: array + type: object + security: + - ApiKeyAuth: [] + summary: list all badges by page + tags: + - AdminBadge /answer/admin/api/dashboard: get: consumes: diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 0d7f2ecf3..071644f4a 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -146,6 +146,8 @@ backend: common: invalid_url: other: Invalid URL. + status_invalid: + other: Invalid status. password: space_invalid: other: Password cannot contain spaces. @@ -776,6 +778,16 @@ backend: other: Famous Link desc: other: Posted an external link with 100 clicks. + default_badge_groups: + getting_started: + name: + other: Getting Started + community: + name: + other: Community + posting: + name: + other: Posting # The following fields are used for interface presentation(Front-end) ui: diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index a61686900..24d7ab5f9 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -107,6 +107,7 @@ const ( InvalidURLError = "error.common.invalid_url" MetaObjectNotFound = "error.meta.object_not_found" BadgeObjectNotFound = "error.badge.object_not_found" + StatusInvalid = "error.common.status_invalid" ) // user external login reasons diff --git a/internal/controller_admin/badge_controller.go b/internal/controller_admin/badge_controller.go new file mode 100644 index 000000000..8842592f5 --- /dev/null +++ b/internal/controller_admin/badge_controller.go @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package controller_admin + +import ( + "github.com/apache/incubator-answer/internal/base/handler" + "github.com/apache/incubator-answer/internal/base/pager" + "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/internal/service/badge" + "github.com/gin-gonic/gin" +) + +type BadgeController struct { + badgeService *badge.BadgeService +} + +func NewBadgeController(badgeService *badge.BadgeService) *BadgeController { + return &BadgeController{ + badgeService: badgeService, + } +} + +// GetBadgeList list all badges by page +// @Summary list all badges by page +// @Description list all badges by page +// @Tags AdminBadge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param page query int false "page" +// @Param page_size query int false "page size" +// @Param status query string false "badge status" Enums(, active, inactive) +// @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListPagedResp} +// @Router /answer/admin/api/badges [get] +func (b *BadgeController) GetBadgeList(ctx *gin.Context) { + req := &schema.GetBadgeListPagedReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, total, err := b.badgeService.ListPaged(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, resp)) +} + +// UpdateBadgeStatus update badge status +// @Summary update badge status +// @Description update badge status +// @Tags AdminBadge +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdateBadgeStatusReq true "UpdateBadgeStatusReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/badge/status [put] +func (b *BadgeController) UpdateBadgeStatus(ctx *gin.Context) { + req := &schema.UpdateBadgeStatusReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + err := b.badgeService.UpdateStatus(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/controller_admin/controller.go b/internal/controller_admin/controller.go index de87d105e..ebf32cbfc 100644 --- a/internal/controller_admin/controller.go +++ b/internal/controller_admin/controller.go @@ -28,4 +28,5 @@ var ProviderSetController = wire.NewSet( NewSiteInfoController, NewRoleController, NewPluginController, + NewBadgeController, ) diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index 1d4216238..0eb302271 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -28,7 +28,7 @@ const ( // BadgeAward badge_award type BadgeAward struct { - ID string `json:"id" xorm:"id"` + ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` UserID string `json:"user_id" xorm:"not null index BIGINT(20) user_id"` diff --git a/internal/entity/badge_entity.go b/internal/entity/badge_entity.go index 96babcb9b..5177c93c6 100644 --- a/internal/entity/badge_entity.go +++ b/internal/entity/badge_entity.go @@ -41,7 +41,7 @@ const ( // Badge badge type Badge struct { - ID string `xorm:"not null pk autoincr BIGINT(20) id"` + ID string `xorm:"not null pk BIGINT(20) id"` CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` Name string `xorm:"not null default '' VARCHAR(256) name"` diff --git a/internal/migrations/v22.go b/internal/migrations/v22.go index f5793d4a1..d3d81d844 100644 --- a/internal/migrations/v22.go +++ b/internal/migrations/v22.go @@ -30,9 +30,9 @@ import ( var ( defaultBadgeGroupTable = []*entity.BadgeGroup{ - {ID: "1", Name: "Getting Started"}, - {ID: "2", Name: "Community"}, - {ID: "3", Name: "Posting"}, + {ID: "1", Name: "badge.default_badge_groups.getting_started.name"}, + {ID: "2", Name: "badge.default_badge_groups.community.name"}, + {ID: "3", Name: "badge.default_badge_groups.posting.name"}, } defaultBadgeTable = []*entity.Badge{ diff --git a/internal/repo/badge/badge_repo.go b/internal/repo/badge/badge_repo.go index d4d2f9922..ebdb04257 100644 --- a/internal/repo/badge/badge_repo.go +++ b/internal/repo/badge/badge_repo.go @@ -22,11 +22,13 @@ package badge import ( "context" "github.com/apache/incubator-answer/internal/base/data" + "github.com/apache/incubator-answer/internal/base/pager" "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/service/badge" "github.com/apache/incubator-answer/internal/service/unique" "github.com/segmentfault/pacman/errors" + "xorm.io/xorm" ) type badgeRepo struct { @@ -90,10 +92,29 @@ func (r *badgeRepo) ListByLevelAndGroup(ctx context.Context, level entity.BadgeL return } +// ListPaged returns a list of activated badges +func (r *badgeRepo) ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { + badges = make([]*entity.Badge, 0) + session := r.data.DB.Context(ctx).Where("status <> ?", entity.BadgeStatusDeleted) + total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // ListActivated returns a list of activated badges -func (r *badgeRepo) ListActivated(ctx context.Context) (badges []*entity.Badge, err error) { +func (r *badgeRepo) ListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { badges = make([]*entity.Badge, 0) - err = r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusActive).Find(&badges) + total = 0 + + session := r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusActive) + if page == 0 || pageSize == 0 { + err = session.Find(&badges) + } else { + total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) + } + if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -101,9 +122,17 @@ func (r *badgeRepo) ListActivated(ctx context.Context) (badges []*entity.Badge, } // ListInactivated returns a list of inactivated badges -func (r *badgeRepo) ListInactivated(ctx context.Context) (badges []*entity.Badge, err error) { +func (r *badgeRepo) ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { badges = make([]*entity.Badge, 0) - err = r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusInactive).Find(&badges) + total = 0 + + session := r.data.DB.Context(ctx).Where("status = ?", entity.BadgeStatusInactive) + if page == 0 || pageSize == 0 { + err = session.Find(&badges) + } else { + total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) + } + if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -115,3 +144,28 @@ func (r *badgeRepo) UpdateAwardCount(ctx context.Context, id string, count int64 _, err = r.data.DB.Context(ctx).Where("id = ?", id).Incr("award_count", count).Update(&entity.Badge{}) return } + +// UpdateStatus updates the award count of a badge +func (r *badgeRepo) UpdateStatus(ctx context.Context, id string, status int8) (err error) { + _, err = r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + _, err = session.ID(id).Update(&entity.Badge{ + Status: status, + }) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(session.Rollback()).WithStack() + return + } + if status >= entity.BadgeStatusDeleted { + _, err = session.Where("badge_id = ?", id).Cols("is_badge_deleted").Update(&entity.BadgeAward{ + IsBadgeDeleted: entity.IsBadgeDeleted, + }) + } else { + _, err = session.Where("badge_id = ?", id).Cols("is_badge_deleted").Update(&entity.BadgeAward{ + IsBadgeDeleted: entity.IsBadgeNotDeleted, + }) + } + return + }) + + return +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index f343d7d11..b4ec3bdcc 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -56,6 +56,7 @@ type AnswerAPIRouter struct { reviewController *controller.ReviewController metaController *controller.MetaController badgeController *controller.BadgeController + adminBadgeController *controller_admin.BadgeController } func NewAnswerAPIRouter( @@ -88,6 +89,7 @@ func NewAnswerAPIRouter( reviewController *controller.ReviewController, metaController *controller.MetaController, badgeController *controller.BadgeController, + adminBadgeController *controller_admin.BadgeController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ langController: langController, @@ -119,6 +121,7 @@ func NewAnswerAPIRouter( reviewController: reviewController, metaController: metaController, badgeController: badgeController, + adminBadgeController: adminBadgeController, } } @@ -369,4 +372,8 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) { r.PUT("/plugin/status", a.pluginController.UpdatePluginStatus) r.GET("/plugin/config", a.pluginController.GetPluginConfig) r.PUT("/plugin/config", a.pluginController.UpdatePluginConfig) + + // badge + r.GET("/badges", a.adminBadgeController.GetBadgeList) + r.PUT("/badge/status", a.adminBadgeController.UpdateBadgeStatus) } diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go index cae2aa66c..39a2e2ae2 100644 --- a/internal/schema/badge_schema.go +++ b/internal/schema/badge_schema.go @@ -21,6 +21,23 @@ package schema import "github.com/apache/incubator-answer/internal/entity" +const ( + BadgeStatusActive BadgeStatus = "active" + BadgeStatusInactive BadgeStatus = "inactive" +) + +type BadgeStatus string + +var BadgeStatusMap = map[int8]BadgeStatus{ + entity.BadgeStatusActive: BadgeStatusActive, + entity.BadgeStatusInactive: BadgeStatusInactive, +} + +var BadgeStatusEMap = map[BadgeStatus]int8{ + BadgeStatusActive: entity.BadgeStatusActive, + BadgeStatusInactive: entity.BadgeStatusInactive, +} + // BadgeListInfo get badge list response type BadgeListInfo struct { // badge id @@ -44,6 +61,43 @@ type GetBadgeListResp struct { GroupName string `json:"group_name" ` } +type UpdateBadgeStatusReq struct { + // badge id + ID string `validate:"required" json:"id"` + // badge status + Status BadgeStatus `validate:"required" json:"status"` +} + +type GetBadgeListPagedReq struct { + // page + Page int `validate:"omitempty,min=1" form:"page"` + // page size + PageSize int `validate:"omitempty,min=1" form:"page_size"` + // badge status + Status BadgeStatus `validate:"omitempty" form:"status"` +} + +type GetBadgeListPagedResp struct { + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge description + Description string `json:"description" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + AwardCount int `json:"award_count" ` + // badge earned count + Earned bool `json:"earned" ` + // badge level + Level entity.BadgeLevel `json:"level" ` + // badge group name + GroupName string `json:"group_name" ` + // badge status + Status BadgeStatus `json:"status"` +} + type GetBadgeInfoResp struct { // badge id ID string `json:"id" ` diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index 32a54a8d9..7daab7965 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -39,10 +39,12 @@ type BadgeRepo interface { ListByLevel(ctx context.Context, level entity.BadgeLevel) ([]*entity.Badge, error) ListByGroup(ctx context.Context, groupID int64) ([]*entity.Badge, error) ListByLevelAndGroup(ctx context.Context, level entity.BadgeLevel, groupID int64) ([]*entity.Badge, error) - ListActivated(ctx context.Context) ([]*entity.Badge, error) - ListInactivated(ctx context.Context) ([]*entity.Badge, error) + ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) + ListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) + ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) - UpdateAwardCount(ctx context.Context, id string, count int64) error + UpdateAwardCount(ctx context.Context, id string, count int64) (err error) + UpdateStatus(ctx context.Context, id string, status int8) (err error) } type BadgeService struct { @@ -82,7 +84,7 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* if err != nil { return } - badges, err = b.badgeRepo.ListActivated(ctx) + badges, _, err = b.badgeRepo.ListActivated(ctx, 0, 0) if err != nil { return } @@ -95,7 +97,7 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* } for _, group := range groups { - groupMap[converter.StringToInt64(group.ID)] = group.Name + groupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name) } for _, badge := range badges { @@ -122,7 +124,7 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* for _, group := range groups { resp = append(resp, &schema.GetBadgeListResp{ - GroupName: group.Name, + GroupName: translator.Tr(handler.GetLangByCtx(ctx), group.Name), Badges: badgesMap[converter.StringToInt64(group.ID)], }) } @@ -130,6 +132,53 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* return } +// ListPaged list all badges by page +func (b *BadgeService) ListPaged(ctx context.Context, req *schema.GetBadgeListPagedReq) (resp []*schema.GetBadgeListPagedResp, total int64, err error) { + var ( + groups []*entity.BadgeGroup + badges []*entity.Badge + groupMap = make(map[int64]string, 0) + ) + + switch req.Status { + case schema.BadgeStatusActive: + badges, total, err = b.badgeRepo.ListActivated(ctx, req.Page, req.PageSize) + case schema.BadgeStatusInactive: + badges, total, err = b.badgeRepo.ListInactivated(ctx, req.Page, req.PageSize) + default: + badges, total, err = b.badgeRepo.ListPaged(ctx, req.Page, req.PageSize) + } + + if err != nil { + return + } + + // find all group and build group map + groups, err = b.badgeGroupRepo.ListGroups(ctx) + if err != nil { + return + } + for _, group := range groups { + groupMap[converter.StringToInt64(group.ID)] = translator.Tr(handler.GetLangByCtx(ctx), group.Name) + } + + resp = make([]*schema.GetBadgeListPagedResp, len(badges)) + + for i, badge := range badges { + resp[i] = &schema.GetBadgeListPagedResp{ + ID: uid.EnShortID(badge.ID), + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Description: translator.Tr(handler.GetLangByCtx(ctx), badge.Description), + Icon: badge.Icon, + AwardCount: badge.AwardCount, + Level: badge.Level, + GroupName: groupMap[badge.BadgeGroupID], + Status: schema.BadgeStatusMap[badge.Status], + } + } + return +} + // GetBadgeInfo get badge info func (b *BadgeService) GetBadgeInfo(ctx *gin.Context, id string, userID string) (info *schema.GetBadgeInfoResp, err error) { var ( @@ -164,3 +213,35 @@ func (b *BadgeService) GetBadgeInfo(ctx *gin.Context, id string, userID string) } return } + +// UpdateStatus update badge status +func (b *BadgeService) UpdateStatus(ctx *gin.Context, req *schema.UpdateBadgeStatusReq) (err error) { + var ( + badge *entity.Badge + exists bool + ) + req.ID = uid.DeShortID(req.ID) + + badge, exists, err = b.badgeRepo.GetByID(ctx, req.ID) + if err != nil { + return + } + if !exists { + err = errors.BadRequest(reason.BadgeObjectNotFound) + return + } + + status, ok := schema.BadgeStatusEMap[req.Status] + // check duplicate action + if badge.Status == status { + return + } + + if !ok { + err = errors.BadRequest(reason.StatusInvalid) + return + } + + err = b.badgeRepo.UpdateStatus(ctx, req.ID, status) + return +} From cf327f59dcd621a914893b01100075549cea0a1b Mon Sep 17 00:00:00 2001 From: kumfo Date: Tue, 13 Aug 2024 17:50:10 +0800 Subject: [PATCH 32/38] feat(badge): badge manage add badge search --- docs/docs.go | 25 ++------ docs/swagger.json | 6 ++ docs/swagger.yaml | 4 ++ internal/controller_admin/badge_controller.go | 1 + internal/repo/badge/badge_repo.go | 9 ++- internal/schema/badge_schema.go | 2 + internal/service/badge/badge_service.go | 61 ++++++++++++++++--- 7 files changed, 78 insertions(+), 30 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index eaa7133a4..7fef400fb 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,22 +1,3 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - // Package docs Code generated by swaggo/swag. DO NOT EDIT package docs @@ -234,6 +215,12 @@ const docTemplate = `{ "description": "badge status", "name": "status", "in": "query" + }, + { + "type": "string", + "description": "search param", + "name": "q", + "in": "query" } ], "responses": { diff --git a/docs/swagger.json b/docs/swagger.json index 116a3a6a1..234d2f135 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -204,6 +204,12 @@ "description": "badge status", "name": "status", "in": "query" + }, + { + "type": "string", + "description": "search param", + "name": "q", + "in": "query" } ], "responses": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 116b59325..1a5979e6a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3023,6 +3023,10 @@ paths: in: query name: status type: string + - description: search param + in: query + name: q + type: string produces: - application/json responses: diff --git a/internal/controller_admin/badge_controller.go b/internal/controller_admin/badge_controller.go index 8842592f5..4a44f1764 100644 --- a/internal/controller_admin/badge_controller.go +++ b/internal/controller_admin/badge_controller.go @@ -47,6 +47,7 @@ func NewBadgeController(badgeService *badge.BadgeService) *BadgeController { // @Param page query int false "page" // @Param page_size query int false "page size" // @Param status query string false "badge status" Enums(, active, inactive) +// @Param q query string false "search param" // @Success 200 {object} handler.RespBody{data=[]schema.GetBadgeListPagedResp} // @Router /answer/admin/api/badges [get] func (b *BadgeController) GetBadgeList(ctx *gin.Context) { diff --git a/internal/repo/badge/badge_repo.go b/internal/repo/badge/badge_repo.go index ebdb04257..8537e3898 100644 --- a/internal/repo/badge/badge_repo.go +++ b/internal/repo/badge/badge_repo.go @@ -95,8 +95,15 @@ func (r *badgeRepo) ListByLevelAndGroup(ctx context.Context, level entity.BadgeL // ListPaged returns a list of activated badges func (r *badgeRepo) ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { badges = make([]*entity.Badge, 0) + total = 0 + session := r.data.DB.Context(ctx).Where("status <> ?", entity.BadgeStatusDeleted) - total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) + if page == 0 || pageSize == 0 { + err = session.Find(&badges) + } else { + total, err = pager.Help(page, pageSize, &badges, &entity.Badge{}, session) + } + if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go index 39a2e2ae2..088193ad8 100644 --- a/internal/schema/badge_schema.go +++ b/internal/schema/badge_schema.go @@ -75,6 +75,8 @@ type GetBadgeListPagedReq struct { PageSize int `validate:"omitempty,min=1" form:"page_size"` // badge status Status BadgeStatus `validate:"omitempty" form:"status"` + // query condition + Query string `validate:"omitempty" form:"q"` } type GetBadgeListPagedResp struct { diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index 7daab7965..c9f3797a3 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -30,6 +30,7 @@ import ( "github.com/apache/incubator-answer/pkg/uid" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" + "strings" ) type BadgeRepo interface { @@ -137,20 +138,44 @@ func (b *BadgeService) ListPaged(ctx context.Context, req *schema.GetBadgeListPa var ( groups []*entity.BadgeGroup badges []*entity.Badge + badge *entity.Badge + exists bool groupMap = make(map[int64]string, 0) ) - switch req.Status { - case schema.BadgeStatusActive: - badges, total, err = b.badgeRepo.ListActivated(ctx, req.Page, req.PageSize) - case schema.BadgeStatusInactive: - badges, total, err = b.badgeRepo.ListInactivated(ctx, req.Page, req.PageSize) - default: - badges, total, err = b.badgeRepo.ListPaged(ctx, req.Page, req.PageSize) - } + total = 0 - if err != nil { - return + if len(req.Query) > 0 { + isID := strings.Index(req.Query, "badge:") + if isID != 0 { + badges, err = b.searchByName(ctx, req.Query) + if err != nil { + return + } + } else { + req.Query = strings.TrimSpace(strings.TrimLeft(req.Query, "badge:")) + id := uid.DeShortID(req.Query) + if len(id) == 0 { + return + } + badge, exists, err = b.badgeRepo.GetByID(ctx, id) + if err != nil || !exists { + return + } + badges = append(badges, badge) + } + } else { + switch req.Status { + case schema.BadgeStatusActive: + badges, total, err = b.badgeRepo.ListActivated(ctx, req.Page, req.PageSize) + case schema.BadgeStatusInactive: + badges, total, err = b.badgeRepo.ListInactivated(ctx, req.Page, req.PageSize) + default: + badges, total, err = b.badgeRepo.ListPaged(ctx, req.Page, req.PageSize) + } + if err != nil { + return + } } // find all group and build group map @@ -179,6 +204,22 @@ func (b *BadgeService) ListPaged(ctx context.Context, req *schema.GetBadgeListPa return } +// searchByName +func (b *BadgeService) searchByName(ctx context.Context, name string) (result []*entity.Badge, err error) { + var badges []*entity.Badge + name = strings.ToLower(name) + result = make([]*entity.Badge, 0) + + badges, _, err = b.badgeRepo.ListPaged(ctx, 0, 0) + for _, badge := range badges { + tn := strings.ToLower(translator.Tr(handler.GetLangByCtx(ctx), badge.Name)) + if strings.Contains(tn, name) { + result = append(result, badge) + } + } + return +} + // GetBadgeInfo get badge info func (b *BadgeService) GetBadgeInfo(ctx *gin.Context, id string, userID string) (info *schema.GetBadgeInfoResp, err error) { var ( From b0c76757cc4c85d6cf35180d3fc51b200058f5ee Mon Sep 17 00:00:00 2001 From: kumfo Date: Wed, 14 Aug 2024 10:05:10 +0800 Subject: [PATCH 33/38] feat(badge): badge search with page --- internal/service/badge/badge_service.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index c9f3797a3..acb68cff7 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -152,6 +152,19 @@ func (b *BadgeService) ListPaged(ctx context.Context, req *schema.GetBadgeListPa if err != nil { return } + // paged result + count := len(badges) + total = int64(count) + start := (req.Page - 1) * req.PageSize + end := req.Page * req.PageSize + if start >= count { + start = count + end = count + } + if end > count { + end = count + } + badges = badges[start:end] } else { req.Query = strings.TrimSpace(strings.TrimLeft(req.Query, "badge:")) id := uid.DeShortID(req.Query) From bede0adbf783a840fdeefec8afeb61e1e71cb864 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Wed, 14 Aug 2024 10:37:37 +0800 Subject: [PATCH 34/38] feat(badge): award badge for user --- cmd/wire_gen.go | 4 +- docs/docs.go | 19 ++++ internal/entity/badge_award_entity.go | 16 ++-- internal/repo/badge/badge_event_rule.go | 2 +- internal/repo/badge_award/badge_award_repo.go | 88 +++++++++++++------ internal/service/badge/badge_award_service.go | 47 ++++------ internal/service/badge/badge_event_handler.go | 34 ++++--- 7 files changed, 129 insertions(+), 81 deletions(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index d4034b6b6..9acd76ee5 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -263,9 +263,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, badgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo) badgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo) eventRuleRepo := badge.NewEventRuleRepo(dataData) - badgeEventService := badge2.NewBadgeEventService(dataData, eventQueueService, badgeRepo, eventRuleRepo) - badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService) badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService) + badgeEventService := badge2.NewBadgeEventService(dataData, eventQueueService, badgeRepo, eventRuleRepo, badgeAwardService) + badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService) badgeController := controller.NewBadgeController(badgeService, badgeAwardService) controller_adminBadgeController := controller_admin.NewBadgeController(badgeService) answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController, badgeController, controller_adminBadgeController) diff --git a/docs/docs.go b/docs/docs.go index 7fef400fb..cfefe0ed9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,3 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + // Package docs Code generated by swaggo/swag. DO NOT EDIT package docs diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index 0eb302271..5ba475cd9 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -24,18 +24,20 @@ import "time" const ( IsBadgeNotDeleted = 0 IsBadgeDeleted = 1 + + BadgeOnceAwardKey = "0" ) // BadgeAward badge_award type BadgeAward struct { ID string `xorm:"not null pk BIGINT(20) id"` - CreatedAt time.Time `json:"created_at" xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` - UpdatedAt time.Time `json:"updated_at" xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` - UserID string `json:"user_id" xorm:"not null index BIGINT(20) user_id"` - BadgeID string `json:"badge_id" xorm:"not null index BIGINT(20) badge_id"` - AwardKey string `json:"award_key" xorm:"not null index VARCHAR(64) award_key"` - BadgeGroupID int64 `json:"badge_group_id" xorm:"not null index BIGINT(20) badge_group_id"` - IsBadgeDeleted int8 `json:"is_badge_deleted" xorm:"not null index TINYINT(1) is_badge_deleted"` + CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"` + UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"` + UserID string `xorm:"not null index BIGINT(20) user_id"` + BadgeID string `xorm:"not null index BIGINT(20) badge_id"` + AwardKey string `xorm:"not null index VARCHAR(64) award_key"` + BadgeGroupID int64 `xorm:"not null index BIGINT(20) badge_group_id"` + IsBadgeDeleted int8 `xorm:"not null TINYINT(1) is_badge_deleted"` } // TableName badge_award table name diff --git a/internal/repo/badge/badge_event_rule.go b/internal/repo/badge/badge_event_rule.go index 41d566864..f912c2bbe 100644 --- a/internal/repo/badge/badge_event_rule.go +++ b/internal/repo/badge/badge_event_rule.go @@ -99,7 +99,7 @@ func (br *eventRuleRepo) FirstUpdateUserProfile(ctx context.Context, return nil, nil } if len(bean.Bio) > 0 { - return append(awards, br.createBadgeAward(event.UserID, b.ID, "")), nil + return append(awards, br.createBadgeAward(event.UserID, b.ID, entity.BadgeOnceAwardKey)), nil } return nil, nil } diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 5a5d56cde..058fa8649 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -21,6 +21,7 @@ package badge_award import ( "context" + "fmt" "github.com/apache/incubator-answer/internal/base/data" "github.com/apache/incubator-answer/internal/base/pager" "github.com/apache/incubator-answer/internal/base/reason" @@ -28,6 +29,7 @@ import ( "github.com/apache/incubator-answer/internal/service/badge" "github.com/apache/incubator-answer/internal/service/unique" "github.com/segmentfault/pacman/errors" + "xorm.io/xorm" ) type badgeAwardRepo struct { @@ -42,38 +44,62 @@ func NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge. } } -func (r *badgeAwardRepo) Add(ctx context.Context, badgeAward *entity.BadgeAward) (err error) { +// AwardBadgeForUser award badge for user +func (r *badgeAwardRepo) AwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error) { badgeAward.ID, err = r.uniqueIDRepo.GenUniqueIDStr(ctx, entity.BadgeAward{}.TableName()) if err != nil { - return + return err } - _, err = r.data.DB.Context(ctx).Insert(badgeAward) - return -} -func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string, singleOrMulti int8) (isAward bool) { - isAward = false - if singleOrMulti == entity.BadgeSingleAward { - _, exists, err := r.GetByUserIdAndBadgeId(ctx, userID, badgeID) + + _, err = r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { + session = session.Context(ctx) + + badgeInfo := &entity.Badge{} + exist, err := session.ID(badgeAward.BadgeID).ForUpdate().Get(badgeInfo) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return nil, err } - if exists { - return true + if !exist { + return nil, fmt.Errorf("badge not exist") } - } else { - _, exists, err := r.GetByUserIdAndBadgeIdAndObjectId(ctx, userID, badgeID, awardKey) + + old := &entity.BadgeAward{} + exist, err = session.Where("user_id = ? AND badge_id = ? AND award_key = ? AND is_badge_deleted = 0", + badgeAward.UserID, badgeAward.BadgeID, badgeAward.AwardKey).Get(old) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return nil, err } - if exists { - return true + if exist { + return nil, fmt.Errorf("badge already awarded") } + + _, err = session.Insert(badgeAward) + if err != nil { + return nil, err + } + + return session.ID(badgeInfo.ID).Incr("award_count", 1).Update(&entity.Badge{}) + }) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + return nil +} - return +// CheckIsAward check this badge is awarded for this user or not +func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID, userID, awardKey string, singleOrMulti int8) ( + isAward bool, err error) { + if singleOrMulti == entity.BadgeSingleAward { + _, isAward, err = r.GetByUserIdAndBadgeId(ctx, userID, badgeID) + } else { + _, isAward, err = r.GetByUserIdAndBadgeIdAndAwardKey(ctx, userID, badgeID, awardKey) + } + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return isAward, err } + func (r *badgeAwardRepo) CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) { return } @@ -161,13 +187,25 @@ func (r *badgeAwardRepo) ListNewestByUserIdAndLevel(ctx context.Context, userID } // GetByUserIdAndBadgeId get badge award by user id and badge id -func (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward, exists bool, err error) { - exists, err = r.data.DB.Context(ctx).Where("user_id = ? AND badge_id = ? AND is_badge_deleted = 0", userID, badgeID).Get(&badgeAward) +func (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) ( + badgeAward *entity.BadgeAward, exists bool, err error) { + badgeAward = &entity.BadgeAward{} + exists, err = r.data.DB.Context(ctx). + Where("user_id = ? AND badge_id = ? AND is_badge_deleted = 0", userID, badgeID).Get(badgeAward) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } -// GetByUserIdAndBadgeIdAndObjectId get badge award by user id, badge id and object id -func (r *badgeAwardRepo) GetByUserIdAndBadgeIdAndObjectId(ctx context.Context, userID string, badgeID string, awardKey string) (badgeAward *entity.BadgeAward, exists bool, err error) { - exists, err = r.data.DB.Context(ctx).Where("user_id = ? AND badge_id = ? AND award_key = ? AND is_badge_deleted = 0", userID, badgeID, awardKey).Get(&badgeAward) +// GetByUserIdAndBadgeIdAndAwardKey get badge award by user id and badge id and award key +func (r *badgeAwardRepo) GetByUserIdAndBadgeIdAndAwardKey(ctx context.Context, userID string, badgeID string, awardKey string) ( + badgeAward *entity.BadgeAward, exists bool, err error) { + badgeAward = &entity.BadgeAward{} + exists, err = r.data.DB.Context(ctx). + Where("user_id = ? AND badge_id = ? AND award_key = ? AND is_badge_deleted = 0", userID, badgeID, awardKey).Get(badgeAward) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } return } diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index 96a9743f9..3883de616 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -33,12 +33,11 @@ import ( "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" - "time" ) type BadgeAwardRepo interface { - Add(ctx context.Context, badgeAward *entity.BadgeAward) (err error) - CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string, singleOrMulti int8) bool + CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string, singleOrMulti int8) (isAward bool, err error) + AwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error) CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) CountByUserId(ctx context.Context, userID string) (awardCount int64) @@ -62,7 +61,7 @@ type BadgeAwardRepo interface { ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward, exists bool, err error) - GetByUserIdAndBadgeIdAndObjectId(ctx context.Context, userID string, badgeID string, awardKey string) (badgeAward *entity.BadgeAward, exists bool, err error) + GetByUserIdAndBadgeIdAndAwardKey(ctx context.Context, userID string, badgeID string, awardKey string) (badgeAward *entity.BadgeAward, exists bool, err error) } type BadgeAwardService struct { @@ -151,48 +150,32 @@ func (b *BadgeAwardService) GetBadgeAwardList( } // Award award badge -func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string, force bool, createdAt time.Time) (err error) { - var ( - badgeData *entity.Badge - exists, awarded bool - ) - - badgeData, exists, err = b.badgeRepo.GetByID(ctx, badgeID) +func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string) (err error) { + badgeData, exists, err := b.badgeRepo.GetByID(ctx, badgeID) if err != nil { - return + return err } if !exists || badgeData.Status == entity.BadgeStatusInactive { - err = errors.BadRequest(reason.BadgeObjectNotFound) - return + return errors.BadRequest(reason.BadgeObjectNotFound) } - awarded = b.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single) - if !force && awarded { - return + alreadyAwarded, err := b.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single) + if err != nil { + return err } - - if createdAt.IsZero() { - createdAt = time.Now() + if alreadyAwarded { + return nil } - err = b.badgeAwardRepo.Add(ctx, &entity.BadgeAward{ - CreatedAt: createdAt, - UpdatedAt: createdAt, + badgeAward := &entity.BadgeAward{ UserID: userID, BadgeID: badgeID, AwardKey: awardKey, BadgeGroupID: badgeData.BadgeGroupID, - IsBadgeDeleted: 0, - }) - if err != nil { - return + IsBadgeDeleted: entity.IsBadgeNotDeleted, } - - // increment badge award count - err = b.badgeRepo.UpdateAwardCount(ctx, badgeID, 1) - - return + return b.badgeAwardRepo.AwardBadgeForUser(ctx, badgeAward) } // GetUserBadgeAwardList get user badge award list diff --git a/internal/service/badge/badge_event_handler.go b/internal/service/badge/badge_event_handler.go index 09331197f..f990260bf 100644 --- a/internal/service/badge/badge_event_handler.go +++ b/internal/service/badge/badge_event_handler.go @@ -21,12 +21,11 @@ package badge import ( "context" + "github.com/apache/incubator-answer/internal/base/data" "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/event_queue" "github.com/segmentfault/pacman/log" - - "github.com/apache/incubator-answer/internal/base/data" - "github.com/apache/incubator-answer/internal/schema" ) type BadgeEventService struct { @@ -35,6 +34,7 @@ type BadgeEventService struct { badgeAwardRepo BadgeAwardRepo badgeRepo BadgeRepo eventRuleRepo EventRuleRepo + badgeAwardService *BadgeAwardService } type EventRuleHandler func(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) @@ -48,12 +48,14 @@ func NewBadgeEventService( eventQueueService event_queue.EventQueueService, badgeRepo BadgeRepo, eventRuleRepo EventRuleRepo, + badgeAwardService *BadgeAwardService, ) *BadgeEventService { n := &BadgeEventService{ data: data, eventQueueService: eventQueueService, badgeRepo: badgeRepo, eventRuleRepo: eventRuleRepo, + badgeAwardService: badgeAwardService, } eventQueueService.RegisterHandler(n.Handler) return n @@ -65,18 +67,22 @@ func (ns *BadgeEventService) Handler(ctx context.Context, msg *schema.EventMsg) return nil } - badgeIDs := make([]string, 0) - for _, award := range awards { - badgeIDs = append(badgeIDs, award.BadgeID) - } + //badgeIDs := make([]string, 0) + //for _, award := range awards { + // badgeIDs = append(badgeIDs, award.BadgeID) + //} + // + //badges, err := ns.badgeRepo.GetByIDs(ctx, badgeIDs) + //if err != nil { + // log.Errorf("error getting badges %+v: %v", badgeIDs, err) + // return err + //} - badges, err := ns.badgeRepo.GetByIDs(ctx, badgeIDs) - if err != nil { - log.Errorf("error getting badges %+v: %v", badgeIDs, err) - return err + for _, award := range awards { + err := ns.badgeAwardService.Award(ctx, award.BadgeID, award.UserID, award.AwardKey) + if err != nil { + log.Debugf("error awarding badge %s: %v", award.BadgeID, err) + } } - - // TODO: award badges to user - log.Debugf("awarding badges %+v to user", badges) return nil } From 46ab8a20fe94a43c72e637415990f2591b76bb28 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Wed, 14 Aug 2024 10:58:39 +0800 Subject: [PATCH 35/38] chore(badge): remove unused comment --- internal/service/badge/badge_event_handler.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/internal/service/badge/badge_event_handler.go b/internal/service/badge/badge_event_handler.go index f990260bf..8a92f08b6 100644 --- a/internal/service/badge/badge_event_handler.go +++ b/internal/service/badge/badge_event_handler.go @@ -67,17 +67,6 @@ func (ns *BadgeEventService) Handler(ctx context.Context, msg *schema.EventMsg) return nil } - //badgeIDs := make([]string, 0) - //for _, award := range awards { - // badgeIDs = append(badgeIDs, award.BadgeID) - //} - // - //badges, err := ns.badgeRepo.GetByIDs(ctx, badgeIDs) - //if err != nil { - // log.Errorf("error getting badges %+v: %v", badgeIDs, err) - // return err - //} - for _, award := range awards { err := ns.badgeAwardService.Award(ctx, award.BadgeID, award.UserID, award.AwardKey) if err != nil { From f0c433cc026d001f920411a1a6e1f144620ee924 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Wed, 14 Aug 2024 12:31:03 +0800 Subject: [PATCH 36/38] fix(badge): fix badge init data --- internal/entity/badge_entity.go | 8 +- internal/migrations/init.go | 20 ++ internal/migrations/init_data.go | 156 ++++++++++++ internal/migrations/v22.go | 268 +++------------------ internal/repo/badge/badge_event_rule.go | 155 ++++++------ internal/service/content/answer_service.go | 6 +- 6 files changed, 285 insertions(+), 328 deletions(-) diff --git a/internal/entity/badge_entity.go b/internal/entity/badge_entity.go index 5177c93c6..a370e2750 100644 --- a/internal/entity/badge_entity.go +++ b/internal/entity/badge_entity.go @@ -47,14 +47,14 @@ type Badge struct { Name string `xorm:"not null default '' VARCHAR(256) name"` Icon string `xorm:"not null default '' VARCHAR(1024) icon"` AwardCount int `xorm:"not null default 0 INT(11) award_count"` - Description string `xorm:"not null default '' MEDIUMTEXT description"` + Description string `xorm:"not null MEDIUMTEXT description"` Status int8 `xorm:"not null default 1 INT(11) status"` BadgeGroupID int64 `xorm:"not null default 0 BIGINT(20) badge_group_id"` Level BadgeLevel `xorm:"not null default 1 TINYINT(4) level"` Single int8 `xorm:"not null default 1 TINYINT(4) single"` - Collect string `xorm:"not null default '' VARCHAR(64) collect"` - Handler string `xorm:"not null default '' VARCHAR(64) handler"` - Param string `xorm:"not null default '' TEXT param"` + Collect string `xorm:"not null default '' VARCHAR(128) collect"` + Handler string `xorm:"not null default '' VARCHAR(128) handler"` + Param string `xorm:"not null TEXT param"` } // TableName badge table name diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 0b4d1ce3c..b74e36886 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -80,6 +80,7 @@ func (m *Mentor) InitDB() error { m.do("init site info privilege rank", m.initSiteInfoPrivilegeRank) m.do("init site info write", m.initSiteInfoWrite) m.do("init default content", m.initDefaultContent) + m.do("init default badges", m.initDefaultBadges) return m.err } @@ -432,3 +433,22 @@ func (m *Mentor) initDefaultContent() { return } } + +func (m *Mentor) initDefaultBadges() { + uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: m.engine}) + + _, m.err = m.engine.Context(m.ctx).Insert(defaultBadgeGroupTable) + if m.err != nil { + return + } + for _, badge := range defaultBadgeTable { + badge.ID, m.err = uniqueIDRepo.GenUniqueIDStr(m.ctx, new(entity.Badge).TableName()) + if m.err != nil { + return + } + if _, m.err = m.engine.Context(m.ctx).Insert(badge); m.err != nil { + return + } + } + return +} diff --git a/internal/migrations/init_data.go b/internal/migrations/init_data.go index f9a09f52a..50a5651b6 100644 --- a/internal/migrations/init_data.go +++ b/internal/migrations/init_data.go @@ -347,4 +347,160 @@ var ( {ID: 129, Key: "rank.question.undeleted", Value: `-1`}, {ID: 130, Key: "rank.tag.undeleted", Value: `-1`}, } + + defaultBadgeGroupTable = []*entity.BadgeGroup{ + {ID: "1", Name: "badge.default_badge_groups.getting_started.name"}, + {ID: "2", Name: "badge.default_badge_groups.community.name"}, + {ID: "3", Name: "badge.default_badge_groups.posting.name"}, + } + + defaultBadgeTable = []*entity.Badge{ + { + Name: "badge.default_badges.autobiographer.name", + Icon: "person-badge-fill", + Description: "badge.default_badges.autobiographer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstUpdateUserProfile", + }, + { + Name: "badge.default_badges.editor.name", + Icon: "pencil-fill", + Description: "badge.default_badges.editor.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstPostEdit", + }, + { + Name: "badge.default_badges.first_flag.name", + Icon: "flag-fill", + Description: "badge.default_badges.first_flag.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstFlaggedPost", + }, + { + Name: "badge.default_badges.first_upvote.name", + Icon: "hand-thumbs-up-fill", + Description: "badge.default_badges.first_upvote.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstVotedPost", + }, + { + Name: "badge.default_badges.first_reaction.name", + Icon: "emoji-smile-fill", + Description: "badge.default_badges.first_reaction.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstReactedPost", + }, + { + Name: "badge.default_badges.first_share.name", + Icon: "share-fill", + Description: "badge.default_badges.first_share.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstSharedPost", + }, + { + Name: "badge.default_badges.scholar.name", + Icon: "check-circle-fill", + Description: "badge.default_badges.scholar.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 1, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "FirstAcceptAnswer", + }, + { + Name: "badge.default_badges.solved.name", + Icon: "check-square-fill", + Description: "badge.default_badges.solved.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 2, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeSingleAward, + Handler: "ReachAnswerAcceptedAmount", + Param: `{"amount":"1"}`, + }, + { + Name: "badge.default_badges.nice_answer.name", + Icon: "chat-square-text-fill", + Description: "badge.default_badges.nice_answer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeMultiAward, + Handler: "ReachAnswerVote", + Param: `{"amount":"10"}`, + }, + { + Name: "badge.default_badges.good_answer.name", + Icon: "chat-square-text-fill", + Description: "badge.default_badges.good_answer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelSilver, + Single: entity.BadgeMultiAward, + Handler: "ReachAnswerVote", + Param: `{"amount":"25"}`, + }, + { + Name: "badge.default_badges.great_answer.name", + Icon: "chat-square-text-fill", + Description: "badge.default_badges.great_answer.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelGold, + Single: entity.BadgeMultiAward, + Handler: "ReachAnswerVote", + Param: `{"amount":"50"}`, + }, + { + Name: "badge.default_badges.nice_question.name", + Icon: "question-circle-fill", + Description: "badge.default_badges.nice_question.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelBronze, + Single: entity.BadgeMultiAward, + Handler: "ReachQuestionVote", + Param: `{"amount":"10"}`, + }, + { + Name: "badge.default_badges.good_question.name", + Icon: "question-circle-fill", + Description: "badge.default_badges.good_question.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelSilver, + Single: entity.BadgeMultiAward, + Handler: "ReachQuestionVote", + Param: `{"amount":"25"}`, + }, + { + Name: "badge.default_badges.great_question.name", + Icon: "question-circle-fill", + Description: "badge.default_badges.great_question.desc", + Status: entity.BadgeStatusActive, + BadgeGroupID: 3, + Level: entity.BadgeLevelGold, + Single: entity.BadgeMultiAward, + Handler: "ReachQuestionVote", + Param: `{"amount":"50"}`, + }, + } ) diff --git a/internal/migrations/v22.go b/internal/migrations/v22.go index d3d81d844..ab7185e32 100644 --- a/internal/migrations/v22.go +++ b/internal/migrations/v22.go @@ -21,265 +21,51 @@ package migrations import ( "context" + "fmt" "github.com/apache/incubator-answer/internal/base/data" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/repo/unique" - "time" "xorm.io/xorm" ) -var ( - defaultBadgeGroupTable = []*entity.BadgeGroup{ - {ID: "1", Name: "badge.default_badge_groups.getting_started.name"}, - {ID: "2", Name: "badge.default_badge_groups.community.name"}, - {ID: "3", Name: "badge.default_badge_groups.posting.name"}, - } - - defaultBadgeTable = []*entity.Badge{ - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.autobiographer.name", - Icon: "person-badge-fill", - AwardCount: 0, - Description: "badge.default_badges.autobiographer.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 1, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeSingleAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.editor.name", - Icon: "pencil-fill", - AwardCount: 0, - Description: "badge.default_badges.editor.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 1, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeSingleAward, - Collect: "question", - Handler: "FirstQuestion", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.first_flag.name", - Icon: "flag-fill", - AwardCount: 0, - Description: "badge.default_badges.first_flag.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 1, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeSingleAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.first_upvote.name", - Icon: "hand-thumbs-up-fill", - AwardCount: 0, - Description: "badge.default_badges.first_upvote.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 1, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeSingleAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.first_reaction.name", - Icon: "emoji-smile-fill", - AwardCount: 0, - Description: "badge.default_badges.first_reaction.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 1, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeSingleAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.first_share.name", - Icon: "share-fill", - AwardCount: 0, - Description: "badge.default_badges.first_share.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 1, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeSingleAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.scholar.name", - Icon: "check-circle-fill", - AwardCount: 0, - Description: "badge.default_badges.scholar.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 1, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeSingleAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.solved.name", - Icon: "check-square-fill", - AwardCount: 0, - Description: "badge.default_badges.solved.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 2, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeSingleAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.nice_answer.name", - Icon: "chat-square-text-fill", - AwardCount: 0, - Description: "badge.default_badges.nice_answer.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 3, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeMultiAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.good_answer.name", - Icon: "chat-square-text-fill", - AwardCount: 0, - Description: "badge.default_badges.good_answer.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 3, - Level: entity.BadgeLevelSilver, - Single: entity.BadgeMultiAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.great_answer.name", - Icon: "chat-square-text-fill", - AwardCount: 0, - Description: "badge.default_badges.great_answer.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 3, - Level: entity.BadgeLevelGold, - Single: entity.BadgeMultiAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.nice_question.name", - Icon: "question-circle-fill", - AwardCount: 0, - Description: "badge.default_badges.nice_question.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 3, - Level: entity.BadgeLevelBronze, - Single: entity.BadgeMultiAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.good_question.name", - Icon: "question-circle-fill", - AwardCount: 0, - Description: "badge.default_badges.good_question.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 3, - Level: entity.BadgeLevelSilver, - Single: entity.BadgeSingleAward, - Collect: "", - Handler: "", - Param: "", - }, - { - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Name: "badge.default_badges.great_question.name", - Icon: "question-circle-fill", - AwardCount: 0, - Description: "badge.default_badges.great_question.desc", - Status: entity.BadgeStatusActive, - BadgeGroupID: 3, - Level: entity.BadgeLevelGold, - Single: entity.BadgeMultiAward, - Collect: "", - Handler: "", - Param: "", - }, - } -) - func addBadges(ctx context.Context, x *xorm.Engine) (err error) { uniqueIDRepo := unique.NewUniqueIDRepo(&data.Data{DB: x}) - // create table - err = x.Context(ctx).Sync(new(entity.Badge)) - if err != nil { - return - } - err = x.Context(ctx).Sync(new(entity.BadgeGroup)) + err = x.Context(ctx).Sync(new(entity.Badge), new(entity.BadgeGroup), new(entity.BadgeAward)) if err != nil { - return + return fmt.Errorf("sync table failed: %w", err) } - err = x.Context(ctx).Sync(new(entity.BadgeAward)) - if err != nil { - return + for _, badgeGroup := range defaultBadgeGroupTable { + exist, err := x.Context(ctx).Get(&entity.BadgeGroup{ID: badgeGroup.ID}) + if err != nil { + return err + } + if exist { + _, err = x.Context(ctx).ID(badgeGroup.ID).Update(badgeGroup) + } else { + _, err = x.Context(ctx).Insert(badgeGroup) + } + if err != nil { + return fmt.Errorf("insert badge group failed: %w", err) + } } - // insert default data - _, err = x.Context(ctx).Insert(defaultBadgeGroupTable) - if err != nil { - return - } for _, badge := range defaultBadgeTable { - badge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, new(entity.Badge).TableName()) + exist, err := x.Context(ctx).Get(&entity.Badge{Name: badge.Name}) if err != nil { - return + return err + } + if exist { + continue } - _, err = x.Context(ctx).Insert(badge) + badge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, new(entity.Badge).TableName()) if err != nil { - return + return err + } + + if _, err := x.Context(ctx).Insert(badge); err != nil { + return err } } return diff --git a/internal/repo/badge/badge_event_rule.go b/internal/repo/badge/badge_event_rule.go index f912c2bbe..f107203e8 100644 --- a/internal/repo/badge/badge_event_rule.go +++ b/internal/repo/badge/badge_event_rule.go @@ -86,91 +86,87 @@ func (br *eventRuleRepo) HandleEventWithRule(ctx context.Context, msg *schema.Ev // FirstUpdateUserProfile first update user profile func (br *eventRuleRepo) FirstUpdateUserProfile(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "FirstUpdateUserProfile") - if b == nil { - return nil, nil - } - bean := &entity.User{ID: event.UserID} - exist, err := br.data.DB.Context(ctx).Get(bean) - if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - if !exist { - return nil, nil - } - if len(bean.Bio) > 0 { - return append(awards, br.createBadgeAward(event.UserID, b.ID, entity.BadgeOnceAwardKey)), nil + badges := br.getBadgesByHandler(ctx, "FirstUpdateUserProfile") + for _, b := range badges { + bean := &entity.User{ID: event.UserID} + exist, err := br.data.DB.Context(ctx).Get(bean) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + if !exist { + continue + } + if len(bean.Bio) > 0 { + awards = append(awards, br.createBadgeAward(event.UserID, "", b)) + } } - return nil, nil + return awards, nil } // FirstPostEdit first post edit func (br *eventRuleRepo) FirstPostEdit(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "FirstPostEdit") - if b == nil { - return nil, nil + badges := br.getBadgesByHandler(ctx, "FirstPostEdit") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } - return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil + return awards, nil } // FirstFlaggedPost first flagged post. func (br *eventRuleRepo) FirstFlaggedPost(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "FirstFlaggedPost") - if b == nil { - return nil, nil + badges := br.getBadgesByHandler(ctx, "FirstFlaggedPost") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } - return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil + return awards, nil } // FirstVotedPost first voted post func (br *eventRuleRepo) FirstVotedPost(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "FirstVotedPost") - if b == nil { - return nil, nil + badges := br.getBadgesByHandler(ctx, "FirstVotedPost") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } - return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil + return awards, nil } // FirstReactedPost first reacted post func (br *eventRuleRepo) FirstReactedPost(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "FirstReactedPost") - if b == nil { - return nil, nil + badges := br.getBadgesByHandler(ctx, "FirstReactedPost") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } - return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil + return awards, nil } // FirstSharedPost first shared post func (br *eventRuleRepo) FirstSharedPost(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "FirstSharedPost") - if b == nil { - return nil, nil + badges := br.getBadgesByHandler(ctx, "FirstSharedPost") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } - return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil + return awards, nil } // FirstAcceptAnswer user first accept answer func (br *eventRuleRepo) FirstAcceptAnswer(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "FirstAcceptAnswer") - if b == nil { - return nil, nil + badges := br.getBadgesByHandler(ctx, "FirstAcceptAnswer") + for _, b := range badges { + awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b)) } - return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil + return awards, nil } // ReachAnswerAcceptedAmount reach answer accepted amount func (br *eventRuleRepo) ReachAnswerAcceptedAmount(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "ReachAnswerAcceptedAmount") - if b == nil { - return nil, nil - } + badges := br.getBadgesByHandler(ctx, "ReachAnswerAcceptedAmount") if len(event.AnswerUserID) == 0 { return nil, nil } @@ -185,79 +181,76 @@ func (br *eventRuleRepo) ReachAnswerAcceptedAmount(ctx context.Context, return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - // get badge requirement - requirement := b.GetIntParam("amount") - if requirement == 0 || amount < requirement { - return nil, nil + for _, b := range badges { + // get badge requirement + requirement := b.GetIntParam("amount") + if requirement == 0 || amount < requirement { + continue + } + awards = append(awards, br.createBadgeAward(event.AnswerUserID, event.AnswerID, b)) } - - return append(awards, br.createBadgeAward(event.UserID, b.ID, event.GetObjectID())), nil + return awards, nil } // ReachAnswerVote reach answer vote func (br *eventRuleRepo) ReachAnswerVote(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "ReachAnswerVote") - if b == nil { - return nil, nil - } - + badges := br.getBadgesByHandler(ctx, "ReachAnswerVote") // get vote amount amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount")) if amount == 0 { return nil, nil } - // get badge requirement - requirement := b.GetIntParam("amount") - if requirement == 0 || int64(amount) < requirement { - return nil, nil + for _, b := range badges { + // get badge requirement + requirement := b.GetIntParam("amount") + if requirement == 0 || int64(amount) < requirement { + continue + } + awards = append(awards, br.createBadgeAward(event.AnswerUserID, event.AnswerID, b)) } - - return append(awards, br.createBadgeAward(event.AnswerUserID, b.ID, event.AnswerID)), nil + return awards, nil } // ReachQuestionVote reach question vote func (br *eventRuleRepo) ReachQuestionVote(ctx context.Context, event *schema.EventMsg) (awards []*entity.BadgeAward, err error) { - b := br.getBadgeByHandler(ctx, "ReachQuestionVote") - if b == nil { - return nil, nil - } - + badges := br.getBadgesByHandler(ctx, "ReachQuestionVote") // get vote amount amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount")) if amount == 0 { return nil, nil } - // get badge requirement - requirement := b.GetIntParam("amount") - if requirement == 0 || int64(amount) < requirement { - return nil, nil + for _, b := range badges { + // get badge requirement + requirement := b.GetIntParam("amount") + if requirement == 0 || int64(amount) < requirement { + continue + } + awards = append(awards, br.createBadgeAward(event.QuestionUserID, event.QuestionID, b)) } - - return append(awards, br.createBadgeAward(event.QuestionUserID, b.ID, event.QuestionID)), nil + return awards, nil } -func (br *eventRuleRepo) getBadgeByHandler(ctx context.Context, handler string) (b *entity.Badge) { - b = &entity.Badge{Handler: handler} - exist, err := br.data.DB.Context(ctx).Get(b) +func (br *eventRuleRepo) getBadgesByHandler(ctx context.Context, handler string) (badges []*entity.Badge) { + badges = make([]*entity.Badge, 0) + err := br.data.DB.Context(ctx).Where("handler = ?", handler).Find(&badges) if err != nil { log.Errorf("error getting badge by handler %s: %v", handler, err) return nil } - if !exist { - log.Errorf("badge not found by handler %s", handler) - return nil - } - return b + return badges } -func (br *eventRuleRepo) createBadgeAward(userID, badgeID, awardKey string) (awards *entity.BadgeAward) { +func (br *eventRuleRepo) createBadgeAward(userID, awardKey string, badge *entity.Badge) (awards *entity.BadgeAward) { + if badge.Single == entity.BadgeSingleAward { + awardKey = entity.BadgeOnceAwardKey + } return &entity.BadgeAward{ UserID: userID, - BadgeID: badgeID, + BadgeID: badge.ID, AwardKey: awardKey, } } diff --git a/internal/service/content/answer_service.go b/internal/service/content/answer_service.go index 5674a79b5..496f7546b 100644 --- a/internal/service/content/answer_service.go +++ b/internal/service/content/answer_service.go @@ -446,8 +446,10 @@ func (as *AnswerService) AcceptAnswer(ctx context.Context, req *schema.AcceptAns oldAnswerInfo.ID = uid.DeShortID(oldAnswerInfo.ID) } - as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID). - QID(questionInfo.ID, questionInfo.UserID).AID(req.AnswerID, req.UserID)) + if acceptedAnswerInfo != nil { + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID). + QID(questionInfo.ID, questionInfo.UserID).AID(acceptedAnswerInfo.ID, acceptedAnswerInfo.UserID)) + } as.updateAnswerRank(ctx, req.UserID, questionInfo, acceptedAnswerInfo, oldAnswerInfo) return nil From 7d040f07dd8faf244eab2695036f69342e7e3b01 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Thu, 15 Aug 2024 18:34:13 +0800 Subject: [PATCH 37/38] feat(badge): add badge notification --- cmd/wire_gen.go | 6 +- i18n/en_US.yaml | 2 + internal/base/constant/cache_key.go | 2 + internal/base/constant/notification.go | 8 ++ .../controller/notification_controller.go | 6 +- .../repo/notification/notification_repo.go | 6 +- internal/schema/badge_schema.go | 18 ++++ internal/schema/notification_schema.go | 76 ++++++++++++- internal/service/badge/badge_award_service.go | 85 +++++++++------ .../notification/notification_service.go | 100 ++++++++++++------ .../notification_common/notification.go | 89 +++++++++++++--- internal/service/object_info/object_info.go | 1 - 12 files changed, 301 insertions(+), 98 deletions(-) diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 9acd76ee5..cc245aea5 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -238,7 +238,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService) notificationRepo := notification2.NewNotificationRepo(dataData) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, notificationQueueService, userExternalLoginRepo, siteInfoCommonService) - notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo, reportRepo, reviewService) + badgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo) + notificationService := notification.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService, userRepo, reportRepo, reviewService, badgeRepo) notificationController := controller.NewNotificationController(notificationService, rankService) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, reviewService, revisionRepo, dataData) dashboardController := controller.NewDashboardController(dashboardService) @@ -259,11 +260,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reviewController := controller.NewReviewController(reviewService, rankService, captchaService) metaService := meta2.NewMetaService(metaCommonService, userCommon, answerRepo, questionRepo, eventQueueService) metaController := controller.NewMetaController(metaService) - badgeRepo := badge.NewBadgeRepo(dataData, uniqueIDRepo) badgeGroupRepo := badge_group.NewBadgeGroupRepo(dataData, uniqueIDRepo) badgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo) eventRuleRepo := badge.NewEventRuleRepo(dataData) - badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService) + badgeAwardService := badge2.NewBadgeAwardService(badgeAwardRepo, badgeRepo, userCommon, objService, notificationQueueService) badgeEventService := badge2.NewBadgeEventService(dataData, eventQueueService, badgeRepo, eventRuleRepo, badgeAwardService) badgeService := badge2.NewBadgeService(badgeRepo, badgeGroupRepo, badgeAwardRepo, badgeEventService) badgeController := controller.NewBadgeController(badgeService, badgeAwardService) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 071644f4a..4de696589 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -465,6 +465,8 @@ backend: other: upvoted comment invited_you_to_answer: other: invited you to answer + earned_badge: + other: You've earned the "{{.BadgeName}}" badge email_tpl: change_email: title: diff --git a/internal/base/constant/cache_key.go b/internal/base/constant/cache_key.go index 4135b53c8..987798d19 100644 --- a/internal/base/constant/cache_key.go +++ b/internal/base/constant/cache_key.go @@ -50,4 +50,6 @@ const ( NewQuestionNotificationLimitMax = 50 RateLimitCacheKeyPrefix = "answer:rate-limit:" RateLimitCacheTime = 5 * time.Minute + RedDotCacheKey = "answer:red-dot:%s:%s" + RedDotCacheTime = 30 * 24 * time.Hour ) diff --git a/internal/base/constant/notification.go b/internal/base/constant/notification.go index ceebe7de8..9a7762d8e 100644 --- a/internal/base/constant/notification.go +++ b/internal/base/constant/notification.go @@ -56,6 +56,8 @@ const ( NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted" // NotificationInvitedYouToAnswer invited you to answer NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer" + // NotificationEarnedBadge earned badge + NotificationEarnedBadge = "notification.action.earned_badge" ) type NotificationChannelKey string @@ -71,6 +73,12 @@ const ( EmailChannel NotificationChannelKey = "email" ) +const ( + NotificationTypeInbox = "inbox" + NotificationTypeAchievement = "achievement" + NotificationTypeBadgeAchievement = "badge" +) + var ( NotificationMsgTypeMapping = map[string]int{ NotificationUpdateQuestion: 1, diff --git a/internal/controller/notification_controller.go b/internal/controller/notification_controller.go index 15796b9c4..952c262e9 100644 --- a/internal/controller/notification_controller.go +++ b/internal/controller/notification_controller.go @@ -105,8 +105,8 @@ func (nc *NotificationController) ClearRedDot(ctx *gin.Context) { req.CanReviewAnswer = canList[1] req.CanReviewTag = canList[2] - RedDot, err := nc.notificationService.ClearRedDot(ctx, req) - handler.HandleResponse(ctx, err, RedDot) + resp, err := nc.notificationService.ClearRedDot(ctx, req) + handler.HandleResponse(ctx, err, resp) } // ClearUnRead @@ -125,7 +125,7 @@ func (nc *NotificationController) ClearUnRead(ctx *gin.Context) { return } userID := middleware.GetLoginUserIDFromContext(ctx) - err := nc.notificationService.ClearUnRead(ctx, userID, req.TypeStr) + err := nc.notificationService.ClearUnRead(ctx, userID, req.NotificationType) handler.HandleResponse(ctx, err, gin.H{}) } diff --git a/internal/repo/notification/notification_repo.go b/internal/repo/notification/notification_repo.go index 6b4f0040d..bd325ef27 100644 --- a/internal/repo/notification/notification_repo.go +++ b/internal/repo/notification/notification_repo.go @@ -69,7 +69,7 @@ func (nr *notificationRepo) UpdateNotificationContent(ctx context.Context, notif func (nr *notificationRepo) ClearUnRead(ctx context.Context, userID string, notificationType int) (err error) { info := &entity.Notification{} info.IsRead = schema.NotificationRead - _, err = nr.data.DB.Context(ctx).Where("user_id =?", userID).And("type =?", notificationType).Cols("is_read").Update(info) + _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("type = ?", notificationType).Cols("is_read").Update(info) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -79,7 +79,7 @@ func (nr *notificationRepo) ClearUnRead(ctx context.Context, userID string, noti func (nr *notificationRepo) ClearIDUnRead(ctx context.Context, userID string, id string) (err error) { info := &entity.Notification{} info.IsRead = schema.NotificationRead - _, err = nr.data.DB.Context(ctx).Where("user_id =?", userID).And("id =?", id).Cols("is_read").Update(info) + _, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("id = ?", id).Cols("is_read").Update(info) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -98,7 +98,7 @@ func (nr *notificationRepo) GetById(ctx context.Context, id string) (*entity.Not func (nr *notificationRepo) GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error) { info := &entity.Notification{} - exist, err := nr.data.DB.Context(ctx).Where("user_id = ? ", userID).And("object_id = ?", objectID).And("type = ?", notificationType).Get(info) + exist, err := nr.data.DB.Context(ctx).Where("user_id = ?", userID).And("object_id = ?", objectID).And("type = ?", notificationType).Get(info) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return info, false, err diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go index 088193ad8..efbcd37ad 100644 --- a/internal/schema/badge_schema.go +++ b/internal/schema/badge_schema.go @@ -171,3 +171,21 @@ type GetUserBadgeAwardListResp struct { // badge level Level entity.BadgeLevel `json:"level" ` } + +// GetBadgeByIDResp get badge by id response +type GetBadgeByIDResp struct { + // badge id + ID string `json:"id" ` + // badge name + Name string `json:"name" ` + // badge description + Description string `json:"description" ` + // badge icon + Icon string `json:"icon" ` + // badge award count + AwardCount int `json:"award_count" ` + // badge is single or multiple + IsSingle bool `json:"is_single" ` + // badge level + Level entity.BadgeLevel `json:"level" ` +} diff --git a/internal/schema/notification_schema.go b/internal/schema/notification_schema.go index 4e0e93169..8d4b694e9 100644 --- a/internal/schema/notification_schema.go +++ b/internal/schema/notification_schema.go @@ -19,6 +19,12 @@ package schema +import ( + "encoding/json" + "github.com/apache/incubator-answer/internal/entity" + "sort" +) + const ( NotificationTypeInbox = 1 NotificationTypeAchievement = 2 @@ -95,10 +101,70 @@ type ObjectInfo struct { } type RedDot struct { - Inbox int64 `json:"inbox"` - Achievement int64 `json:"achievement"` - Revision int64 `json:"revision"` - CanRevision bool `json:"can_revision"` + Inbox int64 `json:"inbox"` + Achievement int64 `json:"achievement"` + Revision int64 `json:"revision"` + CanRevision bool `json:"can_revision"` + BadgeAward *RedDotBadgeAward `json:"badge_award"` +} + +type RedDotBadgeAward struct { + NotificationID string `json:"notification_id"` + BadgeID string `json:"badge_id"` + Name string `json:"name"` + Icon string `json:"icon"` + Level entity.BadgeLevel `json:"level"` +} + +type RedDotBadgeAwardCache struct { + BadgeAwardList map[string]*RedDotBadgeAward `json:"badge_award_list"` +} + +// NewRedDotBadgeAwardCache new red dot badge award cache +func NewRedDotBadgeAwardCache() *RedDotBadgeAwardCache { + return &RedDotBadgeAwardCache{ + BadgeAwardList: make(map[string]*RedDotBadgeAward), + } +} + +// GetBadgeAward get badge award +func (r *RedDotBadgeAwardCache) GetBadgeAward() *RedDotBadgeAward { + if len(r.BadgeAwardList) == 0 { + return nil + } + var ids []string + for _, v := range r.BadgeAwardList { + ids = append(ids, v.NotificationID) + } + sort.Strings(ids) + return r.BadgeAwardList[ids[0]] +} + +// FromJSON from json +func (r *RedDotBadgeAwardCache) FromJSON(data string) { + _ = json.Unmarshal([]byte(data), r) +} + +// ToJSON to json +func (r *RedDotBadgeAwardCache) ToJSON() string { + data, _ := json.Marshal(r) + return string(data) +} + +// AddBadgeAward add badge award +func (r *RedDotBadgeAwardCache) AddBadgeAward(badgeAward *RedDotBadgeAward) { + if r.BadgeAwardList == nil { + r.BadgeAwardList = make(map[string]*RedDotBadgeAward) + } + r.BadgeAwardList[badgeAward.NotificationID] = badgeAward +} + +// RemoveBadgeAward remove badge award +func (r *RedDotBadgeAwardCache) RemoveBadgeAward(notificationID string) { + if r.BadgeAwardList == nil { + return + } + delete(r.BadgeAwardList, notificationID) } type NotificationSearch struct { @@ -112,8 +178,8 @@ type NotificationSearch struct { } type NotificationClearRequest struct { + NotificationType string `validate:"required,oneof=inbox achievement" json:"type"` UserID string `json:"-"` - TypeStr string `json:"type" form:"type"` // inbox achievement CanReviewQuestion bool `json:"-"` CanReviewAnswer bool `json:"-"` CanReviewTag bool `json:"-"` diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index 3883de616..dd78fd021 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -21,11 +21,13 @@ package badge import ( "context" + "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/base/translator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" + "github.com/apache/incubator-answer/internal/service/notice_queue" "github.com/apache/incubator-answer/internal/service/object_info" usercommon "github.com/apache/incubator-answer/internal/service/user_common" "github.com/apache/incubator-answer/pkg/uid" @@ -65,10 +67,11 @@ type BadgeAwardRepo interface { } type BadgeAwardService struct { - badgeAwardRepo BadgeAwardRepo - badgeRepo BadgeRepo - userCommon *usercommon.UserCommon - objectInfoService *object_info.ObjService + badgeAwardRepo BadgeAwardRepo + badgeRepo BadgeRepo + userCommon *usercommon.UserCommon + objectInfoService *object_info.ObjService + notificationQueueService notice_queue.NotificationQueueService } func NewBadgeAwardService( @@ -76,17 +79,19 @@ func NewBadgeAwardService( badgeRepo BadgeRepo, userCommon *usercommon.UserCommon, objectInfoService *object_info.ObjService, + notificationQueueService notice_queue.NotificationQueueService, ) *BadgeAwardService { return &BadgeAwardService{ - badgeAwardRepo: badgeAwardRepo, - badgeRepo: badgeRepo, - userCommon: userCommon, - objectInfoService: objectInfoService, + badgeAwardRepo: badgeAwardRepo, + badgeRepo: badgeRepo, + userCommon: userCommon, + objectInfoService: objectInfoService, + notificationQueueService: notificationQueueService, } } // GetBadgeAwardList get badge award list -func (b *BadgeAwardService) GetBadgeAwardList( +func (bs *BadgeAwardService) GetBadgeAwardList( ctx context.Context, req *schema.GetBadgeAwardWithPageReq, ) (resp []*schema.GetBadgeAwardWithPageResp, total int64, err error) { @@ -94,11 +99,11 @@ func (b *BadgeAwardService) GetBadgeAwardList( badgeAwardList []*entity.BadgeAward ) - req.UserID, err = b.validateUserByUsername(ctx, req.Username) + req.UserID, err = bs.validateUserByUsername(ctx, req.Username) if err != nil { - badgeAwardList, total, err = b.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize) + badgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeId(ctx, req.BadgeID, req.Page, req.PageSize) } else { - badgeAwardList, total, err = b.badgeAwardRepo.ListPagedByBadgeIdAndUserId(ctx, req.BadgeID, req.UserID, req.Page, req.PageSize) + badgeAwardList, total, err = bs.badgeAwardRepo.ListPagedByBadgeIdAndUserId(ctx, req.BadgeID, req.UserID, req.Page, req.PageSize) } if err != nil { @@ -113,7 +118,7 @@ func (b *BadgeAwardService) GetBadgeAwardList( ) // if exist object info - objInfo, e := b.objectInfoService.GetInfo(ctx, badgeAward.AwardKey) + objInfo, e := bs.objectInfoService.GetInfo(ctx, badgeAward.AwardKey) if e == nil && !objInfo.IsDeleted() { objectID = objInfo.ObjectID questionID = objInfo.QuestionID @@ -135,7 +140,7 @@ func (b *BadgeAwardService) GetBadgeAwardList( } // get user info - userInfo, exists, e := b.userCommon.GetUserBasicInfoByID(ctx, badgeAward.UserID) + userInfo, exists, e := bs.userCommon.GetUserBasicInfoByID(ctx, badgeAward.UserID) if e != nil { log.Errorf("user not found by id: %s, err: %v", badgeAward.UserID, e) } @@ -150,8 +155,8 @@ func (b *BadgeAwardService) GetBadgeAwardList( } // Award award badge -func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string) (err error) { - badgeData, exists, err := b.badgeRepo.GetByID(ctx, badgeID) +func (bs *BadgeAwardService) Award(ctx context.Context, badgeID string, userID string, awardKey string) (err error) { + badgeData, exists, err := bs.badgeRepo.GetByID(ctx, badgeID) if err != nil { return err } @@ -160,7 +165,7 @@ func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID st return errors.BadRequest(reason.BadgeObjectNotFound) } - alreadyAwarded, err := b.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single) + alreadyAwarded, err := bs.badgeAwardRepo.CheckIsAward(ctx, badgeID, userID, awardKey, badgeData.Single) if err != nil { return err } @@ -175,11 +180,27 @@ func (b *BadgeAwardService) Award(ctx context.Context, badgeID string, userID st BadgeGroupID: badgeData.BadgeGroupID, IsBadgeDeleted: entity.IsBadgeNotDeleted, } - return b.badgeAwardRepo.AwardBadgeForUser(ctx, badgeAward) + err = bs.badgeAwardRepo.AwardBadgeForUser(ctx, badgeAward) + if err != nil { + return err + } + + msg := &schema.NotificationMsg{ + TriggerUserID: badgeAward.UserID, + ReceiverUserID: badgeAward.UserID, + Type: schema.NotificationTypeAchievement, + ObjectID: badgeAward.ID, + ObjectType: constant.BadgeAwardObjectType, + Title: badgeData.Name, + ExtraInfo: map[string]string{"badge_id": badgeData.ID}, + NotificationAction: constant.NotificationEarnedBadge, + } + bs.notificationQueueService.Send(ctx, msg) + return nil } // GetUserBadgeAwardList get user badge award list -func (b *BadgeAwardService) GetUserBadgeAwardList( +func (bs *BadgeAwardService) GetUserBadgeAwardList( ctx *gin.Context, req *schema.GetUserBadgeAwardListReq, ) ( @@ -191,12 +212,12 @@ func (b *BadgeAwardService) GetUserBadgeAwardList( earnedCounts []*entity.BadgeEarnedCount ) - req.UserID, err = b.validateUserByUsername(ctx, req.Username) + req.UserID, err = bs.validateUserByUsername(ctx, req.Username) if err != nil { return } - earnedCounts, err = b.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID) + earnedCounts, err = bs.badgeAwardRepo.SumUserEarnedGroupByBadgeID(ctx, req.UserID) if err != nil { return } @@ -204,7 +225,7 @@ func (b *BadgeAwardService) GetUserBadgeAwardList( resp = make([]*schema.GetUserBadgeAwardListResp, total) for i, earnedCount := range earnedCounts { - badge, exists, e := b.badgeRepo.GetByID(ctx, earnedCount.BadgeID) + badge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID) if e != nil { err = e return @@ -225,24 +246,18 @@ func (b *BadgeAwardService) GetUserBadgeAwardList( } // GetUserRecentBadgeAwardList get user badge award list -func (b *BadgeAwardService) GetUserRecentBadgeAwardList( - ctx *gin.Context, - req *schema.GetUserBadgeAwardListReq, -) ( - resp []*schema.GetUserBadgeAwardListResp, - total int64, - err error, -) { +func (bs *BadgeAwardService) GetUserRecentBadgeAwardList(ctx *gin.Context, req *schema.GetUserBadgeAwardListReq) ( + resp []*schema.GetUserBadgeAwardListResp, total int64, err error) { var ( earnedCounts []*entity.BadgeAwardRecent ) - req.UserID, err = b.validateUserByUsername(ctx, req.Username) + req.UserID, err = bs.validateUserByUsername(ctx, req.Username) if err != nil { return } - earnedCounts, err = b.badgeAwardRepo.ListNewestEarned(ctx, req.UserID, req.Limit) + earnedCounts, err = bs.badgeAwardRepo.ListNewestEarned(ctx, req.UserID, req.Limit) if err != nil { return } @@ -251,7 +266,7 @@ func (b *BadgeAwardService) GetUserRecentBadgeAwardList( resp = make([]*schema.GetUserBadgeAwardListResp, total) for i, earnedCount := range earnedCounts { - badge, exists, e := b.badgeRepo.GetByID(ctx, earnedCount.BadgeID) + badge, exists, e := bs.badgeRepo.GetByID(ctx, earnedCount.BadgeID) if e != nil { err = e return @@ -278,14 +293,14 @@ type userReq struct { Username string } -func (b *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) { +func (bs *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) { var ( userInfo *schema.UserBasicInfo exist bool ) // validate user exists or not if len(userName) > 0 { - userInfo, exist, err = b.userCommon.GetUserBasicInfoByUserName(ctx, userName) + userInfo, exist, err = bs.userCommon.GetUserBasicInfoByUserName(ctx, userName) if err != nil { return } diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index 71febb677..b73d4fdad 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -23,7 +23,7 @@ import ( "context" "encoding/json" "fmt" - + "github.com/apache/incubator-answer/internal/service/badge" "github.com/apache/incubator-answer/internal/service/report_common" "github.com/apache/incubator-answer/internal/service/review" usercommon "github.com/apache/incubator-answer/internal/service/user_common" @@ -52,6 +52,7 @@ type NotificationService struct { reportRepo report_common.ReportRepo reviewService *review.ReviewService userRepo usercommon.UserRepo + badgeRepo badge.BadgeRepo } func NewNotificationService( @@ -62,6 +63,7 @@ func NewNotificationService( userRepo usercommon.UserRepo, reportRepo report_common.ReportRepo, reviewService *review.ReviewService, + badgeRepo badge.BadgeRepo, ) *NotificationService { return &NotificationService{ data: data, @@ -71,35 +73,60 @@ func NewNotificationService( userRepo: userRepo, reportRepo: reportRepo, reviewService: reviewService, + badgeRepo: badgeRepo, } } func (ns *NotificationService) GetRedDot(ctx context.Context, req *schema.GetRedDot) (resp *schema.RedDot, err error) { + inboxKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, req.UserID) + achievementKey := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, req.UserID) + redBot := &schema.RedDot{} - inboxKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeInbox, req.UserID) - achievementKey := fmt.Sprintf("answer_RedDot_%d_%s", schema.NotificationTypeAchievement, req.UserID) - inboxValue, _, err := ns.data.Cache.GetInt64(ctx, inboxKey) - if err != nil { - redBot.Inbox = 0 - } else { - redBot.Inbox = inboxValue - } - achievementValue, _, err := ns.data.Cache.GetInt64(ctx, achievementKey) - if err != nil { - redBot.Achievement = 0 - } else { - redBot.Achievement = achievementValue - } - revisionCount := &schema.RevisionSearch{} - _ = copier.Copy(revisionCount, req) + redBot.Inbox, _, err = ns.data.Cache.GetInt64(ctx, inboxKey) + redBot.Achievement, _, err = ns.data.Cache.GetInt64(ctx, achievementKey) + + // get review amount if req.CanReviewAnswer || req.CanReviewQuestion || req.CanReviewTag { redBot.CanRevision = true redBot.Revision = ns.countAllReviewAmount(ctx, req) } + // get badge award + redBot.BadgeAward = ns.getBadgeAward(ctx, req.UserID) return redBot, nil } +func (ns *NotificationService) getBadgeAward(ctx context.Context, userID string) (badgeAward *schema.RedDotBadgeAward) { + key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) + cacheData, exist, err := ns.data.Cache.GetString(ctx, key) + if err != nil { + log.Errorf("get badge award failed: %v", err) + return nil + } + if !exist { + return nil + } + + c := schema.NewRedDotBadgeAwardCache() + c.FromJSON(cacheData) + award := c.GetBadgeAward() + if award == nil { + return nil + } + badgeInfo, exists, err := ns.badgeRepo.GetByID(ctx, award.BadgeID) + if err != nil { + log.Errorf("get badge info failed: %v", err) + return nil + } + if !exists { + return nil + } + award.Name = translator.Tr(handler.GetLangByCtx(ctx), badgeInfo.Name) + award.Icon = badgeInfo.Icon + award.Level = badgeInfo.Level + return award +} + func (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *schema.GetRedDot) (amount int64) { // get queue amount if req.IsAdmin { @@ -137,21 +164,16 @@ func (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *sc } func (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) { - botType, ok := schema.NotificationType[req.TypeStr] - if ok { - key := fmt.Sprintf("answer_RedDot_%d_%s", botType, req.UserID) - err := ns.data.Cache.Del(ctx, key) - if err != nil { - log.Error("ClearRedDot del cache error", err.Error()) - } - } - getRedDotreq := &schema.GetRedDot{} - _ = copier.Copy(getRedDotreq, req) - return ns.GetRedDot(ctx, getRedDotreq) + key := fmt.Sprintf(constant.RedDotCacheKey, req.NotificationType, req.UserID) + _ = ns.data.Cache.Del(ctx, key) + + resp := &schema.GetRedDot{} + _ = copier.Copy(resp, req) + return ns.GetRedDot(ctx, resp) } -func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, botTypeStr string) error { - botType, ok := schema.NotificationType[botTypeStr] +func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, notificationType string) error { + botType, ok := schema.NotificationType[notificationType] if ok { err := ns.notificationRepo.ClearUnRead(ctx, userID, botType) if err != nil { @@ -164,19 +186,23 @@ func (ns *NotificationService) ClearUnRead(ctx context.Context, userID string, b func (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string, id string) error { notificationInfo, exist, err := ns.notificationRepo.GetById(ctx, id) if err != nil { - log.Error("notificationRepo.GetById error", err.Error()) + log.Errorf("get notification failed: %v", err) return nil } - if !exist { + if !exist || notificationInfo.UserID != userID { return nil } - if notificationInfo.UserID == userID && notificationInfo.IsRead == schema.NotificationNotRead { + if notificationInfo.IsRead == schema.NotificationNotRead { err := ns.notificationRepo.ClearIDUnRead(ctx, userID, id) if err != nil { return err } } + err = ns.notificationCommon.RemoveBadgeAwardAlertCache(ctx, userID, id) + if err != nil { + log.Errorf("remove badge award alert cache failed: %v", err) + } return nil } @@ -224,6 +250,14 @@ func (ns *NotificationService) formatNotificationPage(ctx context.Context, notif item.NotificationAction == constant.NotificationDownVotedTheAnswer { item.UserInfo = nil } + // If notification is badge, the user info is not needed and the title need to be translated. + if item.ObjectInfo.ObjectType == constant.BadgeAwardObjectType { + badgeName := translator.Tr(lang, item.ObjectInfo.Title) + item.ObjectInfo.Title = translator.TrWithData(lang, constant.NotificationEarnedBadge, struct { + BadgeName string + }{BadgeName: badgeName}) + item.UserInfo = nil + } item.ID = notificationInfo.ID item.NotificationAction = translator.Tr(lang, item.NotificationAction) diff --git a/internal/service/notification_common/notification.go b/internal/service/notification_common/notification.go index 319403b24..a3129b3a4 100644 --- a/internal/service/notification_common/notification.go +++ b/internal/service/notification_common/notification.go @@ -103,7 +103,7 @@ func NewNotificationCommon( // ObjectInfo.Title // ObjectInfo.ObjectID // ObjectInfo.ObjectType -func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) error { +func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) (err error) { if msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() { return nil } @@ -119,17 +119,25 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N Type: msg.Type, } var questionID string // just for notify all followers - objInfo, err := ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID) - if err != nil { - log.Error(err) - } else { - req.ObjectInfo.Title = objInfo.Title - questionID = objInfo.QuestionID + var objInfo *schema.SimpleObjectInfo + if msg.ObjectType == constant.BadgeAwardObjectType { + req.ObjectInfo.Title = msg.Title objectMap := make(map[string]string) - objectMap["question"] = uid.DeShortID(objInfo.QuestionID) - objectMap["answer"] = uid.DeShortID(objInfo.AnswerID) - objectMap["comment"] = objInfo.CommentID + objectMap["badge_id"] = msg.ExtraInfo["badge_id"] req.ObjectInfo.ObjectMap = objectMap + } else { + objInfo, err = ns.objectInfoService.GetInfo(ctx, req.ObjectInfo.ObjectID) + if err != nil { + log.Error(err) + } else { + req.ObjectInfo.Title = objInfo.Title + questionID = objInfo.QuestionID + objectMap := make(map[string]string) + objectMap["question"] = uid.DeShortID(objInfo.QuestionID) + objectMap["answer"] = uid.DeShortID(objInfo.AnswerID) + objectMap["comment"] = objInfo.CommentID + req.ObjectInfo.ObjectMap = objectMap + } } if msg.Type == schema.NotificationTypeAchievement { @@ -188,10 +196,13 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N if err != nil { return fmt.Errorf("add notification error: %w", err) } - err = ns.addRedDot(ctx, info.UserID, info.Type) + err = ns.addRedDot(ctx, info.UserID, msg.Type) if err != nil { log.Error("addRedDot Error", err.Error()) } + if req.ObjectInfo.ObjectType == constant.BadgeAwardObjectType { + err = ns.AddBadgeAwardAlertCache(ctx, info.UserID, info.ID, req.ObjectInfo.ObjectMap["badge_id"]) + } go ns.SendNotificationToAllFollower(ctx, msg, questionID) @@ -201,19 +212,67 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N return nil } -func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, botType int) error { - key := fmt.Sprintf("answer_RedDot_%d_%s", botType, userID) - err := ns.data.Cache.SetInt64(ctx, key, 1, 30*24*time.Hour) //Expiration time is one month. +func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, noticeType int) error { + var key string + if noticeType == schema.NotificationTypeInbox { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) + } else { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) + } + err := ns.data.Cache.SetInt64(ctx, key, 1, constant.RedDotCacheTime) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } return nil } +// AddBadgeAwardAlertCache add badge award alert cache +func (ns *NotificationCommon) AddBadgeAwardAlertCache(ctx context.Context, userID, notificationID, badgeID string) (err error) { + key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) + cacheData, exist, err := ns.data.Cache.GetString(ctx, key) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if !exist { + c := schema.NewRedDotBadgeAwardCache() + c.AddBadgeAward(&schema.RedDotBadgeAward{ + NotificationID: notificationID, + BadgeID: badgeID, + }) + return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) + } + c := schema.NewRedDotBadgeAwardCache() + c.FromJSON(cacheData) + c.AddBadgeAward(&schema.RedDotBadgeAward{ + NotificationID: notificationID, + BadgeID: badgeID, + }) + return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) +} + +// RemoveBadgeAwardAlertCache remove badge award alert cache +func (ns *NotificationCommon) RemoveBadgeAwardAlertCache(ctx context.Context, userID, notificationID string) (err error) { + key := fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeBadgeAchievement, userID) + cacheData, exist, err := ns.data.Cache.GetString(ctx, key) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if !exist { + return nil + } + c := schema.NewRedDotBadgeAwardCache() + c.FromJSON(cacheData) + c.RemoveBadgeAward(notificationID) + if len(c.BadgeAwardList) == 0 { + return ns.data.Cache.Del(ctx, key) + } + return ns.data.Cache.SetString(ctx, key, c.ToJSON(), constant.RedDotCacheTime) +} + // SendNotificationToAllFollower send notification to all followers func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context, msg *schema.NotificationMsg, questionID string) { - if msg.NoNeedPushAllFollow { + if msg.NoNeedPushAllFollow || len(questionID) == 0 { return } if msg.NotificationAction != constant.NotificationUpdateQuestion && diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go index 6c2e89a9d..9a85f07e8 100644 --- a/internal/service/object_info/object_info.go +++ b/internal/service/object_info/object_info.go @@ -21,7 +21,6 @@ package object_info import ( "context" - "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/schema" From ebc858579b65c8b4a4496530477adf4e12d39f6d Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 19 Aug 2024 14:10:13 +0800 Subject: [PATCH 38/38] chore(badge): delete not used func --- internal/repo/badge/badge_repo.go | 36 ---------------- internal/repo/badge_award/badge_award_repo.go | 41 +------------------ internal/service/badge/badge_award_service.go | 13 ------ internal/service/badge/badge_service.go | 4 -- 4 files changed, 1 insertion(+), 93 deletions(-) diff --git a/internal/repo/badge/badge_repo.go b/internal/repo/badge/badge_repo.go index 8537e3898..80689541f 100644 --- a/internal/repo/badge/badge_repo.go +++ b/internal/repo/badge/badge_repo.go @@ -62,36 +62,6 @@ func (r *badgeRepo) GetByIDs(ctx context.Context, ids []string) (badges []*entit return } -// ListByLevel returns a list of badges by level -func (r *badgeRepo) ListByLevel(ctx context.Context, level entity.BadgeLevel) (badges []*entity.Badge, err error) { - badges = make([]*entity.Badge, 0) - err = r.data.DB.Context(ctx).Where("level = ?", level).Find(&badges) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// ListByGroup returns a list of badges by group -func (r *badgeRepo) ListByGroup(ctx context.Context, groupID int64) (badges []*entity.Badge, err error) { - badges = make([]*entity.Badge, 0) - err = r.data.DB.Context(ctx).Where("group_id = ?", groupID).Find(&badges) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - -// ListByLevelAndGroup returns a list of badges by level and group -func (r *badgeRepo) ListByLevelAndGroup(ctx context.Context, level entity.BadgeLevel, groupID int64) (badges []*entity.Badge, err error) { - badges = make([]*entity.Badge, 0) - err = r.data.DB.Context(ctx).Where("level = ? AND group_id = ?", level, groupID).Find(&badges) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - // ListPaged returns a list of activated badges func (r *badgeRepo) ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) { badges = make([]*entity.Badge, 0) @@ -146,12 +116,6 @@ func (r *badgeRepo) ListInactivated(ctx context.Context, page int, pageSize int) return } -// UpdateAwardCount updates the award count of a badge -func (r *badgeRepo) UpdateAwardCount(ctx context.Context, id string, count int64) (err error) { - _, err = r.data.DB.Context(ctx).Where("id = ?", id).Incr("award_count", count).Update(&entity.Badge{}) - return -} - // UpdateStatus updates the award count of a badge func (r *badgeRepo) UpdateStatus(ctx context.Context, id string, status int8) (err error) { _, err = r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 058fa8649..377677d19 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -100,12 +100,6 @@ func (r *badgeAwardRepo) CheckIsAward(ctx context.Context, badgeID, userID, awar return isAward, err } -func (r *badgeAwardRepo) CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) { - return -} -func (r *badgeAwardRepo) CountByUserId(ctx context.Context, userID string) (awardCount int64) { - return -} func (r *badgeAwardRepo) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) { awardCount, err := r.data.DB.Context(ctx).Where("user_id = ? AND badge_id = ?", userID, badgeID).Count(&entity.BadgeAward{}) if err != nil { @@ -113,22 +107,11 @@ func (r *badgeAwardRepo) CountByUserIdAndBadgeId(ctx context.Context, userID str } return } -func (r *badgeAwardRepo) CountByObjectId(ctx context.Context, awardKey string) (awardCount int64) { - return -} -func (r *badgeAwardRepo) CountByObjectIdAndBadgeId(ctx context.Context, awardKey string, badgeID string) (awardCount int64) { - return -} -func (r *badgeAwardRepo) CountBadgesByUserIdAndObjectId(ctx context.Context, userID string, awardKey string, badgeID string) (awardCount int64) { - return -} + func (r *badgeAwardRepo) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) { err = r.data.DB.Context(ctx).Select("badge_id, count(`id`) AS earned_count").Where("user_id = ?", userID).GroupBy("badge_id").Find(&earnedCounts) return } -func (r *badgeAwardRepo) ListAllByUserId(ctx context.Context, userID string) (badgeAwards []*entity.BadgeAward) { - return -} // ListPagedByBadgeId list badge awards by badge id func (r *badgeAwardRepo) ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) { @@ -151,21 +134,6 @@ func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeI } return } -func (r *badgeAwardRepo) ListPagedByObjectId(ctx context.Context, badgeID string, awardKey string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { - return -} -func (r *badgeAwardRepo) ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, awardKey string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { - return -} -func (r *badgeAwardRepo) ListTagPagedByBadgeId(ctx context.Context, badgeIDs []string, page int, pageSize int, filterUserID string) (badgeAwards []*entity.BadgeAward, total int64, err error) { - return -} -func (r *badgeAwardRepo) ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { - return -} -func (r *badgeAwardRepo) ListPagedLatest(ctx context.Context, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { - return -} // ListNewestEarned list newest earned badge awards func (r *badgeAwardRepo) ListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error) { @@ -179,13 +147,6 @@ func (r *badgeAwardRepo) ListNewestEarned(ctx context.Context, userID string, li return } -func (r *badgeAwardRepo) ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int) (badgeAwards []*entity.BadgeAward, total int64, err error) { - return -} -func (r *badgeAwardRepo) ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) { - return -} - // GetByUserIdAndBadgeId get badge award by user id and badge id func (r *badgeAwardRepo) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) ( badgeAward *entity.BadgeAward, exists bool, err error) { diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index dd78fd021..02cdcf1c8 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -41,26 +41,13 @@ type BadgeAwardRepo interface { CheckIsAward(ctx context.Context, badgeID string, userID string, awardKey string, singleOrMulti int8) (isAward bool, err error) AwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error) - CountByUserIdAndBadgeLevel(ctx context.Context, userID string, badgeLevel entity.BadgeLevel) (awardCount int64) - CountByUserId(ctx context.Context, userID string) (awardCount int64) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) - CountByObjectId(ctx context.Context, awardKey string) (awardCount int64) - CountByObjectIdAndBadgeId(ctx context.Context, awardKey string, badgeID string) (awardCount int64) - CountBadgesByUserIdAndObjectId(ctx context.Context, userID string, awardKey string, badgeID string) (awardCount int64) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) - ListAllByUserId(ctx context.Context, userID string) (badgeAwards []*entity.BadgeAward) ListPagedByBadgeId(ctx context.Context, badgeID string, page int, pageSize int) (badgeAwardList []*entity.BadgeAward, total int64, err error) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeID string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) - ListPagedByObjectId(ctx context.Context, badgeID string, awardKey string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) - ListPagedByObjectIdAndUserId(ctx context.Context, badgeID string, awardKey string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) - ListTagPagedByBadgeId(ctx context.Context, badgeIDs []string, page int, pageSize int, filterUserID string) (badgeAwards []*entity.BadgeAward, total int64, err error) - ListTagPagedByBadgeIdAndUserId(ctx context.Context, badgeIDs []string, userID string, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) - ListPagedLatest(ctx context.Context, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) ListNewestEarned(ctx context.Context, userID string, limit int) (badgeAwards []*entity.BadgeAwardRecent, err error) - ListNewestEarnedByLevel(ctx context.Context, userID string, level entity.BadgeLevel, num int) (badgeAwards []*entity.BadgeAward, total int64, err error) - ListNewestByUserIdAndLevel(ctx context.Context, userID string, level int, page int, pageSize int) (badgeAwards []*entity.BadgeAward, total int64, err error) GetByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (badgeAward *entity.BadgeAward, exists bool, err error) GetByUserIdAndBadgeIdAndAwardKey(ctx context.Context, userID string, badgeID string, awardKey string) (badgeAward *entity.BadgeAward, exists bool, err error) diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index acb68cff7..031f0f152 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -37,14 +37,10 @@ type BadgeRepo interface { GetByID(ctx context.Context, id string) (badge *entity.Badge, exists bool, err error) GetByIDs(ctx context.Context, ids []string) (badges []*entity.Badge, err error) - ListByLevel(ctx context.Context, level entity.BadgeLevel) ([]*entity.Badge, error) - ListByGroup(ctx context.Context, groupID int64) ([]*entity.Badge, error) - ListByLevelAndGroup(ctx context.Context, level entity.BadgeLevel, groupID int64) ([]*entity.Badge, error) ListPaged(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) ListActivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) - UpdateAwardCount(ctx context.Context, id string, count int64) (err error) UpdateStatus(ctx context.Context, id string, status int8) (err error) }