From fa19fe63c994486bfd34df941fabf1eeac5361ba Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 25 Jul 2024 16:40:04 +0800 Subject: [PATCH 001/129] 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 002/129] 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 003/129] 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 004/129] 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 005/129] 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 006/129] 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 007/129] 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 008/129] 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 009/129] 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 010/129] 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 011/129] 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 012/129] 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 013/129] 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 014/129] 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 d39c12f8f13c6a22509c77949a103b4ca20e0ecb Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Tue, 30 Jul 2024 10:47:12 +0800 Subject: [PATCH 015/129] 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 98a2de61f1a52a05547bfad48fd1c5a0c26434d4 Mon Sep 17 00:00:00 2001 From: Sonui Date: Wed, 31 Jul 2024 12:49:00 +0800 Subject: [PATCH 016/129] 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 2167a8243c5660d68ac504cae03d6bfd1d6fb842 Mon Sep 17 00:00:00 2001 From: Sonui Date: Thu, 1 Aug 2024 23:53:55 +0800 Subject: [PATCH 017/129] 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 8762f5b751a99da9e525b2f1c4625a5472813c4e Mon Sep 17 00:00:00 2001 From: Sonui Date: Fri, 2 Aug 2024 00:01:01 +0800 Subject: [PATCH 018/129] 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 5afa01ba8982ac91bac6e4d7ed399bd7e4fb98c8 Mon Sep 17 00:00:00 2001 From: Sonui Date: Fri, 2 Aug 2024 09:33:48 +0800 Subject: [PATCH 019/129] 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 11fb3aafd879c001cc98d11214406140a02d0daf 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 020/129] 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 c3a17046c6c3be1cec16ba49d07d9f7742b7260f Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 6 Aug 2024 16:43:13 +0800 Subject: [PATCH 021/129] fix: gavatar use sha256 for hash --- ui/package.json | 2 +- ui/pnpm-lock.yaml | 30 +++++-------------- ui/src/pages/Users/Settings/Profile/index.tsx | 4 +-- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/ui/package.json b/ui/package.json index 19e669818..8a087428c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -31,9 +31,9 @@ "diff": "^5.1.0", "front-matter": "^4.0.2", "i18next": "^21.9.0", + "js-sha256": "0.11.0", "lodash": "^4.17.21", "marked": "^4.0.19", - "md5": "^2.3.0", "next-share": "^0.18.1", "qrcode": "^1.5.1", "qs": "^6.11.0", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index b855bfa05..77bbee2d4 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -53,15 +53,15 @@ importers: i18next: specifier: ^21.9.0 version: 21.9.2 + js-sha256: + specifier: 0.11.0 + version: 0.11.0 lodash: specifier: ^4.17.21 version: 4.17.21 marked: specifier: ^4.0.19 version: 4.1.0 - md5: - specifier: ^2.3.0 - version: 2.3.0 next-share: specifier: ^0.18.1 version: 0.18.1(react-dom@18.2.0)(react-scripts@5.0.1)(react@18.2.0) @@ -4642,10 +4642,6 @@ packages: resolution: {integrity: sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==} engines: {node: '>=12.20'} - /charenc@0.0.2: - resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - dev: false - /check-types@11.1.2: resolution: {integrity: sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==} @@ -4977,10 +4973,6 @@ packages: shebang-command: 2.0.0 which: 2.0.2 - /crypt@0.0.2: - resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} - dev: false - /crypto-random-string@2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -7207,10 +7199,6 @@ packages: call-bind: 1.0.5 has-tostringtag: 1.0.0 - /is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - dev: false - /is-callable@1.2.6: resolution: {integrity: sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q==} engines: {node: '>= 0.4'} @@ -8017,6 +8005,10 @@ packages: - ts-node - utf-8-validate + /js-sha256@0.11.0: + resolution: {integrity: sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==} + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -8362,14 +8354,6 @@ packages: hasBin: true dev: false - /md5@2.3.0: - resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - is-buffer: 1.1.6 - dev: false - /mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index ab2ee0f1a..9af28ce6e 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -21,7 +21,7 @@ import React, { FormEvent, useState, useEffect } from 'react'; import { Form, Button, Stack, ButtonGroup } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import MD5 from 'md5'; +import { sha256 } from 'js-sha256'; import type { FormDataType } from '@/common/interface'; import { UploadImg, Avatar, Icon, ImgViewer } from '@/components'; @@ -273,7 +273,7 @@ const Index: React.FC = () => { setFormData({ ...formData }); if (res.e_mail) { const str = res.e_mail.toLowerCase().trim(); - const hash = MD5(str); + const hash = sha256(str); setMailHash(hash); } }); 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 022/129] 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 023/129] 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 024/129] 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 025/129] 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 2e626660e671b39d06324139341b5e326d4c7d9d Mon Sep 17 00:00:00 2001 From: "James Roller, Jr." Date: Mon, 5 Aug 2024 20:10:12 -0700 Subject: [PATCH 026/129] add pvc dataSource - allow for restoring a volumeSnapshot --- charts/templates/pvc.yaml | 8 +++++++- charts/values.yaml | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/charts/templates/pvc.yaml b/charts/templates/pvc.yaml index 5dfab8fca..640fb9fe0 100644 --- a/charts/templates/pvc.yaml +++ b/charts/templates/pvc.yaml @@ -20,7 +20,7 @@ kind: PersistentVolumeClaim apiVersion: v1 metadata: name: {{ include "answer.fullname" . }}-claim - {{- with .Values.persistence.annotations }} + {{- with .Values.persistence.annotations }} annotations: {{ toYaml . | indent 4 }} {{- end }} @@ -39,4 +39,10 @@ spec: resources: requests: storage: {{ .Values.persistence.size | quote }} + {{- with .Values.persistence.dataSource }} + dataSource: + name: {{ .name }} + kind: {{ .kind | default "VolumeSnapshot" }} + apiGroup: {{ .apiGroup | default "snapshot.storage.k8s.io" }} + {{- end }} {{- end }} \ No newline at end of file diff --git a/charts/values.yaml b/charts/values.yaml index 8bb36c46f..d932db848 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -68,7 +68,7 @@ env: # Configure extra containers extraContainers: [] - # - name: cloudsql-proxy + # - name: cloudsql-proxy # image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.1.2 # command: # - /cloud-sql-proxy @@ -91,6 +91,12 @@ persistence: accessMode: ReadWriteOnce size: 5Gi annotations: {} + # To restore a PVC from a VolumeSnapshot, set the dataSource; + # the kind and apiGroup are optional and default to the shown values + dataSource: {} + # name: my-volume-snapshot + # kind: VolumeSnapshot + # apiGroup: snapshot.storage.k8s.io imagePullSecrets: [] nameOverride: "" From 07e4f549b13216f76a69a897fb73d2a8fcd55de4 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Thu, 8 Aug 2024 16:19:47 +0800 Subject: [PATCH 027/129] 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 028/129] 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 029/129] 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 030/129] 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 031/129] 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 032/129] 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 033/129] 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 034/129] 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 035/129] 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 036/129] 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 037/129] 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 038/129] 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 039/129] 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 040/129] 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 041/129] 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 042/129] 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 043/129] 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 044/129] 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 045/129] 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 5e5551a16fb67e92ba8fed3c32432123bc81319f Mon Sep 17 00:00:00 2001 From: Sonui Date: Wed, 7 Aug 2024 22:51:23 +0800 Subject: [PATCH 046/129] perf: remove invalid nil pointer check in Translate function --- plugin/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/plugin.go b/plugin/plugin.go index 61ed0d8f7..852868557 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -219,7 +219,7 @@ func MakeTranslator(key string) Translator { // Translate translates the key to the current language of the context func (t Translator) Translate(ctx *GinContext) string { - if &t == nil || t.Fn == nil { + if t.Fn == nil { return "" } return t.Fn(ctx) From 7d5bfaa3fbbfc65efee99515c32902190c058917 Mon Sep 17 00:00:00 2001 From: Sonui Date: Wed, 14 Aug 2024 00:21:55 +0800 Subject: [PATCH 047/129] perf: update check-asf-header.sh script to support Podman --- script/check-asf-header.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/script/check-asf-header.sh b/script/check-asf-header.sh index ff765eab5..808efa108 100755 --- a/script/check-asf-header.sh +++ b/script/check-asf-header.sh @@ -16,5 +16,16 @@ # specific language governing permissions and limitations # under the License. -docker run -it --rm -v $(pwd):/github/workspace ghcr.io/korandoru/hawkeye-native format +# check if docker or podman is installed +if command -v docker >/dev/null 2>&1; then + CONTAINER_RUNTIME="docker" +elif command -v podman >/dev/null 2>&1; then + CONTAINER_RUNTIME="podman" +else + echo "Neither Docker nor Podman is installed. Please install either Docker or Podman." + exit 1 +fi + +$CONTAINER_RUNTIME run -it --rm -v "$(pwd)":/github/workspace ghcr.io/korandoru/hawkeye-native format + gofmt -w -l . From d63ce3673b98a52ef5ece8e297a74b52134ab21f Mon Sep 17 00:00:00 2001 From: Sonui Date: Thu, 15 Aug 2024 21:15:02 +0800 Subject: [PATCH 048/129] perf: remove extra characters in header.html --- ui/template/header.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/template/header.html b/ui/template/header.html index 653f9a28a..0243a43d7 100644 --- a/ui/template/header.html +++ b/ui/template/header.html @@ -68,14 +68,14 @@ From ebc858579b65c8b4a4496530477adf4e12d39f6d Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 19 Aug 2024 14:10:13 +0800 Subject: [PATCH 049/129] 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) } From c50e63dd34ce9ddb7babd769146278ef41e5bfcb Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 19 Aug 2024 15:15:07 +0800 Subject: [PATCH 050/129] fix(dev): delete test file --- dev/i18n/zh_CN.yaml | 2035 ------------------------------------------- 1 file changed, 2035 deletions(-) delete mode 100644 dev/i18n/zh_CN.yaml diff --git a/dev/i18n/zh_CN.yaml b/dev/i18n/zh_CN.yaml deleted file mode 100644 index 6613bfc22..000000000 --- a/dev/i18n/zh_CN.yaml +++ /dev/null @@ -1,2035 +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. - -# The following fields are used for back-end -backend: - base: - success: - other: 成功。 - unknown: - other: 未知错误。 - request_format_error: - other: 请求格式错误。 - unauthorized_error: - other: 未授权。 - database_error: - other: 数据服务器错误。 - forbidden_error: - other: 禁止访问。 - duplicate_request_error: - other: 重复提交。 - action: - report: - other: 举报 - edit: - other: 编辑 - delete: - other: 删除 - close: - other: 关闭 - reopen: - other: 重新打开 - forbidden_error: - other: 禁止访问。 - pin: - other: 置顶 - hide: - other: 列表隐藏 - unpin: - other: 取消置顶 - show: - other: 列表显示 - invite_someone_to_answer: - other: 编辑 - undelete: - other: 撤消删除 - role: - name: - user: - other: 用户 - admin: - other: 管理员 - moderator: - other: 版主 - description: - user: - other: 默认没有特殊权限。 - admin: - other: 拥有管理网站的全部权限。 - moderator: - other: 拥有除访问后台管理以外的所有权限。 - privilege: - level_1: - description: - other: 级别 1(少量声望要求,适合私有团队、群组) - level_2: - description: - other: 级别 2(低声望要求,适合初启动的社区) - level_3: - description: - other: 级别 3(高声望要求,适合成熟的社区) - level_custom: - description: - other: 自定义等级 - rank_question_add_label: - other: 提问 - rank_answer_add_label: - other: 写答案 - rank_comment_add_label: - other: 写评论 - rank_report_add_label: - other: 举报 - rank_comment_vote_up_label: - other: 点赞评论 - rank_link_url_limit_label: - other: 每次发布超过 2 个链接 - rank_question_vote_up_label: - other: 点赞问题 - rank_answer_vote_up_label: - other: 点赞答案 - rank_question_vote_down_label: - other: 点踩问题 - rank_answer_vote_down_label: - other: 点踩答案 - rank_invite_someone_to_answer_label: - other: 邀请回答 - rank_tag_add_label: - other: 创建新标签 - rank_tag_edit_label: - other: 编辑标签描述(需要审核) - rank_question_edit_label: - other: 编辑别人的问题(需要审核) - rank_answer_edit_label: - other: 编辑别人的答案(需要审核) - rank_question_edit_without_review_label: - other: 编辑别人的问题无需审核 - rank_answer_edit_without_review_label: - other: 编辑别人的答案无需审核 - rank_question_audit_label: - other: 审核问题编辑 - rank_answer_audit_label: - other: 审核回答编辑 - rank_tag_audit_label: - other: 审核标签编辑 - rank_tag_edit_without_review_label: - other: 编辑标签描述无需审核 - rank_tag_synonym_label: - other: 管理标签同义词 - email: - other: 邮箱 - e_mail: - other: 邮箱 - password: - other: 密码 - pass: - other: 密码 - original_text: - other: 本帖 - email_or_password_wrong_error: - other: 邮箱和密码不匹配。 - error: - common: - invalid_url: - other: 无效的 URL。 - password: - space_invalid: - other: 密码不得含有空格。 - admin: - cannot_update_their_password: - other: 你无法修改自己的密码。 - cannot_edit_their_profile: - other: 您不能修改您的个人资料。 - cannot_modify_self_status: - other: 你无法修改自己的状态。 - email_or_password_wrong: - other: 邮箱和密码不匹配。 - answer: - not_found: - other: 没有找到答案。 - cannot_deleted: - other: 没有删除权限。 - cannot_update: - other: 没有更新权限。 - question_closed_cannot_add: - other: 问题已关闭,无法添加。 - comment: - edit_without_permission: - other: 不允许编辑评论。 - not_found: - other: 评论未找到。 - cannot_edit_after_deadline: - other: 评论时间太久,无法修改。 - email: - duplicate: - other: 邮箱已存在。 - need_to_be_verified: - other: 邮箱需要验证。 - verify_url_expired: - other: 邮箱验证的网址已过期,请重新发送邮件。 - illegal_email_domain_error: - other: 此邮箱不在允许注册的邮箱域中。请使用其他邮箱尝试。 - lang: - not_found: - other: 语言文件未找到。 - object: - captcha_verification_failed: - other: 验证码错误。 - disallow_follow: - other: 你不能关注。 - disallow_vote: - other: 你不能投票。 - disallow_vote_your_self: - other: 你不能为自己的帖子投票。 - not_found: - other: 对象未找到。 - verification_failed: - other: 验证失败。 - email_or_password_incorrect: - other: 邮箱和密码不匹配。 - old_password_verification_failed: - other: 旧密码验证失败。 - new_password_same_as_previous_setting: - other: 新密码和旧密码相同。 - already_deleted: - other: 该帖子已被删除。 - meta: - object_not_found: - other: Meta 对象未找到 - question: - already_deleted: - other: 该帖子已被删除。 - under_review: - other: 您的帖子正在等待审核。它将在它获得批准后可见。 - not_found: - other: 问题未找到。 - cannot_deleted: - other: 没有删除权限。 - cannot_close: - other: 没有关闭权限。 - cannot_update: - other: 没有更新权限。 - rank: - fail_to_meet_the_condition: - other: 声望值未达到要求。 - vote_fail_to_meet_the_condition: - other: 感谢投票。你至少需要 {{.Rank}} 声望才能投票。 - no_enough_rank_to_operate: - other: 你至少需要 {{.Rank}} 声望才能执行此操作。 - report: - handle_failed: - other: 报告处理失败。 - not_found: - other: 报告未找到。 - tag: - already_exist: - other: 标签已存在。 - not_found: - other: 标签未找到。 - recommend_tag_not_found: - other: 推荐标签不存在。 - recommend_tag_enter: - other: 请选择至少一个必选标签。 - not_contain_synonym_tags: - other: 不应包含同义词标签。 - cannot_update: - other: 没有更新权限。 - is_used_cannot_delete: - other: 你不能删除这个正在使用的标签。 - cannot_set_synonym_as_itself: - other: 你不能将当前标签设为自己的同义词。 - smtp: - config_from_name_cannot_be_email: - other: 发件人名称不能是邮箱地址。 - theme: - not_found: - other: 主题未找到。 - revision: - review_underway: - other: 目前无法编辑,有一个版本在审阅队列中。 - no_permission: - other: 无权限修改。 - user: - external_login_missing_user_id: - other: 第三方平台没有提供唯一的 UserID,所以你不能登录,请联系网站管理员。 - external_login_unbinding_forbidden: - other: 请在移除此登录之前为你的账户设置登录密码。 - email_or_password_wrong: - other: - other: 邮箱和密码不匹配。 - not_found: - other: 用户未找到。 - suspended: - other: 用户已被封禁。 - username_invalid: - other: 用户名无效。 - username_duplicate: - other: 用户名已被使用。 - set_avatar: - other: 头像设置错误。 - cannot_update_your_role: - other: 你不能修改自己的角色。 - not_allowed_registration: - other: 该网站暂未开放注册。 - not_allowed_login_via_password: - other: 该网站暂不支持密码登录。 - access_denied: - other: 拒绝访问 - page_access_denied: - other: 您没有权限访问此页面。 - add_bulk_users_format_error: - other: "发生错误,{{.Field}} 格式错误,在 '{{.Content}}' 行数 {{.Line}}. {{.ExtraMessage}}" - add_bulk_users_amount_error: - other: "一次性添加的用户数量应在 1-{{.MaxAmount}} 之间。" - config: - read_config_failed: - other: 读取配置失败 - database: - connection_failed: - other: 数据库连接失败 - create_table_failed: - other: 创建表失败 - install: - create_config_failed: - other: 无法创建 config.yaml 文件。 - upload: - unsupported_file_format: - other: 不支持的文件格式。 - site_info: - config_not_found: - other: 未找到网站的该配置信息。 - reason: - spam: - name: - other: 垃圾信息 - desc: - other: 这个帖子是一个广告,或是破坏性行为。它对当前的主题无帮助或无关。 - rude_or_abusive: - name: - other: 粗鲁或辱骂的 - desc: - other: - - 一个有理智的人都会认为这种内容不适合进行尊重性的讨论。 - - 论坛 - a_duplicate: - name: - other: 重复内容 - desc: - other: 该问题有人问过,而且已经有了答案。 - placeholder: - other: 输入已有的问题链接 - not_a_answer: - name: - other: 不是答案 - desc: - other: - - 这张贴作为答案,但它不会试图回答 - - 这可能是一个编辑、一个评论、另一个问题。 - - 或全部删除。 - no_longer_needed: - name: - other: 不再需要 - desc: - other: 该评论已过时,对话性质或与此帖子无关。 - something: - name: - other: 其他原因 - desc: - other: 此帖子需要工作人员注意,因为是上述所列以外的其他理由。 - placeholder: - other: 让我们具体知道你关心的什么 - community_specific: - name: - other: 社区特定原因 - desc: - other: 该问题不符合社区准则。 - not_clarity: - name: - other: 需要细节或澄清 - desc: - other: 该问题目前涵盖多个问题。它应该侧重在一个问题上。 - looks_ok: - name: - other: 看起来没问题 - desc: - other: 这个帖子是好的,不是低质量。 - needs_edit: - name: - other: 需要编辑,我已做了修改。 - desc: - other: 改进和纠正你自己帖子中的问题。 - needs_close: - name: - other: 需要关闭 - desc: - other: 关闭的问题不能回答,但仍然可以编辑、投票和评论。 - needs_delete: - name: - other: 需要删除 - desc: - other: 该帖子将被删除。 - question: - close: - duplicate: - name: - other: 垃圾信息 - desc: - other: 此问题以前就有人问过,而且已经有了答案。 - guideline: - name: - other: 社区特定原因 - desc: - other: 该问题不符合社区准则。 - multiple: - name: - other: 需要细节或澄清 - desc: - other: - - 该问题目前涵盖多个问题。它应该侧重在一个问题上。 - - 只关注一个问题。 - other: - name: - other: 其他原因 - desc: - other: 该帖子存在上面没有列出的另一个原因。 - operation_type: - asked: - other: 提问于 - answered: - other: 回答于 - modified: - other: 修改于 - deleted_title: - other: 删除的问题 - notification: - action: - update_question: - other: 更新了问题 - answer_the_question: - other: 回答了问题 - update_answer: - other: 更新了答案 - accept_answer: - other: 采纳了答案 - comment_question: - other: 评论了问题 - comment_answer: - other: 评论了答案 - reply_to_you: - other: 回复了你 - mention_you: - other: 提到了你 - your_question_is_closed: - other: 你的问题已被关闭 - your_question_was_deleted: - other: 你的问题已被删除 - your_answer_was_deleted: - other: 你的答案已被删除 - your_comment_was_deleted: - other: 你的评论已被删除 - up_voted_question: - other: 点赞问题 - down_voted_question: - other: 点踩问题 - up_voted_answer: - other: 点赞答案 - down_voted_answer: - other: 点踩回答 - up_voted_comment: - other: 点赞评论 - invited_you_to_answer: - other: 邀请你回答 - email_tpl: - change_email: - title: - other: "[{{.SiteName}}] 确认你的新邮箱地址" - body: - other: "请点击以下链接确认你在 {{.SiteName}} 上的新邮箱地址:
\n{{.ChangeEmailUrl}}

\n\n如果你没有请求此更改,请忽略此邮件。\n" - new_answer: - title: - other: "[{{.SiteName}}] {{.DisplayName}} 回答了你的问题" - body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" - invited_you_to_answer: - title: - other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题" - body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
我想你可能知道答案。

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" - new_comment: - title: - other: "[{{.SiteName}}] {{.DisplayName}} 评论了你的帖子" - body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" - new_question: - title: - other: "[{{.SiteName}}] 新问题: {{.QuestionTitle}}" - body: - other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\n取消订阅" - pass_reset: - title: - other: "[{{.SiteName }}] 重置密码" - body: - other: "有人要求在 [{{.SiteName}}] 上重置你的密码。

\n\n如果这不是你的操作,请安心忽略此电子邮件。

\n\n请点击以下链接设置一个新密码:
\n{{.PassResetUrl}}\n" - register: - title: - other: "[{{.SiteName}}] 确认你的新账户" - body: - other: "欢迎加入 {{.SiteName}}!

\n\n请点击以下链接确认并激活你的新账户:
\n{{.RegisterUrl}}

\n\n如果上面的链接不能点击,请将其复制并粘贴到你的浏览器地址栏中。\n" - test: - title: - other: "[{{.SiteName}}] 测试邮件" - body: - other: "这是一封测试邮件。" - action_activity_type: - upvote: - other: 点赞 - upvoted: - other: 点赞 - downvote: - other: 点踩 - downvoted: - other: 点踩 - accept: - other: 采纳 - accepted: - other: 已采纳 - edit: - other: 编辑 - review: - queued_post: - other: 排队的帖子 - flagged_post: - other: 举报的帖子 - suggested_post_edit: - other: 建议的编辑 - reaction: - tooltip: - other: "{{ .Names }} 以及另外 {{ .Count }} 个..." -plugin: - s3_cdn: - backend: - info: - name: - other: S3 存储 CDN - description: - other: 上传文件到S3存储 - config: - endpoint: - title: - other: Endpoint - description: - other: S3存储的Endpoint - bucket_name: - title: - other: Bucket名称 - description: - other: S3存储的Bucket名称 - object_key_prefix: - title: - other: 对象Key前缀 - description: - other: 对象键的前缀,如'answer/data/',以'/'结尾 - access_key_id: - title: - other: AccessKeyID - description: - other: S3存储的AccessKeyID - access_key_secret: - title: - other: AccessKeySecret - description: - other: S3存储的AccessKeySecret - access_token: - title: - other: AccessToken - description: - other: 访问 S3 所需的 AccessToken - visit_url_prefix: - title: - other: 访问URL前缀 - description: - other: 上传静态文件CDN最终访问地址的前缀,以 '/' 结尾 https://static.example.com/xxx/ - max_file_size: - title: - other: 文件最大大小(MB) - description: - other: 限制上传文件的最大大小,单位MB,默认为10MB - region: - title: - other: 区域(Region) - description: - other: S3存储区域 - disable_ssl: - title: - other: 禁用SSL - description: - other: 我们建议您使用SSL访问S3存储。如果您想禁用SSL,请选中此选项。 - err: - mis_storage_config: - other: 错误的存储配置导致上传失败 - file_not_found: - other: 文件未找到 - unsupported_file_type: - other: 不支持的文件类型 - over_file_size_limit: - other: 超过文件大小限制 - upload_file_failed: - other: 上传文件失败 - aliyunoss_cdn: - backend: - info: - name: - other: 阿里云CDN OSS存储 - description: - other: 上传文件到阿里云CDN OSS存储 - config: - endpoint: - title: - other: Endpoint - description: - other: 阿里云OSS存储的Endpoint - bucket_name: - title: - other: Bucket名称 - description: - other: 阿里云OSS存储的Bucket名称 - object_key_prefix: - title: - other: 对象Key前缀 - description: - other: 对象键的前缀,如'answer/data/',以'/'结尾 - access_key_id: - title: - other: AccessKeyID - description: - other: 阿里云OSS存储的AccessKeyID - access_key_secret: - title: - other: AccessKeySecret - description: - other: 阿里云OSS存储的AccessKeySecret - visit_url_prefix: - title: - other: 访问URL前缀 - description: - other: CDN最终访问地址的前缀,以 '/' 结尾 https://example.com/xxx/ - max_file_size: - title: - other: 最大文件大小(MB) - description: - other: 限制上传文件的最大大小,单位为MB,默认为 10MB - err: - mis_storage_config: - other: 错误的存储配置导致上传失败 - file_not_found: - other: 文件未找到 - unsupported_file_type: - other: 不支持的文件类型 - over_file_size_limit: - other: 超过文件大小限制 - upload_file_failed: - other: 上传文件失败 -# The following fields are used for interface presentation(Front-end) -ui: - how_to_format: - title: 如何排版 - desc: >- -
  • 添加链接

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 **粗体**

  • 使用 4 个空格缩进代码

  • 在行首添加 > 表示引用

  • 反引号进行转义 `像 _这样_`

  • 使用 ``` 创建代码块

    ```
    这是代码块
    ```
- pagination: - prev: 上一页 - next: 下一页 - page_title: - question: 问题 - questions: 问题 - tag: 标签 - tags: 标签 - tag_wiki: 标签维基 - create_tag: 创建标签 - edit_tag: 编辑标签 - ask_a_question: 提问题 - edit_question: 编辑问题 - edit_answer: 编辑回答 - search: 搜索 - posts_containing: 帖子包含 - settings: 设置 - notifications: 通知 - login: 登录 - sign_up: 注册 - account_recovery: 账号恢复 - account_activation: 账号激活 - confirm_email: 确认电子邮件 - account_suspended: 账号已被封禁 - admin: 后台管理 - change_email: 修改邮箱 - install: Answer 安装 - upgrade: Answer 升级 - maintenance: 网站维护 - users: 用户 - oauth_callback: 处理中 - http_404: HTTP 错误 404 - http_50X: HTTP 错误 500 - http_403: HTTP 错误 403 - logout: 退出 - notifications: - title: 通知 - inbox: 收件箱 - achievement: 成就 - new_alerts: 新通知 - all_read: 全部标记为已读 - show_more: 显示更多 - someone: 有人 - inbox_type: - all: 全部 - posts: 帖子 - invites: 邀请 - votes: 投票 - suspended: - title: 你的账号账号已被封禁 - until_time: "你的账号被封禁直到 {{ time }}。" - forever: 你的账号已被永久封禁。 - end: 你违反了我们的社区准则。 - contact_us: 联系我们 - editor: - blockquote: - text: 引用 - bold: - text: 粗体 - chart: - text: 图表 - flow_chart: 流程图 - sequence_diagram: 时序图 - class_diagram: 类图 - state_diagram: 状态图 - entity_relationship_diagram: 实体关系图 - user_defined_diagram: 用户自定义图表 - gantt_chart: 甘特图 - pie_chart: 饼图 - code: - text: 代码块 - add_code: 添加代码块 - form: - fields: - code: - label: 代码块 - msg: - empty: 代码块不能为空 - language: - label: 语言 - placeholder: 自动识别 - btn_cancel: 取消 - btn_confirm: 添加 - formula: - text: 公式 - options: - inline: 行内公式 - block: 块级公式 - heading: - text: 标题 - options: - h1: 标题 1 - h2: 标题 2 - h3: 标题 3 - h4: 标题 4 - h5: 标题 5 - h6: 标题 6 - help: - text: 帮助 - hr: - text: 水平线 - image: - text: 图片 - add_image: 添加图片 - tab_image: 上传图片 - form_image: - fields: - file: - label: 图像文件 - btn: 选择图片 - msg: - empty: 请选择图片文件。 - only_image: 只能上传图片文件。 - max_size: 文件大小不能超过 4 MB。 - desc: - label: 描述 - tab_url: 图片地址 - form_url: - fields: - url: - label: 图片地址 - msg: - empty: 图片地址不能为空 - name: - label: 描述 - btn_cancel: 取消 - btn_confirm: 添加 - uploading: 上传中 - indent: - text: 缩进 - outdent: - text: 减少缩进 - italic: - text: 斜体 - link: - text: 超链接 - add_link: 添加超链接 - form: - fields: - url: - label: 链接 - msg: - empty: 链接不能为空。 - name: - label: 描述 - btn_cancel: 取消 - btn_confirm: 添加 - ordered_list: - text: 有序列表 - unordered_list: - text: 无序列表 - table: - text: 表格 - heading: 表头 - cell: 单元格 - close_modal: - title: 关闭原因是... - btn_cancel: 取消 - btn_submit: 提交 - remark: - empty: 不能为空。 - msg: - empty: 请选择一个原因。 - report_modal: - flag_title: 我举报这篇帖子的原因是... - close_title: 我关闭这篇帖子的原因是... - review_question_title: 审查问题 - review_answer_title: 审查回答 - review_comment_title: 审查评论 - btn_cancel: 取消 - btn_submit: 提交 - remark: - empty: 不能为空 - msg: - empty: 请选择一个原因。 - not_a_url: URL 格式不正确。 - url_not_match: URL 来源与当前网站不匹配。 - tag_modal: - title: 创建新标签 - form: - fields: - display_name: - label: 显示名称 - msg: - empty: 显示名称不能为空。 - range: 显示名称不能超过 35 个字符。 - slug_name: - label: URL 固定链接 - desc: URL 固定链接不能超过 35 个字符。 - msg: - empty: URL 固定链接不能为空。 - range: URL 固定链接不能超过 35 个字符。 - character: URL 固定链接包含非法字符。 - desc: - label: 描述 - revision: - label: 编辑历史 - edit_summary: - label: 编辑备注 - placeholder: >- - 简单描述更改原因(更正拼写、修复语法、改进格式) - btn_cancel: 取消 - btn_submit: 提交 - btn_post: 发布新标签 - tag_info: - created_at: 创建于 - edited_at: 编辑于 - history: 历史 - synonyms: - title: 同义词 - text: 以下标签将被重置到 - empty: 此标签目前没有同义词。 - btn_add: 添加同义词 - btn_edit: 编辑 - btn_save: 保存 - synonyms_text: 以下标签将被重置到 - delete: - title: 删除标签 - tip_with_posts: >- -

我们不允许 删除带有帖子的标签

请先从帖子中移除此标签。

- tip_with_synonyms: >- -

我们不允许 删除带有同义词的标签

请先从此标签中删除同义词。

- tip: 确定要删除吗? - close: 关闭 - edit_tag: - title: 编辑标签 - default_reason: 编辑标签 - default_first_reason: 添加标签 - btn_save_edits: 保存更改 - btn_cancel: 取消 - dates: - long_date: MM 月 DD 日 - long_date_with_year: "YYYY 年 MM 月 DD 日" - long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm" - now: 刚刚 - x_seconds_ago: "{{count}} 秒前" - x_minutes_ago: "{{count}} 分钟前" - x_hours_ago: "{{count}} 小时前" - hour: 小时 - day: 天 - hours: 小时 - days: 日 - reaction: - heart: 爱心 - smile: 微笑 - frown: 愁 - btn_label: 添加或删除回应。 - undo_emoji: 撤销 {{ emoji }} 回应 - react_emoji: 用 {{ emoji }} 回应 - unreact_emoji: 撤销 {{ emoji }} - comment: - btn_add_comment: 添加评论 - reply_to: 回复 - btn_reply: 回复 - btn_edit: 编辑 - btn_delete: 删除 - btn_flag: 举报 - btn_save_edits: 保存更改 - btn_cancel: 取消 - show_more: "{{count}} 条剩余评论" - tip_question: >- - 使用评论提问更多信息或者提出改进意见。避免在评论里回答问题。 - tip_answer: >- - 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 - tip_vote: 它给帖子添加了一些有用的内容 - edit_answer: - title: 编辑回答 - default_reason: 编辑回答 - default_first_reason: 添加答案 - form: - fields: - revision: - label: 编辑历史 - answer: - label: 回答内容 - feedback: - characters: 内容长度至少 6 个字符 - edit_summary: - label: 编辑摘要 - placeholder: >- - 简单描述更改原因(更正拼写、修复语法、改进格式) - btn_save_edits: 保存更改 - btn_cancel: 取消 - tags: - title: 标签 - sort_buttons: - popular: 热门 - name: 名称 - newest: 最新 - button_follow: 关注 - button_following: 已关注 - tag_label: 个问题 - search_placeholder: 通过标签名称过滤 - no_desc: 此标签无描述。 - more: 更多 - ask: - title: 新增问题 - edit_title: 编辑问题 - default_reason: 编辑问题 - default_first_reason: 新增问题 - similar_questions: 相似问题 - form: - fields: - revision: - label: 修订版本 - title: - label: 标题 - placeholder: 请详细描述你的问题,想象你在问一个人 - msg: - empty: 标题不能为空。 - range: 标题最多 150 个字符 - body: - label: 内容 - msg: - empty: 内容不能为空。 - tags: - label: 标签 - msg: - empty: 必须选择一个标签 - answer: - label: 回答内容 - msg: - empty: 回答内容不能为空 - edit_summary: - label: 编辑备注 - placeholder: >- - 简单描述更改原因(更正拼写、修复语法、改进格式) - btn_post_question: 提交问题 - btn_save_edits: 保存更改 - answer_question: 回答自己的问题 - post_question&answer: 提交问题和回答 - tag_selector: - add_btn: 添加标签 - create_btn: 创建新标签 - search_tag: 搜索标签 - hint: "描述您的问题是关于什么,至少需要一个标签。" - no_result: 没有匹配的标签 - tag_required_text: 必选标签(至少一个) - header: - nav: - question: 问题 - tag: 标签 - user: 用户 - profile: 用户主页 - setting: 账号设置 - logout: 退出 - admin: 后台管理 - review: 审查 - bookmark: 收藏夹 - moderation: 管理 - search: - placeholder: 搜索 - footer: - build_on: >- - 由 <1>Apache Answer 提供动力 - 驱动问答社区的开源软件。
用爱制造 © {{cc}}. - upload_img: - name: 更改 - loading: 加载中... - pic_auth_code: - title: 验证码 - placeholder: 输入图片中的文字 - msg: - empty: 验证码不能为空。 - inactive: - first: >- - 就差一步!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活你的账户。 - info: "如果没有收到,请检查你的垃圾邮件文件夹。" - another: >- - 我们向你的邮箱 {{mail}} 发送了另一封激活电子邮件。可能需要几分钟才能到达;请务必检查您的垃圾邮件箱。 - btn_name: 重新发送激活邮件 - change_btn_name: 更改邮箱 - msg: - empty: 不能为空。 - resend_email: - url_label: 确定要重新发送激活邮件吗? - url_text: 你也可以将上面的激活链接给该用户。 - login: - login_to_continue: 登录以继续 - info_sign: 没有账户?<1>注册 - info_login: 已经有账户?<1>登录 - agreements: 登录即表示您同意<1>隐私政策和<3>服务条款。 - forgot_pass: 忘记密码? - name: - label: 名字 - msg: - empty: 名字不能为空 - range: 名字应该在 4 到 30 个字符之间 - character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' - email: - label: 邮箱 - msg: - empty: 邮箱不能为空 - password: - label: 密码 - msg: - empty: 密码不能为空 - different: 两次输入密码不一致 - account_forgot: - page_title: 忘记密码 - btn_name: 发送恢复邮件 - send_success: >- - 如果存在邮箱为 {{mail}} 账户,你将很快收到一封重置密码的说明邮件。 - email: - label: 邮箱 - msg: - empty: 邮箱不能为空 - change_email: - btn_cancel: 取消 - btn_update: 更新电子邮件地址 - send_success: >- - 如果存在邮箱为 {{mail}} 的账户,你将很快收到一封重置密码的说明邮件。 - email: - label: 新的电子邮件地址 - msg: - empty: 邮箱不能为空。 - oauth: - connect: 连接到 {{ auth_name }} - remove: 移除 {{ auth_name }} - oauth_bind_email: - subtitle: 向你的账户添加恢复邮件地址。 - btn_update: 更新电子邮件地址 - email: - label: 邮箱 - msg: - empty: 邮箱不能为空。 - modal_title: 邮箱已经存在。 - modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? - modal_cancel: 更改邮箱 - modal_confirm: 连接到已有账户 - password_reset: - page_title: 密码重置 - btn_name: 重置我的密码 - reset_success: >- - 你已经成功更改密码;你将被重定向到登录页面。 - link_invalid: >- - 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? - to_login: 前往登录页面 - password: - label: 密码 - msg: - empty: 密码不能为空。 - length: 密码长度在8-32个字符之间 - different: 两次输入密码不一致 - password_confirm: - label: 确认新密码 - settings: - page_title: 设置 - goto_modify: 前往修改 - nav: - profile: 我的资料 - notification: 通知 - account: 账号 - interface: 界面 - profile: - heading: 个人资料 - btn_name: 保存 - display_name: - label: 显示名称 - msg: 昵称不能为空。 - msg_range: 显示名称不能超过 30 个字符。 - username: - label: 用户名 - caption: 用户可以通过 "@用户名" 来提及你。 - msg: 用户名不能为空 - msg_range: 用户名不能超过 30 个字符。 - character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' - avatar: - label: 头像 - gravatar: Gravatar - gravatar_text: 你可以更改图像在 - custom: 自定义 - custom_text: 你可以上传你的图片。 - default: 系统 - msg: 请上传头像 - bio: - label: 关于我 - website: - label: 网站 - placeholder: "https://example.com" - msg: 网址格式不正确 - location: - label: 位置 - placeholder: "城市,国家" - notification: - heading: 邮件通知 - turn_on: 开启 - inbox: - label: 收件箱通知 - description: 你的提问有新的回答,评论,邀请回答和其他。 - all_new_question: - label: 所有新问题 - description: 获取所有新问题的通知。每周最多有50个问题。 - all_new_question_for_following_tags: - label: 所有关注标签的新问题 - description: 获取关注的标签下新问题通知。 - account: - heading: 账号 - change_email_btn: 更改邮箱 - change_pass_btn: 更改密码 - change_email_info: >- - 邮件已发送。请根据指引完成验证。 - email: - label: 电子邮件地址 - new_email: - label: 新的电子邮件地址 - msg: 新邮箱不能为空。 - pass: - label: 当前密码 - msg: 密码不能为空。 - password_title: 密码 - current_pass: - label: 当前密码 - msg: - empty: 当前密码不能为空 - length: 密码长度必须在 8 至 32 之间 - different: 两次输入的密码不匹配 - new_pass: - label: 新密码 - pass_confirm: - label: 确认新密码 - interface: - heading: 界面 - lang: - label: 界面语言 - text: 设置用户界面语言,在刷新页面后生效。 - my_logins: - title: 我的登录 - label: 使用这些账户登录或注册本网站。 - modal_title: 移除登录 - modal_content: 你确定要从账户里移除该登录? - modal_confirm_btn: 移除 - remove_success: 移除成功 - toast: - update: 更新成功 - update_password: 密码更新成功。 - flag_success: 感谢标记。 - forbidden_operate_self: 禁止对自己执行操作 - review: 您的修订将在审阅通过后显示。 - sent_success: 发送成功 - related_question: - title: 相关问题 - answers: 个回答 - invite_to_answer: - title: 受邀人 - desc: 邀请你认为可能知道答案的人。 - invite: 邀请回答 - add: 添加人员 - search: 搜索人员 - question_detail: - action: 操作 - Asked: 提问于 - asked: 提问于 - update: 修改于 - edit: 编辑于 - commented: 评论 - Views: 阅读次数 - Follow: 关注此问题 - Following: 已关注 - follow_tip: 关注此问题以接收通知 - answered: 回答于 - closed_in: 关闭于 - show_exist: 查看类似问题。 - useful: 有用的 - question_useful: 它是有用和明确的 - question_un_useful: 它不明确或没用的 - question_bookmark: 收藏该问题 - answer_useful: 这是有用的 - answer_un_useful: 它是没有用的 - answers: - title: 个回答 - score: 评分 - newest: 最新 - oldest: 最旧 - btn_accept: 采纳 - btn_accepted: 已被采纳 - write_answer: - title: 你的回答 - edit_answer: 编辑我的回答 - btn_name: 提交你的回答 - add_another_answer: 添加另一个回答 - confirm_title: 继续回答 - continue: 继续 - confirm_info: >- -

你确定要提交一个新的回答吗?

作为替代,你可以通过编辑来完善和改进之前的回答。

- empty: 回答内容不能为空。 - characters: 内容长度至少 6 个字符。 - tips: - header_1: 感谢你的回答 - li1_1: 请务必确定在 回答问题。提供详细信息并分享你的研究。 - li1_2: 用参考资料或个人经历来支持你所做的任何陈述。 - header_2: 但是 请避免... - li2_1: 请求帮助,寻求澄清,或答复其他答案。 - reopen: - confirm_btn: 重新打开 - title: 重新打开这个帖子 - content: 确定要重新打开吗? - list: - confirm_btn: 列表显示 - title: 列表中显示这个帖子 - content: 确定要列表中显示这个帖子吗? - unlist: - confirm_btn: 列表隐藏 - title: 从列表中隐藏这个帖子 - content: 确定要从列表中隐藏这个帖子吗? - pin: - title: 置顶该帖子 - content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。 - confirm_btn: 置顶 - delete: - title: 删除 - question: >- - 我们不建议 删除有回答的帖子。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? - answer_accepted: >- -

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? - other: 你确定要删除? - tip_answer_deleted: 该回答已被删除 - undelete_title: 撤销删除本帖 - undelete_desc: 你确定你要撤销删除吗? - btns: - confirm: 确认 - cancel: 取消 - edit: 编辑 - save: 保存 - delete: 删除 - undelete: 撤消删除 - list: 列表显示 - unlist: 列表隐藏 - unlisted: 已隐藏 - login: 登录 - signup: 注册 - logout: 退出 - verify: 验证 - add_question: 我要提问 - approve: 批准 - reject: 拒绝 - skip: 跳过 - discard_draft: 丢弃草稿 - pinned: 已置顶 - all: 全部 - question: 问题 - answer: 回答 - comment: 评论 - refresh: 刷新 - resend: 重新发送 - deactivate: 取消激活 - active: 激活 - suspend: 封禁 - unsuspend: 解禁 - close: 关闭 - reopen: 重新打开 - ok: 确定 - light: 浅色 - dark: 深色 - system_setting: 跟随系统 - default: 默认 - reset: 重置 - tag: 标签 - post_lowercase: 帖子 - filter: 筛选 - ignore: 忽略 - submit: 提交 - normal: 正常 - closed: 已关闭 - deleted: 已删除 - pending: 等待处理 - more: 更多 - search: - title: 搜索结果 - keywords: 关键词 - options: 选项 - follow: 关注 - following: 已关注 - counts: "{{count}} 个结果" - more: 更多 - sort_btns: - relevance: 相关性 - newest: 最新的 - active: 活跃的 - score: 评分 - more: 更多 - tips: - title: 高级搜索提示 - tag: "<1>[tag] 在指定标签中搜索" - user: "<1>user:username 根据作者搜索" - answer: "<1>answers:0 搜索未回答的问题" - score: "<1>score:3 评分 3+ 的帖子" - question: "<1>is:question 搜索问题" - is_answer: "<1>is:answer 搜索回答" - empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 - share: - name: 分享 - copy: 复制链接 - via: 分享到... - copied: 已复制 - facebook: 分享到 Facebook - twitter: 分享到 Twitter - cannot_vote_for_self: 你不能给自己的帖子投票。 - modal_confirm: - title: 发生错误... - account_result: - success: 你的账号已通过验证,即将返回首页。 - link: 返回首页 - invalid: >- - 抱歉,此验证链接已失效。也许你的账号已经激活了? - confirm_new_email: 你的电子邮箱已更新 - confirm_new_email_invalid: >- - 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? - unsubscribe: - page_title: 退订 - success_title: 退订成功 - success_desc: 您已成功退订,并且将不会再收到我们的邮件。 - link: 更改设置 - question: - following_tags: 已关注的标签 - edit: 编辑 - save: 保存 - follow_tag_tip: 关注标签来筛选你的问题列表。 - hot_questions: 热门问题 - all_questions: 全部问题 - x_questions: "{{ count }} 个问题" - x_answers: "{{ count }} 个回答" - questions: 问题 - answers: 回答 - newest: 最新 - active: 活跃 - hot: 热门 - score: 评分 - unanswered: 未回答 - modified: 更新于 - answered: 回答于 - asked: 提问于 - closed: 已关闭 - follow_a_tag: 关注一个标签 - more: 更多 - personal: - overview: 概览 - answers: 回答 - answer: 回答 - questions: 问题 - question: 问题 - bookmarks: 收藏 - reputation: 声望 - comments: 评论 - votes: 得票 - newest: 最新 - score: 评分 - edit_profile: 编辑资料 - visited_x_days: "已访问 {{ count }} 天" - viewed: 浏览次数 - joined: 加入于 - last_login: 上次登录 - about_me: 关于我 - about_me_empty: "// Hello, World!" - top_answers: 高分回答 - top_questions: 高分问题 - stats: 状态 - list_empty: 没有找到相关的内容。
试试看其他选项卡? - accepted: 已采纳 - answered: 回答于 - asked: 提问于 - downvoted: 点踩 - mod_short: 版主 - mod_long: 版主 - x_reputation: 声望 - x_votes: 得票 - x_answers: 个回答 - x_questions: 个问题 - install: - title: 安装 - next: 下一步 - done: 完成 - config_yaml_error: 无法创建 config.yaml 文件。 - lang: - label: 请选择一种语言 - db_type: - label: 数据库引擎 - db_username: - label: 用户名 - placeholder: root - msg: 用户名不能为空 - db_password: - label: 密码 - placeholder: root - msg: 密码不能为空 - db_host: - label: 数据库主机 - placeholder: "db:3306" - msg: 数据库地址不能为空 - db_name: - label: 数据库名 - placeholder: 回答 - msg: 数据库名称不能为空。 - db_file: - label: 数据库文件 - placeholder: /data/answer.db - msg: 数据库文件不能为空。 - config_yaml: - title: 创建 config.yaml - label: 已创建 config.yaml 文件。 - desc: >- - 你可以手动在 <1>/var/wwww/xxx/ 目录中创建 <1>config.yaml 文件并粘贴以下文本。 - info: 完成后,点击“下一步”按钮。 - site_information: 站点信息 - admin_account: 管理员账号 - site_name: - label: 站点名称 - msg: 站点名称不能为空。 - msg_max_length: 站点名称长度不得超过 30 个字符。 - site_url: - label: 网站网址 - text: 此网站的网址。 - msg: - empty: 网址不能为空。 - incorrect: 网址格式不正确。 - max_length: 网址长度不得超过 512 个字符。 - contact_email: - label: 联系邮箱 - text: 负责本网站的主要联系人的电子邮件地址。 - msg: - empty: 联系人邮箱不能为空。 - incorrect: 联系人邮箱地址不正确。 - login_required: - label: 私有的 - switch: 需要登录 - text: 只有登录用户才能访问这个社区。 - admin_name: - label: 名字 - msg: 名字不能为空。 - character: '只能由 "a-z", "0-9", " - . _" 组成' - msg_max_length: 名字长度不能超过 30 个字符。 - admin_password: - label: 密码 - text: >- - 您需要此密码才能登录。请将其存储在一个安全的位置。 - msg: 密码不能为空。 - msg_min_length: 密码必须至少 8 个字符长。 - msg_max_length: 密码长度不能超过 32 个字符。 - admin_email: - label: 邮箱 - text: 您需要此电子邮件才能登录。 - msg: - empty: 邮箱不能为空。 - incorrect: 邮箱格式不正确。 - ready_title: 您的网站已准备好 - ready_desc: >- - 如果你想改变更多的设置,请访问 <1>管理区域;在网站菜单中找到它。 - good_luck: "玩得愉快,祝你好运!" - warn_title: 警告 - warn_desc: >- - 文件 <1>config.yaml 已存在。如果你要重置该文件中的任何配置项,请先删除它。 - install_now: 您可以尝试 <1>现在安装。 - installed: 已安裝 - installed_desc: >- - 你似乎已经安装过了。如果要重新安装,请先清除旧的数据库表。 - db_failed: 数据连接异常! - db_failed_desc: >- - 这或者意味着数据库信息在 <1>config.yaml 文件不正确,或者无法与数据库服务器建立联系。这可能意味着你的主机数据库服务器故障。 - counts: - views: 次浏览 - votes: 个点赞 - answers: 个回答 - accepted: 已被采纳 - page_error: - http_error: HTTP 错误 {{ code }} - desc_403: 您无权访问此页面。 - desc_404: 很抱歉,此页面不存在。 - desc_50X: 服务器遇到了一个错误,无法完成你的请求。 - back_home: 返回首页 - page_maintenance: - desc: "我们正在进行维护,我们将很快回来。" - nav_menus: - dashboard: 后台管理 - contents: 内容管理 - questions: 问题 - answers: 回答 - users: 用户管理 - flags: 举报管理 - settings: 站点设置 - general: 一般 - interface: 界面 - smtp: SMTP - branding: 品牌 - legal: 法律条款 - write: 撰写 - tos: 服务条款 - privacy: 隐私政策 - seo: SEO - customize: 自定义 - themes: 主题 - css_html: CSS/HTML - login: 登录 - privileges: 特权 - plugins: 插件 - installed_plugins: 已安装插件 - website_welcome: 欢迎来到 {{site_name}} - user_center: - login: 登录 - qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。 - login_failed_email_tip: 登录失败,请允许此应用访问您的邮箱信息,然后重试。 - admin: - admin_header: - title: 后台管理 - dashboard: - title: 后台管理 - welcome: 欢迎来到管理后台! - site_statistics: 站点统计 - questions: "问题:" - answers: "回答:" - comments: "评论:" - votes: "投票:" - users: "用户:" - flags: "举报:" - reviews: "审查:" - site_health: 网站健康 - version: "版本" - https: "HTTPS:" - upload_folder: "上传文件夹:" - run_mode: "运行模式:" - private: 私有 - public: 公开 - smtp: "SMTP:" - timezone: "时区:" - system_info: 系统信息 - go_version: "Go版本:" - database: "数据库:" - database_size: "数据库大小:" - storage_used: "已用存储空间:" - uptime: "运行时间:" - links: 链接 - plugins: 插件 - github: GitHub - blog: 博客 - contact: 联系 - forum: 论坛 - documents: 文档 - feedback: 用户反馈 - support: 帮助 - review: 审查 - config: 配置 - update_to: 更新到 - latest: 最新版本 - check_failed: 校验失败 - "yes": "是" - "no": "否" - not_allowed: 拒绝 - allowed: 允许 - enabled: 已启用 - disabled: 停用 - writable: 可写 - not_writable: 不可写 - flags: - title: 举报 - pending: 等待处理 - completed: 已完成 - flagged: 被举报内容 - flagged_type: 标记了 {{ type }} - created: 创建于 - action: 操作 - review: 审查 - user_role_modal: - title: 更改用户状态为... - btn_cancel: 取消 - btn_submit: 提交 - new_password_modal: - title: 设置新密码 - form: - fields: - password: - label: 密码 - text: 用户将被退出,需要再次登录。 - msg: 密码的长度必须是8-32个字符。 - btn_cancel: 取消 - btn_submit: 提交 - edit_profile_modal: - title: 编辑资料 - form: - fields: - username: - label: 用户名 - msg_range: 用户名不能超过 30 个字符。 - email: - label: 电子邮件地址 - msg_invalid: 无效的邮箱地址 - edit_success: 修改成功 - btn_cancel: 取消 - btn_submit: 提交 - user_modal: - title: 添加新用户 - form: - fields: - users: - label: 批量添加用户 - placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" - text: 用逗号分隔“name, email, password”,每行一个用户。 - msg: "请输入用户的邮箱,每行一个。" - display_name: - label: 显示名称 - msg: 显示名称长度必须为 4-30 个字符 - email: - label: 邮箱 - msg: 邮箱无效。 - password: - label: 密码 - msg: 密码的长度必须是8-32个字符。 - btn_cancel: 取消 - btn_submit: 提交 - users: - title: 用户 - name: 名称 - email: 邮箱 - reputation: 声望 - created_at: 创建时间 - delete_at: 删除时间 - suspend_at: 封禁时间 - status: 状态 - role: 角色 - action: 操作 - change: 更改 - all: 全部 - staff: 工作人员 - more: 更多 - inactive: 不活跃 - suspended: 已封禁 - deleted: 已删除 - normal: 正常 - Moderator: 版主 - Admin: 管理员 - User: 用户 - filter: - placeholder: "按名称筛选,用户:id" - set_new_password: 设置新密码 - edit_profile: 编辑资料 - change_status: 更改状态 - change_role: 更改角色 - show_logs: 显示日志 - add_user: 添加用户 - deactivate_user: - title: 停用用户 - content: 未激活的用户必须重新验证他们的邮箱。 - delete_user: - title: 删除此用户 - content: 确定要删除此用户?此操作无法撤销! - remove: 移除内容 - label: 删除所有问题、 答案、 评论等 - text: 如果你只想删除用户账户,请不要选中此项。 - suspend_user: - title: 挂起此用户 - content: 被封禁的用户将无法登录。 - questions: - page_title: 问题 - unlisted: 已隐藏 - post: 标题 - votes: 得票数 - answers: 回答数 - created: 创建于 - status: 状态 - action: 操作 - change: 更改 - pending: 等待处理 - filter: - placeholder: "按标题过滤,问题:id" - answers: - page_title: 回答 - post: 标题 - votes: 得票数 - created: 创建于 - status: 状态 - action: 操作 - change: 更改 - filter: - placeholder: "按标题筛选,答案:id" - general: - page_title: 一般 - name: - label: 站点名称 - msg: 不能为空 - text: "站点的名称,作为站点的标题。" - site_url: - label: 网站网址 - msg: 网站网址不能为空。 - validate: 请输入一个有效的 URL。 - text: 此网站的地址。 - short_desc: - label: 简短站点描述 - msg: 简短网站描述不能为空。 - text: "简短的标语,作为网站主页的标题(Html 的 title 标签)。" - desc: - label: 站点描述 - msg: 网站描述不能为空。 - text: "使用一句话描述本站,作为网站的描述(Html 的 meta 标签)。" - contact_email: - label: 联系邮箱 - msg: 联系人邮箱不能为空。 - validate: 联系人邮箱无效。 - text: 本网站的主要联系邮箱地址。 - check_update: - label: 软件更新 - text: 自动检查软件更新 - interface: - page_title: 界面 - language: - label: 界面语言 - msg: 不能为空 - text: 设置用户界面语言,在刷新页面后生效。 - time_zone: - label: 时区 - msg: 时区不能为空。 - text: 选择一个与您相同时区的城市。 - smtp: - page_title: SMTP - from_email: - label: 发件人邮箱 - msg: 发件人邮箱不能为空。 - text: 用于发送邮件的地址。 - from_name: - label: 发件人 - msg: 不能为空 - text: 发件人的名字。 - smtp_host: - label: SMTP 主机 - msg: 不能为空 - text: 邮件服务器 - encryption: - label: 加密 - msg: 不能为空 - text: 对于大多数服务器而言,SSL 是推荐开启的。 - ssl: SSL - tls: TLS - none: 无加密 - smtp_port: - label: SMTP 端口 - msg: SMTP 端口必须在 1 ~ 65535 之间。 - text: 邮件服务器的端口号。 - smtp_username: - label: SMTP 用户名 - msg: 不能为空 - smtp_password: - label: SMTP 密码 - msg: 不能为空 - test_email_recipient: - label: 测试收件邮箱 - text: 提供用于接收测试邮件的邮箱地址。 - msg: 测试收件邮箱无效 - smtp_authentication: - label: 启用身份验证 - title: SMTP 身份验证 - msg: 不能为空 - "yes": "是" - "no": "否" - branding: - page_title: 品牌 - logo: - label: 网站标志(Logo) - msg: 图标不能为空。 - text: 在你的网站左上方的Logo图标。使用一个高度为56,长宽比大于3:1的宽长方形图像。如果留空,将显示网站标题文本。 - mobile_logo: - label: 移动端 Logo - text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空,将使用 "Logo"设置中的图像。 - square_icon: - label: 方形图标 - msg: 方形图标不能为空。 - text: 用作元数据图标的基础的图像。最好是大于512x512。 - favicon: - label: 收藏夹图标 - text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空,将使用“方形图标”。 - legal: - page_title: 法律条款 - terms_of_service: - label: 服务条款 - text: "您可以在此添加服务内容的条款。如果您已经在别处托管了文档,请在这里提供完整的URL。" - privacy_policy: - label: 隐私政策 - text: "您可以在此添加隐私政策内容。如果您已经在别处托管了文档,请在这里提供完整的URL。" - write: - page_title: 编辑 - restrict_answer: - title: 限制一个回答 - label: 每个用户对于每个问题只能有一个回答 - text: "用户可以使用编辑按钮优化已有的回答" - recommend_tags: - label: 推荐标签 - text: "请在上方输入标签固定链接,每行一个标签。" - required_tag: - title: 必需的标签 - label: 根据需要设置推荐标签 - text: "每个新问题必须至少有一个推荐标签。" - reserved_tags: - label: 保留标签 - text: "保留的标签只能由版主添加到一个帖子中。" - seo: - page_title: 搜索引擎优化 - permalink: - label: 固定链接 - text: 自定义URL结构可以提高可用性,以及你的链接的向前兼容性。 - robots: - label: robots.txt - text: 这将永久覆盖任何相关的网站设置。 - themes: - page_title: 主题 - themes: - label: 主题 - text: 选择一个现有主题。 - color_scheme: - label: 配色方案 - navbar_style: - label: 导航栏样式 - primary_color: - label: 主色调 - text: 修改您主题使用的颜色 - css_and_html: - page_title: CSS 与 HTML - custom_css: - label: 自定义 CSS - text: > - - head: - label: 头部 - text: > - - header: - label: 页眉 - text: > - - footer: - label: 页脚 - text: 这将在 之前插入. - sidebar: - label: 侧边栏 - text: 这将插入侧边栏中。 - login: - page_title: 登录 - membership: - title: 会员 - label: 允许新注册 - text: 关闭以防止任何人创建新账户。 - email_registration: - title: 邮箱注册 - label: 允许邮箱注册 - text: 关闭以阻止任何人通过邮箱创建新账户。 - allowed_email_domains: - title: 允许的邮箱域 - text: 允许注册账户的邮箱域。每行一个域名。留空时忽略。 - private: - title: 非公开的 - label: 需要登录 - text: 只有登录用户才能访问这个社区。 - password_login: - title: 密码登录 - label: 允许使用邮箱和密码登录 - text: "警告:如果您未配置过其他登录方式,关闭密码登录后您则可能无法登录。" - installed_plugins: - title: 已安装插件 - plugin_link: 插件扩展功能。您可以在<1>插件仓库中找到插件。 - filter: - all: 全部 - active: 已启用 - inactive: 未启用 - outdated: 已过期 - plugins: - label: 插件 - text: 选择一个现有的插件。 - name: 名称 - version: 版本 - status: 状态 - action: 操作 - deactivate: 停用 - activate: 启用 - settings: 设置 - settings_users: - title: 用户 - avatar: - label: 默认头像 - text: 没有自定义头像的用户。 - gravatar_base_url: - label: Gravatar 根路径 URL - text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。 - profile_editable: - title: 个人资料可编辑 - allow_update_display_name: - label: 允许用户修改显示名称 - allow_update_username: - label: 允许用户修改用户名 - allow_update_avatar: - label: 允许用户修改个人头像 - allow_update_bio: - label: 允许用户修改个人介绍 - allow_update_website: - label: 允许用户修改个人主页网址 - allow_update_location: - label: 允许用户更改位置 - privilege: - title: 特权 - level: - label: 级别所需声望 - text: 选择特权所需的声望值 - msg: - should_be_number: 输入必须是数字 - number_larger_1: 数字应该大于等于 1 - form: - optional: (选填) - empty: 不能为空 - invalid: 是无效的 - btn_submit: 保存 - not_found_props: "所需属性 {{ key }} 未找到。" - select: 选择 - page_review: - review: 评论 - proposed: 提案 - question_edit: 问题编辑 - answer_edit: 回答编辑 - tag_edit: '标签管理: 编辑标签' - edit_summary: 编辑备注 - edit_question: 编辑问题 - edit_answer: 编辑回答 - edit_tag: 编辑标签 - empty: 没有剩余的审核任务。 - approve_revision_tip: 您是否批准此修订? - approve_flag_tip: 您是否批准此举报? - approve_post_tip: 您是否批准此帖子? - approve_user_tip: 您是否批准此修订? - suggest_edits: 建议的编辑 - flag_post: 举报帖子 - flag_user: 举报用户 - queued_post: 排队的帖子 - queued_user: 排队用户 - filter_label: 类型 - reputation: 声望值 - flag_post_type: 举报这个帖子的类型是 {{ type }} - flag_user_type: 举报这个用户的类型是 {{ type }} - edit_post: 编辑帖子 - list_post: 文章列表 - unlist_post: 隐藏的帖子 - timeline: - undeleted: 取消删除 - deleted: 删除 - downvote: 反对 - upvote: 点赞 - accept: 采纳 - cancelled: 已取消 - commented: '评论:' - rollback: 回滚 - edited: 最后编辑于 - answered: 回答于 - asked: 提问于 - closed: 关闭 - reopened: 重新开启 - created: 创建于 - pin: 已置顶 - unpin: 取消置頂 - show: 已显示 - hide: 已隐藏 - title: "历史记录" - tag_title: "时间线" - show_votes: "显示投票" - n_or_a: N/A - title_for_question: "时间线" - title_for_answer: "{{ title }} 的 {{ author }} 回答时间线" - title_for_tag: "时间线" - datetime: 日期时间 - type: 类型 - by: 由 - comment: 评论 - no_data: "空空如也" - users: - title: 用户 - users_with_the_most_reputation: 本周声望最高的用户 - users_with_the_most_vote: 本周投票最多的用户 - staffs: 我们的社区工作人员 - reputation: 声望值 - votes: 投票 - prompt: - leave_page: 确定要离开此页面? - changes_not_save: 您的更改尚未保存 - draft: - discard_confirm: 您确定要丢弃您的草稿吗? - messages: - post_deleted: 该帖子已被删除。 - post_pin: 该帖子已被置顶。 - post_unpin: 该帖子已被取消置顶。 - post_hide_list: 此帖子已经从列表中隐藏。 - post_show_list: 该帖子已显示到列表中。 - post_reopen: 这个帖子已被重新打开. - post_list: 这个帖子已经被显示 - post_unlist: 这个帖子已经被隐藏 - post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。 From 83155783e74b04f8c199d6f5680c4a9c8b4ab25a Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 19 Aug 2024 15:20:38 +0800 Subject: [PATCH 051/129] Revert "fix(dev): delete test file" This reverts commit c50e63dd34ce9ddb7babd769146278ef41e5bfcb. --- dev/i18n/zh_CN.yaml | 2035 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2035 insertions(+) create mode 100644 dev/i18n/zh_CN.yaml diff --git a/dev/i18n/zh_CN.yaml b/dev/i18n/zh_CN.yaml new file mode 100644 index 000000000..6613bfc22 --- /dev/null +++ b/dev/i18n/zh_CN.yaml @@ -0,0 +1,2035 @@ +# 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. + +# The following fields are used for back-end +backend: + base: + success: + other: 成功。 + unknown: + other: 未知错误。 + request_format_error: + other: 请求格式错误。 + unauthorized_error: + other: 未授权。 + database_error: + other: 数据服务器错误。 + forbidden_error: + other: 禁止访问。 + duplicate_request_error: + other: 重复提交。 + action: + report: + other: 举报 + edit: + other: 编辑 + delete: + other: 删除 + close: + other: 关闭 + reopen: + other: 重新打开 + forbidden_error: + other: 禁止访问。 + pin: + other: 置顶 + hide: + other: 列表隐藏 + unpin: + other: 取消置顶 + show: + other: 列表显示 + invite_someone_to_answer: + other: 编辑 + undelete: + other: 撤消删除 + role: + name: + user: + other: 用户 + admin: + other: 管理员 + moderator: + other: 版主 + description: + user: + other: 默认没有特殊权限。 + admin: + other: 拥有管理网站的全部权限。 + moderator: + other: 拥有除访问后台管理以外的所有权限。 + privilege: + level_1: + description: + other: 级别 1(少量声望要求,适合私有团队、群组) + level_2: + description: + other: 级别 2(低声望要求,适合初启动的社区) + level_3: + description: + other: 级别 3(高声望要求,适合成熟的社区) + level_custom: + description: + other: 自定义等级 + rank_question_add_label: + other: 提问 + rank_answer_add_label: + other: 写答案 + rank_comment_add_label: + other: 写评论 + rank_report_add_label: + other: 举报 + rank_comment_vote_up_label: + other: 点赞评论 + rank_link_url_limit_label: + other: 每次发布超过 2 个链接 + rank_question_vote_up_label: + other: 点赞问题 + rank_answer_vote_up_label: + other: 点赞答案 + rank_question_vote_down_label: + other: 点踩问题 + rank_answer_vote_down_label: + other: 点踩答案 + rank_invite_someone_to_answer_label: + other: 邀请回答 + rank_tag_add_label: + other: 创建新标签 + rank_tag_edit_label: + other: 编辑标签描述(需要审核) + rank_question_edit_label: + other: 编辑别人的问题(需要审核) + rank_answer_edit_label: + other: 编辑别人的答案(需要审核) + rank_question_edit_without_review_label: + other: 编辑别人的问题无需审核 + rank_answer_edit_without_review_label: + other: 编辑别人的答案无需审核 + rank_question_audit_label: + other: 审核问题编辑 + rank_answer_audit_label: + other: 审核回答编辑 + rank_tag_audit_label: + other: 审核标签编辑 + rank_tag_edit_without_review_label: + other: 编辑标签描述无需审核 + rank_tag_synonym_label: + other: 管理标签同义词 + email: + other: 邮箱 + e_mail: + other: 邮箱 + password: + other: 密码 + pass: + other: 密码 + original_text: + other: 本帖 + email_or_password_wrong_error: + other: 邮箱和密码不匹配。 + error: + common: + invalid_url: + other: 无效的 URL。 + password: + space_invalid: + other: 密码不得含有空格。 + admin: + cannot_update_their_password: + other: 你无法修改自己的密码。 + cannot_edit_their_profile: + other: 您不能修改您的个人资料。 + cannot_modify_self_status: + other: 你无法修改自己的状态。 + email_or_password_wrong: + other: 邮箱和密码不匹配。 + answer: + not_found: + other: 没有找到答案。 + cannot_deleted: + other: 没有删除权限。 + cannot_update: + other: 没有更新权限。 + question_closed_cannot_add: + other: 问题已关闭,无法添加。 + comment: + edit_without_permission: + other: 不允许编辑评论。 + not_found: + other: 评论未找到。 + cannot_edit_after_deadline: + other: 评论时间太久,无法修改。 + email: + duplicate: + other: 邮箱已存在。 + need_to_be_verified: + other: 邮箱需要验证。 + verify_url_expired: + other: 邮箱验证的网址已过期,请重新发送邮件。 + illegal_email_domain_error: + other: 此邮箱不在允许注册的邮箱域中。请使用其他邮箱尝试。 + lang: + not_found: + other: 语言文件未找到。 + object: + captcha_verification_failed: + other: 验证码错误。 + disallow_follow: + other: 你不能关注。 + disallow_vote: + other: 你不能投票。 + disallow_vote_your_self: + other: 你不能为自己的帖子投票。 + not_found: + other: 对象未找到。 + verification_failed: + other: 验证失败。 + email_or_password_incorrect: + other: 邮箱和密码不匹配。 + old_password_verification_failed: + other: 旧密码验证失败。 + new_password_same_as_previous_setting: + other: 新密码和旧密码相同。 + already_deleted: + other: 该帖子已被删除。 + meta: + object_not_found: + other: Meta 对象未找到 + question: + already_deleted: + other: 该帖子已被删除。 + under_review: + other: 您的帖子正在等待审核。它将在它获得批准后可见。 + not_found: + other: 问题未找到。 + cannot_deleted: + other: 没有删除权限。 + cannot_close: + other: 没有关闭权限。 + cannot_update: + other: 没有更新权限。 + rank: + fail_to_meet_the_condition: + other: 声望值未达到要求。 + vote_fail_to_meet_the_condition: + other: 感谢投票。你至少需要 {{.Rank}} 声望才能投票。 + no_enough_rank_to_operate: + other: 你至少需要 {{.Rank}} 声望才能执行此操作。 + report: + handle_failed: + other: 报告处理失败。 + not_found: + other: 报告未找到。 + tag: + already_exist: + other: 标签已存在。 + not_found: + other: 标签未找到。 + recommend_tag_not_found: + other: 推荐标签不存在。 + recommend_tag_enter: + other: 请选择至少一个必选标签。 + not_contain_synonym_tags: + other: 不应包含同义词标签。 + cannot_update: + other: 没有更新权限。 + is_used_cannot_delete: + other: 你不能删除这个正在使用的标签。 + cannot_set_synonym_as_itself: + other: 你不能将当前标签设为自己的同义词。 + smtp: + config_from_name_cannot_be_email: + other: 发件人名称不能是邮箱地址。 + theme: + not_found: + other: 主题未找到。 + revision: + review_underway: + other: 目前无法编辑,有一个版本在审阅队列中。 + no_permission: + other: 无权限修改。 + user: + external_login_missing_user_id: + other: 第三方平台没有提供唯一的 UserID,所以你不能登录,请联系网站管理员。 + external_login_unbinding_forbidden: + other: 请在移除此登录之前为你的账户设置登录密码。 + email_or_password_wrong: + other: + other: 邮箱和密码不匹配。 + not_found: + other: 用户未找到。 + suspended: + other: 用户已被封禁。 + username_invalid: + other: 用户名无效。 + username_duplicate: + other: 用户名已被使用。 + set_avatar: + other: 头像设置错误。 + cannot_update_your_role: + other: 你不能修改自己的角色。 + not_allowed_registration: + other: 该网站暂未开放注册。 + not_allowed_login_via_password: + other: 该网站暂不支持密码登录。 + access_denied: + other: 拒绝访问 + page_access_denied: + other: 您没有权限访问此页面。 + add_bulk_users_format_error: + other: "发生错误,{{.Field}} 格式错误,在 '{{.Content}}' 行数 {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "一次性添加的用户数量应在 1-{{.MaxAmount}} 之间。" + config: + read_config_failed: + other: 读取配置失败 + database: + connection_failed: + other: 数据库连接失败 + create_table_failed: + other: 创建表失败 + install: + create_config_failed: + other: 无法创建 config.yaml 文件。 + upload: + unsupported_file_format: + other: 不支持的文件格式。 + site_info: + config_not_found: + other: 未找到网站的该配置信息。 + reason: + spam: + name: + other: 垃圾信息 + desc: + other: 这个帖子是一个广告,或是破坏性行为。它对当前的主题无帮助或无关。 + rude_or_abusive: + name: + other: 粗鲁或辱骂的 + desc: + other: + - 一个有理智的人都会认为这种内容不适合进行尊重性的讨论。 + - 论坛 + a_duplicate: + name: + other: 重复内容 + desc: + other: 该问题有人问过,而且已经有了答案。 + placeholder: + other: 输入已有的问题链接 + not_a_answer: + name: + other: 不是答案 + desc: + other: + - 这张贴作为答案,但它不会试图回答 + - 这可能是一个编辑、一个评论、另一个问题。 + - 或全部删除。 + no_longer_needed: + name: + other: 不再需要 + desc: + other: 该评论已过时,对话性质或与此帖子无关。 + something: + name: + other: 其他原因 + desc: + other: 此帖子需要工作人员注意,因为是上述所列以外的其他理由。 + placeholder: + other: 让我们具体知道你关心的什么 + community_specific: + name: + other: 社区特定原因 + desc: + other: 该问题不符合社区准则。 + not_clarity: + name: + other: 需要细节或澄清 + desc: + other: 该问题目前涵盖多个问题。它应该侧重在一个问题上。 + looks_ok: + name: + other: 看起来没问题 + desc: + other: 这个帖子是好的,不是低质量。 + needs_edit: + name: + other: 需要编辑,我已做了修改。 + desc: + other: 改进和纠正你自己帖子中的问题。 + needs_close: + name: + other: 需要关闭 + desc: + other: 关闭的问题不能回答,但仍然可以编辑、投票和评论。 + needs_delete: + name: + other: 需要删除 + desc: + other: 该帖子将被删除。 + question: + close: + duplicate: + name: + other: 垃圾信息 + desc: + other: 此问题以前就有人问过,而且已经有了答案。 + guideline: + name: + other: 社区特定原因 + desc: + other: 该问题不符合社区准则。 + multiple: + name: + other: 需要细节或澄清 + desc: + other: + - 该问题目前涵盖多个问题。它应该侧重在一个问题上。 + - 只关注一个问题。 + other: + name: + other: 其他原因 + desc: + other: 该帖子存在上面没有列出的另一个原因。 + operation_type: + asked: + other: 提问于 + answered: + other: 回答于 + modified: + other: 修改于 + deleted_title: + other: 删除的问题 + notification: + action: + update_question: + other: 更新了问题 + answer_the_question: + other: 回答了问题 + update_answer: + other: 更新了答案 + accept_answer: + other: 采纳了答案 + comment_question: + other: 评论了问题 + comment_answer: + other: 评论了答案 + reply_to_you: + other: 回复了你 + mention_you: + other: 提到了你 + your_question_is_closed: + other: 你的问题已被关闭 + your_question_was_deleted: + other: 你的问题已被删除 + your_answer_was_deleted: + other: 你的答案已被删除 + your_comment_was_deleted: + other: 你的评论已被删除 + up_voted_question: + other: 点赞问题 + down_voted_question: + other: 点踩问题 + up_voted_answer: + other: 点赞答案 + down_voted_answer: + other: 点踩回答 + up_voted_comment: + other: 点赞评论 + invited_you_to_answer: + other: 邀请你回答 + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] 确认你的新邮箱地址" + body: + other: "请点击以下链接确认你在 {{.SiteName}} 上的新邮箱地址:
\n{{.ChangeEmailUrl}}

\n\n如果你没有请求此更改,请忽略此邮件。\n" + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 回答了你的问题" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
我想你可能知道答案。

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 评论了你的帖子" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" + new_question: + title: + other: "[{{.SiteName}}] 新问题: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\n取消订阅" + pass_reset: + title: + other: "[{{.SiteName }}] 重置密码" + body: + other: "有人要求在 [{{.SiteName}}] 上重置你的密码。

\n\n如果这不是你的操作,请安心忽略此电子邮件。

\n\n请点击以下链接设置一个新密码:
\n{{.PassResetUrl}}\n" + register: + title: + other: "[{{.SiteName}}] 确认你的新账户" + body: + other: "欢迎加入 {{.SiteName}}!

\n\n请点击以下链接确认并激活你的新账户:
\n{{.RegisterUrl}}

\n\n如果上面的链接不能点击,请将其复制并粘贴到你的浏览器地址栏中。\n" + test: + title: + other: "[{{.SiteName}}] 测试邮件" + body: + other: "这是一封测试邮件。" + action_activity_type: + upvote: + other: 点赞 + upvoted: + other: 点赞 + downvote: + other: 点踩 + downvoted: + other: 点踩 + accept: + other: 采纳 + accepted: + other: 已采纳 + edit: + other: 编辑 + review: + queued_post: + other: 排队的帖子 + flagged_post: + other: 举报的帖子 + suggested_post_edit: + other: 建议的编辑 + reaction: + tooltip: + other: "{{ .Names }} 以及另外 {{ .Count }} 个..." +plugin: + s3_cdn: + backend: + info: + name: + other: S3 存储 CDN + description: + other: 上传文件到S3存储 + config: + endpoint: + title: + other: Endpoint + description: + other: S3存储的Endpoint + bucket_name: + title: + other: Bucket名称 + description: + other: S3存储的Bucket名称 + object_key_prefix: + title: + other: 对象Key前缀 + description: + other: 对象键的前缀,如'answer/data/',以'/'结尾 + access_key_id: + title: + other: AccessKeyID + description: + other: S3存储的AccessKeyID + access_key_secret: + title: + other: AccessKeySecret + description: + other: S3存储的AccessKeySecret + access_token: + title: + other: AccessToken + description: + other: 访问 S3 所需的 AccessToken + visit_url_prefix: + title: + other: 访问URL前缀 + description: + other: 上传静态文件CDN最终访问地址的前缀,以 '/' 结尾 https://static.example.com/xxx/ + max_file_size: + title: + other: 文件最大大小(MB) + description: + other: 限制上传文件的最大大小,单位MB,默认为10MB + region: + title: + other: 区域(Region) + description: + other: S3存储区域 + disable_ssl: + title: + other: 禁用SSL + description: + other: 我们建议您使用SSL访问S3存储。如果您想禁用SSL,请选中此选项。 + err: + mis_storage_config: + other: 错误的存储配置导致上传失败 + file_not_found: + other: 文件未找到 + unsupported_file_type: + other: 不支持的文件类型 + over_file_size_limit: + other: 超过文件大小限制 + upload_file_failed: + other: 上传文件失败 + aliyunoss_cdn: + backend: + info: + name: + other: 阿里云CDN OSS存储 + description: + other: 上传文件到阿里云CDN OSS存储 + config: + endpoint: + title: + other: Endpoint + description: + other: 阿里云OSS存储的Endpoint + bucket_name: + title: + other: Bucket名称 + description: + other: 阿里云OSS存储的Bucket名称 + object_key_prefix: + title: + other: 对象Key前缀 + description: + other: 对象键的前缀,如'answer/data/',以'/'结尾 + access_key_id: + title: + other: AccessKeyID + description: + other: 阿里云OSS存储的AccessKeyID + access_key_secret: + title: + other: AccessKeySecret + description: + other: 阿里云OSS存储的AccessKeySecret + visit_url_prefix: + title: + other: 访问URL前缀 + description: + other: CDN最终访问地址的前缀,以 '/' 结尾 https://example.com/xxx/ + max_file_size: + title: + other: 最大文件大小(MB) + description: + other: 限制上传文件的最大大小,单位为MB,默认为 10MB + err: + mis_storage_config: + other: 错误的存储配置导致上传失败 + file_not_found: + other: 文件未找到 + unsupported_file_type: + other: 不支持的文件类型 + over_file_size_limit: + other: 超过文件大小限制 + upload_file_failed: + other: 上传文件失败 +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何排版 + desc: >- +
  • 添加链接

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 **粗体**

  • 使用 4 个空格缩进代码

  • 在行首添加 > 表示引用

  • 反引号进行转义 `像 _这样_`

  • 使用 ``` 创建代码块

    ```
    这是代码块
    ```
+ pagination: + prev: 上一页 + next: 下一页 + page_title: + question: 问题 + questions: 问题 + tag: 标签 + tags: 标签 + tag_wiki: 标签维基 + create_tag: 创建标签 + edit_tag: 编辑标签 + ask_a_question: 提问题 + edit_question: 编辑问题 + edit_answer: 编辑回答 + search: 搜索 + posts_containing: 帖子包含 + settings: 设置 + notifications: 通知 + login: 登录 + sign_up: 注册 + account_recovery: 账号恢复 + account_activation: 账号激活 + confirm_email: 确认电子邮件 + account_suspended: 账号已被封禁 + admin: 后台管理 + change_email: 修改邮箱 + install: Answer 安装 + upgrade: Answer 升级 + maintenance: 网站维护 + users: 用户 + oauth_callback: 处理中 + http_404: HTTP 错误 404 + http_50X: HTTP 错误 500 + http_403: HTTP 错误 403 + logout: 退出 + notifications: + title: 通知 + inbox: 收件箱 + achievement: 成就 + new_alerts: 新通知 + all_read: 全部标记为已读 + show_more: 显示更多 + someone: 有人 + inbox_type: + all: 全部 + posts: 帖子 + invites: 邀请 + votes: 投票 + suspended: + title: 你的账号账号已被封禁 + until_time: "你的账号被封禁直到 {{ time }}。" + forever: 你的账号已被永久封禁。 + end: 你违反了我们的社区准则。 + contact_us: 联系我们 + editor: + blockquote: + text: 引用 + bold: + text: 粗体 + chart: + text: 图表 + flow_chart: 流程图 + sequence_diagram: 时序图 + class_diagram: 类图 + state_diagram: 状态图 + entity_relationship_diagram: 实体关系图 + user_defined_diagram: 用户自定义图表 + gantt_chart: 甘特图 + pie_chart: 饼图 + code: + text: 代码块 + add_code: 添加代码块 + form: + fields: + code: + label: 代码块 + msg: + empty: 代码块不能为空 + language: + label: 语言 + placeholder: 自动识别 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 行内公式 + block: 块级公式 + heading: + text: 标题 + options: + h1: 标题 1 + h2: 标题 2 + h3: 标题 3 + h4: 标题 4 + h5: 标题 5 + h6: 标题 6 + help: + text: 帮助 + hr: + text: 水平线 + image: + text: 图片 + add_image: 添加图片 + tab_image: 上传图片 + form_image: + fields: + file: + label: 图像文件 + btn: 选择图片 + msg: + empty: 请选择图片文件。 + only_image: 只能上传图片文件。 + max_size: 文件大小不能超过 4 MB。 + desc: + label: 描述 + tab_url: 图片地址 + form_url: + fields: + url: + label: 图片地址 + msg: + empty: 图片地址不能为空 + name: + label: 描述 + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上传中 + indent: + text: 缩进 + outdent: + text: 减少缩进 + italic: + text: 斜体 + link: + text: 超链接 + add_link: 添加超链接 + form: + fields: + url: + label: 链接 + msg: + empty: 链接不能为空。 + name: + label: 描述 + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: 有序列表 + unordered_list: + text: 无序列表 + table: + text: 表格 + heading: 表头 + cell: 单元格 + close_modal: + title: 关闭原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空。 + msg: + empty: 请选择一个原因。 + report_modal: + flag_title: 我举报这篇帖子的原因是... + close_title: 我关闭这篇帖子的原因是... + review_question_title: 审查问题 + review_answer_title: 审查回答 + review_comment_title: 审查评论 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空 + msg: + empty: 请选择一个原因。 + not_a_url: URL 格式不正确。 + url_not_match: URL 来源与当前网站不匹配。 + tag_modal: + title: 创建新标签 + form: + fields: + display_name: + label: 显示名称 + msg: + empty: 显示名称不能为空。 + range: 显示名称不能超过 35 个字符。 + slug_name: + label: URL 固定链接 + desc: URL 固定链接不能超过 35 个字符。 + msg: + empty: URL 固定链接不能为空。 + range: URL 固定链接不能超过 35 个字符。 + character: URL 固定链接包含非法字符。 + desc: + label: 描述 + revision: + label: 编辑历史 + edit_summary: + label: 编辑备注 + placeholder: >- + 简单描述更改原因(更正拼写、修复语法、改进格式) + btn_cancel: 取消 + btn_submit: 提交 + btn_post: 发布新标签 + tag_info: + created_at: 创建于 + edited_at: 编辑于 + history: 历史 + synonyms: + title: 同义词 + text: 以下标签将被重置到 + empty: 此标签目前没有同义词。 + btn_add: 添加同义词 + btn_edit: 编辑 + btn_save: 保存 + synonyms_text: 以下标签将被重置到 + delete: + title: 删除标签 + tip_with_posts: >- +

我们不允许 删除带有帖子的标签

请先从帖子中移除此标签。

+ tip_with_synonyms: >- +

我们不允许 删除带有同义词的标签

请先从此标签中删除同义词。

+ tip: 确定要删除吗? + close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + default_first_reason: 添加标签 + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: MM 月 DD 日 + long_date_with_year: "YYYY 年 MM 月 DD 日" + long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm" + now: 刚刚 + x_seconds_ago: "{{count}} 秒前" + x_minutes_ago: "{{count}} 分钟前" + x_hours_ago: "{{count}} 小时前" + hour: 小时 + day: 天 + hours: 小时 + days: 日 + reaction: + heart: 爱心 + smile: 微笑 + frown: 愁 + btn_label: 添加或删除回应。 + undo_emoji: 撤销 {{ emoji }} 回应 + react_emoji: 用 {{ emoji }} 回应 + unreact_emoji: 撤销 {{ emoji }} + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存更改 + btn_cancel: 取消 + show_more: "{{count}} 条剩余评论" + tip_question: >- + 使用评论提问更多信息或者提出改进意见。避免在评论里回答问题。 + tip_answer: >- + 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + tip_vote: 它给帖子添加了一些有用的内容 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + default_first_reason: 添加答案 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + feedback: + characters: 内容长度至少 6 个字符 + edit_summary: + label: 编辑摘要 + placeholder: >- + 简单描述更改原因(更正拼写、修复语法、改进格式) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名称过滤 + no_desc: 此标签无描述。 + more: 更多 + ask: + title: 新增问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + default_first_reason: 新增问题 + similar_questions: 相似问题 + form: + fields: + revision: + label: 修订版本 + title: + label: 标题 + placeholder: 请详细描述你的问题,想象你在问一个人 + msg: + empty: 标题不能为空。 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空。 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + edit_summary: + label: 编辑备注 + placeholder: >- + 简单描述更改原因(更正拼写、修复语法、改进格式) + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 回答自己的问题 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: "描述您的问题是关于什么,至少需要一个标签。" + no_result: 没有匹配的标签 + tag_required_text: 必选标签(至少一个) + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + profile: 用户主页 + setting: 账号设置 + logout: 退出 + admin: 后台管理 + review: 审查 + bookmark: 收藏夹 + moderation: 管理 + search: + placeholder: 搜索 + footer: + build_on: >- + 由 <1>Apache Answer 提供动力 - 驱动问答社区的开源软件。
用爱制造 © {{cc}}. + upload_img: + name: 更改 + loading: 加载中... + pic_auth_code: + title: 验证码 + placeholder: 输入图片中的文字 + msg: + empty: 验证码不能为空。 + inactive: + first: >- + 就差一步!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活你的账户。 + info: "如果没有收到,请检查你的垃圾邮件文件夹。" + another: >- + 我们向你的邮箱 {{mail}} 发送了另一封激活电子邮件。可能需要几分钟才能到达;请务必检查您的垃圾邮件箱。 + btn_name: 重新发送激活邮件 + change_btn_name: 更改邮箱 + msg: + empty: 不能为空。 + resend_email: + url_label: 确定要重新发送激活邮件吗? + url_text: 你也可以将上面的激活链接给该用户。 + login: + login_to_continue: 登录以继续 + info_sign: 没有账户?<1>注册 + info_login: 已经有账户?<1>登录 + agreements: 登录即表示您同意<1>隐私政策和<3>服务条款。 + forgot_pass: 忘记密码? + name: + label: 名字 + msg: + empty: 名字不能为空 + range: 名字应该在 4 到 30 个字符之间 + character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password: + label: 密码 + msg: + empty: 密码不能为空 + different: 两次输入密码不一致 + account_forgot: + page_title: 忘记密码 + btn_name: 发送恢复邮件 + send_success: >- + 如果存在邮箱为 {{mail}} 账户,你将很快收到一封重置密码的说明邮件。 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + change_email: + btn_cancel: 取消 + btn_update: 更新电子邮件地址 + send_success: >- + 如果存在邮箱为 {{mail}} 的账户,你将很快收到一封重置密码的说明邮件。 + email: + label: 新的电子邮件地址 + msg: + empty: 邮箱不能为空。 + oauth: + connect: 连接到 {{ auth_name }} + remove: 移除 {{ auth_name }} + oauth_bind_email: + subtitle: 向你的账户添加恢复邮件地址。 + btn_update: 更新电子邮件地址 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空。 + modal_title: 邮箱已经存在。 + modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? + modal_cancel: 更改邮箱 + modal_confirm: 连接到已有账户 + password_reset: + page_title: 密码重置 + btn_name: 重置我的密码 + reset_success: >- + 你已经成功更改密码;你将被重定向到登录页面。 + link_invalid: >- + 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? + to_login: 前往登录页面 + password: + label: 密码 + msg: + empty: 密码不能为空。 + length: 密码长度在8-32个字符之间 + different: 两次输入密码不一致 + password_confirm: + label: 确认新密码 + settings: + page_title: 设置 + goto_modify: 前往修改 + nav: + profile: 我的资料 + notification: 通知 + account: 账号 + interface: 界面 + profile: + heading: 个人资料 + btn_name: 保存 + display_name: + label: 显示名称 + msg: 昵称不能为空。 + msg_range: 显示名称不能超过 30 个字符。 + username: + label: 用户名 + caption: 用户可以通过 "@用户名" 来提及你。 + msg: 用户名不能为空 + msg_range: 用户名不能超过 30 个字符。 + character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' + avatar: + label: 头像 + gravatar: Gravatar + gravatar_text: 你可以更改图像在 + custom: 自定义 + custom_text: 你可以上传你的图片。 + default: 系统 + msg: 请上传头像 + bio: + label: 关于我 + website: + label: 网站 + placeholder: "https://example.com" + msg: 网址格式不正确 + location: + label: 位置 + placeholder: "城市,国家" + notification: + heading: 邮件通知 + turn_on: 开启 + inbox: + label: 收件箱通知 + description: 你的提问有新的回答,评论,邀请回答和其他。 + all_new_question: + label: 所有新问题 + description: 获取所有新问题的通知。每周最多有50个问题。 + all_new_question_for_following_tags: + label: 所有关注标签的新问题 + description: 获取关注的标签下新问题通知。 + account: + heading: 账号 + change_email_btn: 更改邮箱 + change_pass_btn: 更改密码 + change_email_info: >- + 邮件已发送。请根据指引完成验证。 + email: + label: 电子邮件地址 + new_email: + label: 新的电子邮件地址 + msg: 新邮箱不能为空。 + pass: + label: 当前密码 + msg: 密码不能为空。 + password_title: 密码 + current_pass: + label: 当前密码 + msg: + empty: 当前密码不能为空 + length: 密码长度必须在 8 至 32 之间 + different: 两次输入的密码不匹配 + new_pass: + label: 新密码 + pass_confirm: + label: 确认新密码 + interface: + heading: 界面 + lang: + label: 界面语言 + text: 设置用户界面语言,在刷新页面后生效。 + my_logins: + title: 我的登录 + label: 使用这些账户登录或注册本网站。 + modal_title: 移除登录 + modal_content: 你确定要从账户里移除该登录? + modal_confirm_btn: 移除 + remove_success: 移除成功 + toast: + update: 更新成功 + update_password: 密码更新成功。 + flag_success: 感谢标记。 + forbidden_operate_self: 禁止对自己执行操作 + review: 您的修订将在审阅通过后显示。 + sent_success: 发送成功 + related_question: + title: 相关问题 + answers: 个回答 + invite_to_answer: + title: 受邀人 + desc: 邀请你认为可能知道答案的人。 + invite: 邀请回答 + add: 添加人员 + search: 搜索人员 + question_detail: + action: 操作 + Asked: 提问于 + asked: 提问于 + update: 修改于 + edit: 编辑于 + commented: 评论 + Views: 阅读次数 + Follow: 关注此问题 + Following: 已关注 + follow_tip: 关注此问题以接收通知 + answered: 回答于 + closed_in: 关闭于 + show_exist: 查看类似问题。 + useful: 有用的 + question_useful: 它是有用和明确的 + question_un_useful: 它不明确或没用的 + question_bookmark: 收藏该问题 + answer_useful: 这是有用的 + answer_un_useful: 它是没有用的 + answers: + title: 个回答 + score: 评分 + newest: 最新 + oldest: 最旧 + btn_accept: 采纳 + btn_accepted: 已被采纳 + write_answer: + title: 你的回答 + edit_answer: 编辑我的回答 + btn_name: 提交你的回答 + add_another_answer: 添加另一个回答 + confirm_title: 继续回答 + continue: 继续 + confirm_info: >- +

你确定要提交一个新的回答吗?

作为替代,你可以通过编辑来完善和改进之前的回答。

+ empty: 回答内容不能为空。 + characters: 内容长度至少 6 个字符。 + tips: + header_1: 感谢你的回答 + li1_1: 请务必确定在 回答问题。提供详细信息并分享你的研究。 + li1_2: 用参考资料或个人经历来支持你所做的任何陈述。 + header_2: 但是 请避免... + li2_1: 请求帮助,寻求澄清,或答复其他答案。 + reopen: + confirm_btn: 重新打开 + title: 重新打开这个帖子 + content: 确定要重新打开吗? + list: + confirm_btn: 列表显示 + title: 列表中显示这个帖子 + content: 确定要列表中显示这个帖子吗? + unlist: + confirm_btn: 列表隐藏 + title: 从列表中隐藏这个帖子 + content: 确定要从列表中隐藏这个帖子吗? + pin: + title: 置顶该帖子 + content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。 + confirm_btn: 置顶 + delete: + title: 删除 + question: >- + 我们不建议 删除有回答的帖子。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_answer_deleted: 该回答已被删除 + undelete_title: 撤销删除本帖 + undelete_desc: 你确定你要撤销删除吗? + btns: + confirm: 确认 + cancel: 取消 + edit: 编辑 + save: 保存 + delete: 删除 + undelete: 撤消删除 + list: 列表显示 + unlist: 列表隐藏 + unlisted: 已隐藏 + login: 登录 + signup: 注册 + logout: 退出 + verify: 验证 + add_question: 我要提问 + approve: 批准 + reject: 拒绝 + skip: 跳过 + discard_draft: 丢弃草稿 + pinned: 已置顶 + all: 全部 + question: 问题 + answer: 回答 + comment: 评论 + refresh: 刷新 + resend: 重新发送 + deactivate: 取消激活 + active: 激活 + suspend: 封禁 + unsuspend: 解禁 + close: 关闭 + reopen: 重新打开 + ok: 确定 + light: 浅色 + dark: 深色 + system_setting: 跟随系统 + default: 默认 + reset: 重置 + tag: 标签 + post_lowercase: 帖子 + filter: 筛选 + ignore: 忽略 + submit: 提交 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + pending: 等待处理 + more: 更多 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: "{{count}} 个结果" + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + more: 更多 + tips: + title: 高级搜索提示 + tag: "<1>[tag] 在指定标签中搜索" + user: "<1>user:username 根据作者搜索" + answer: "<1>answers:0 搜索未回答的问题" + score: "<1>score:3 评分 3+ 的帖子" + question: "<1>is:question 搜索问题" + is_answer: "<1>is:answer 搜索回答" + empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 + share: + name: 分享 + copy: 复制链接 + via: 分享到... + copied: 已复制 + facebook: 分享到 Facebook + twitter: 分享到 Twitter + cannot_vote_for_self: 你不能给自己的帖子投票。 + modal_confirm: + title: 发生错误... + account_result: + success: 你的账号已通过验证,即将返回首页。 + link: 返回首页 + invalid: >- + 抱歉,此验证链接已失效。也许你的账号已经激活了? + confirm_new_email: 你的电子邮箱已更新 + confirm_new_email_invalid: >- + 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? + unsubscribe: + page_title: 退订 + success_title: 退订成功 + success_desc: 您已成功退订,并且将不会再收到我们的邮件。 + link: 更改设置 + question: + following_tags: 已关注的标签 + edit: 编辑 + save: 保存 + follow_tag_tip: 关注标签来筛选你的问题列表。 + hot_questions: 热门问题 + all_questions: 全部问题 + x_questions: "{{ count }} 个问题" + x_answers: "{{ count }} 个回答" + questions: 问题 + answers: 回答 + newest: 最新 + active: 活跃 + hot: 热门 + score: 评分 + unanswered: 未回答 + modified: 更新于 + answered: 回答于 + asked: 提问于 + closed: 已关闭 + follow_a_tag: 关注一个标签 + more: 更多 + personal: + overview: 概览 + answers: 回答 + answer: 回答 + questions: 问题 + question: 问题 + bookmarks: 收藏 + reputation: 声望 + comments: 评论 + votes: 得票 + newest: 最新 + score: 评分 + edit_profile: 编辑资料 + visited_x_days: "已访问 {{ count }} 天" + viewed: 浏览次数 + joined: 加入于 + last_login: 上次登录 + about_me: 关于我 + about_me_empty: "// Hello, World!" + top_answers: 高分回答 + top_questions: 高分问题 + stats: 状态 + list_empty: 没有找到相关的内容。
试试看其他选项卡? + accepted: 已采纳 + answered: 回答于 + asked: 提问于 + downvoted: 点踩 + mod_short: 版主 + mod_long: 版主 + x_reputation: 声望 + x_votes: 得票 + x_answers: 个回答 + x_questions: 个问题 + install: + title: 安装 + next: 下一步 + done: 完成 + config_yaml_error: 无法创建 config.yaml 文件。 + lang: + label: 请选择一种语言 + db_type: + label: 数据库引擎 + db_username: + label: 用户名 + placeholder: root + msg: 用户名不能为空 + db_password: + label: 密码 + placeholder: root + msg: 密码不能为空 + db_host: + label: 数据库主机 + placeholder: "db:3306" + msg: 数据库地址不能为空 + db_name: + label: 数据库名 + placeholder: 回答 + msg: 数据库名称不能为空。 + db_file: + label: 数据库文件 + placeholder: /data/answer.db + msg: 数据库文件不能为空。 + config_yaml: + title: 创建 config.yaml + label: 已创建 config.yaml 文件。 + desc: >- + 你可以手动在 <1>/var/wwww/xxx/ 目录中创建 <1>config.yaml 文件并粘贴以下文本。 + info: 完成后,点击“下一步”按钮。 + site_information: 站点信息 + admin_account: 管理员账号 + site_name: + label: 站点名称 + msg: 站点名称不能为空。 + msg_max_length: 站点名称长度不得超过 30 个字符。 + site_url: + label: 网站网址 + text: 此网站的网址。 + msg: + empty: 网址不能为空。 + incorrect: 网址格式不正确。 + max_length: 网址长度不得超过 512 个字符。 + contact_email: + label: 联系邮箱 + text: 负责本网站的主要联系人的电子邮件地址。 + msg: + empty: 联系人邮箱不能为空。 + incorrect: 联系人邮箱地址不正确。 + login_required: + label: 私有的 + switch: 需要登录 + text: 只有登录用户才能访问这个社区。 + admin_name: + label: 名字 + msg: 名字不能为空。 + character: '只能由 "a-z", "0-9", " - . _" 组成' + msg_max_length: 名字长度不能超过 30 个字符。 + admin_password: + label: 密码 + text: >- + 您需要此密码才能登录。请将其存储在一个安全的位置。 + msg: 密码不能为空。 + msg_min_length: 密码必须至少 8 个字符长。 + msg_max_length: 密码长度不能超过 32 个字符。 + admin_email: + label: 邮箱 + text: 您需要此电子邮件才能登录。 + msg: + empty: 邮箱不能为空。 + incorrect: 邮箱格式不正确。 + ready_title: 您的网站已准备好 + ready_desc: >- + 如果你想改变更多的设置,请访问 <1>管理区域;在网站菜单中找到它。 + good_luck: "玩得愉快,祝你好运!" + warn_title: 警告 + warn_desc: >- + 文件 <1>config.yaml 已存在。如果你要重置该文件中的任何配置项,请先删除它。 + install_now: 您可以尝试 <1>现在安装。 + installed: 已安裝 + installed_desc: >- + 你似乎已经安装过了。如果要重新安装,请先清除旧的数据库表。 + db_failed: 数据连接异常! + db_failed_desc: >- + 这或者意味着数据库信息在 <1>config.yaml 文件不正确,或者无法与数据库服务器建立联系。这可能意味着你的主机数据库服务器故障。 + counts: + views: 次浏览 + votes: 个点赞 + answers: 个回答 + accepted: 已被采纳 + page_error: + http_error: HTTP 错误 {{ code }} + desc_403: 您无权访问此页面。 + desc_404: 很抱歉,此页面不存在。 + desc_50X: 服务器遇到了一个错误,无法完成你的请求。 + back_home: 返回首页 + page_maintenance: + desc: "我们正在进行维护,我们将很快回来。" + nav_menus: + dashboard: 后台管理 + contents: 内容管理 + questions: 问题 + answers: 回答 + users: 用户管理 + flags: 举报管理 + settings: 站点设置 + general: 一般 + interface: 界面 + smtp: SMTP + branding: 品牌 + legal: 法律条款 + write: 撰写 + tos: 服务条款 + privacy: 隐私政策 + seo: SEO + customize: 自定义 + themes: 主题 + css_html: CSS/HTML + login: 登录 + privileges: 特权 + plugins: 插件 + installed_plugins: 已安装插件 + website_welcome: 欢迎来到 {{site_name}} + user_center: + login: 登录 + qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。 + login_failed_email_tip: 登录失败,请允许此应用访问您的邮箱信息,然后重试。 + admin: + admin_header: + title: 后台管理 + dashboard: + title: 后台管理 + welcome: 欢迎来到管理后台! + site_statistics: 站点统计 + questions: "问题:" + answers: "回答:" + comments: "评论:" + votes: "投票:" + users: "用户:" + flags: "举报:" + reviews: "审查:" + site_health: 网站健康 + version: "版本" + https: "HTTPS:" + upload_folder: "上传文件夹:" + run_mode: "运行模式:" + private: 私有 + public: 公开 + smtp: "SMTP:" + timezone: "时区:" + system_info: 系统信息 + go_version: "Go版本:" + database: "数据库:" + database_size: "数据库大小:" + storage_used: "已用存储空间:" + uptime: "运行时间:" + links: 链接 + plugins: 插件 + github: GitHub + blog: 博客 + contact: 联系 + forum: 论坛 + documents: 文档 + feedback: 用户反馈 + support: 帮助 + review: 审查 + config: 配置 + update_to: 更新到 + latest: 最新版本 + check_failed: 校验失败 + "yes": "是" + "no": "否" + not_allowed: 拒绝 + allowed: 允许 + enabled: 已启用 + disabled: 停用 + writable: 可写 + not_writable: 不可写 + flags: + title: 举报 + pending: 等待处理 + completed: 已完成 + flagged: 被举报内容 + flagged_type: 标记了 {{ type }} + created: 创建于 + action: 操作 + review: 审查 + user_role_modal: + title: 更改用户状态为... + btn_cancel: 取消 + btn_submit: 提交 + new_password_modal: + title: 设置新密码 + form: + fields: + password: + label: 密码 + text: 用户将被退出,需要再次登录。 + msg: 密码的长度必须是8-32个字符。 + btn_cancel: 取消 + btn_submit: 提交 + edit_profile_modal: + title: 编辑资料 + form: + fields: + username: + label: 用户名 + msg_range: 用户名不能超过 30 个字符。 + email: + label: 电子邮件地址 + msg_invalid: 无效的邮箱地址 + edit_success: 修改成功 + btn_cancel: 取消 + btn_submit: 提交 + user_modal: + title: 添加新用户 + form: + fields: + users: + label: 批量添加用户 + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: 用逗号分隔“name, email, password”,每行一个用户。 + msg: "请输入用户的邮箱,每行一个。" + display_name: + label: 显示名称 + msg: 显示名称长度必须为 4-30 个字符 + email: + label: 邮箱 + msg: 邮箱无效。 + password: + label: 密码 + msg: 密码的长度必须是8-32个字符。 + btn_cancel: 取消 + btn_submit: 提交 + users: + title: 用户 + name: 名称 + email: 邮箱 + reputation: 声望 + created_at: 创建时间 + delete_at: 删除时间 + suspend_at: 封禁时间 + status: 状态 + role: 角色 + action: 操作 + change: 更改 + all: 全部 + staff: 工作人员 + more: 更多 + inactive: 不活跃 + suspended: 已封禁 + deleted: 已删除 + normal: 正常 + Moderator: 版主 + Admin: 管理员 + User: 用户 + filter: + placeholder: "按名称筛选,用户:id" + set_new_password: 设置新密码 + edit_profile: 编辑资料 + change_status: 更改状态 + change_role: 更改角色 + show_logs: 显示日志 + add_user: 添加用户 + deactivate_user: + title: 停用用户 + content: 未激活的用户必须重新验证他们的邮箱。 + delete_user: + title: 删除此用户 + content: 确定要删除此用户?此操作无法撤销! + remove: 移除内容 + label: 删除所有问题、 答案、 评论等 + text: 如果你只想删除用户账户,请不要选中此项。 + suspend_user: + title: 挂起此用户 + content: 被封禁的用户将无法登录。 + questions: + page_title: 问题 + unlisted: 已隐藏 + post: 标题 + votes: 得票数 + answers: 回答数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + pending: 等待处理 + filter: + placeholder: "按标题过滤,问题:id" + answers: + page_title: 回答 + post: 标题 + votes: 得票数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + filter: + placeholder: "按标题筛选,答案:id" + general: + page_title: 一般 + name: + label: 站点名称 + msg: 不能为空 + text: "站点的名称,作为站点的标题。" + site_url: + label: 网站网址 + msg: 网站网址不能为空。 + validate: 请输入一个有效的 URL。 + text: 此网站的地址。 + short_desc: + label: 简短站点描述 + msg: 简短网站描述不能为空。 + text: "简短的标语,作为网站主页的标题(Html 的 title 标签)。" + desc: + label: 站点描述 + msg: 网站描述不能为空。 + text: "使用一句话描述本站,作为网站的描述(Html 的 meta 标签)。" + contact_email: + label: 联系邮箱 + msg: 联系人邮箱不能为空。 + validate: 联系人邮箱无效。 + text: 本网站的主要联系邮箱地址。 + check_update: + label: 软件更新 + text: 自动检查软件更新 + interface: + page_title: 界面 + language: + label: 界面语言 + msg: 不能为空 + text: 设置用户界面语言,在刷新页面后生效。 + time_zone: + label: 时区 + msg: 时区不能为空。 + text: 选择一个与您相同时区的城市。 + smtp: + page_title: SMTP + from_email: + label: 发件人邮箱 + msg: 发件人邮箱不能为空。 + text: 用于发送邮件的地址。 + from_name: + label: 发件人 + msg: 不能为空 + text: 发件人的名字。 + smtp_host: + label: SMTP 主机 + msg: 不能为空 + text: 邮件服务器 + encryption: + label: 加密 + msg: 不能为空 + text: 对于大多数服务器而言,SSL 是推荐开启的。 + ssl: SSL + tls: TLS + none: 无加密 + smtp_port: + label: SMTP 端口 + msg: SMTP 端口必须在 1 ~ 65535 之间。 + text: 邮件服务器的端口号。 + smtp_username: + label: SMTP 用户名 + msg: 不能为空 + smtp_password: + label: SMTP 密码 + msg: 不能为空 + test_email_recipient: + label: 测试收件邮箱 + text: 提供用于接收测试邮件的邮箱地址。 + msg: 测试收件邮箱无效 + smtp_authentication: + label: 启用身份验证 + title: SMTP 身份验证 + msg: 不能为空 + "yes": "是" + "no": "否" + branding: + page_title: 品牌 + logo: + label: 网站标志(Logo) + msg: 图标不能为空。 + text: 在你的网站左上方的Logo图标。使用一个高度为56,长宽比大于3:1的宽长方形图像。如果留空,将显示网站标题文本。 + mobile_logo: + label: 移动端 Logo + text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空,将使用 "Logo"设置中的图像。 + square_icon: + label: 方形图标 + msg: 方形图标不能为空。 + text: 用作元数据图标的基础的图像。最好是大于512x512。 + favicon: + label: 收藏夹图标 + text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空,将使用“方形图标”。 + legal: + page_title: 法律条款 + terms_of_service: + label: 服务条款 + text: "您可以在此添加服务内容的条款。如果您已经在别处托管了文档,请在这里提供完整的URL。" + privacy_policy: + label: 隐私政策 + text: "您可以在此添加隐私政策内容。如果您已经在别处托管了文档,请在这里提供完整的URL。" + write: + page_title: 编辑 + restrict_answer: + title: 限制一个回答 + label: 每个用户对于每个问题只能有一个回答 + text: "用户可以使用编辑按钮优化已有的回答" + recommend_tags: + label: 推荐标签 + text: "请在上方输入标签固定链接,每行一个标签。" + required_tag: + title: 必需的标签 + label: 根据需要设置推荐标签 + text: "每个新问题必须至少有一个推荐标签。" + reserved_tags: + label: 保留标签 + text: "保留的标签只能由版主添加到一个帖子中。" + seo: + page_title: 搜索引擎优化 + permalink: + label: 固定链接 + text: 自定义URL结构可以提高可用性,以及你的链接的向前兼容性。 + robots: + label: robots.txt + text: 这将永久覆盖任何相关的网站设置。 + themes: + page_title: 主题 + themes: + label: 主题 + text: 选择一个现有主题。 + color_scheme: + label: 配色方案 + navbar_style: + label: 导航栏样式 + primary_color: + label: 主色调 + text: 修改您主题使用的颜色 + css_and_html: + page_title: CSS 与 HTML + custom_css: + label: 自定义 CSS + text: > + + head: + label: 头部 + text: > + + header: + label: 页眉 + text: > + + footer: + label: 页脚 + text: 这将在 之前插入. + sidebar: + label: 侧边栏 + text: 这将插入侧边栏中。 + login: + page_title: 登录 + membership: + title: 会员 + label: 允许新注册 + text: 关闭以防止任何人创建新账户。 + email_registration: + title: 邮箱注册 + label: 允许邮箱注册 + text: 关闭以阻止任何人通过邮箱创建新账户。 + allowed_email_domains: + title: 允许的邮箱域 + text: 允许注册账户的邮箱域。每行一个域名。留空时忽略。 + private: + title: 非公开的 + label: 需要登录 + text: 只有登录用户才能访问这个社区。 + password_login: + title: 密码登录 + label: 允许使用邮箱和密码登录 + text: "警告:如果您未配置过其他登录方式,关闭密码登录后您则可能无法登录。" + installed_plugins: + title: 已安装插件 + plugin_link: 插件扩展功能。您可以在<1>插件仓库中找到插件。 + filter: + all: 全部 + active: 已启用 + inactive: 未启用 + outdated: 已过期 + plugins: + label: 插件 + text: 选择一个现有的插件。 + name: 名称 + version: 版本 + status: 状态 + action: 操作 + deactivate: 停用 + activate: 启用 + settings: 设置 + settings_users: + title: 用户 + avatar: + label: 默认头像 + text: 没有自定义头像的用户。 + gravatar_base_url: + label: Gravatar 根路径 URL + text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。 + profile_editable: + title: 个人资料可编辑 + allow_update_display_name: + label: 允许用户修改显示名称 + allow_update_username: + label: 允许用户修改用户名 + allow_update_avatar: + label: 允许用户修改个人头像 + allow_update_bio: + label: 允许用户修改个人介绍 + allow_update_website: + label: 允许用户修改个人主页网址 + allow_update_location: + label: 允许用户更改位置 + privilege: + title: 特权 + level: + label: 级别所需声望 + text: 选择特权所需的声望值 + msg: + should_be_number: 输入必须是数字 + number_larger_1: 数字应该大于等于 1 + form: + optional: (选填) + empty: 不能为空 + invalid: 是无效的 + btn_submit: 保存 + not_found_props: "所需属性 {{ key }} 未找到。" + select: 选择 + page_review: + review: 评论 + proposed: 提案 + question_edit: 问题编辑 + answer_edit: 回答编辑 + tag_edit: '标签管理: 编辑标签' + edit_summary: 编辑备注 + edit_question: 编辑问题 + edit_answer: 编辑回答 + edit_tag: 编辑标签 + empty: 没有剩余的审核任务。 + approve_revision_tip: 您是否批准此修订? + approve_flag_tip: 您是否批准此举报? + approve_post_tip: 您是否批准此帖子? + approve_user_tip: 您是否批准此修订? + suggest_edits: 建议的编辑 + flag_post: 举报帖子 + flag_user: 举报用户 + queued_post: 排队的帖子 + queued_user: 排队用户 + filter_label: 类型 + reputation: 声望值 + flag_post_type: 举报这个帖子的类型是 {{ type }} + flag_user_type: 举报这个用户的类型是 {{ type }} + edit_post: 编辑帖子 + list_post: 文章列表 + unlist_post: 隐藏的帖子 + timeline: + undeleted: 取消删除 + deleted: 删除 + downvote: 反对 + upvote: 点赞 + accept: 采纳 + cancelled: 已取消 + commented: '评论:' + rollback: 回滚 + edited: 最后编辑于 + answered: 回答于 + asked: 提问于 + closed: 关闭 + reopened: 重新开启 + created: 创建于 + pin: 已置顶 + unpin: 取消置頂 + show: 已显示 + hide: 已隐藏 + title: "历史记录" + tag_title: "时间线" + show_votes: "显示投票" + n_or_a: N/A + title_for_question: "时间线" + title_for_answer: "{{ title }} 的 {{ author }} 回答时间线" + title_for_tag: "时间线" + datetime: 日期时间 + type: 类型 + by: 由 + comment: 评论 + no_data: "空空如也" + users: + title: 用户 + users_with_the_most_reputation: 本周声望最高的用户 + users_with_the_most_vote: 本周投票最多的用户 + staffs: 我们的社区工作人员 + reputation: 声望值 + votes: 投票 + prompt: + leave_page: 确定要离开此页面? + changes_not_save: 您的更改尚未保存 + draft: + discard_confirm: 您确定要丢弃您的草稿吗? + messages: + post_deleted: 该帖子已被删除。 + post_pin: 该帖子已被置顶。 + post_unpin: 该帖子已被取消置顶。 + post_hide_list: 此帖子已经从列表中隐藏。 + post_show_list: 该帖子已显示到列表中。 + post_reopen: 这个帖子已被重新打开. + post_list: 这个帖子已经被显示 + post_unlist: 这个帖子已经被隐藏 + post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。 From 38834e6294b566e29fdeca2ed318cdce6bf3dc26 Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Mon, 5 Aug 2024 17:03:23 +0800 Subject: [PATCH 052/129] 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 1b043d3a64fc6f7f9b30ee1e735bd7d37a4492ff Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Tue, 6 Aug 2024 15:39:55 +0800 Subject: [PATCH 053/129] 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 016abd1a9825ca710cee5b6df03ee5e27008e00e Mon Sep 17 00:00:00 2001 From: "James Roller, Jr." Date: Mon, 5 Aug 2024 20:10:12 -0700 Subject: [PATCH 054/129] add pvc dataSource - allow for restoring a volumeSnapshot --- charts/templates/pvc.yaml | 8 +++++++- charts/values.yaml | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/charts/templates/pvc.yaml b/charts/templates/pvc.yaml index 5dfab8fca..640fb9fe0 100644 --- a/charts/templates/pvc.yaml +++ b/charts/templates/pvc.yaml @@ -20,7 +20,7 @@ kind: PersistentVolumeClaim apiVersion: v1 metadata: name: {{ include "answer.fullname" . }}-claim - {{- with .Values.persistence.annotations }} + {{- with .Values.persistence.annotations }} annotations: {{ toYaml . | indent 4 }} {{- end }} @@ -39,4 +39,10 @@ spec: resources: requests: storage: {{ .Values.persistence.size | quote }} + {{- with .Values.persistence.dataSource }} + dataSource: + name: {{ .name }} + kind: {{ .kind | default "VolumeSnapshot" }} + apiGroup: {{ .apiGroup | default "snapshot.storage.k8s.io" }} + {{- end }} {{- end }} \ No newline at end of file diff --git a/charts/values.yaml b/charts/values.yaml index 8bb36c46f..d932db848 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -68,7 +68,7 @@ env: # Configure extra containers extraContainers: [] - # - name: cloudsql-proxy + # - name: cloudsql-proxy # image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.1.2 # command: # - /cloud-sql-proxy @@ -91,6 +91,12 @@ persistence: accessMode: ReadWriteOnce size: 5Gi annotations: {} + # To restore a PVC from a VolumeSnapshot, set the dataSource; + # the kind and apiGroup are optional and default to the shown values + dataSource: {} + # name: my-volume-snapshot + # kind: VolumeSnapshot + # apiGroup: snapshot.storage.k8s.io imagePullSecrets: [] nameOverride: "" From eda39380758e838a6c294d2a52ce1c325ebb2cd3 Mon Sep 17 00:00:00 2001 From: Sonui Date: Wed, 7 Aug 2024 22:51:23 +0800 Subject: [PATCH 055/129] perf: remove invalid nil pointer check in Translate function --- plugin/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/plugin.go b/plugin/plugin.go index 61ed0d8f7..852868557 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -219,7 +219,7 @@ func MakeTranslator(key string) Translator { // Translate translates the key to the current language of the context func (t Translator) Translate(ctx *GinContext) string { - if &t == nil || t.Fn == nil { + if t.Fn == nil { return "" } return t.Fn(ctx) From 4b3e0eaa4e407842d62ec19b4313942b53d3809e Mon Sep 17 00:00:00 2001 From: Sonui Date: Wed, 14 Aug 2024 00:21:55 +0800 Subject: [PATCH 056/129] perf: update check-asf-header.sh script to support Podman --- script/check-asf-header.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/script/check-asf-header.sh b/script/check-asf-header.sh index ff765eab5..808efa108 100755 --- a/script/check-asf-header.sh +++ b/script/check-asf-header.sh @@ -16,5 +16,16 @@ # specific language governing permissions and limitations # under the License. -docker run -it --rm -v $(pwd):/github/workspace ghcr.io/korandoru/hawkeye-native format +# check if docker or podman is installed +if command -v docker >/dev/null 2>&1; then + CONTAINER_RUNTIME="docker" +elif command -v podman >/dev/null 2>&1; then + CONTAINER_RUNTIME="podman" +else + echo "Neither Docker nor Podman is installed. Please install either Docker or Podman." + exit 1 +fi + +$CONTAINER_RUNTIME run -it --rm -v "$(pwd)":/github/workspace ghcr.io/korandoru/hawkeye-native format + gofmt -w -l . From 969169243bac95cc15eeee53e5dd4ebb307c37b1 Mon Sep 17 00:00:00 2001 From: Sonui Date: Thu, 15 Aug 2024 21:15:02 +0800 Subject: [PATCH 057/129] perf: remove extra characters in header.html --- ui/template/header.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/template/header.html b/ui/template/header.html index 653f9a28a..0243a43d7 100644 --- a/ui/template/header.html +++ b/ui/template/header.html @@ -68,14 +68,14 @@ From b41bafad30587ccada6818b58dd3328f22876aa4 Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 19 Aug 2024 15:15:07 +0800 Subject: [PATCH 058/129] fix(dev): delete test file --- dev/i18n/zh_CN.yaml | 2035 ------------------------------------------- 1 file changed, 2035 deletions(-) delete mode 100644 dev/i18n/zh_CN.yaml diff --git a/dev/i18n/zh_CN.yaml b/dev/i18n/zh_CN.yaml deleted file mode 100644 index 6613bfc22..000000000 --- a/dev/i18n/zh_CN.yaml +++ /dev/null @@ -1,2035 +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. - -# The following fields are used for back-end -backend: - base: - success: - other: 成功。 - unknown: - other: 未知错误。 - request_format_error: - other: 请求格式错误。 - unauthorized_error: - other: 未授权。 - database_error: - other: 数据服务器错误。 - forbidden_error: - other: 禁止访问。 - duplicate_request_error: - other: 重复提交。 - action: - report: - other: 举报 - edit: - other: 编辑 - delete: - other: 删除 - close: - other: 关闭 - reopen: - other: 重新打开 - forbidden_error: - other: 禁止访问。 - pin: - other: 置顶 - hide: - other: 列表隐藏 - unpin: - other: 取消置顶 - show: - other: 列表显示 - invite_someone_to_answer: - other: 编辑 - undelete: - other: 撤消删除 - role: - name: - user: - other: 用户 - admin: - other: 管理员 - moderator: - other: 版主 - description: - user: - other: 默认没有特殊权限。 - admin: - other: 拥有管理网站的全部权限。 - moderator: - other: 拥有除访问后台管理以外的所有权限。 - privilege: - level_1: - description: - other: 级别 1(少量声望要求,适合私有团队、群组) - level_2: - description: - other: 级别 2(低声望要求,适合初启动的社区) - level_3: - description: - other: 级别 3(高声望要求,适合成熟的社区) - level_custom: - description: - other: 自定义等级 - rank_question_add_label: - other: 提问 - rank_answer_add_label: - other: 写答案 - rank_comment_add_label: - other: 写评论 - rank_report_add_label: - other: 举报 - rank_comment_vote_up_label: - other: 点赞评论 - rank_link_url_limit_label: - other: 每次发布超过 2 个链接 - rank_question_vote_up_label: - other: 点赞问题 - rank_answer_vote_up_label: - other: 点赞答案 - rank_question_vote_down_label: - other: 点踩问题 - rank_answer_vote_down_label: - other: 点踩答案 - rank_invite_someone_to_answer_label: - other: 邀请回答 - rank_tag_add_label: - other: 创建新标签 - rank_tag_edit_label: - other: 编辑标签描述(需要审核) - rank_question_edit_label: - other: 编辑别人的问题(需要审核) - rank_answer_edit_label: - other: 编辑别人的答案(需要审核) - rank_question_edit_without_review_label: - other: 编辑别人的问题无需审核 - rank_answer_edit_without_review_label: - other: 编辑别人的答案无需审核 - rank_question_audit_label: - other: 审核问题编辑 - rank_answer_audit_label: - other: 审核回答编辑 - rank_tag_audit_label: - other: 审核标签编辑 - rank_tag_edit_without_review_label: - other: 编辑标签描述无需审核 - rank_tag_synonym_label: - other: 管理标签同义词 - email: - other: 邮箱 - e_mail: - other: 邮箱 - password: - other: 密码 - pass: - other: 密码 - original_text: - other: 本帖 - email_or_password_wrong_error: - other: 邮箱和密码不匹配。 - error: - common: - invalid_url: - other: 无效的 URL。 - password: - space_invalid: - other: 密码不得含有空格。 - admin: - cannot_update_their_password: - other: 你无法修改自己的密码。 - cannot_edit_their_profile: - other: 您不能修改您的个人资料。 - cannot_modify_self_status: - other: 你无法修改自己的状态。 - email_or_password_wrong: - other: 邮箱和密码不匹配。 - answer: - not_found: - other: 没有找到答案。 - cannot_deleted: - other: 没有删除权限。 - cannot_update: - other: 没有更新权限。 - question_closed_cannot_add: - other: 问题已关闭,无法添加。 - comment: - edit_without_permission: - other: 不允许编辑评论。 - not_found: - other: 评论未找到。 - cannot_edit_after_deadline: - other: 评论时间太久,无法修改。 - email: - duplicate: - other: 邮箱已存在。 - need_to_be_verified: - other: 邮箱需要验证。 - verify_url_expired: - other: 邮箱验证的网址已过期,请重新发送邮件。 - illegal_email_domain_error: - other: 此邮箱不在允许注册的邮箱域中。请使用其他邮箱尝试。 - lang: - not_found: - other: 语言文件未找到。 - object: - captcha_verification_failed: - other: 验证码错误。 - disallow_follow: - other: 你不能关注。 - disallow_vote: - other: 你不能投票。 - disallow_vote_your_self: - other: 你不能为自己的帖子投票。 - not_found: - other: 对象未找到。 - verification_failed: - other: 验证失败。 - email_or_password_incorrect: - other: 邮箱和密码不匹配。 - old_password_verification_failed: - other: 旧密码验证失败。 - new_password_same_as_previous_setting: - other: 新密码和旧密码相同。 - already_deleted: - other: 该帖子已被删除。 - meta: - object_not_found: - other: Meta 对象未找到 - question: - already_deleted: - other: 该帖子已被删除。 - under_review: - other: 您的帖子正在等待审核。它将在它获得批准后可见。 - not_found: - other: 问题未找到。 - cannot_deleted: - other: 没有删除权限。 - cannot_close: - other: 没有关闭权限。 - cannot_update: - other: 没有更新权限。 - rank: - fail_to_meet_the_condition: - other: 声望值未达到要求。 - vote_fail_to_meet_the_condition: - other: 感谢投票。你至少需要 {{.Rank}} 声望才能投票。 - no_enough_rank_to_operate: - other: 你至少需要 {{.Rank}} 声望才能执行此操作。 - report: - handle_failed: - other: 报告处理失败。 - not_found: - other: 报告未找到。 - tag: - already_exist: - other: 标签已存在。 - not_found: - other: 标签未找到。 - recommend_tag_not_found: - other: 推荐标签不存在。 - recommend_tag_enter: - other: 请选择至少一个必选标签。 - not_contain_synonym_tags: - other: 不应包含同义词标签。 - cannot_update: - other: 没有更新权限。 - is_used_cannot_delete: - other: 你不能删除这个正在使用的标签。 - cannot_set_synonym_as_itself: - other: 你不能将当前标签设为自己的同义词。 - smtp: - config_from_name_cannot_be_email: - other: 发件人名称不能是邮箱地址。 - theme: - not_found: - other: 主题未找到。 - revision: - review_underway: - other: 目前无法编辑,有一个版本在审阅队列中。 - no_permission: - other: 无权限修改。 - user: - external_login_missing_user_id: - other: 第三方平台没有提供唯一的 UserID,所以你不能登录,请联系网站管理员。 - external_login_unbinding_forbidden: - other: 请在移除此登录之前为你的账户设置登录密码。 - email_or_password_wrong: - other: - other: 邮箱和密码不匹配。 - not_found: - other: 用户未找到。 - suspended: - other: 用户已被封禁。 - username_invalid: - other: 用户名无效。 - username_duplicate: - other: 用户名已被使用。 - set_avatar: - other: 头像设置错误。 - cannot_update_your_role: - other: 你不能修改自己的角色。 - not_allowed_registration: - other: 该网站暂未开放注册。 - not_allowed_login_via_password: - other: 该网站暂不支持密码登录。 - access_denied: - other: 拒绝访问 - page_access_denied: - other: 您没有权限访问此页面。 - add_bulk_users_format_error: - other: "发生错误,{{.Field}} 格式错误,在 '{{.Content}}' 行数 {{.Line}}. {{.ExtraMessage}}" - add_bulk_users_amount_error: - other: "一次性添加的用户数量应在 1-{{.MaxAmount}} 之间。" - config: - read_config_failed: - other: 读取配置失败 - database: - connection_failed: - other: 数据库连接失败 - create_table_failed: - other: 创建表失败 - install: - create_config_failed: - other: 无法创建 config.yaml 文件。 - upload: - unsupported_file_format: - other: 不支持的文件格式。 - site_info: - config_not_found: - other: 未找到网站的该配置信息。 - reason: - spam: - name: - other: 垃圾信息 - desc: - other: 这个帖子是一个广告,或是破坏性行为。它对当前的主题无帮助或无关。 - rude_or_abusive: - name: - other: 粗鲁或辱骂的 - desc: - other: - - 一个有理智的人都会认为这种内容不适合进行尊重性的讨论。 - - 论坛 - a_duplicate: - name: - other: 重复内容 - desc: - other: 该问题有人问过,而且已经有了答案。 - placeholder: - other: 输入已有的问题链接 - not_a_answer: - name: - other: 不是答案 - desc: - other: - - 这张贴作为答案,但它不会试图回答 - - 这可能是一个编辑、一个评论、另一个问题。 - - 或全部删除。 - no_longer_needed: - name: - other: 不再需要 - desc: - other: 该评论已过时,对话性质或与此帖子无关。 - something: - name: - other: 其他原因 - desc: - other: 此帖子需要工作人员注意,因为是上述所列以外的其他理由。 - placeholder: - other: 让我们具体知道你关心的什么 - community_specific: - name: - other: 社区特定原因 - desc: - other: 该问题不符合社区准则。 - not_clarity: - name: - other: 需要细节或澄清 - desc: - other: 该问题目前涵盖多个问题。它应该侧重在一个问题上。 - looks_ok: - name: - other: 看起来没问题 - desc: - other: 这个帖子是好的,不是低质量。 - needs_edit: - name: - other: 需要编辑,我已做了修改。 - desc: - other: 改进和纠正你自己帖子中的问题。 - needs_close: - name: - other: 需要关闭 - desc: - other: 关闭的问题不能回答,但仍然可以编辑、投票和评论。 - needs_delete: - name: - other: 需要删除 - desc: - other: 该帖子将被删除。 - question: - close: - duplicate: - name: - other: 垃圾信息 - desc: - other: 此问题以前就有人问过,而且已经有了答案。 - guideline: - name: - other: 社区特定原因 - desc: - other: 该问题不符合社区准则。 - multiple: - name: - other: 需要细节或澄清 - desc: - other: - - 该问题目前涵盖多个问题。它应该侧重在一个问题上。 - - 只关注一个问题。 - other: - name: - other: 其他原因 - desc: - other: 该帖子存在上面没有列出的另一个原因。 - operation_type: - asked: - other: 提问于 - answered: - other: 回答于 - modified: - other: 修改于 - deleted_title: - other: 删除的问题 - notification: - action: - update_question: - other: 更新了问题 - answer_the_question: - other: 回答了问题 - update_answer: - other: 更新了答案 - accept_answer: - other: 采纳了答案 - comment_question: - other: 评论了问题 - comment_answer: - other: 评论了答案 - reply_to_you: - other: 回复了你 - mention_you: - other: 提到了你 - your_question_is_closed: - other: 你的问题已被关闭 - your_question_was_deleted: - other: 你的问题已被删除 - your_answer_was_deleted: - other: 你的答案已被删除 - your_comment_was_deleted: - other: 你的评论已被删除 - up_voted_question: - other: 点赞问题 - down_voted_question: - other: 点踩问题 - up_voted_answer: - other: 点赞答案 - down_voted_answer: - other: 点踩回答 - up_voted_comment: - other: 点赞评论 - invited_you_to_answer: - other: 邀请你回答 - email_tpl: - change_email: - title: - other: "[{{.SiteName}}] 确认你的新邮箱地址" - body: - other: "请点击以下链接确认你在 {{.SiteName}} 上的新邮箱地址:
\n{{.ChangeEmailUrl}}

\n\n如果你没有请求此更改,请忽略此邮件。\n" - new_answer: - title: - other: "[{{.SiteName}}] {{.DisplayName}} 回答了你的问题" - body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" - invited_you_to_answer: - title: - other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题" - body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
我想你可能知道答案。

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" - new_comment: - title: - other: "[{{.SiteName}}] {{.DisplayName}} 评论了你的帖子" - body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" - new_question: - title: - other: "[{{.SiteName}}] 新问题: {{.QuestionTitle}}" - body: - other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\n取消订阅" - pass_reset: - title: - other: "[{{.SiteName }}] 重置密码" - body: - other: "有人要求在 [{{.SiteName}}] 上重置你的密码。

\n\n如果这不是你的操作,请安心忽略此电子邮件。

\n\n请点击以下链接设置一个新密码:
\n{{.PassResetUrl}}\n" - register: - title: - other: "[{{.SiteName}}] 确认你的新账户" - body: - other: "欢迎加入 {{.SiteName}}!

\n\n请点击以下链接确认并激活你的新账户:
\n{{.RegisterUrl}}

\n\n如果上面的链接不能点击,请将其复制并粘贴到你的浏览器地址栏中。\n" - test: - title: - other: "[{{.SiteName}}] 测试邮件" - body: - other: "这是一封测试邮件。" - action_activity_type: - upvote: - other: 点赞 - upvoted: - other: 点赞 - downvote: - other: 点踩 - downvoted: - other: 点踩 - accept: - other: 采纳 - accepted: - other: 已采纳 - edit: - other: 编辑 - review: - queued_post: - other: 排队的帖子 - flagged_post: - other: 举报的帖子 - suggested_post_edit: - other: 建议的编辑 - reaction: - tooltip: - other: "{{ .Names }} 以及另外 {{ .Count }} 个..." -plugin: - s3_cdn: - backend: - info: - name: - other: S3 存储 CDN - description: - other: 上传文件到S3存储 - config: - endpoint: - title: - other: Endpoint - description: - other: S3存储的Endpoint - bucket_name: - title: - other: Bucket名称 - description: - other: S3存储的Bucket名称 - object_key_prefix: - title: - other: 对象Key前缀 - description: - other: 对象键的前缀,如'answer/data/',以'/'结尾 - access_key_id: - title: - other: AccessKeyID - description: - other: S3存储的AccessKeyID - access_key_secret: - title: - other: AccessKeySecret - description: - other: S3存储的AccessKeySecret - access_token: - title: - other: AccessToken - description: - other: 访问 S3 所需的 AccessToken - visit_url_prefix: - title: - other: 访问URL前缀 - description: - other: 上传静态文件CDN最终访问地址的前缀,以 '/' 结尾 https://static.example.com/xxx/ - max_file_size: - title: - other: 文件最大大小(MB) - description: - other: 限制上传文件的最大大小,单位MB,默认为10MB - region: - title: - other: 区域(Region) - description: - other: S3存储区域 - disable_ssl: - title: - other: 禁用SSL - description: - other: 我们建议您使用SSL访问S3存储。如果您想禁用SSL,请选中此选项。 - err: - mis_storage_config: - other: 错误的存储配置导致上传失败 - file_not_found: - other: 文件未找到 - unsupported_file_type: - other: 不支持的文件类型 - over_file_size_limit: - other: 超过文件大小限制 - upload_file_failed: - other: 上传文件失败 - aliyunoss_cdn: - backend: - info: - name: - other: 阿里云CDN OSS存储 - description: - other: 上传文件到阿里云CDN OSS存储 - config: - endpoint: - title: - other: Endpoint - description: - other: 阿里云OSS存储的Endpoint - bucket_name: - title: - other: Bucket名称 - description: - other: 阿里云OSS存储的Bucket名称 - object_key_prefix: - title: - other: 对象Key前缀 - description: - other: 对象键的前缀,如'answer/data/',以'/'结尾 - access_key_id: - title: - other: AccessKeyID - description: - other: 阿里云OSS存储的AccessKeyID - access_key_secret: - title: - other: AccessKeySecret - description: - other: 阿里云OSS存储的AccessKeySecret - visit_url_prefix: - title: - other: 访问URL前缀 - description: - other: CDN最终访问地址的前缀,以 '/' 结尾 https://example.com/xxx/ - max_file_size: - title: - other: 最大文件大小(MB) - description: - other: 限制上传文件的最大大小,单位为MB,默认为 10MB - err: - mis_storage_config: - other: 错误的存储配置导致上传失败 - file_not_found: - other: 文件未找到 - unsupported_file_type: - other: 不支持的文件类型 - over_file_size_limit: - other: 超过文件大小限制 - upload_file_failed: - other: 上传文件失败 -# The following fields are used for interface presentation(Front-end) -ui: - how_to_format: - title: 如何排版 - desc: >- -
  • 添加链接

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 **粗体**

  • 使用 4 个空格缩进代码

  • 在行首添加 > 表示引用

  • 反引号进行转义 `像 _这样_`

  • 使用 ``` 创建代码块

    ```
    这是代码块
    ```
- pagination: - prev: 上一页 - next: 下一页 - page_title: - question: 问题 - questions: 问题 - tag: 标签 - tags: 标签 - tag_wiki: 标签维基 - create_tag: 创建标签 - edit_tag: 编辑标签 - ask_a_question: 提问题 - edit_question: 编辑问题 - edit_answer: 编辑回答 - search: 搜索 - posts_containing: 帖子包含 - settings: 设置 - notifications: 通知 - login: 登录 - sign_up: 注册 - account_recovery: 账号恢复 - account_activation: 账号激活 - confirm_email: 确认电子邮件 - account_suspended: 账号已被封禁 - admin: 后台管理 - change_email: 修改邮箱 - install: Answer 安装 - upgrade: Answer 升级 - maintenance: 网站维护 - users: 用户 - oauth_callback: 处理中 - http_404: HTTP 错误 404 - http_50X: HTTP 错误 500 - http_403: HTTP 错误 403 - logout: 退出 - notifications: - title: 通知 - inbox: 收件箱 - achievement: 成就 - new_alerts: 新通知 - all_read: 全部标记为已读 - show_more: 显示更多 - someone: 有人 - inbox_type: - all: 全部 - posts: 帖子 - invites: 邀请 - votes: 投票 - suspended: - title: 你的账号账号已被封禁 - until_time: "你的账号被封禁直到 {{ time }}。" - forever: 你的账号已被永久封禁。 - end: 你违反了我们的社区准则。 - contact_us: 联系我们 - editor: - blockquote: - text: 引用 - bold: - text: 粗体 - chart: - text: 图表 - flow_chart: 流程图 - sequence_diagram: 时序图 - class_diagram: 类图 - state_diagram: 状态图 - entity_relationship_diagram: 实体关系图 - user_defined_diagram: 用户自定义图表 - gantt_chart: 甘特图 - pie_chart: 饼图 - code: - text: 代码块 - add_code: 添加代码块 - form: - fields: - code: - label: 代码块 - msg: - empty: 代码块不能为空 - language: - label: 语言 - placeholder: 自动识别 - btn_cancel: 取消 - btn_confirm: 添加 - formula: - text: 公式 - options: - inline: 行内公式 - block: 块级公式 - heading: - text: 标题 - options: - h1: 标题 1 - h2: 标题 2 - h3: 标题 3 - h4: 标题 4 - h5: 标题 5 - h6: 标题 6 - help: - text: 帮助 - hr: - text: 水平线 - image: - text: 图片 - add_image: 添加图片 - tab_image: 上传图片 - form_image: - fields: - file: - label: 图像文件 - btn: 选择图片 - msg: - empty: 请选择图片文件。 - only_image: 只能上传图片文件。 - max_size: 文件大小不能超过 4 MB。 - desc: - label: 描述 - tab_url: 图片地址 - form_url: - fields: - url: - label: 图片地址 - msg: - empty: 图片地址不能为空 - name: - label: 描述 - btn_cancel: 取消 - btn_confirm: 添加 - uploading: 上传中 - indent: - text: 缩进 - outdent: - text: 减少缩进 - italic: - text: 斜体 - link: - text: 超链接 - add_link: 添加超链接 - form: - fields: - url: - label: 链接 - msg: - empty: 链接不能为空。 - name: - label: 描述 - btn_cancel: 取消 - btn_confirm: 添加 - ordered_list: - text: 有序列表 - unordered_list: - text: 无序列表 - table: - text: 表格 - heading: 表头 - cell: 单元格 - close_modal: - title: 关闭原因是... - btn_cancel: 取消 - btn_submit: 提交 - remark: - empty: 不能为空。 - msg: - empty: 请选择一个原因。 - report_modal: - flag_title: 我举报这篇帖子的原因是... - close_title: 我关闭这篇帖子的原因是... - review_question_title: 审查问题 - review_answer_title: 审查回答 - review_comment_title: 审查评论 - btn_cancel: 取消 - btn_submit: 提交 - remark: - empty: 不能为空 - msg: - empty: 请选择一个原因。 - not_a_url: URL 格式不正确。 - url_not_match: URL 来源与当前网站不匹配。 - tag_modal: - title: 创建新标签 - form: - fields: - display_name: - label: 显示名称 - msg: - empty: 显示名称不能为空。 - range: 显示名称不能超过 35 个字符。 - slug_name: - label: URL 固定链接 - desc: URL 固定链接不能超过 35 个字符。 - msg: - empty: URL 固定链接不能为空。 - range: URL 固定链接不能超过 35 个字符。 - character: URL 固定链接包含非法字符。 - desc: - label: 描述 - revision: - label: 编辑历史 - edit_summary: - label: 编辑备注 - placeholder: >- - 简单描述更改原因(更正拼写、修复语法、改进格式) - btn_cancel: 取消 - btn_submit: 提交 - btn_post: 发布新标签 - tag_info: - created_at: 创建于 - edited_at: 编辑于 - history: 历史 - synonyms: - title: 同义词 - text: 以下标签将被重置到 - empty: 此标签目前没有同义词。 - btn_add: 添加同义词 - btn_edit: 编辑 - btn_save: 保存 - synonyms_text: 以下标签将被重置到 - delete: - title: 删除标签 - tip_with_posts: >- -

我们不允许 删除带有帖子的标签

请先从帖子中移除此标签。

- tip_with_synonyms: >- -

我们不允许 删除带有同义词的标签

请先从此标签中删除同义词。

- tip: 确定要删除吗? - close: 关闭 - edit_tag: - title: 编辑标签 - default_reason: 编辑标签 - default_first_reason: 添加标签 - btn_save_edits: 保存更改 - btn_cancel: 取消 - dates: - long_date: MM 月 DD 日 - long_date_with_year: "YYYY 年 MM 月 DD 日" - long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm" - now: 刚刚 - x_seconds_ago: "{{count}} 秒前" - x_minutes_ago: "{{count}} 分钟前" - x_hours_ago: "{{count}} 小时前" - hour: 小时 - day: 天 - hours: 小时 - days: 日 - reaction: - heart: 爱心 - smile: 微笑 - frown: 愁 - btn_label: 添加或删除回应。 - undo_emoji: 撤销 {{ emoji }} 回应 - react_emoji: 用 {{ emoji }} 回应 - unreact_emoji: 撤销 {{ emoji }} - comment: - btn_add_comment: 添加评论 - reply_to: 回复 - btn_reply: 回复 - btn_edit: 编辑 - btn_delete: 删除 - btn_flag: 举报 - btn_save_edits: 保存更改 - btn_cancel: 取消 - show_more: "{{count}} 条剩余评论" - tip_question: >- - 使用评论提问更多信息或者提出改进意见。避免在评论里回答问题。 - tip_answer: >- - 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 - tip_vote: 它给帖子添加了一些有用的内容 - edit_answer: - title: 编辑回答 - default_reason: 编辑回答 - default_first_reason: 添加答案 - form: - fields: - revision: - label: 编辑历史 - answer: - label: 回答内容 - feedback: - characters: 内容长度至少 6 个字符 - edit_summary: - label: 编辑摘要 - placeholder: >- - 简单描述更改原因(更正拼写、修复语法、改进格式) - btn_save_edits: 保存更改 - btn_cancel: 取消 - tags: - title: 标签 - sort_buttons: - popular: 热门 - name: 名称 - newest: 最新 - button_follow: 关注 - button_following: 已关注 - tag_label: 个问题 - search_placeholder: 通过标签名称过滤 - no_desc: 此标签无描述。 - more: 更多 - ask: - title: 新增问题 - edit_title: 编辑问题 - default_reason: 编辑问题 - default_first_reason: 新增问题 - similar_questions: 相似问题 - form: - fields: - revision: - label: 修订版本 - title: - label: 标题 - placeholder: 请详细描述你的问题,想象你在问一个人 - msg: - empty: 标题不能为空。 - range: 标题最多 150 个字符 - body: - label: 内容 - msg: - empty: 内容不能为空。 - tags: - label: 标签 - msg: - empty: 必须选择一个标签 - answer: - label: 回答内容 - msg: - empty: 回答内容不能为空 - edit_summary: - label: 编辑备注 - placeholder: >- - 简单描述更改原因(更正拼写、修复语法、改进格式) - btn_post_question: 提交问题 - btn_save_edits: 保存更改 - answer_question: 回答自己的问题 - post_question&answer: 提交问题和回答 - tag_selector: - add_btn: 添加标签 - create_btn: 创建新标签 - search_tag: 搜索标签 - hint: "描述您的问题是关于什么,至少需要一个标签。" - no_result: 没有匹配的标签 - tag_required_text: 必选标签(至少一个) - header: - nav: - question: 问题 - tag: 标签 - user: 用户 - profile: 用户主页 - setting: 账号设置 - logout: 退出 - admin: 后台管理 - review: 审查 - bookmark: 收藏夹 - moderation: 管理 - search: - placeholder: 搜索 - footer: - build_on: >- - 由 <1>Apache Answer 提供动力 - 驱动问答社区的开源软件。
用爱制造 © {{cc}}. - upload_img: - name: 更改 - loading: 加载中... - pic_auth_code: - title: 验证码 - placeholder: 输入图片中的文字 - msg: - empty: 验证码不能为空。 - inactive: - first: >- - 就差一步!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活你的账户。 - info: "如果没有收到,请检查你的垃圾邮件文件夹。" - another: >- - 我们向你的邮箱 {{mail}} 发送了另一封激活电子邮件。可能需要几分钟才能到达;请务必检查您的垃圾邮件箱。 - btn_name: 重新发送激活邮件 - change_btn_name: 更改邮箱 - msg: - empty: 不能为空。 - resend_email: - url_label: 确定要重新发送激活邮件吗? - url_text: 你也可以将上面的激活链接给该用户。 - login: - login_to_continue: 登录以继续 - info_sign: 没有账户?<1>注册 - info_login: 已经有账户?<1>登录 - agreements: 登录即表示您同意<1>隐私政策和<3>服务条款。 - forgot_pass: 忘记密码? - name: - label: 名字 - msg: - empty: 名字不能为空 - range: 名字应该在 4 到 30 个字符之间 - character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' - email: - label: 邮箱 - msg: - empty: 邮箱不能为空 - password: - label: 密码 - msg: - empty: 密码不能为空 - different: 两次输入密码不一致 - account_forgot: - page_title: 忘记密码 - btn_name: 发送恢复邮件 - send_success: >- - 如果存在邮箱为 {{mail}} 账户,你将很快收到一封重置密码的说明邮件。 - email: - label: 邮箱 - msg: - empty: 邮箱不能为空 - change_email: - btn_cancel: 取消 - btn_update: 更新电子邮件地址 - send_success: >- - 如果存在邮箱为 {{mail}} 的账户,你将很快收到一封重置密码的说明邮件。 - email: - label: 新的电子邮件地址 - msg: - empty: 邮箱不能为空。 - oauth: - connect: 连接到 {{ auth_name }} - remove: 移除 {{ auth_name }} - oauth_bind_email: - subtitle: 向你的账户添加恢复邮件地址。 - btn_update: 更新电子邮件地址 - email: - label: 邮箱 - msg: - empty: 邮箱不能为空。 - modal_title: 邮箱已经存在。 - modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? - modal_cancel: 更改邮箱 - modal_confirm: 连接到已有账户 - password_reset: - page_title: 密码重置 - btn_name: 重置我的密码 - reset_success: >- - 你已经成功更改密码;你将被重定向到登录页面。 - link_invalid: >- - 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? - to_login: 前往登录页面 - password: - label: 密码 - msg: - empty: 密码不能为空。 - length: 密码长度在8-32个字符之间 - different: 两次输入密码不一致 - password_confirm: - label: 确认新密码 - settings: - page_title: 设置 - goto_modify: 前往修改 - nav: - profile: 我的资料 - notification: 通知 - account: 账号 - interface: 界面 - profile: - heading: 个人资料 - btn_name: 保存 - display_name: - label: 显示名称 - msg: 昵称不能为空。 - msg_range: 显示名称不能超过 30 个字符。 - username: - label: 用户名 - caption: 用户可以通过 "@用户名" 来提及你。 - msg: 用户名不能为空 - msg_range: 用户名不能超过 30 个字符。 - character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' - avatar: - label: 头像 - gravatar: Gravatar - gravatar_text: 你可以更改图像在 - custom: 自定义 - custom_text: 你可以上传你的图片。 - default: 系统 - msg: 请上传头像 - bio: - label: 关于我 - website: - label: 网站 - placeholder: "https://example.com" - msg: 网址格式不正确 - location: - label: 位置 - placeholder: "城市,国家" - notification: - heading: 邮件通知 - turn_on: 开启 - inbox: - label: 收件箱通知 - description: 你的提问有新的回答,评论,邀请回答和其他。 - all_new_question: - label: 所有新问题 - description: 获取所有新问题的通知。每周最多有50个问题。 - all_new_question_for_following_tags: - label: 所有关注标签的新问题 - description: 获取关注的标签下新问题通知。 - account: - heading: 账号 - change_email_btn: 更改邮箱 - change_pass_btn: 更改密码 - change_email_info: >- - 邮件已发送。请根据指引完成验证。 - email: - label: 电子邮件地址 - new_email: - label: 新的电子邮件地址 - msg: 新邮箱不能为空。 - pass: - label: 当前密码 - msg: 密码不能为空。 - password_title: 密码 - current_pass: - label: 当前密码 - msg: - empty: 当前密码不能为空 - length: 密码长度必须在 8 至 32 之间 - different: 两次输入的密码不匹配 - new_pass: - label: 新密码 - pass_confirm: - label: 确认新密码 - interface: - heading: 界面 - lang: - label: 界面语言 - text: 设置用户界面语言,在刷新页面后生效。 - my_logins: - title: 我的登录 - label: 使用这些账户登录或注册本网站。 - modal_title: 移除登录 - modal_content: 你确定要从账户里移除该登录? - modal_confirm_btn: 移除 - remove_success: 移除成功 - toast: - update: 更新成功 - update_password: 密码更新成功。 - flag_success: 感谢标记。 - forbidden_operate_self: 禁止对自己执行操作 - review: 您的修订将在审阅通过后显示。 - sent_success: 发送成功 - related_question: - title: 相关问题 - answers: 个回答 - invite_to_answer: - title: 受邀人 - desc: 邀请你认为可能知道答案的人。 - invite: 邀请回答 - add: 添加人员 - search: 搜索人员 - question_detail: - action: 操作 - Asked: 提问于 - asked: 提问于 - update: 修改于 - edit: 编辑于 - commented: 评论 - Views: 阅读次数 - Follow: 关注此问题 - Following: 已关注 - follow_tip: 关注此问题以接收通知 - answered: 回答于 - closed_in: 关闭于 - show_exist: 查看类似问题。 - useful: 有用的 - question_useful: 它是有用和明确的 - question_un_useful: 它不明确或没用的 - question_bookmark: 收藏该问题 - answer_useful: 这是有用的 - answer_un_useful: 它是没有用的 - answers: - title: 个回答 - score: 评分 - newest: 最新 - oldest: 最旧 - btn_accept: 采纳 - btn_accepted: 已被采纳 - write_answer: - title: 你的回答 - edit_answer: 编辑我的回答 - btn_name: 提交你的回答 - add_another_answer: 添加另一个回答 - confirm_title: 继续回答 - continue: 继续 - confirm_info: >- -

你确定要提交一个新的回答吗?

作为替代,你可以通过编辑来完善和改进之前的回答。

- empty: 回答内容不能为空。 - characters: 内容长度至少 6 个字符。 - tips: - header_1: 感谢你的回答 - li1_1: 请务必确定在 回答问题。提供详细信息并分享你的研究。 - li1_2: 用参考资料或个人经历来支持你所做的任何陈述。 - header_2: 但是 请避免... - li2_1: 请求帮助,寻求澄清,或答复其他答案。 - reopen: - confirm_btn: 重新打开 - title: 重新打开这个帖子 - content: 确定要重新打开吗? - list: - confirm_btn: 列表显示 - title: 列表中显示这个帖子 - content: 确定要列表中显示这个帖子吗? - unlist: - confirm_btn: 列表隐藏 - title: 从列表中隐藏这个帖子 - content: 确定要从列表中隐藏这个帖子吗? - pin: - title: 置顶该帖子 - content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。 - confirm_btn: 置顶 - delete: - title: 删除 - question: >- - 我们不建议 删除有回答的帖子。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? - answer_accepted: >- -

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? - other: 你确定要删除? - tip_answer_deleted: 该回答已被删除 - undelete_title: 撤销删除本帖 - undelete_desc: 你确定你要撤销删除吗? - btns: - confirm: 确认 - cancel: 取消 - edit: 编辑 - save: 保存 - delete: 删除 - undelete: 撤消删除 - list: 列表显示 - unlist: 列表隐藏 - unlisted: 已隐藏 - login: 登录 - signup: 注册 - logout: 退出 - verify: 验证 - add_question: 我要提问 - approve: 批准 - reject: 拒绝 - skip: 跳过 - discard_draft: 丢弃草稿 - pinned: 已置顶 - all: 全部 - question: 问题 - answer: 回答 - comment: 评论 - refresh: 刷新 - resend: 重新发送 - deactivate: 取消激活 - active: 激活 - suspend: 封禁 - unsuspend: 解禁 - close: 关闭 - reopen: 重新打开 - ok: 确定 - light: 浅色 - dark: 深色 - system_setting: 跟随系统 - default: 默认 - reset: 重置 - tag: 标签 - post_lowercase: 帖子 - filter: 筛选 - ignore: 忽略 - submit: 提交 - normal: 正常 - closed: 已关闭 - deleted: 已删除 - pending: 等待处理 - more: 更多 - search: - title: 搜索结果 - keywords: 关键词 - options: 选项 - follow: 关注 - following: 已关注 - counts: "{{count}} 个结果" - more: 更多 - sort_btns: - relevance: 相关性 - newest: 最新的 - active: 活跃的 - score: 评分 - more: 更多 - tips: - title: 高级搜索提示 - tag: "<1>[tag] 在指定标签中搜索" - user: "<1>user:username 根据作者搜索" - answer: "<1>answers:0 搜索未回答的问题" - score: "<1>score:3 评分 3+ 的帖子" - question: "<1>is:question 搜索问题" - is_answer: "<1>is:answer 搜索回答" - empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 - share: - name: 分享 - copy: 复制链接 - via: 分享到... - copied: 已复制 - facebook: 分享到 Facebook - twitter: 分享到 Twitter - cannot_vote_for_self: 你不能给自己的帖子投票。 - modal_confirm: - title: 发生错误... - account_result: - success: 你的账号已通过验证,即将返回首页。 - link: 返回首页 - invalid: >- - 抱歉,此验证链接已失效。也许你的账号已经激活了? - confirm_new_email: 你的电子邮箱已更新 - confirm_new_email_invalid: >- - 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? - unsubscribe: - page_title: 退订 - success_title: 退订成功 - success_desc: 您已成功退订,并且将不会再收到我们的邮件。 - link: 更改设置 - question: - following_tags: 已关注的标签 - edit: 编辑 - save: 保存 - follow_tag_tip: 关注标签来筛选你的问题列表。 - hot_questions: 热门问题 - all_questions: 全部问题 - x_questions: "{{ count }} 个问题" - x_answers: "{{ count }} 个回答" - questions: 问题 - answers: 回答 - newest: 最新 - active: 活跃 - hot: 热门 - score: 评分 - unanswered: 未回答 - modified: 更新于 - answered: 回答于 - asked: 提问于 - closed: 已关闭 - follow_a_tag: 关注一个标签 - more: 更多 - personal: - overview: 概览 - answers: 回答 - answer: 回答 - questions: 问题 - question: 问题 - bookmarks: 收藏 - reputation: 声望 - comments: 评论 - votes: 得票 - newest: 最新 - score: 评分 - edit_profile: 编辑资料 - visited_x_days: "已访问 {{ count }} 天" - viewed: 浏览次数 - joined: 加入于 - last_login: 上次登录 - about_me: 关于我 - about_me_empty: "// Hello, World!" - top_answers: 高分回答 - top_questions: 高分问题 - stats: 状态 - list_empty: 没有找到相关的内容。
试试看其他选项卡? - accepted: 已采纳 - answered: 回答于 - asked: 提问于 - downvoted: 点踩 - mod_short: 版主 - mod_long: 版主 - x_reputation: 声望 - x_votes: 得票 - x_answers: 个回答 - x_questions: 个问题 - install: - title: 安装 - next: 下一步 - done: 完成 - config_yaml_error: 无法创建 config.yaml 文件。 - lang: - label: 请选择一种语言 - db_type: - label: 数据库引擎 - db_username: - label: 用户名 - placeholder: root - msg: 用户名不能为空 - db_password: - label: 密码 - placeholder: root - msg: 密码不能为空 - db_host: - label: 数据库主机 - placeholder: "db:3306" - msg: 数据库地址不能为空 - db_name: - label: 数据库名 - placeholder: 回答 - msg: 数据库名称不能为空。 - db_file: - label: 数据库文件 - placeholder: /data/answer.db - msg: 数据库文件不能为空。 - config_yaml: - title: 创建 config.yaml - label: 已创建 config.yaml 文件。 - desc: >- - 你可以手动在 <1>/var/wwww/xxx/ 目录中创建 <1>config.yaml 文件并粘贴以下文本。 - info: 完成后,点击“下一步”按钮。 - site_information: 站点信息 - admin_account: 管理员账号 - site_name: - label: 站点名称 - msg: 站点名称不能为空。 - msg_max_length: 站点名称长度不得超过 30 个字符。 - site_url: - label: 网站网址 - text: 此网站的网址。 - msg: - empty: 网址不能为空。 - incorrect: 网址格式不正确。 - max_length: 网址长度不得超过 512 个字符。 - contact_email: - label: 联系邮箱 - text: 负责本网站的主要联系人的电子邮件地址。 - msg: - empty: 联系人邮箱不能为空。 - incorrect: 联系人邮箱地址不正确。 - login_required: - label: 私有的 - switch: 需要登录 - text: 只有登录用户才能访问这个社区。 - admin_name: - label: 名字 - msg: 名字不能为空。 - character: '只能由 "a-z", "0-9", " - . _" 组成' - msg_max_length: 名字长度不能超过 30 个字符。 - admin_password: - label: 密码 - text: >- - 您需要此密码才能登录。请将其存储在一个安全的位置。 - msg: 密码不能为空。 - msg_min_length: 密码必须至少 8 个字符长。 - msg_max_length: 密码长度不能超过 32 个字符。 - admin_email: - label: 邮箱 - text: 您需要此电子邮件才能登录。 - msg: - empty: 邮箱不能为空。 - incorrect: 邮箱格式不正确。 - ready_title: 您的网站已准备好 - ready_desc: >- - 如果你想改变更多的设置,请访问 <1>管理区域;在网站菜单中找到它。 - good_luck: "玩得愉快,祝你好运!" - warn_title: 警告 - warn_desc: >- - 文件 <1>config.yaml 已存在。如果你要重置该文件中的任何配置项,请先删除它。 - install_now: 您可以尝试 <1>现在安装。 - installed: 已安裝 - installed_desc: >- - 你似乎已经安装过了。如果要重新安装,请先清除旧的数据库表。 - db_failed: 数据连接异常! - db_failed_desc: >- - 这或者意味着数据库信息在 <1>config.yaml 文件不正确,或者无法与数据库服务器建立联系。这可能意味着你的主机数据库服务器故障。 - counts: - views: 次浏览 - votes: 个点赞 - answers: 个回答 - accepted: 已被采纳 - page_error: - http_error: HTTP 错误 {{ code }} - desc_403: 您无权访问此页面。 - desc_404: 很抱歉,此页面不存在。 - desc_50X: 服务器遇到了一个错误,无法完成你的请求。 - back_home: 返回首页 - page_maintenance: - desc: "我们正在进行维护,我们将很快回来。" - nav_menus: - dashboard: 后台管理 - contents: 内容管理 - questions: 问题 - answers: 回答 - users: 用户管理 - flags: 举报管理 - settings: 站点设置 - general: 一般 - interface: 界面 - smtp: SMTP - branding: 品牌 - legal: 法律条款 - write: 撰写 - tos: 服务条款 - privacy: 隐私政策 - seo: SEO - customize: 自定义 - themes: 主题 - css_html: CSS/HTML - login: 登录 - privileges: 特权 - plugins: 插件 - installed_plugins: 已安装插件 - website_welcome: 欢迎来到 {{site_name}} - user_center: - login: 登录 - qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。 - login_failed_email_tip: 登录失败,请允许此应用访问您的邮箱信息,然后重试。 - admin: - admin_header: - title: 后台管理 - dashboard: - title: 后台管理 - welcome: 欢迎来到管理后台! - site_statistics: 站点统计 - questions: "问题:" - answers: "回答:" - comments: "评论:" - votes: "投票:" - users: "用户:" - flags: "举报:" - reviews: "审查:" - site_health: 网站健康 - version: "版本" - https: "HTTPS:" - upload_folder: "上传文件夹:" - run_mode: "运行模式:" - private: 私有 - public: 公开 - smtp: "SMTP:" - timezone: "时区:" - system_info: 系统信息 - go_version: "Go版本:" - database: "数据库:" - database_size: "数据库大小:" - storage_used: "已用存储空间:" - uptime: "运行时间:" - links: 链接 - plugins: 插件 - github: GitHub - blog: 博客 - contact: 联系 - forum: 论坛 - documents: 文档 - feedback: 用户反馈 - support: 帮助 - review: 审查 - config: 配置 - update_to: 更新到 - latest: 最新版本 - check_failed: 校验失败 - "yes": "是" - "no": "否" - not_allowed: 拒绝 - allowed: 允许 - enabled: 已启用 - disabled: 停用 - writable: 可写 - not_writable: 不可写 - flags: - title: 举报 - pending: 等待处理 - completed: 已完成 - flagged: 被举报内容 - flagged_type: 标记了 {{ type }} - created: 创建于 - action: 操作 - review: 审查 - user_role_modal: - title: 更改用户状态为... - btn_cancel: 取消 - btn_submit: 提交 - new_password_modal: - title: 设置新密码 - form: - fields: - password: - label: 密码 - text: 用户将被退出,需要再次登录。 - msg: 密码的长度必须是8-32个字符。 - btn_cancel: 取消 - btn_submit: 提交 - edit_profile_modal: - title: 编辑资料 - form: - fields: - username: - label: 用户名 - msg_range: 用户名不能超过 30 个字符。 - email: - label: 电子邮件地址 - msg_invalid: 无效的邮箱地址 - edit_success: 修改成功 - btn_cancel: 取消 - btn_submit: 提交 - user_modal: - title: 添加新用户 - form: - fields: - users: - label: 批量添加用户 - placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" - text: 用逗号分隔“name, email, password”,每行一个用户。 - msg: "请输入用户的邮箱,每行一个。" - display_name: - label: 显示名称 - msg: 显示名称长度必须为 4-30 个字符 - email: - label: 邮箱 - msg: 邮箱无效。 - password: - label: 密码 - msg: 密码的长度必须是8-32个字符。 - btn_cancel: 取消 - btn_submit: 提交 - users: - title: 用户 - name: 名称 - email: 邮箱 - reputation: 声望 - created_at: 创建时间 - delete_at: 删除时间 - suspend_at: 封禁时间 - status: 状态 - role: 角色 - action: 操作 - change: 更改 - all: 全部 - staff: 工作人员 - more: 更多 - inactive: 不活跃 - suspended: 已封禁 - deleted: 已删除 - normal: 正常 - Moderator: 版主 - Admin: 管理员 - User: 用户 - filter: - placeholder: "按名称筛选,用户:id" - set_new_password: 设置新密码 - edit_profile: 编辑资料 - change_status: 更改状态 - change_role: 更改角色 - show_logs: 显示日志 - add_user: 添加用户 - deactivate_user: - title: 停用用户 - content: 未激活的用户必须重新验证他们的邮箱。 - delete_user: - title: 删除此用户 - content: 确定要删除此用户?此操作无法撤销! - remove: 移除内容 - label: 删除所有问题、 答案、 评论等 - text: 如果你只想删除用户账户,请不要选中此项。 - suspend_user: - title: 挂起此用户 - content: 被封禁的用户将无法登录。 - questions: - page_title: 问题 - unlisted: 已隐藏 - post: 标题 - votes: 得票数 - answers: 回答数 - created: 创建于 - status: 状态 - action: 操作 - change: 更改 - pending: 等待处理 - filter: - placeholder: "按标题过滤,问题:id" - answers: - page_title: 回答 - post: 标题 - votes: 得票数 - created: 创建于 - status: 状态 - action: 操作 - change: 更改 - filter: - placeholder: "按标题筛选,答案:id" - general: - page_title: 一般 - name: - label: 站点名称 - msg: 不能为空 - text: "站点的名称,作为站点的标题。" - site_url: - label: 网站网址 - msg: 网站网址不能为空。 - validate: 请输入一个有效的 URL。 - text: 此网站的地址。 - short_desc: - label: 简短站点描述 - msg: 简短网站描述不能为空。 - text: "简短的标语,作为网站主页的标题(Html 的 title 标签)。" - desc: - label: 站点描述 - msg: 网站描述不能为空。 - text: "使用一句话描述本站,作为网站的描述(Html 的 meta 标签)。" - contact_email: - label: 联系邮箱 - msg: 联系人邮箱不能为空。 - validate: 联系人邮箱无效。 - text: 本网站的主要联系邮箱地址。 - check_update: - label: 软件更新 - text: 自动检查软件更新 - interface: - page_title: 界面 - language: - label: 界面语言 - msg: 不能为空 - text: 设置用户界面语言,在刷新页面后生效。 - time_zone: - label: 时区 - msg: 时区不能为空。 - text: 选择一个与您相同时区的城市。 - smtp: - page_title: SMTP - from_email: - label: 发件人邮箱 - msg: 发件人邮箱不能为空。 - text: 用于发送邮件的地址。 - from_name: - label: 发件人 - msg: 不能为空 - text: 发件人的名字。 - smtp_host: - label: SMTP 主机 - msg: 不能为空 - text: 邮件服务器 - encryption: - label: 加密 - msg: 不能为空 - text: 对于大多数服务器而言,SSL 是推荐开启的。 - ssl: SSL - tls: TLS - none: 无加密 - smtp_port: - label: SMTP 端口 - msg: SMTP 端口必须在 1 ~ 65535 之间。 - text: 邮件服务器的端口号。 - smtp_username: - label: SMTP 用户名 - msg: 不能为空 - smtp_password: - label: SMTP 密码 - msg: 不能为空 - test_email_recipient: - label: 测试收件邮箱 - text: 提供用于接收测试邮件的邮箱地址。 - msg: 测试收件邮箱无效 - smtp_authentication: - label: 启用身份验证 - title: SMTP 身份验证 - msg: 不能为空 - "yes": "是" - "no": "否" - branding: - page_title: 品牌 - logo: - label: 网站标志(Logo) - msg: 图标不能为空。 - text: 在你的网站左上方的Logo图标。使用一个高度为56,长宽比大于3:1的宽长方形图像。如果留空,将显示网站标题文本。 - mobile_logo: - label: 移动端 Logo - text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空,将使用 "Logo"设置中的图像。 - square_icon: - label: 方形图标 - msg: 方形图标不能为空。 - text: 用作元数据图标的基础的图像。最好是大于512x512。 - favicon: - label: 收藏夹图标 - text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空,将使用“方形图标”。 - legal: - page_title: 法律条款 - terms_of_service: - label: 服务条款 - text: "您可以在此添加服务内容的条款。如果您已经在别处托管了文档,请在这里提供完整的URL。" - privacy_policy: - label: 隐私政策 - text: "您可以在此添加隐私政策内容。如果您已经在别处托管了文档,请在这里提供完整的URL。" - write: - page_title: 编辑 - restrict_answer: - title: 限制一个回答 - label: 每个用户对于每个问题只能有一个回答 - text: "用户可以使用编辑按钮优化已有的回答" - recommend_tags: - label: 推荐标签 - text: "请在上方输入标签固定链接,每行一个标签。" - required_tag: - title: 必需的标签 - label: 根据需要设置推荐标签 - text: "每个新问题必须至少有一个推荐标签。" - reserved_tags: - label: 保留标签 - text: "保留的标签只能由版主添加到一个帖子中。" - seo: - page_title: 搜索引擎优化 - permalink: - label: 固定链接 - text: 自定义URL结构可以提高可用性,以及你的链接的向前兼容性。 - robots: - label: robots.txt - text: 这将永久覆盖任何相关的网站设置。 - themes: - page_title: 主题 - themes: - label: 主题 - text: 选择一个现有主题。 - color_scheme: - label: 配色方案 - navbar_style: - label: 导航栏样式 - primary_color: - label: 主色调 - text: 修改您主题使用的颜色 - css_and_html: - page_title: CSS 与 HTML - custom_css: - label: 自定义 CSS - text: > - - head: - label: 头部 - text: > - - header: - label: 页眉 - text: > - - footer: - label: 页脚 - text: 这将在 之前插入. - sidebar: - label: 侧边栏 - text: 这将插入侧边栏中。 - login: - page_title: 登录 - membership: - title: 会员 - label: 允许新注册 - text: 关闭以防止任何人创建新账户。 - email_registration: - title: 邮箱注册 - label: 允许邮箱注册 - text: 关闭以阻止任何人通过邮箱创建新账户。 - allowed_email_domains: - title: 允许的邮箱域 - text: 允许注册账户的邮箱域。每行一个域名。留空时忽略。 - private: - title: 非公开的 - label: 需要登录 - text: 只有登录用户才能访问这个社区。 - password_login: - title: 密码登录 - label: 允许使用邮箱和密码登录 - text: "警告:如果您未配置过其他登录方式,关闭密码登录后您则可能无法登录。" - installed_plugins: - title: 已安装插件 - plugin_link: 插件扩展功能。您可以在<1>插件仓库中找到插件。 - filter: - all: 全部 - active: 已启用 - inactive: 未启用 - outdated: 已过期 - plugins: - label: 插件 - text: 选择一个现有的插件。 - name: 名称 - version: 版本 - status: 状态 - action: 操作 - deactivate: 停用 - activate: 启用 - settings: 设置 - settings_users: - title: 用户 - avatar: - label: 默认头像 - text: 没有自定义头像的用户。 - gravatar_base_url: - label: Gravatar 根路径 URL - text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。 - profile_editable: - title: 个人资料可编辑 - allow_update_display_name: - label: 允许用户修改显示名称 - allow_update_username: - label: 允许用户修改用户名 - allow_update_avatar: - label: 允许用户修改个人头像 - allow_update_bio: - label: 允许用户修改个人介绍 - allow_update_website: - label: 允许用户修改个人主页网址 - allow_update_location: - label: 允许用户更改位置 - privilege: - title: 特权 - level: - label: 级别所需声望 - text: 选择特权所需的声望值 - msg: - should_be_number: 输入必须是数字 - number_larger_1: 数字应该大于等于 1 - form: - optional: (选填) - empty: 不能为空 - invalid: 是无效的 - btn_submit: 保存 - not_found_props: "所需属性 {{ key }} 未找到。" - select: 选择 - page_review: - review: 评论 - proposed: 提案 - question_edit: 问题编辑 - answer_edit: 回答编辑 - tag_edit: '标签管理: 编辑标签' - edit_summary: 编辑备注 - edit_question: 编辑问题 - edit_answer: 编辑回答 - edit_tag: 编辑标签 - empty: 没有剩余的审核任务。 - approve_revision_tip: 您是否批准此修订? - approve_flag_tip: 您是否批准此举报? - approve_post_tip: 您是否批准此帖子? - approve_user_tip: 您是否批准此修订? - suggest_edits: 建议的编辑 - flag_post: 举报帖子 - flag_user: 举报用户 - queued_post: 排队的帖子 - queued_user: 排队用户 - filter_label: 类型 - reputation: 声望值 - flag_post_type: 举报这个帖子的类型是 {{ type }} - flag_user_type: 举报这个用户的类型是 {{ type }} - edit_post: 编辑帖子 - list_post: 文章列表 - unlist_post: 隐藏的帖子 - timeline: - undeleted: 取消删除 - deleted: 删除 - downvote: 反对 - upvote: 点赞 - accept: 采纳 - cancelled: 已取消 - commented: '评论:' - rollback: 回滚 - edited: 最后编辑于 - answered: 回答于 - asked: 提问于 - closed: 关闭 - reopened: 重新开启 - created: 创建于 - pin: 已置顶 - unpin: 取消置頂 - show: 已显示 - hide: 已隐藏 - title: "历史记录" - tag_title: "时间线" - show_votes: "显示投票" - n_or_a: N/A - title_for_question: "时间线" - title_for_answer: "{{ title }} 的 {{ author }} 回答时间线" - title_for_tag: "时间线" - datetime: 日期时间 - type: 类型 - by: 由 - comment: 评论 - no_data: "空空如也" - users: - title: 用户 - users_with_the_most_reputation: 本周声望最高的用户 - users_with_the_most_vote: 本周投票最多的用户 - staffs: 我们的社区工作人员 - reputation: 声望值 - votes: 投票 - prompt: - leave_page: 确定要离开此页面? - changes_not_save: 您的更改尚未保存 - draft: - discard_confirm: 您确定要丢弃您的草稿吗? - messages: - post_deleted: 该帖子已被删除。 - post_pin: 该帖子已被置顶。 - post_unpin: 该帖子已被取消置顶。 - post_hide_list: 此帖子已经从列表中隐藏。 - post_show_list: 该帖子已显示到列表中。 - post_reopen: 这个帖子已被重新打开. - post_list: 这个帖子已经被显示 - post_unlist: 这个帖子已经被隐藏 - post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。 From e9c925082b7a11391e59082f308989c37c2fe0d6 Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 19 Aug 2024 15:20:38 +0800 Subject: [PATCH 059/129] Revert "fix(dev): delete test file" This reverts commit c50e63dd34ce9ddb7babd769146278ef41e5bfcb. --- dev/i18n/zh_CN.yaml | 2035 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2035 insertions(+) create mode 100644 dev/i18n/zh_CN.yaml diff --git a/dev/i18n/zh_CN.yaml b/dev/i18n/zh_CN.yaml new file mode 100644 index 000000000..6613bfc22 --- /dev/null +++ b/dev/i18n/zh_CN.yaml @@ -0,0 +1,2035 @@ +# 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. + +# The following fields are used for back-end +backend: + base: + success: + other: 成功。 + unknown: + other: 未知错误。 + request_format_error: + other: 请求格式错误。 + unauthorized_error: + other: 未授权。 + database_error: + other: 数据服务器错误。 + forbidden_error: + other: 禁止访问。 + duplicate_request_error: + other: 重复提交。 + action: + report: + other: 举报 + edit: + other: 编辑 + delete: + other: 删除 + close: + other: 关闭 + reopen: + other: 重新打开 + forbidden_error: + other: 禁止访问。 + pin: + other: 置顶 + hide: + other: 列表隐藏 + unpin: + other: 取消置顶 + show: + other: 列表显示 + invite_someone_to_answer: + other: 编辑 + undelete: + other: 撤消删除 + role: + name: + user: + other: 用户 + admin: + other: 管理员 + moderator: + other: 版主 + description: + user: + other: 默认没有特殊权限。 + admin: + other: 拥有管理网站的全部权限。 + moderator: + other: 拥有除访问后台管理以外的所有权限。 + privilege: + level_1: + description: + other: 级别 1(少量声望要求,适合私有团队、群组) + level_2: + description: + other: 级别 2(低声望要求,适合初启动的社区) + level_3: + description: + other: 级别 3(高声望要求,适合成熟的社区) + level_custom: + description: + other: 自定义等级 + rank_question_add_label: + other: 提问 + rank_answer_add_label: + other: 写答案 + rank_comment_add_label: + other: 写评论 + rank_report_add_label: + other: 举报 + rank_comment_vote_up_label: + other: 点赞评论 + rank_link_url_limit_label: + other: 每次发布超过 2 个链接 + rank_question_vote_up_label: + other: 点赞问题 + rank_answer_vote_up_label: + other: 点赞答案 + rank_question_vote_down_label: + other: 点踩问题 + rank_answer_vote_down_label: + other: 点踩答案 + rank_invite_someone_to_answer_label: + other: 邀请回答 + rank_tag_add_label: + other: 创建新标签 + rank_tag_edit_label: + other: 编辑标签描述(需要审核) + rank_question_edit_label: + other: 编辑别人的问题(需要审核) + rank_answer_edit_label: + other: 编辑别人的答案(需要审核) + rank_question_edit_without_review_label: + other: 编辑别人的问题无需审核 + rank_answer_edit_without_review_label: + other: 编辑别人的答案无需审核 + rank_question_audit_label: + other: 审核问题编辑 + rank_answer_audit_label: + other: 审核回答编辑 + rank_tag_audit_label: + other: 审核标签编辑 + rank_tag_edit_without_review_label: + other: 编辑标签描述无需审核 + rank_tag_synonym_label: + other: 管理标签同义词 + email: + other: 邮箱 + e_mail: + other: 邮箱 + password: + other: 密码 + pass: + other: 密码 + original_text: + other: 本帖 + email_or_password_wrong_error: + other: 邮箱和密码不匹配。 + error: + common: + invalid_url: + other: 无效的 URL。 + password: + space_invalid: + other: 密码不得含有空格。 + admin: + cannot_update_their_password: + other: 你无法修改自己的密码。 + cannot_edit_their_profile: + other: 您不能修改您的个人资料。 + cannot_modify_self_status: + other: 你无法修改自己的状态。 + email_or_password_wrong: + other: 邮箱和密码不匹配。 + answer: + not_found: + other: 没有找到答案。 + cannot_deleted: + other: 没有删除权限。 + cannot_update: + other: 没有更新权限。 + question_closed_cannot_add: + other: 问题已关闭,无法添加。 + comment: + edit_without_permission: + other: 不允许编辑评论。 + not_found: + other: 评论未找到。 + cannot_edit_after_deadline: + other: 评论时间太久,无法修改。 + email: + duplicate: + other: 邮箱已存在。 + need_to_be_verified: + other: 邮箱需要验证。 + verify_url_expired: + other: 邮箱验证的网址已过期,请重新发送邮件。 + illegal_email_domain_error: + other: 此邮箱不在允许注册的邮箱域中。请使用其他邮箱尝试。 + lang: + not_found: + other: 语言文件未找到。 + object: + captcha_verification_failed: + other: 验证码错误。 + disallow_follow: + other: 你不能关注。 + disallow_vote: + other: 你不能投票。 + disallow_vote_your_self: + other: 你不能为自己的帖子投票。 + not_found: + other: 对象未找到。 + verification_failed: + other: 验证失败。 + email_or_password_incorrect: + other: 邮箱和密码不匹配。 + old_password_verification_failed: + other: 旧密码验证失败。 + new_password_same_as_previous_setting: + other: 新密码和旧密码相同。 + already_deleted: + other: 该帖子已被删除。 + meta: + object_not_found: + other: Meta 对象未找到 + question: + already_deleted: + other: 该帖子已被删除。 + under_review: + other: 您的帖子正在等待审核。它将在它获得批准后可见。 + not_found: + other: 问题未找到。 + cannot_deleted: + other: 没有删除权限。 + cannot_close: + other: 没有关闭权限。 + cannot_update: + other: 没有更新权限。 + rank: + fail_to_meet_the_condition: + other: 声望值未达到要求。 + vote_fail_to_meet_the_condition: + other: 感谢投票。你至少需要 {{.Rank}} 声望才能投票。 + no_enough_rank_to_operate: + other: 你至少需要 {{.Rank}} 声望才能执行此操作。 + report: + handle_failed: + other: 报告处理失败。 + not_found: + other: 报告未找到。 + tag: + already_exist: + other: 标签已存在。 + not_found: + other: 标签未找到。 + recommend_tag_not_found: + other: 推荐标签不存在。 + recommend_tag_enter: + other: 请选择至少一个必选标签。 + not_contain_synonym_tags: + other: 不应包含同义词标签。 + cannot_update: + other: 没有更新权限。 + is_used_cannot_delete: + other: 你不能删除这个正在使用的标签。 + cannot_set_synonym_as_itself: + other: 你不能将当前标签设为自己的同义词。 + smtp: + config_from_name_cannot_be_email: + other: 发件人名称不能是邮箱地址。 + theme: + not_found: + other: 主题未找到。 + revision: + review_underway: + other: 目前无法编辑,有一个版本在审阅队列中。 + no_permission: + other: 无权限修改。 + user: + external_login_missing_user_id: + other: 第三方平台没有提供唯一的 UserID,所以你不能登录,请联系网站管理员。 + external_login_unbinding_forbidden: + other: 请在移除此登录之前为你的账户设置登录密码。 + email_or_password_wrong: + other: + other: 邮箱和密码不匹配。 + not_found: + other: 用户未找到。 + suspended: + other: 用户已被封禁。 + username_invalid: + other: 用户名无效。 + username_duplicate: + other: 用户名已被使用。 + set_avatar: + other: 头像设置错误。 + cannot_update_your_role: + other: 你不能修改自己的角色。 + not_allowed_registration: + other: 该网站暂未开放注册。 + not_allowed_login_via_password: + other: 该网站暂不支持密码登录。 + access_denied: + other: 拒绝访问 + page_access_denied: + other: 您没有权限访问此页面。 + add_bulk_users_format_error: + other: "发生错误,{{.Field}} 格式错误,在 '{{.Content}}' 行数 {{.Line}}. {{.ExtraMessage}}" + add_bulk_users_amount_error: + other: "一次性添加的用户数量应在 1-{{.MaxAmount}} 之间。" + config: + read_config_failed: + other: 读取配置失败 + database: + connection_failed: + other: 数据库连接失败 + create_table_failed: + other: 创建表失败 + install: + create_config_failed: + other: 无法创建 config.yaml 文件。 + upload: + unsupported_file_format: + other: 不支持的文件格式。 + site_info: + config_not_found: + other: 未找到网站的该配置信息。 + reason: + spam: + name: + other: 垃圾信息 + desc: + other: 这个帖子是一个广告,或是破坏性行为。它对当前的主题无帮助或无关。 + rude_or_abusive: + name: + other: 粗鲁或辱骂的 + desc: + other: + - 一个有理智的人都会认为这种内容不适合进行尊重性的讨论。 + - 论坛 + a_duplicate: + name: + other: 重复内容 + desc: + other: 该问题有人问过,而且已经有了答案。 + placeholder: + other: 输入已有的问题链接 + not_a_answer: + name: + other: 不是答案 + desc: + other: + - 这张贴作为答案,但它不会试图回答 + - 这可能是一个编辑、一个评论、另一个问题。 + - 或全部删除。 + no_longer_needed: + name: + other: 不再需要 + desc: + other: 该评论已过时,对话性质或与此帖子无关。 + something: + name: + other: 其他原因 + desc: + other: 此帖子需要工作人员注意,因为是上述所列以外的其他理由。 + placeholder: + other: 让我们具体知道你关心的什么 + community_specific: + name: + other: 社区特定原因 + desc: + other: 该问题不符合社区准则。 + not_clarity: + name: + other: 需要细节或澄清 + desc: + other: 该问题目前涵盖多个问题。它应该侧重在一个问题上。 + looks_ok: + name: + other: 看起来没问题 + desc: + other: 这个帖子是好的,不是低质量。 + needs_edit: + name: + other: 需要编辑,我已做了修改。 + desc: + other: 改进和纠正你自己帖子中的问题。 + needs_close: + name: + other: 需要关闭 + desc: + other: 关闭的问题不能回答,但仍然可以编辑、投票和评论。 + needs_delete: + name: + other: 需要删除 + desc: + other: 该帖子将被删除。 + question: + close: + duplicate: + name: + other: 垃圾信息 + desc: + other: 此问题以前就有人问过,而且已经有了答案。 + guideline: + name: + other: 社区特定原因 + desc: + other: 该问题不符合社区准则。 + multiple: + name: + other: 需要细节或澄清 + desc: + other: + - 该问题目前涵盖多个问题。它应该侧重在一个问题上。 + - 只关注一个问题。 + other: + name: + other: 其他原因 + desc: + other: 该帖子存在上面没有列出的另一个原因。 + operation_type: + asked: + other: 提问于 + answered: + other: 回答于 + modified: + other: 修改于 + deleted_title: + other: 删除的问题 + notification: + action: + update_question: + other: 更新了问题 + answer_the_question: + other: 回答了问题 + update_answer: + other: 更新了答案 + accept_answer: + other: 采纳了答案 + comment_question: + other: 评论了问题 + comment_answer: + other: 评论了答案 + reply_to_you: + other: 回复了你 + mention_you: + other: 提到了你 + your_question_is_closed: + other: 你的问题已被关闭 + your_question_was_deleted: + other: 你的问题已被删除 + your_answer_was_deleted: + other: 你的答案已被删除 + your_comment_was_deleted: + other: 你的评论已被删除 + up_voted_question: + other: 点赞问题 + down_voted_question: + other: 点踩问题 + up_voted_answer: + other: 点赞答案 + down_voted_answer: + other: 点踩回答 + up_voted_comment: + other: 点赞评论 + invited_you_to_answer: + other: 邀请你回答 + email_tpl: + change_email: + title: + other: "[{{.SiteName}}] 确认你的新邮箱地址" + body: + other: "请点击以下链接确认你在 {{.SiteName}} 上的新邮箱地址:
\n{{.ChangeEmailUrl}}

\n\n如果你没有请求此更改,请忽略此邮件。\n" + new_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 回答了你的问题" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" + invited_you_to_answer: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
我想你可能知道答案。

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" + new_comment: + title: + other: "[{{.SiteName}}] {{.DisplayName}} 评论了你的帖子" + body: + other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" + new_question: + title: + other: "[{{.SiteName}}] 新问题: {{.QuestionTitle}}" + body: + other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\n取消订阅" + pass_reset: + title: + other: "[{{.SiteName }}] 重置密码" + body: + other: "有人要求在 [{{.SiteName}}] 上重置你的密码。

\n\n如果这不是你的操作,请安心忽略此电子邮件。

\n\n请点击以下链接设置一个新密码:
\n{{.PassResetUrl}}\n" + register: + title: + other: "[{{.SiteName}}] 确认你的新账户" + body: + other: "欢迎加入 {{.SiteName}}!

\n\n请点击以下链接确认并激活你的新账户:
\n{{.RegisterUrl}}

\n\n如果上面的链接不能点击,请将其复制并粘贴到你的浏览器地址栏中。\n" + test: + title: + other: "[{{.SiteName}}] 测试邮件" + body: + other: "这是一封测试邮件。" + action_activity_type: + upvote: + other: 点赞 + upvoted: + other: 点赞 + downvote: + other: 点踩 + downvoted: + other: 点踩 + accept: + other: 采纳 + accepted: + other: 已采纳 + edit: + other: 编辑 + review: + queued_post: + other: 排队的帖子 + flagged_post: + other: 举报的帖子 + suggested_post_edit: + other: 建议的编辑 + reaction: + tooltip: + other: "{{ .Names }} 以及另外 {{ .Count }} 个..." +plugin: + s3_cdn: + backend: + info: + name: + other: S3 存储 CDN + description: + other: 上传文件到S3存储 + config: + endpoint: + title: + other: Endpoint + description: + other: S3存储的Endpoint + bucket_name: + title: + other: Bucket名称 + description: + other: S3存储的Bucket名称 + object_key_prefix: + title: + other: 对象Key前缀 + description: + other: 对象键的前缀,如'answer/data/',以'/'结尾 + access_key_id: + title: + other: AccessKeyID + description: + other: S3存储的AccessKeyID + access_key_secret: + title: + other: AccessKeySecret + description: + other: S3存储的AccessKeySecret + access_token: + title: + other: AccessToken + description: + other: 访问 S3 所需的 AccessToken + visit_url_prefix: + title: + other: 访问URL前缀 + description: + other: 上传静态文件CDN最终访问地址的前缀,以 '/' 结尾 https://static.example.com/xxx/ + max_file_size: + title: + other: 文件最大大小(MB) + description: + other: 限制上传文件的最大大小,单位MB,默认为10MB + region: + title: + other: 区域(Region) + description: + other: S3存储区域 + disable_ssl: + title: + other: 禁用SSL + description: + other: 我们建议您使用SSL访问S3存储。如果您想禁用SSL,请选中此选项。 + err: + mis_storage_config: + other: 错误的存储配置导致上传失败 + file_not_found: + other: 文件未找到 + unsupported_file_type: + other: 不支持的文件类型 + over_file_size_limit: + other: 超过文件大小限制 + upload_file_failed: + other: 上传文件失败 + aliyunoss_cdn: + backend: + info: + name: + other: 阿里云CDN OSS存储 + description: + other: 上传文件到阿里云CDN OSS存储 + config: + endpoint: + title: + other: Endpoint + description: + other: 阿里云OSS存储的Endpoint + bucket_name: + title: + other: Bucket名称 + description: + other: 阿里云OSS存储的Bucket名称 + object_key_prefix: + title: + other: 对象Key前缀 + description: + other: 对象键的前缀,如'answer/data/',以'/'结尾 + access_key_id: + title: + other: AccessKeyID + description: + other: 阿里云OSS存储的AccessKeyID + access_key_secret: + title: + other: AccessKeySecret + description: + other: 阿里云OSS存储的AccessKeySecret + visit_url_prefix: + title: + other: 访问URL前缀 + description: + other: CDN最终访问地址的前缀,以 '/' 结尾 https://example.com/xxx/ + max_file_size: + title: + other: 最大文件大小(MB) + description: + other: 限制上传文件的最大大小,单位为MB,默认为 10MB + err: + mis_storage_config: + other: 错误的存储配置导致上传失败 + file_not_found: + other: 文件未找到 + unsupported_file_type: + other: 不支持的文件类型 + over_file_size_limit: + other: 超过文件大小限制 + upload_file_failed: + other: 上传文件失败 +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何排版 + desc: >- +
  • 添加链接

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 **粗体**

  • 使用 4 个空格缩进代码

  • 在行首添加 > 表示引用

  • 反引号进行转义 `像 _这样_`

  • 使用 ``` 创建代码块

    ```
    这是代码块
    ```
+ pagination: + prev: 上一页 + next: 下一页 + page_title: + question: 问题 + questions: 问题 + tag: 标签 + tags: 标签 + tag_wiki: 标签维基 + create_tag: 创建标签 + edit_tag: 编辑标签 + ask_a_question: 提问题 + edit_question: 编辑问题 + edit_answer: 编辑回答 + search: 搜索 + posts_containing: 帖子包含 + settings: 设置 + notifications: 通知 + login: 登录 + sign_up: 注册 + account_recovery: 账号恢复 + account_activation: 账号激活 + confirm_email: 确认电子邮件 + account_suspended: 账号已被封禁 + admin: 后台管理 + change_email: 修改邮箱 + install: Answer 安装 + upgrade: Answer 升级 + maintenance: 网站维护 + users: 用户 + oauth_callback: 处理中 + http_404: HTTP 错误 404 + http_50X: HTTP 错误 500 + http_403: HTTP 错误 403 + logout: 退出 + notifications: + title: 通知 + inbox: 收件箱 + achievement: 成就 + new_alerts: 新通知 + all_read: 全部标记为已读 + show_more: 显示更多 + someone: 有人 + inbox_type: + all: 全部 + posts: 帖子 + invites: 邀请 + votes: 投票 + suspended: + title: 你的账号账号已被封禁 + until_time: "你的账号被封禁直到 {{ time }}。" + forever: 你的账号已被永久封禁。 + end: 你违反了我们的社区准则。 + contact_us: 联系我们 + editor: + blockquote: + text: 引用 + bold: + text: 粗体 + chart: + text: 图表 + flow_chart: 流程图 + sequence_diagram: 时序图 + class_diagram: 类图 + state_diagram: 状态图 + entity_relationship_diagram: 实体关系图 + user_defined_diagram: 用户自定义图表 + gantt_chart: 甘特图 + pie_chart: 饼图 + code: + text: 代码块 + add_code: 添加代码块 + form: + fields: + code: + label: 代码块 + msg: + empty: 代码块不能为空 + language: + label: 语言 + placeholder: 自动识别 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 行内公式 + block: 块级公式 + heading: + text: 标题 + options: + h1: 标题 1 + h2: 标题 2 + h3: 标题 3 + h4: 标题 4 + h5: 标题 5 + h6: 标题 6 + help: + text: 帮助 + hr: + text: 水平线 + image: + text: 图片 + add_image: 添加图片 + tab_image: 上传图片 + form_image: + fields: + file: + label: 图像文件 + btn: 选择图片 + msg: + empty: 请选择图片文件。 + only_image: 只能上传图片文件。 + max_size: 文件大小不能超过 4 MB。 + desc: + label: 描述 + tab_url: 图片地址 + form_url: + fields: + url: + label: 图片地址 + msg: + empty: 图片地址不能为空 + name: + label: 描述 + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上传中 + indent: + text: 缩进 + outdent: + text: 减少缩进 + italic: + text: 斜体 + link: + text: 超链接 + add_link: 添加超链接 + form: + fields: + url: + label: 链接 + msg: + empty: 链接不能为空。 + name: + label: 描述 + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: 有序列表 + unordered_list: + text: 无序列表 + table: + text: 表格 + heading: 表头 + cell: 单元格 + close_modal: + title: 关闭原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空。 + msg: + empty: 请选择一个原因。 + report_modal: + flag_title: 我举报这篇帖子的原因是... + close_title: 我关闭这篇帖子的原因是... + review_question_title: 审查问题 + review_answer_title: 审查回答 + review_comment_title: 审查评论 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空 + msg: + empty: 请选择一个原因。 + not_a_url: URL 格式不正确。 + url_not_match: URL 来源与当前网站不匹配。 + tag_modal: + title: 创建新标签 + form: + fields: + display_name: + label: 显示名称 + msg: + empty: 显示名称不能为空。 + range: 显示名称不能超过 35 个字符。 + slug_name: + label: URL 固定链接 + desc: URL 固定链接不能超过 35 个字符。 + msg: + empty: URL 固定链接不能为空。 + range: URL 固定链接不能超过 35 个字符。 + character: URL 固定链接包含非法字符。 + desc: + label: 描述 + revision: + label: 编辑历史 + edit_summary: + label: 编辑备注 + placeholder: >- + 简单描述更改原因(更正拼写、修复语法、改进格式) + btn_cancel: 取消 + btn_submit: 提交 + btn_post: 发布新标签 + tag_info: + created_at: 创建于 + edited_at: 编辑于 + history: 历史 + synonyms: + title: 同义词 + text: 以下标签将被重置到 + empty: 此标签目前没有同义词。 + btn_add: 添加同义词 + btn_edit: 编辑 + btn_save: 保存 + synonyms_text: 以下标签将被重置到 + delete: + title: 删除标签 + tip_with_posts: >- +

我们不允许 删除带有帖子的标签

请先从帖子中移除此标签。

+ tip_with_synonyms: >- +

我们不允许 删除带有同义词的标签

请先从此标签中删除同义词。

+ tip: 确定要删除吗? + close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + default_first_reason: 添加标签 + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: MM 月 DD 日 + long_date_with_year: "YYYY 年 MM 月 DD 日" + long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm" + now: 刚刚 + x_seconds_ago: "{{count}} 秒前" + x_minutes_ago: "{{count}} 分钟前" + x_hours_ago: "{{count}} 小时前" + hour: 小时 + day: 天 + hours: 小时 + days: 日 + reaction: + heart: 爱心 + smile: 微笑 + frown: 愁 + btn_label: 添加或删除回应。 + undo_emoji: 撤销 {{ emoji }} 回应 + react_emoji: 用 {{ emoji }} 回应 + unreact_emoji: 撤销 {{ emoji }} + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存更改 + btn_cancel: 取消 + show_more: "{{count}} 条剩余评论" + tip_question: >- + 使用评论提问更多信息或者提出改进意见。避免在评论里回答问题。 + tip_answer: >- + 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + tip_vote: 它给帖子添加了一些有用的内容 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + default_first_reason: 添加答案 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + feedback: + characters: 内容长度至少 6 个字符 + edit_summary: + label: 编辑摘要 + placeholder: >- + 简单描述更改原因(更正拼写、修复语法、改进格式) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名称过滤 + no_desc: 此标签无描述。 + more: 更多 + ask: + title: 新增问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + default_first_reason: 新增问题 + similar_questions: 相似问题 + form: + fields: + revision: + label: 修订版本 + title: + label: 标题 + placeholder: 请详细描述你的问题,想象你在问一个人 + msg: + empty: 标题不能为空。 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空。 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + edit_summary: + label: 编辑备注 + placeholder: >- + 简单描述更改原因(更正拼写、修复语法、改进格式) + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 回答自己的问题 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: "描述您的问题是关于什么,至少需要一个标签。" + no_result: 没有匹配的标签 + tag_required_text: 必选标签(至少一个) + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + profile: 用户主页 + setting: 账号设置 + logout: 退出 + admin: 后台管理 + review: 审查 + bookmark: 收藏夹 + moderation: 管理 + search: + placeholder: 搜索 + footer: + build_on: >- + 由 <1>Apache Answer 提供动力 - 驱动问答社区的开源软件。
用爱制造 © {{cc}}. + upload_img: + name: 更改 + loading: 加载中... + pic_auth_code: + title: 验证码 + placeholder: 输入图片中的文字 + msg: + empty: 验证码不能为空。 + inactive: + first: >- + 就差一步!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活你的账户。 + info: "如果没有收到,请检查你的垃圾邮件文件夹。" + another: >- + 我们向你的邮箱 {{mail}} 发送了另一封激活电子邮件。可能需要几分钟才能到达;请务必检查您的垃圾邮件箱。 + btn_name: 重新发送激活邮件 + change_btn_name: 更改邮箱 + msg: + empty: 不能为空。 + resend_email: + url_label: 确定要重新发送激活邮件吗? + url_text: 你也可以将上面的激活链接给该用户。 + login: + login_to_continue: 登录以继续 + info_sign: 没有账户?<1>注册 + info_login: 已经有账户?<1>登录 + agreements: 登录即表示您同意<1>隐私政策和<3>服务条款。 + forgot_pass: 忘记密码? + name: + label: 名字 + msg: + empty: 名字不能为空 + range: 名字应该在 4 到 30 个字符之间 + character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password: + label: 密码 + msg: + empty: 密码不能为空 + different: 两次输入密码不一致 + account_forgot: + page_title: 忘记密码 + btn_name: 发送恢复邮件 + send_success: >- + 如果存在邮箱为 {{mail}} 账户,你将很快收到一封重置密码的说明邮件。 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + change_email: + btn_cancel: 取消 + btn_update: 更新电子邮件地址 + send_success: >- + 如果存在邮箱为 {{mail}} 的账户,你将很快收到一封重置密码的说明邮件。 + email: + label: 新的电子邮件地址 + msg: + empty: 邮箱不能为空。 + oauth: + connect: 连接到 {{ auth_name }} + remove: 移除 {{ auth_name }} + oauth_bind_email: + subtitle: 向你的账户添加恢复邮件地址。 + btn_update: 更新电子邮件地址 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空。 + modal_title: 邮箱已经存在。 + modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? + modal_cancel: 更改邮箱 + modal_confirm: 连接到已有账户 + password_reset: + page_title: 密码重置 + btn_name: 重置我的密码 + reset_success: >- + 你已经成功更改密码;你将被重定向到登录页面。 + link_invalid: >- + 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? + to_login: 前往登录页面 + password: + label: 密码 + msg: + empty: 密码不能为空。 + length: 密码长度在8-32个字符之间 + different: 两次输入密码不一致 + password_confirm: + label: 确认新密码 + settings: + page_title: 设置 + goto_modify: 前往修改 + nav: + profile: 我的资料 + notification: 通知 + account: 账号 + interface: 界面 + profile: + heading: 个人资料 + btn_name: 保存 + display_name: + label: 显示名称 + msg: 昵称不能为空。 + msg_range: 显示名称不能超过 30 个字符。 + username: + label: 用户名 + caption: 用户可以通过 "@用户名" 来提及你。 + msg: 用户名不能为空 + msg_range: 用户名不能超过 30 个字符。 + character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' + avatar: + label: 头像 + gravatar: Gravatar + gravatar_text: 你可以更改图像在 + custom: 自定义 + custom_text: 你可以上传你的图片。 + default: 系统 + msg: 请上传头像 + bio: + label: 关于我 + website: + label: 网站 + placeholder: "https://example.com" + msg: 网址格式不正确 + location: + label: 位置 + placeholder: "城市,国家" + notification: + heading: 邮件通知 + turn_on: 开启 + inbox: + label: 收件箱通知 + description: 你的提问有新的回答,评论,邀请回答和其他。 + all_new_question: + label: 所有新问题 + description: 获取所有新问题的通知。每周最多有50个问题。 + all_new_question_for_following_tags: + label: 所有关注标签的新问题 + description: 获取关注的标签下新问题通知。 + account: + heading: 账号 + change_email_btn: 更改邮箱 + change_pass_btn: 更改密码 + change_email_info: >- + 邮件已发送。请根据指引完成验证。 + email: + label: 电子邮件地址 + new_email: + label: 新的电子邮件地址 + msg: 新邮箱不能为空。 + pass: + label: 当前密码 + msg: 密码不能为空。 + password_title: 密码 + current_pass: + label: 当前密码 + msg: + empty: 当前密码不能为空 + length: 密码长度必须在 8 至 32 之间 + different: 两次输入的密码不匹配 + new_pass: + label: 新密码 + pass_confirm: + label: 确认新密码 + interface: + heading: 界面 + lang: + label: 界面语言 + text: 设置用户界面语言,在刷新页面后生效。 + my_logins: + title: 我的登录 + label: 使用这些账户登录或注册本网站。 + modal_title: 移除登录 + modal_content: 你确定要从账户里移除该登录? + modal_confirm_btn: 移除 + remove_success: 移除成功 + toast: + update: 更新成功 + update_password: 密码更新成功。 + flag_success: 感谢标记。 + forbidden_operate_self: 禁止对自己执行操作 + review: 您的修订将在审阅通过后显示。 + sent_success: 发送成功 + related_question: + title: 相关问题 + answers: 个回答 + invite_to_answer: + title: 受邀人 + desc: 邀请你认为可能知道答案的人。 + invite: 邀请回答 + add: 添加人员 + search: 搜索人员 + question_detail: + action: 操作 + Asked: 提问于 + asked: 提问于 + update: 修改于 + edit: 编辑于 + commented: 评论 + Views: 阅读次数 + Follow: 关注此问题 + Following: 已关注 + follow_tip: 关注此问题以接收通知 + answered: 回答于 + closed_in: 关闭于 + show_exist: 查看类似问题。 + useful: 有用的 + question_useful: 它是有用和明确的 + question_un_useful: 它不明确或没用的 + question_bookmark: 收藏该问题 + answer_useful: 这是有用的 + answer_un_useful: 它是没有用的 + answers: + title: 个回答 + score: 评分 + newest: 最新 + oldest: 最旧 + btn_accept: 采纳 + btn_accepted: 已被采纳 + write_answer: + title: 你的回答 + edit_answer: 编辑我的回答 + btn_name: 提交你的回答 + add_another_answer: 添加另一个回答 + confirm_title: 继续回答 + continue: 继续 + confirm_info: >- +

你确定要提交一个新的回答吗?

作为替代,你可以通过编辑来完善和改进之前的回答。

+ empty: 回答内容不能为空。 + characters: 内容长度至少 6 个字符。 + tips: + header_1: 感谢你的回答 + li1_1: 请务必确定在 回答问题。提供详细信息并分享你的研究。 + li1_2: 用参考资料或个人经历来支持你所做的任何陈述。 + header_2: 但是 请避免... + li2_1: 请求帮助,寻求澄清,或答复其他答案。 + reopen: + confirm_btn: 重新打开 + title: 重新打开这个帖子 + content: 确定要重新打开吗? + list: + confirm_btn: 列表显示 + title: 列表中显示这个帖子 + content: 确定要列表中显示这个帖子吗? + unlist: + confirm_btn: 列表隐藏 + title: 从列表中隐藏这个帖子 + content: 确定要从列表中隐藏这个帖子吗? + pin: + title: 置顶该帖子 + content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。 + confirm_btn: 置顶 + delete: + title: 删除 + question: >- + 我们不建议 删除有回答的帖子。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_answer_deleted: 该回答已被删除 + undelete_title: 撤销删除本帖 + undelete_desc: 你确定你要撤销删除吗? + btns: + confirm: 确认 + cancel: 取消 + edit: 编辑 + save: 保存 + delete: 删除 + undelete: 撤消删除 + list: 列表显示 + unlist: 列表隐藏 + unlisted: 已隐藏 + login: 登录 + signup: 注册 + logout: 退出 + verify: 验证 + add_question: 我要提问 + approve: 批准 + reject: 拒绝 + skip: 跳过 + discard_draft: 丢弃草稿 + pinned: 已置顶 + all: 全部 + question: 问题 + answer: 回答 + comment: 评论 + refresh: 刷新 + resend: 重新发送 + deactivate: 取消激活 + active: 激活 + suspend: 封禁 + unsuspend: 解禁 + close: 关闭 + reopen: 重新打开 + ok: 确定 + light: 浅色 + dark: 深色 + system_setting: 跟随系统 + default: 默认 + reset: 重置 + tag: 标签 + post_lowercase: 帖子 + filter: 筛选 + ignore: 忽略 + submit: 提交 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + pending: 等待处理 + more: 更多 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: "{{count}} 个结果" + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + more: 更多 + tips: + title: 高级搜索提示 + tag: "<1>[tag] 在指定标签中搜索" + user: "<1>user:username 根据作者搜索" + answer: "<1>answers:0 搜索未回答的问题" + score: "<1>score:3 评分 3+ 的帖子" + question: "<1>is:question 搜索问题" + is_answer: "<1>is:answer 搜索回答" + empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 + share: + name: 分享 + copy: 复制链接 + via: 分享到... + copied: 已复制 + facebook: 分享到 Facebook + twitter: 分享到 Twitter + cannot_vote_for_self: 你不能给自己的帖子投票。 + modal_confirm: + title: 发生错误... + account_result: + success: 你的账号已通过验证,即将返回首页。 + link: 返回首页 + invalid: >- + 抱歉,此验证链接已失效。也许你的账号已经激活了? + confirm_new_email: 你的电子邮箱已更新 + confirm_new_email_invalid: >- + 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? + unsubscribe: + page_title: 退订 + success_title: 退订成功 + success_desc: 您已成功退订,并且将不会再收到我们的邮件。 + link: 更改设置 + question: + following_tags: 已关注的标签 + edit: 编辑 + save: 保存 + follow_tag_tip: 关注标签来筛选你的问题列表。 + hot_questions: 热门问题 + all_questions: 全部问题 + x_questions: "{{ count }} 个问题" + x_answers: "{{ count }} 个回答" + questions: 问题 + answers: 回答 + newest: 最新 + active: 活跃 + hot: 热门 + score: 评分 + unanswered: 未回答 + modified: 更新于 + answered: 回答于 + asked: 提问于 + closed: 已关闭 + follow_a_tag: 关注一个标签 + more: 更多 + personal: + overview: 概览 + answers: 回答 + answer: 回答 + questions: 问题 + question: 问题 + bookmarks: 收藏 + reputation: 声望 + comments: 评论 + votes: 得票 + newest: 最新 + score: 评分 + edit_profile: 编辑资料 + visited_x_days: "已访问 {{ count }} 天" + viewed: 浏览次数 + joined: 加入于 + last_login: 上次登录 + about_me: 关于我 + about_me_empty: "// Hello, World!" + top_answers: 高分回答 + top_questions: 高分问题 + stats: 状态 + list_empty: 没有找到相关的内容。
试试看其他选项卡? + accepted: 已采纳 + answered: 回答于 + asked: 提问于 + downvoted: 点踩 + mod_short: 版主 + mod_long: 版主 + x_reputation: 声望 + x_votes: 得票 + x_answers: 个回答 + x_questions: 个问题 + install: + title: 安装 + next: 下一步 + done: 完成 + config_yaml_error: 无法创建 config.yaml 文件。 + lang: + label: 请选择一种语言 + db_type: + label: 数据库引擎 + db_username: + label: 用户名 + placeholder: root + msg: 用户名不能为空 + db_password: + label: 密码 + placeholder: root + msg: 密码不能为空 + db_host: + label: 数据库主机 + placeholder: "db:3306" + msg: 数据库地址不能为空 + db_name: + label: 数据库名 + placeholder: 回答 + msg: 数据库名称不能为空。 + db_file: + label: 数据库文件 + placeholder: /data/answer.db + msg: 数据库文件不能为空。 + config_yaml: + title: 创建 config.yaml + label: 已创建 config.yaml 文件。 + desc: >- + 你可以手动在 <1>/var/wwww/xxx/ 目录中创建 <1>config.yaml 文件并粘贴以下文本。 + info: 完成后,点击“下一步”按钮。 + site_information: 站点信息 + admin_account: 管理员账号 + site_name: + label: 站点名称 + msg: 站点名称不能为空。 + msg_max_length: 站点名称长度不得超过 30 个字符。 + site_url: + label: 网站网址 + text: 此网站的网址。 + msg: + empty: 网址不能为空。 + incorrect: 网址格式不正确。 + max_length: 网址长度不得超过 512 个字符。 + contact_email: + label: 联系邮箱 + text: 负责本网站的主要联系人的电子邮件地址。 + msg: + empty: 联系人邮箱不能为空。 + incorrect: 联系人邮箱地址不正确。 + login_required: + label: 私有的 + switch: 需要登录 + text: 只有登录用户才能访问这个社区。 + admin_name: + label: 名字 + msg: 名字不能为空。 + character: '只能由 "a-z", "0-9", " - . _" 组成' + msg_max_length: 名字长度不能超过 30 个字符。 + admin_password: + label: 密码 + text: >- + 您需要此密码才能登录。请将其存储在一个安全的位置。 + msg: 密码不能为空。 + msg_min_length: 密码必须至少 8 个字符长。 + msg_max_length: 密码长度不能超过 32 个字符。 + admin_email: + label: 邮箱 + text: 您需要此电子邮件才能登录。 + msg: + empty: 邮箱不能为空。 + incorrect: 邮箱格式不正确。 + ready_title: 您的网站已准备好 + ready_desc: >- + 如果你想改变更多的设置,请访问 <1>管理区域;在网站菜单中找到它。 + good_luck: "玩得愉快,祝你好运!" + warn_title: 警告 + warn_desc: >- + 文件 <1>config.yaml 已存在。如果你要重置该文件中的任何配置项,请先删除它。 + install_now: 您可以尝试 <1>现在安装。 + installed: 已安裝 + installed_desc: >- + 你似乎已经安装过了。如果要重新安装,请先清除旧的数据库表。 + db_failed: 数据连接异常! + db_failed_desc: >- + 这或者意味着数据库信息在 <1>config.yaml 文件不正确,或者无法与数据库服务器建立联系。这可能意味着你的主机数据库服务器故障。 + counts: + views: 次浏览 + votes: 个点赞 + answers: 个回答 + accepted: 已被采纳 + page_error: + http_error: HTTP 错误 {{ code }} + desc_403: 您无权访问此页面。 + desc_404: 很抱歉,此页面不存在。 + desc_50X: 服务器遇到了一个错误,无法完成你的请求。 + back_home: 返回首页 + page_maintenance: + desc: "我们正在进行维护,我们将很快回来。" + nav_menus: + dashboard: 后台管理 + contents: 内容管理 + questions: 问题 + answers: 回答 + users: 用户管理 + flags: 举报管理 + settings: 站点设置 + general: 一般 + interface: 界面 + smtp: SMTP + branding: 品牌 + legal: 法律条款 + write: 撰写 + tos: 服务条款 + privacy: 隐私政策 + seo: SEO + customize: 自定义 + themes: 主题 + css_html: CSS/HTML + login: 登录 + privileges: 特权 + plugins: 插件 + installed_plugins: 已安装插件 + website_welcome: 欢迎来到 {{site_name}} + user_center: + login: 登录 + qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。 + login_failed_email_tip: 登录失败,请允许此应用访问您的邮箱信息,然后重试。 + admin: + admin_header: + title: 后台管理 + dashboard: + title: 后台管理 + welcome: 欢迎来到管理后台! + site_statistics: 站点统计 + questions: "问题:" + answers: "回答:" + comments: "评论:" + votes: "投票:" + users: "用户:" + flags: "举报:" + reviews: "审查:" + site_health: 网站健康 + version: "版本" + https: "HTTPS:" + upload_folder: "上传文件夹:" + run_mode: "运行模式:" + private: 私有 + public: 公开 + smtp: "SMTP:" + timezone: "时区:" + system_info: 系统信息 + go_version: "Go版本:" + database: "数据库:" + database_size: "数据库大小:" + storage_used: "已用存储空间:" + uptime: "运行时间:" + links: 链接 + plugins: 插件 + github: GitHub + blog: 博客 + contact: 联系 + forum: 论坛 + documents: 文档 + feedback: 用户反馈 + support: 帮助 + review: 审查 + config: 配置 + update_to: 更新到 + latest: 最新版本 + check_failed: 校验失败 + "yes": "是" + "no": "否" + not_allowed: 拒绝 + allowed: 允许 + enabled: 已启用 + disabled: 停用 + writable: 可写 + not_writable: 不可写 + flags: + title: 举报 + pending: 等待处理 + completed: 已完成 + flagged: 被举报内容 + flagged_type: 标记了 {{ type }} + created: 创建于 + action: 操作 + review: 审查 + user_role_modal: + title: 更改用户状态为... + btn_cancel: 取消 + btn_submit: 提交 + new_password_modal: + title: 设置新密码 + form: + fields: + password: + label: 密码 + text: 用户将被退出,需要再次登录。 + msg: 密码的长度必须是8-32个字符。 + btn_cancel: 取消 + btn_submit: 提交 + edit_profile_modal: + title: 编辑资料 + form: + fields: + username: + label: 用户名 + msg_range: 用户名不能超过 30 个字符。 + email: + label: 电子邮件地址 + msg_invalid: 无效的邮箱地址 + edit_success: 修改成功 + btn_cancel: 取消 + btn_submit: 提交 + user_modal: + title: 添加新用户 + form: + fields: + users: + label: 批量添加用户 + placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" + text: 用逗号分隔“name, email, password”,每行一个用户。 + msg: "请输入用户的邮箱,每行一个。" + display_name: + label: 显示名称 + msg: 显示名称长度必须为 4-30 个字符 + email: + label: 邮箱 + msg: 邮箱无效。 + password: + label: 密码 + msg: 密码的长度必须是8-32个字符。 + btn_cancel: 取消 + btn_submit: 提交 + users: + title: 用户 + name: 名称 + email: 邮箱 + reputation: 声望 + created_at: 创建时间 + delete_at: 删除时间 + suspend_at: 封禁时间 + status: 状态 + role: 角色 + action: 操作 + change: 更改 + all: 全部 + staff: 工作人员 + more: 更多 + inactive: 不活跃 + suspended: 已封禁 + deleted: 已删除 + normal: 正常 + Moderator: 版主 + Admin: 管理员 + User: 用户 + filter: + placeholder: "按名称筛选,用户:id" + set_new_password: 设置新密码 + edit_profile: 编辑资料 + change_status: 更改状态 + change_role: 更改角色 + show_logs: 显示日志 + add_user: 添加用户 + deactivate_user: + title: 停用用户 + content: 未激活的用户必须重新验证他们的邮箱。 + delete_user: + title: 删除此用户 + content: 确定要删除此用户?此操作无法撤销! + remove: 移除内容 + label: 删除所有问题、 答案、 评论等 + text: 如果你只想删除用户账户,请不要选中此项。 + suspend_user: + title: 挂起此用户 + content: 被封禁的用户将无法登录。 + questions: + page_title: 问题 + unlisted: 已隐藏 + post: 标题 + votes: 得票数 + answers: 回答数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + pending: 等待处理 + filter: + placeholder: "按标题过滤,问题:id" + answers: + page_title: 回答 + post: 标题 + votes: 得票数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + filter: + placeholder: "按标题筛选,答案:id" + general: + page_title: 一般 + name: + label: 站点名称 + msg: 不能为空 + text: "站点的名称,作为站点的标题。" + site_url: + label: 网站网址 + msg: 网站网址不能为空。 + validate: 请输入一个有效的 URL。 + text: 此网站的地址。 + short_desc: + label: 简短站点描述 + msg: 简短网站描述不能为空。 + text: "简短的标语,作为网站主页的标题(Html 的 title 标签)。" + desc: + label: 站点描述 + msg: 网站描述不能为空。 + text: "使用一句话描述本站,作为网站的描述(Html 的 meta 标签)。" + contact_email: + label: 联系邮箱 + msg: 联系人邮箱不能为空。 + validate: 联系人邮箱无效。 + text: 本网站的主要联系邮箱地址。 + check_update: + label: 软件更新 + text: 自动检查软件更新 + interface: + page_title: 界面 + language: + label: 界面语言 + msg: 不能为空 + text: 设置用户界面语言,在刷新页面后生效。 + time_zone: + label: 时区 + msg: 时区不能为空。 + text: 选择一个与您相同时区的城市。 + smtp: + page_title: SMTP + from_email: + label: 发件人邮箱 + msg: 发件人邮箱不能为空。 + text: 用于发送邮件的地址。 + from_name: + label: 发件人 + msg: 不能为空 + text: 发件人的名字。 + smtp_host: + label: SMTP 主机 + msg: 不能为空 + text: 邮件服务器 + encryption: + label: 加密 + msg: 不能为空 + text: 对于大多数服务器而言,SSL 是推荐开启的。 + ssl: SSL + tls: TLS + none: 无加密 + smtp_port: + label: SMTP 端口 + msg: SMTP 端口必须在 1 ~ 65535 之间。 + text: 邮件服务器的端口号。 + smtp_username: + label: SMTP 用户名 + msg: 不能为空 + smtp_password: + label: SMTP 密码 + msg: 不能为空 + test_email_recipient: + label: 测试收件邮箱 + text: 提供用于接收测试邮件的邮箱地址。 + msg: 测试收件邮箱无效 + smtp_authentication: + label: 启用身份验证 + title: SMTP 身份验证 + msg: 不能为空 + "yes": "是" + "no": "否" + branding: + page_title: 品牌 + logo: + label: 网站标志(Logo) + msg: 图标不能为空。 + text: 在你的网站左上方的Logo图标。使用一个高度为56,长宽比大于3:1的宽长方形图像。如果留空,将显示网站标题文本。 + mobile_logo: + label: 移动端 Logo + text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空,将使用 "Logo"设置中的图像。 + square_icon: + label: 方形图标 + msg: 方形图标不能为空。 + text: 用作元数据图标的基础的图像。最好是大于512x512。 + favicon: + label: 收藏夹图标 + text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空,将使用“方形图标”。 + legal: + page_title: 法律条款 + terms_of_service: + label: 服务条款 + text: "您可以在此添加服务内容的条款。如果您已经在别处托管了文档,请在这里提供完整的URL。" + privacy_policy: + label: 隐私政策 + text: "您可以在此添加隐私政策内容。如果您已经在别处托管了文档,请在这里提供完整的URL。" + write: + page_title: 编辑 + restrict_answer: + title: 限制一个回答 + label: 每个用户对于每个问题只能有一个回答 + text: "用户可以使用编辑按钮优化已有的回答" + recommend_tags: + label: 推荐标签 + text: "请在上方输入标签固定链接,每行一个标签。" + required_tag: + title: 必需的标签 + label: 根据需要设置推荐标签 + text: "每个新问题必须至少有一个推荐标签。" + reserved_tags: + label: 保留标签 + text: "保留的标签只能由版主添加到一个帖子中。" + seo: + page_title: 搜索引擎优化 + permalink: + label: 固定链接 + text: 自定义URL结构可以提高可用性,以及你的链接的向前兼容性。 + robots: + label: robots.txt + text: 这将永久覆盖任何相关的网站设置。 + themes: + page_title: 主题 + themes: + label: 主题 + text: 选择一个现有主题。 + color_scheme: + label: 配色方案 + navbar_style: + label: 导航栏样式 + primary_color: + label: 主色调 + text: 修改您主题使用的颜色 + css_and_html: + page_title: CSS 与 HTML + custom_css: + label: 自定义 CSS + text: > + + head: + label: 头部 + text: > + + header: + label: 页眉 + text: > + + footer: + label: 页脚 + text: 这将在 之前插入. + sidebar: + label: 侧边栏 + text: 这将插入侧边栏中。 + login: + page_title: 登录 + membership: + title: 会员 + label: 允许新注册 + text: 关闭以防止任何人创建新账户。 + email_registration: + title: 邮箱注册 + label: 允许邮箱注册 + text: 关闭以阻止任何人通过邮箱创建新账户。 + allowed_email_domains: + title: 允许的邮箱域 + text: 允许注册账户的邮箱域。每行一个域名。留空时忽略。 + private: + title: 非公开的 + label: 需要登录 + text: 只有登录用户才能访问这个社区。 + password_login: + title: 密码登录 + label: 允许使用邮箱和密码登录 + text: "警告:如果您未配置过其他登录方式,关闭密码登录后您则可能无法登录。" + installed_plugins: + title: 已安装插件 + plugin_link: 插件扩展功能。您可以在<1>插件仓库中找到插件。 + filter: + all: 全部 + active: 已启用 + inactive: 未启用 + outdated: 已过期 + plugins: + label: 插件 + text: 选择一个现有的插件。 + name: 名称 + version: 版本 + status: 状态 + action: 操作 + deactivate: 停用 + activate: 启用 + settings: 设置 + settings_users: + title: 用户 + avatar: + label: 默认头像 + text: 没有自定义头像的用户。 + gravatar_base_url: + label: Gravatar 根路径 URL + text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。 + profile_editable: + title: 个人资料可编辑 + allow_update_display_name: + label: 允许用户修改显示名称 + allow_update_username: + label: 允许用户修改用户名 + allow_update_avatar: + label: 允许用户修改个人头像 + allow_update_bio: + label: 允许用户修改个人介绍 + allow_update_website: + label: 允许用户修改个人主页网址 + allow_update_location: + label: 允许用户更改位置 + privilege: + title: 特权 + level: + label: 级别所需声望 + text: 选择特权所需的声望值 + msg: + should_be_number: 输入必须是数字 + number_larger_1: 数字应该大于等于 1 + form: + optional: (选填) + empty: 不能为空 + invalid: 是无效的 + btn_submit: 保存 + not_found_props: "所需属性 {{ key }} 未找到。" + select: 选择 + page_review: + review: 评论 + proposed: 提案 + question_edit: 问题编辑 + answer_edit: 回答编辑 + tag_edit: '标签管理: 编辑标签' + edit_summary: 编辑备注 + edit_question: 编辑问题 + edit_answer: 编辑回答 + edit_tag: 编辑标签 + empty: 没有剩余的审核任务。 + approve_revision_tip: 您是否批准此修订? + approve_flag_tip: 您是否批准此举报? + approve_post_tip: 您是否批准此帖子? + approve_user_tip: 您是否批准此修订? + suggest_edits: 建议的编辑 + flag_post: 举报帖子 + flag_user: 举报用户 + queued_post: 排队的帖子 + queued_user: 排队用户 + filter_label: 类型 + reputation: 声望值 + flag_post_type: 举报这个帖子的类型是 {{ type }} + flag_user_type: 举报这个用户的类型是 {{ type }} + edit_post: 编辑帖子 + list_post: 文章列表 + unlist_post: 隐藏的帖子 + timeline: + undeleted: 取消删除 + deleted: 删除 + downvote: 反对 + upvote: 点赞 + accept: 采纳 + cancelled: 已取消 + commented: '评论:' + rollback: 回滚 + edited: 最后编辑于 + answered: 回答于 + asked: 提问于 + closed: 关闭 + reopened: 重新开启 + created: 创建于 + pin: 已置顶 + unpin: 取消置頂 + show: 已显示 + hide: 已隐藏 + title: "历史记录" + tag_title: "时间线" + show_votes: "显示投票" + n_or_a: N/A + title_for_question: "时间线" + title_for_answer: "{{ title }} 的 {{ author }} 回答时间线" + title_for_tag: "时间线" + datetime: 日期时间 + type: 类型 + by: 由 + comment: 评论 + no_data: "空空如也" + users: + title: 用户 + users_with_the_most_reputation: 本周声望最高的用户 + users_with_the_most_vote: 本周投票最多的用户 + staffs: 我们的社区工作人员 + reputation: 声望值 + votes: 投票 + prompt: + leave_page: 确定要离开此页面? + changes_not_save: 您的更改尚未保存 + draft: + discard_confirm: 您确定要丢弃您的草稿吗? + messages: + post_deleted: 该帖子已被删除。 + post_pin: 该帖子已被置顶。 + post_unpin: 该帖子已被取消置顶。 + post_hide_list: 此帖子已经从列表中隐藏。 + post_show_list: 该帖子已显示到列表中。 + post_reopen: 这个帖子已被重新打开. + post_list: 这个帖子已经被显示 + post_unlist: 这个帖子已经被隐藏 + post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。 From f660b6368ddc8c909c0d08d5a54334d7d30e734b Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 19 Aug 2024 15:22:06 +0800 Subject: [PATCH 060/129] fix(dev): delete test file --- dev/i18n/zh_CN.yaml | 2035 ------------------------------------------- 1 file changed, 2035 deletions(-) delete mode 100644 dev/i18n/zh_CN.yaml diff --git a/dev/i18n/zh_CN.yaml b/dev/i18n/zh_CN.yaml deleted file mode 100644 index 6613bfc22..000000000 --- a/dev/i18n/zh_CN.yaml +++ /dev/null @@ -1,2035 +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. - -# The following fields are used for back-end -backend: - base: - success: - other: 成功。 - unknown: - other: 未知错误。 - request_format_error: - other: 请求格式错误。 - unauthorized_error: - other: 未授权。 - database_error: - other: 数据服务器错误。 - forbidden_error: - other: 禁止访问。 - duplicate_request_error: - other: 重复提交。 - action: - report: - other: 举报 - edit: - other: 编辑 - delete: - other: 删除 - close: - other: 关闭 - reopen: - other: 重新打开 - forbidden_error: - other: 禁止访问。 - pin: - other: 置顶 - hide: - other: 列表隐藏 - unpin: - other: 取消置顶 - show: - other: 列表显示 - invite_someone_to_answer: - other: 编辑 - undelete: - other: 撤消删除 - role: - name: - user: - other: 用户 - admin: - other: 管理员 - moderator: - other: 版主 - description: - user: - other: 默认没有特殊权限。 - admin: - other: 拥有管理网站的全部权限。 - moderator: - other: 拥有除访问后台管理以外的所有权限。 - privilege: - level_1: - description: - other: 级别 1(少量声望要求,适合私有团队、群组) - level_2: - description: - other: 级别 2(低声望要求,适合初启动的社区) - level_3: - description: - other: 级别 3(高声望要求,适合成熟的社区) - level_custom: - description: - other: 自定义等级 - rank_question_add_label: - other: 提问 - rank_answer_add_label: - other: 写答案 - rank_comment_add_label: - other: 写评论 - rank_report_add_label: - other: 举报 - rank_comment_vote_up_label: - other: 点赞评论 - rank_link_url_limit_label: - other: 每次发布超过 2 个链接 - rank_question_vote_up_label: - other: 点赞问题 - rank_answer_vote_up_label: - other: 点赞答案 - rank_question_vote_down_label: - other: 点踩问题 - rank_answer_vote_down_label: - other: 点踩答案 - rank_invite_someone_to_answer_label: - other: 邀请回答 - rank_tag_add_label: - other: 创建新标签 - rank_tag_edit_label: - other: 编辑标签描述(需要审核) - rank_question_edit_label: - other: 编辑别人的问题(需要审核) - rank_answer_edit_label: - other: 编辑别人的答案(需要审核) - rank_question_edit_without_review_label: - other: 编辑别人的问题无需审核 - rank_answer_edit_without_review_label: - other: 编辑别人的答案无需审核 - rank_question_audit_label: - other: 审核问题编辑 - rank_answer_audit_label: - other: 审核回答编辑 - rank_tag_audit_label: - other: 审核标签编辑 - rank_tag_edit_without_review_label: - other: 编辑标签描述无需审核 - rank_tag_synonym_label: - other: 管理标签同义词 - email: - other: 邮箱 - e_mail: - other: 邮箱 - password: - other: 密码 - pass: - other: 密码 - original_text: - other: 本帖 - email_or_password_wrong_error: - other: 邮箱和密码不匹配。 - error: - common: - invalid_url: - other: 无效的 URL。 - password: - space_invalid: - other: 密码不得含有空格。 - admin: - cannot_update_their_password: - other: 你无法修改自己的密码。 - cannot_edit_their_profile: - other: 您不能修改您的个人资料。 - cannot_modify_self_status: - other: 你无法修改自己的状态。 - email_or_password_wrong: - other: 邮箱和密码不匹配。 - answer: - not_found: - other: 没有找到答案。 - cannot_deleted: - other: 没有删除权限。 - cannot_update: - other: 没有更新权限。 - question_closed_cannot_add: - other: 问题已关闭,无法添加。 - comment: - edit_without_permission: - other: 不允许编辑评论。 - not_found: - other: 评论未找到。 - cannot_edit_after_deadline: - other: 评论时间太久,无法修改。 - email: - duplicate: - other: 邮箱已存在。 - need_to_be_verified: - other: 邮箱需要验证。 - verify_url_expired: - other: 邮箱验证的网址已过期,请重新发送邮件。 - illegal_email_domain_error: - other: 此邮箱不在允许注册的邮箱域中。请使用其他邮箱尝试。 - lang: - not_found: - other: 语言文件未找到。 - object: - captcha_verification_failed: - other: 验证码错误。 - disallow_follow: - other: 你不能关注。 - disallow_vote: - other: 你不能投票。 - disallow_vote_your_self: - other: 你不能为自己的帖子投票。 - not_found: - other: 对象未找到。 - verification_failed: - other: 验证失败。 - email_or_password_incorrect: - other: 邮箱和密码不匹配。 - old_password_verification_failed: - other: 旧密码验证失败。 - new_password_same_as_previous_setting: - other: 新密码和旧密码相同。 - already_deleted: - other: 该帖子已被删除。 - meta: - object_not_found: - other: Meta 对象未找到 - question: - already_deleted: - other: 该帖子已被删除。 - under_review: - other: 您的帖子正在等待审核。它将在它获得批准后可见。 - not_found: - other: 问题未找到。 - cannot_deleted: - other: 没有删除权限。 - cannot_close: - other: 没有关闭权限。 - cannot_update: - other: 没有更新权限。 - rank: - fail_to_meet_the_condition: - other: 声望值未达到要求。 - vote_fail_to_meet_the_condition: - other: 感谢投票。你至少需要 {{.Rank}} 声望才能投票。 - no_enough_rank_to_operate: - other: 你至少需要 {{.Rank}} 声望才能执行此操作。 - report: - handle_failed: - other: 报告处理失败。 - not_found: - other: 报告未找到。 - tag: - already_exist: - other: 标签已存在。 - not_found: - other: 标签未找到。 - recommend_tag_not_found: - other: 推荐标签不存在。 - recommend_tag_enter: - other: 请选择至少一个必选标签。 - not_contain_synonym_tags: - other: 不应包含同义词标签。 - cannot_update: - other: 没有更新权限。 - is_used_cannot_delete: - other: 你不能删除这个正在使用的标签。 - cannot_set_synonym_as_itself: - other: 你不能将当前标签设为自己的同义词。 - smtp: - config_from_name_cannot_be_email: - other: 发件人名称不能是邮箱地址。 - theme: - not_found: - other: 主题未找到。 - revision: - review_underway: - other: 目前无法编辑,有一个版本在审阅队列中。 - no_permission: - other: 无权限修改。 - user: - external_login_missing_user_id: - other: 第三方平台没有提供唯一的 UserID,所以你不能登录,请联系网站管理员。 - external_login_unbinding_forbidden: - other: 请在移除此登录之前为你的账户设置登录密码。 - email_or_password_wrong: - other: - other: 邮箱和密码不匹配。 - not_found: - other: 用户未找到。 - suspended: - other: 用户已被封禁。 - username_invalid: - other: 用户名无效。 - username_duplicate: - other: 用户名已被使用。 - set_avatar: - other: 头像设置错误。 - cannot_update_your_role: - other: 你不能修改自己的角色。 - not_allowed_registration: - other: 该网站暂未开放注册。 - not_allowed_login_via_password: - other: 该网站暂不支持密码登录。 - access_denied: - other: 拒绝访问 - page_access_denied: - other: 您没有权限访问此页面。 - add_bulk_users_format_error: - other: "发生错误,{{.Field}} 格式错误,在 '{{.Content}}' 行数 {{.Line}}. {{.ExtraMessage}}" - add_bulk_users_amount_error: - other: "一次性添加的用户数量应在 1-{{.MaxAmount}} 之间。" - config: - read_config_failed: - other: 读取配置失败 - database: - connection_failed: - other: 数据库连接失败 - create_table_failed: - other: 创建表失败 - install: - create_config_failed: - other: 无法创建 config.yaml 文件。 - upload: - unsupported_file_format: - other: 不支持的文件格式。 - site_info: - config_not_found: - other: 未找到网站的该配置信息。 - reason: - spam: - name: - other: 垃圾信息 - desc: - other: 这个帖子是一个广告,或是破坏性行为。它对当前的主题无帮助或无关。 - rude_or_abusive: - name: - other: 粗鲁或辱骂的 - desc: - other: - - 一个有理智的人都会认为这种内容不适合进行尊重性的讨论。 - - 论坛 - a_duplicate: - name: - other: 重复内容 - desc: - other: 该问题有人问过,而且已经有了答案。 - placeholder: - other: 输入已有的问题链接 - not_a_answer: - name: - other: 不是答案 - desc: - other: - - 这张贴作为答案,但它不会试图回答 - - 这可能是一个编辑、一个评论、另一个问题。 - - 或全部删除。 - no_longer_needed: - name: - other: 不再需要 - desc: - other: 该评论已过时,对话性质或与此帖子无关。 - something: - name: - other: 其他原因 - desc: - other: 此帖子需要工作人员注意,因为是上述所列以外的其他理由。 - placeholder: - other: 让我们具体知道你关心的什么 - community_specific: - name: - other: 社区特定原因 - desc: - other: 该问题不符合社区准则。 - not_clarity: - name: - other: 需要细节或澄清 - desc: - other: 该问题目前涵盖多个问题。它应该侧重在一个问题上。 - looks_ok: - name: - other: 看起来没问题 - desc: - other: 这个帖子是好的,不是低质量。 - needs_edit: - name: - other: 需要编辑,我已做了修改。 - desc: - other: 改进和纠正你自己帖子中的问题。 - needs_close: - name: - other: 需要关闭 - desc: - other: 关闭的问题不能回答,但仍然可以编辑、投票和评论。 - needs_delete: - name: - other: 需要删除 - desc: - other: 该帖子将被删除。 - question: - close: - duplicate: - name: - other: 垃圾信息 - desc: - other: 此问题以前就有人问过,而且已经有了答案。 - guideline: - name: - other: 社区特定原因 - desc: - other: 该问题不符合社区准则。 - multiple: - name: - other: 需要细节或澄清 - desc: - other: - - 该问题目前涵盖多个问题。它应该侧重在一个问题上。 - - 只关注一个问题。 - other: - name: - other: 其他原因 - desc: - other: 该帖子存在上面没有列出的另一个原因。 - operation_type: - asked: - other: 提问于 - answered: - other: 回答于 - modified: - other: 修改于 - deleted_title: - other: 删除的问题 - notification: - action: - update_question: - other: 更新了问题 - answer_the_question: - other: 回答了问题 - update_answer: - other: 更新了答案 - accept_answer: - other: 采纳了答案 - comment_question: - other: 评论了问题 - comment_answer: - other: 评论了答案 - reply_to_you: - other: 回复了你 - mention_you: - other: 提到了你 - your_question_is_closed: - other: 你的问题已被关闭 - your_question_was_deleted: - other: 你的问题已被删除 - your_answer_was_deleted: - other: 你的答案已被删除 - your_comment_was_deleted: - other: 你的评论已被删除 - up_voted_question: - other: 点赞问题 - down_voted_question: - other: 点踩问题 - up_voted_answer: - other: 点赞答案 - down_voted_answer: - other: 点踩回答 - up_voted_comment: - other: 点赞评论 - invited_you_to_answer: - other: 邀请你回答 - email_tpl: - change_email: - title: - other: "[{{.SiteName}}] 确认你的新邮箱地址" - body: - other: "请点击以下链接确认你在 {{.SiteName}} 上的新邮箱地址:
\n{{.ChangeEmailUrl}}

\n\n如果你没有请求此更改,请忽略此邮件。\n" - new_answer: - title: - other: "[{{.SiteName}}] {{.DisplayName}} 回答了你的问题" - body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" - invited_you_to_answer: - title: - other: "[{{.SiteName}}] {{.DisplayName}} 邀请您回答问题" - body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
我想你可能知道答案。

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" - new_comment: - title: - other: "[{{.SiteName}}] {{.DisplayName}} 评论了你的帖子" - body: - other: "{{.QuestionTitle}}

\n\n{{.DisplayName}}:
\n
{{.CommentSummary}}

\n在 {{.SiteName}} 上查看

\n\n--
\n取消订阅" - new_question: - title: - other: "[{{.SiteName}}] 新问题: {{.QuestionTitle}}" - body: - other: "{{.QuestionTitle}}
\n{{.Tags}}

\n\n--
\n取消订阅" - pass_reset: - title: - other: "[{{.SiteName }}] 重置密码" - body: - other: "有人要求在 [{{.SiteName}}] 上重置你的密码。

\n\n如果这不是你的操作,请安心忽略此电子邮件。

\n\n请点击以下链接设置一个新密码:
\n{{.PassResetUrl}}\n" - register: - title: - other: "[{{.SiteName}}] 确认你的新账户" - body: - other: "欢迎加入 {{.SiteName}}!

\n\n请点击以下链接确认并激活你的新账户:
\n{{.RegisterUrl}}

\n\n如果上面的链接不能点击,请将其复制并粘贴到你的浏览器地址栏中。\n" - test: - title: - other: "[{{.SiteName}}] 测试邮件" - body: - other: "这是一封测试邮件。" - action_activity_type: - upvote: - other: 点赞 - upvoted: - other: 点赞 - downvote: - other: 点踩 - downvoted: - other: 点踩 - accept: - other: 采纳 - accepted: - other: 已采纳 - edit: - other: 编辑 - review: - queued_post: - other: 排队的帖子 - flagged_post: - other: 举报的帖子 - suggested_post_edit: - other: 建议的编辑 - reaction: - tooltip: - other: "{{ .Names }} 以及另外 {{ .Count }} 个..." -plugin: - s3_cdn: - backend: - info: - name: - other: S3 存储 CDN - description: - other: 上传文件到S3存储 - config: - endpoint: - title: - other: Endpoint - description: - other: S3存储的Endpoint - bucket_name: - title: - other: Bucket名称 - description: - other: S3存储的Bucket名称 - object_key_prefix: - title: - other: 对象Key前缀 - description: - other: 对象键的前缀,如'answer/data/',以'/'结尾 - access_key_id: - title: - other: AccessKeyID - description: - other: S3存储的AccessKeyID - access_key_secret: - title: - other: AccessKeySecret - description: - other: S3存储的AccessKeySecret - access_token: - title: - other: AccessToken - description: - other: 访问 S3 所需的 AccessToken - visit_url_prefix: - title: - other: 访问URL前缀 - description: - other: 上传静态文件CDN最终访问地址的前缀,以 '/' 结尾 https://static.example.com/xxx/ - max_file_size: - title: - other: 文件最大大小(MB) - description: - other: 限制上传文件的最大大小,单位MB,默认为10MB - region: - title: - other: 区域(Region) - description: - other: S3存储区域 - disable_ssl: - title: - other: 禁用SSL - description: - other: 我们建议您使用SSL访问S3存储。如果您想禁用SSL,请选中此选项。 - err: - mis_storage_config: - other: 错误的存储配置导致上传失败 - file_not_found: - other: 文件未找到 - unsupported_file_type: - other: 不支持的文件类型 - over_file_size_limit: - other: 超过文件大小限制 - upload_file_failed: - other: 上传文件失败 - aliyunoss_cdn: - backend: - info: - name: - other: 阿里云CDN OSS存储 - description: - other: 上传文件到阿里云CDN OSS存储 - config: - endpoint: - title: - other: Endpoint - description: - other: 阿里云OSS存储的Endpoint - bucket_name: - title: - other: Bucket名称 - description: - other: 阿里云OSS存储的Bucket名称 - object_key_prefix: - title: - other: 对象Key前缀 - description: - other: 对象键的前缀,如'answer/data/',以'/'结尾 - access_key_id: - title: - other: AccessKeyID - description: - other: 阿里云OSS存储的AccessKeyID - access_key_secret: - title: - other: AccessKeySecret - description: - other: 阿里云OSS存储的AccessKeySecret - visit_url_prefix: - title: - other: 访问URL前缀 - description: - other: CDN最终访问地址的前缀,以 '/' 结尾 https://example.com/xxx/ - max_file_size: - title: - other: 最大文件大小(MB) - description: - other: 限制上传文件的最大大小,单位为MB,默认为 10MB - err: - mis_storage_config: - other: 错误的存储配置导致上传失败 - file_not_found: - other: 文件未找到 - unsupported_file_type: - other: 不支持的文件类型 - over_file_size_limit: - other: 超过文件大小限制 - upload_file_failed: - other: 上传文件失败 -# The following fields are used for interface presentation(Front-end) -ui: - how_to_format: - title: 如何排版 - desc: >- -
  • 添加链接

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 **粗体**

  • 使用 4 个空格缩进代码

  • 在行首添加 > 表示引用

  • 反引号进行转义 `像 _这样_`

  • 使用 ``` 创建代码块

    ```
    这是代码块
    ```
- pagination: - prev: 上一页 - next: 下一页 - page_title: - question: 问题 - questions: 问题 - tag: 标签 - tags: 标签 - tag_wiki: 标签维基 - create_tag: 创建标签 - edit_tag: 编辑标签 - ask_a_question: 提问题 - edit_question: 编辑问题 - edit_answer: 编辑回答 - search: 搜索 - posts_containing: 帖子包含 - settings: 设置 - notifications: 通知 - login: 登录 - sign_up: 注册 - account_recovery: 账号恢复 - account_activation: 账号激活 - confirm_email: 确认电子邮件 - account_suspended: 账号已被封禁 - admin: 后台管理 - change_email: 修改邮箱 - install: Answer 安装 - upgrade: Answer 升级 - maintenance: 网站维护 - users: 用户 - oauth_callback: 处理中 - http_404: HTTP 错误 404 - http_50X: HTTP 错误 500 - http_403: HTTP 错误 403 - logout: 退出 - notifications: - title: 通知 - inbox: 收件箱 - achievement: 成就 - new_alerts: 新通知 - all_read: 全部标记为已读 - show_more: 显示更多 - someone: 有人 - inbox_type: - all: 全部 - posts: 帖子 - invites: 邀请 - votes: 投票 - suspended: - title: 你的账号账号已被封禁 - until_time: "你的账号被封禁直到 {{ time }}。" - forever: 你的账号已被永久封禁。 - end: 你违反了我们的社区准则。 - contact_us: 联系我们 - editor: - blockquote: - text: 引用 - bold: - text: 粗体 - chart: - text: 图表 - flow_chart: 流程图 - sequence_diagram: 时序图 - class_diagram: 类图 - state_diagram: 状态图 - entity_relationship_diagram: 实体关系图 - user_defined_diagram: 用户自定义图表 - gantt_chart: 甘特图 - pie_chart: 饼图 - code: - text: 代码块 - add_code: 添加代码块 - form: - fields: - code: - label: 代码块 - msg: - empty: 代码块不能为空 - language: - label: 语言 - placeholder: 自动识别 - btn_cancel: 取消 - btn_confirm: 添加 - formula: - text: 公式 - options: - inline: 行内公式 - block: 块级公式 - heading: - text: 标题 - options: - h1: 标题 1 - h2: 标题 2 - h3: 标题 3 - h4: 标题 4 - h5: 标题 5 - h6: 标题 6 - help: - text: 帮助 - hr: - text: 水平线 - image: - text: 图片 - add_image: 添加图片 - tab_image: 上传图片 - form_image: - fields: - file: - label: 图像文件 - btn: 选择图片 - msg: - empty: 请选择图片文件。 - only_image: 只能上传图片文件。 - max_size: 文件大小不能超过 4 MB。 - desc: - label: 描述 - tab_url: 图片地址 - form_url: - fields: - url: - label: 图片地址 - msg: - empty: 图片地址不能为空 - name: - label: 描述 - btn_cancel: 取消 - btn_confirm: 添加 - uploading: 上传中 - indent: - text: 缩进 - outdent: - text: 减少缩进 - italic: - text: 斜体 - link: - text: 超链接 - add_link: 添加超链接 - form: - fields: - url: - label: 链接 - msg: - empty: 链接不能为空。 - name: - label: 描述 - btn_cancel: 取消 - btn_confirm: 添加 - ordered_list: - text: 有序列表 - unordered_list: - text: 无序列表 - table: - text: 表格 - heading: 表头 - cell: 单元格 - close_modal: - title: 关闭原因是... - btn_cancel: 取消 - btn_submit: 提交 - remark: - empty: 不能为空。 - msg: - empty: 请选择一个原因。 - report_modal: - flag_title: 我举报这篇帖子的原因是... - close_title: 我关闭这篇帖子的原因是... - review_question_title: 审查问题 - review_answer_title: 审查回答 - review_comment_title: 审查评论 - btn_cancel: 取消 - btn_submit: 提交 - remark: - empty: 不能为空 - msg: - empty: 请选择一个原因。 - not_a_url: URL 格式不正确。 - url_not_match: URL 来源与当前网站不匹配。 - tag_modal: - title: 创建新标签 - form: - fields: - display_name: - label: 显示名称 - msg: - empty: 显示名称不能为空。 - range: 显示名称不能超过 35 个字符。 - slug_name: - label: URL 固定链接 - desc: URL 固定链接不能超过 35 个字符。 - msg: - empty: URL 固定链接不能为空。 - range: URL 固定链接不能超过 35 个字符。 - character: URL 固定链接包含非法字符。 - desc: - label: 描述 - revision: - label: 编辑历史 - edit_summary: - label: 编辑备注 - placeholder: >- - 简单描述更改原因(更正拼写、修复语法、改进格式) - btn_cancel: 取消 - btn_submit: 提交 - btn_post: 发布新标签 - tag_info: - created_at: 创建于 - edited_at: 编辑于 - history: 历史 - synonyms: - title: 同义词 - text: 以下标签将被重置到 - empty: 此标签目前没有同义词。 - btn_add: 添加同义词 - btn_edit: 编辑 - btn_save: 保存 - synonyms_text: 以下标签将被重置到 - delete: - title: 删除标签 - tip_with_posts: >- -

我们不允许 删除带有帖子的标签

请先从帖子中移除此标签。

- tip_with_synonyms: >- -

我们不允许 删除带有同义词的标签

请先从此标签中删除同义词。

- tip: 确定要删除吗? - close: 关闭 - edit_tag: - title: 编辑标签 - default_reason: 编辑标签 - default_first_reason: 添加标签 - btn_save_edits: 保存更改 - btn_cancel: 取消 - dates: - long_date: MM 月 DD 日 - long_date_with_year: "YYYY 年 MM 月 DD 日" - long_date_with_time: "YYYY 年 MM 月 DD 日 HH:mm" - now: 刚刚 - x_seconds_ago: "{{count}} 秒前" - x_minutes_ago: "{{count}} 分钟前" - x_hours_ago: "{{count}} 小时前" - hour: 小时 - day: 天 - hours: 小时 - days: 日 - reaction: - heart: 爱心 - smile: 微笑 - frown: 愁 - btn_label: 添加或删除回应。 - undo_emoji: 撤销 {{ emoji }} 回应 - react_emoji: 用 {{ emoji }} 回应 - unreact_emoji: 撤销 {{ emoji }} - comment: - btn_add_comment: 添加评论 - reply_to: 回复 - btn_reply: 回复 - btn_edit: 编辑 - btn_delete: 删除 - btn_flag: 举报 - btn_save_edits: 保存更改 - btn_cancel: 取消 - show_more: "{{count}} 条剩余评论" - tip_question: >- - 使用评论提问更多信息或者提出改进意见。避免在评论里回答问题。 - tip_answer: >- - 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 - tip_vote: 它给帖子添加了一些有用的内容 - edit_answer: - title: 编辑回答 - default_reason: 编辑回答 - default_first_reason: 添加答案 - form: - fields: - revision: - label: 编辑历史 - answer: - label: 回答内容 - feedback: - characters: 内容长度至少 6 个字符 - edit_summary: - label: 编辑摘要 - placeholder: >- - 简单描述更改原因(更正拼写、修复语法、改进格式) - btn_save_edits: 保存更改 - btn_cancel: 取消 - tags: - title: 标签 - sort_buttons: - popular: 热门 - name: 名称 - newest: 最新 - button_follow: 关注 - button_following: 已关注 - tag_label: 个问题 - search_placeholder: 通过标签名称过滤 - no_desc: 此标签无描述。 - more: 更多 - ask: - title: 新增问题 - edit_title: 编辑问题 - default_reason: 编辑问题 - default_first_reason: 新增问题 - similar_questions: 相似问题 - form: - fields: - revision: - label: 修订版本 - title: - label: 标题 - placeholder: 请详细描述你的问题,想象你在问一个人 - msg: - empty: 标题不能为空。 - range: 标题最多 150 个字符 - body: - label: 内容 - msg: - empty: 内容不能为空。 - tags: - label: 标签 - msg: - empty: 必须选择一个标签 - answer: - label: 回答内容 - msg: - empty: 回答内容不能为空 - edit_summary: - label: 编辑备注 - placeholder: >- - 简单描述更改原因(更正拼写、修复语法、改进格式) - btn_post_question: 提交问题 - btn_save_edits: 保存更改 - answer_question: 回答自己的问题 - post_question&answer: 提交问题和回答 - tag_selector: - add_btn: 添加标签 - create_btn: 创建新标签 - search_tag: 搜索标签 - hint: "描述您的问题是关于什么,至少需要一个标签。" - no_result: 没有匹配的标签 - tag_required_text: 必选标签(至少一个) - header: - nav: - question: 问题 - tag: 标签 - user: 用户 - profile: 用户主页 - setting: 账号设置 - logout: 退出 - admin: 后台管理 - review: 审查 - bookmark: 收藏夹 - moderation: 管理 - search: - placeholder: 搜索 - footer: - build_on: >- - 由 <1>Apache Answer 提供动力 - 驱动问答社区的开源软件。
用爱制造 © {{cc}}. - upload_img: - name: 更改 - loading: 加载中... - pic_auth_code: - title: 验证码 - placeholder: 输入图片中的文字 - msg: - empty: 验证码不能为空。 - inactive: - first: >- - 就差一步!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活你的账户。 - info: "如果没有收到,请检查你的垃圾邮件文件夹。" - another: >- - 我们向你的邮箱 {{mail}} 发送了另一封激活电子邮件。可能需要几分钟才能到达;请务必检查您的垃圾邮件箱。 - btn_name: 重新发送激活邮件 - change_btn_name: 更改邮箱 - msg: - empty: 不能为空。 - resend_email: - url_label: 确定要重新发送激活邮件吗? - url_text: 你也可以将上面的激活链接给该用户。 - login: - login_to_continue: 登录以继续 - info_sign: 没有账户?<1>注册 - info_login: 已经有账户?<1>登录 - agreements: 登录即表示您同意<1>隐私政策和<3>服务条款。 - forgot_pass: 忘记密码? - name: - label: 名字 - msg: - empty: 名字不能为空 - range: 名字应该在 4 到 30 个字符之间 - character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' - email: - label: 邮箱 - msg: - empty: 邮箱不能为空 - password: - label: 密码 - msg: - empty: 密码不能为空 - different: 两次输入密码不一致 - account_forgot: - page_title: 忘记密码 - btn_name: 发送恢复邮件 - send_success: >- - 如果存在邮箱为 {{mail}} 账户,你将很快收到一封重置密码的说明邮件。 - email: - label: 邮箱 - msg: - empty: 邮箱不能为空 - change_email: - btn_cancel: 取消 - btn_update: 更新电子邮件地址 - send_success: >- - 如果存在邮箱为 {{mail}} 的账户,你将很快收到一封重置密码的说明邮件。 - email: - label: 新的电子邮件地址 - msg: - empty: 邮箱不能为空。 - oauth: - connect: 连接到 {{ auth_name }} - remove: 移除 {{ auth_name }} - oauth_bind_email: - subtitle: 向你的账户添加恢复邮件地址。 - btn_update: 更新电子邮件地址 - email: - label: 邮箱 - msg: - empty: 邮箱不能为空。 - modal_title: 邮箱已经存在。 - modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? - modal_cancel: 更改邮箱 - modal_confirm: 连接到已有账户 - password_reset: - page_title: 密码重置 - btn_name: 重置我的密码 - reset_success: >- - 你已经成功更改密码;你将被重定向到登录页面。 - link_invalid: >- - 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? - to_login: 前往登录页面 - password: - label: 密码 - msg: - empty: 密码不能为空。 - length: 密码长度在8-32个字符之间 - different: 两次输入密码不一致 - password_confirm: - label: 确认新密码 - settings: - page_title: 设置 - goto_modify: 前往修改 - nav: - profile: 我的资料 - notification: 通知 - account: 账号 - interface: 界面 - profile: - heading: 个人资料 - btn_name: 保存 - display_name: - label: 显示名称 - msg: 昵称不能为空。 - msg_range: 显示名称不能超过 30 个字符。 - username: - label: 用户名 - caption: 用户可以通过 "@用户名" 来提及你。 - msg: 用户名不能为空 - msg_range: 用户名不能超过 30 个字符。 - character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成' - avatar: - label: 头像 - gravatar: Gravatar - gravatar_text: 你可以更改图像在 - custom: 自定义 - custom_text: 你可以上传你的图片。 - default: 系统 - msg: 请上传头像 - bio: - label: 关于我 - website: - label: 网站 - placeholder: "https://example.com" - msg: 网址格式不正确 - location: - label: 位置 - placeholder: "城市,国家" - notification: - heading: 邮件通知 - turn_on: 开启 - inbox: - label: 收件箱通知 - description: 你的提问有新的回答,评论,邀请回答和其他。 - all_new_question: - label: 所有新问题 - description: 获取所有新问题的通知。每周最多有50个问题。 - all_new_question_for_following_tags: - label: 所有关注标签的新问题 - description: 获取关注的标签下新问题通知。 - account: - heading: 账号 - change_email_btn: 更改邮箱 - change_pass_btn: 更改密码 - change_email_info: >- - 邮件已发送。请根据指引完成验证。 - email: - label: 电子邮件地址 - new_email: - label: 新的电子邮件地址 - msg: 新邮箱不能为空。 - pass: - label: 当前密码 - msg: 密码不能为空。 - password_title: 密码 - current_pass: - label: 当前密码 - msg: - empty: 当前密码不能为空 - length: 密码长度必须在 8 至 32 之间 - different: 两次输入的密码不匹配 - new_pass: - label: 新密码 - pass_confirm: - label: 确认新密码 - interface: - heading: 界面 - lang: - label: 界面语言 - text: 设置用户界面语言,在刷新页面后生效。 - my_logins: - title: 我的登录 - label: 使用这些账户登录或注册本网站。 - modal_title: 移除登录 - modal_content: 你确定要从账户里移除该登录? - modal_confirm_btn: 移除 - remove_success: 移除成功 - toast: - update: 更新成功 - update_password: 密码更新成功。 - flag_success: 感谢标记。 - forbidden_operate_self: 禁止对自己执行操作 - review: 您的修订将在审阅通过后显示。 - sent_success: 发送成功 - related_question: - title: 相关问题 - answers: 个回答 - invite_to_answer: - title: 受邀人 - desc: 邀请你认为可能知道答案的人。 - invite: 邀请回答 - add: 添加人员 - search: 搜索人员 - question_detail: - action: 操作 - Asked: 提问于 - asked: 提问于 - update: 修改于 - edit: 编辑于 - commented: 评论 - Views: 阅读次数 - Follow: 关注此问题 - Following: 已关注 - follow_tip: 关注此问题以接收通知 - answered: 回答于 - closed_in: 关闭于 - show_exist: 查看类似问题。 - useful: 有用的 - question_useful: 它是有用和明确的 - question_un_useful: 它不明确或没用的 - question_bookmark: 收藏该问题 - answer_useful: 这是有用的 - answer_un_useful: 它是没有用的 - answers: - title: 个回答 - score: 评分 - newest: 最新 - oldest: 最旧 - btn_accept: 采纳 - btn_accepted: 已被采纳 - write_answer: - title: 你的回答 - edit_answer: 编辑我的回答 - btn_name: 提交你的回答 - add_another_answer: 添加另一个回答 - confirm_title: 继续回答 - continue: 继续 - confirm_info: >- -

你确定要提交一个新的回答吗?

作为替代,你可以通过编辑来完善和改进之前的回答。

- empty: 回答内容不能为空。 - characters: 内容长度至少 6 个字符。 - tips: - header_1: 感谢你的回答 - li1_1: 请务必确定在 回答问题。提供详细信息并分享你的研究。 - li1_2: 用参考资料或个人经历来支持你所做的任何陈述。 - header_2: 但是 请避免... - li2_1: 请求帮助,寻求澄清,或答复其他答案。 - reopen: - confirm_btn: 重新打开 - title: 重新打开这个帖子 - content: 确定要重新打开吗? - list: - confirm_btn: 列表显示 - title: 列表中显示这个帖子 - content: 确定要列表中显示这个帖子吗? - unlist: - confirm_btn: 列表隐藏 - title: 从列表中隐藏这个帖子 - content: 确定要从列表中隐藏这个帖子吗? - pin: - title: 置顶该帖子 - content: 你确定要全局置顶吗?这个帖子将出现在所有帖子列表的顶部。 - confirm_btn: 置顶 - delete: - title: 删除 - question: >- - 我们不建议 删除有回答的帖子。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? - answer_accepted: >- -

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该帖子中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? - other: 你确定要删除? - tip_answer_deleted: 该回答已被删除 - undelete_title: 撤销删除本帖 - undelete_desc: 你确定你要撤销删除吗? - btns: - confirm: 确认 - cancel: 取消 - edit: 编辑 - save: 保存 - delete: 删除 - undelete: 撤消删除 - list: 列表显示 - unlist: 列表隐藏 - unlisted: 已隐藏 - login: 登录 - signup: 注册 - logout: 退出 - verify: 验证 - add_question: 我要提问 - approve: 批准 - reject: 拒绝 - skip: 跳过 - discard_draft: 丢弃草稿 - pinned: 已置顶 - all: 全部 - question: 问题 - answer: 回答 - comment: 评论 - refresh: 刷新 - resend: 重新发送 - deactivate: 取消激活 - active: 激活 - suspend: 封禁 - unsuspend: 解禁 - close: 关闭 - reopen: 重新打开 - ok: 确定 - light: 浅色 - dark: 深色 - system_setting: 跟随系统 - default: 默认 - reset: 重置 - tag: 标签 - post_lowercase: 帖子 - filter: 筛选 - ignore: 忽略 - submit: 提交 - normal: 正常 - closed: 已关闭 - deleted: 已删除 - pending: 等待处理 - more: 更多 - search: - title: 搜索结果 - keywords: 关键词 - options: 选项 - follow: 关注 - following: 已关注 - counts: "{{count}} 个结果" - more: 更多 - sort_btns: - relevance: 相关性 - newest: 最新的 - active: 活跃的 - score: 评分 - more: 更多 - tips: - title: 高级搜索提示 - tag: "<1>[tag] 在指定标签中搜索" - user: "<1>user:username 根据作者搜索" - answer: "<1>answers:0 搜索未回答的问题" - score: "<1>score:3 评分 3+ 的帖子" - question: "<1>is:question 搜索问题" - is_answer: "<1>is:answer 搜索回答" - empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 - share: - name: 分享 - copy: 复制链接 - via: 分享到... - copied: 已复制 - facebook: 分享到 Facebook - twitter: 分享到 Twitter - cannot_vote_for_self: 你不能给自己的帖子投票。 - modal_confirm: - title: 发生错误... - account_result: - success: 你的账号已通过验证,即将返回首页。 - link: 返回首页 - invalid: >- - 抱歉,此验证链接已失效。也许你的账号已经激活了? - confirm_new_email: 你的电子邮箱已更新 - confirm_new_email_invalid: >- - 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? - unsubscribe: - page_title: 退订 - success_title: 退订成功 - success_desc: 您已成功退订,并且将不会再收到我们的邮件。 - link: 更改设置 - question: - following_tags: 已关注的标签 - edit: 编辑 - save: 保存 - follow_tag_tip: 关注标签来筛选你的问题列表。 - hot_questions: 热门问题 - all_questions: 全部问题 - x_questions: "{{ count }} 个问题" - x_answers: "{{ count }} 个回答" - questions: 问题 - answers: 回答 - newest: 最新 - active: 活跃 - hot: 热门 - score: 评分 - unanswered: 未回答 - modified: 更新于 - answered: 回答于 - asked: 提问于 - closed: 已关闭 - follow_a_tag: 关注一个标签 - more: 更多 - personal: - overview: 概览 - answers: 回答 - answer: 回答 - questions: 问题 - question: 问题 - bookmarks: 收藏 - reputation: 声望 - comments: 评论 - votes: 得票 - newest: 最新 - score: 评分 - edit_profile: 编辑资料 - visited_x_days: "已访问 {{ count }} 天" - viewed: 浏览次数 - joined: 加入于 - last_login: 上次登录 - about_me: 关于我 - about_me_empty: "// Hello, World!" - top_answers: 高分回答 - top_questions: 高分问题 - stats: 状态 - list_empty: 没有找到相关的内容。
试试看其他选项卡? - accepted: 已采纳 - answered: 回答于 - asked: 提问于 - downvoted: 点踩 - mod_short: 版主 - mod_long: 版主 - x_reputation: 声望 - x_votes: 得票 - x_answers: 个回答 - x_questions: 个问题 - install: - title: 安装 - next: 下一步 - done: 完成 - config_yaml_error: 无法创建 config.yaml 文件。 - lang: - label: 请选择一种语言 - db_type: - label: 数据库引擎 - db_username: - label: 用户名 - placeholder: root - msg: 用户名不能为空 - db_password: - label: 密码 - placeholder: root - msg: 密码不能为空 - db_host: - label: 数据库主机 - placeholder: "db:3306" - msg: 数据库地址不能为空 - db_name: - label: 数据库名 - placeholder: 回答 - msg: 数据库名称不能为空。 - db_file: - label: 数据库文件 - placeholder: /data/answer.db - msg: 数据库文件不能为空。 - config_yaml: - title: 创建 config.yaml - label: 已创建 config.yaml 文件。 - desc: >- - 你可以手动在 <1>/var/wwww/xxx/ 目录中创建 <1>config.yaml 文件并粘贴以下文本。 - info: 完成后,点击“下一步”按钮。 - site_information: 站点信息 - admin_account: 管理员账号 - site_name: - label: 站点名称 - msg: 站点名称不能为空。 - msg_max_length: 站点名称长度不得超过 30 个字符。 - site_url: - label: 网站网址 - text: 此网站的网址。 - msg: - empty: 网址不能为空。 - incorrect: 网址格式不正确。 - max_length: 网址长度不得超过 512 个字符。 - contact_email: - label: 联系邮箱 - text: 负责本网站的主要联系人的电子邮件地址。 - msg: - empty: 联系人邮箱不能为空。 - incorrect: 联系人邮箱地址不正确。 - login_required: - label: 私有的 - switch: 需要登录 - text: 只有登录用户才能访问这个社区。 - admin_name: - label: 名字 - msg: 名字不能为空。 - character: '只能由 "a-z", "0-9", " - . _" 组成' - msg_max_length: 名字长度不能超过 30 个字符。 - admin_password: - label: 密码 - text: >- - 您需要此密码才能登录。请将其存储在一个安全的位置。 - msg: 密码不能为空。 - msg_min_length: 密码必须至少 8 个字符长。 - msg_max_length: 密码长度不能超过 32 个字符。 - admin_email: - label: 邮箱 - text: 您需要此电子邮件才能登录。 - msg: - empty: 邮箱不能为空。 - incorrect: 邮箱格式不正确。 - ready_title: 您的网站已准备好 - ready_desc: >- - 如果你想改变更多的设置,请访问 <1>管理区域;在网站菜单中找到它。 - good_luck: "玩得愉快,祝你好运!" - warn_title: 警告 - warn_desc: >- - 文件 <1>config.yaml 已存在。如果你要重置该文件中的任何配置项,请先删除它。 - install_now: 您可以尝试 <1>现在安装。 - installed: 已安裝 - installed_desc: >- - 你似乎已经安装过了。如果要重新安装,请先清除旧的数据库表。 - db_failed: 数据连接异常! - db_failed_desc: >- - 这或者意味着数据库信息在 <1>config.yaml 文件不正确,或者无法与数据库服务器建立联系。这可能意味着你的主机数据库服务器故障。 - counts: - views: 次浏览 - votes: 个点赞 - answers: 个回答 - accepted: 已被采纳 - page_error: - http_error: HTTP 错误 {{ code }} - desc_403: 您无权访问此页面。 - desc_404: 很抱歉,此页面不存在。 - desc_50X: 服务器遇到了一个错误,无法完成你的请求。 - back_home: 返回首页 - page_maintenance: - desc: "我们正在进行维护,我们将很快回来。" - nav_menus: - dashboard: 后台管理 - contents: 内容管理 - questions: 问题 - answers: 回答 - users: 用户管理 - flags: 举报管理 - settings: 站点设置 - general: 一般 - interface: 界面 - smtp: SMTP - branding: 品牌 - legal: 法律条款 - write: 撰写 - tos: 服务条款 - privacy: 隐私政策 - seo: SEO - customize: 自定义 - themes: 主题 - css_html: CSS/HTML - login: 登录 - privileges: 特权 - plugins: 插件 - installed_plugins: 已安装插件 - website_welcome: 欢迎来到 {{site_name}} - user_center: - login: 登录 - qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。 - login_failed_email_tip: 登录失败,请允许此应用访问您的邮箱信息,然后重试。 - admin: - admin_header: - title: 后台管理 - dashboard: - title: 后台管理 - welcome: 欢迎来到管理后台! - site_statistics: 站点统计 - questions: "问题:" - answers: "回答:" - comments: "评论:" - votes: "投票:" - users: "用户:" - flags: "举报:" - reviews: "审查:" - site_health: 网站健康 - version: "版本" - https: "HTTPS:" - upload_folder: "上传文件夹:" - run_mode: "运行模式:" - private: 私有 - public: 公开 - smtp: "SMTP:" - timezone: "时区:" - system_info: 系统信息 - go_version: "Go版本:" - database: "数据库:" - database_size: "数据库大小:" - storage_used: "已用存储空间:" - uptime: "运行时间:" - links: 链接 - plugins: 插件 - github: GitHub - blog: 博客 - contact: 联系 - forum: 论坛 - documents: 文档 - feedback: 用户反馈 - support: 帮助 - review: 审查 - config: 配置 - update_to: 更新到 - latest: 最新版本 - check_failed: 校验失败 - "yes": "是" - "no": "否" - not_allowed: 拒绝 - allowed: 允许 - enabled: 已启用 - disabled: 停用 - writable: 可写 - not_writable: 不可写 - flags: - title: 举报 - pending: 等待处理 - completed: 已完成 - flagged: 被举报内容 - flagged_type: 标记了 {{ type }} - created: 创建于 - action: 操作 - review: 审查 - user_role_modal: - title: 更改用户状态为... - btn_cancel: 取消 - btn_submit: 提交 - new_password_modal: - title: 设置新密码 - form: - fields: - password: - label: 密码 - text: 用户将被退出,需要再次登录。 - msg: 密码的长度必须是8-32个字符。 - btn_cancel: 取消 - btn_submit: 提交 - edit_profile_modal: - title: 编辑资料 - form: - fields: - username: - label: 用户名 - msg_range: 用户名不能超过 30 个字符。 - email: - label: 电子邮件地址 - msg_invalid: 无效的邮箱地址 - edit_success: 修改成功 - btn_cancel: 取消 - btn_submit: 提交 - user_modal: - title: 添加新用户 - form: - fields: - users: - label: 批量添加用户 - placeholder: "John Smith, john@example.com, BUSYopr2\nAlice, alice@example.com, fpDntV8q" - text: 用逗号分隔“name, email, password”,每行一个用户。 - msg: "请输入用户的邮箱,每行一个。" - display_name: - label: 显示名称 - msg: 显示名称长度必须为 4-30 个字符 - email: - label: 邮箱 - msg: 邮箱无效。 - password: - label: 密码 - msg: 密码的长度必须是8-32个字符。 - btn_cancel: 取消 - btn_submit: 提交 - users: - title: 用户 - name: 名称 - email: 邮箱 - reputation: 声望 - created_at: 创建时间 - delete_at: 删除时间 - suspend_at: 封禁时间 - status: 状态 - role: 角色 - action: 操作 - change: 更改 - all: 全部 - staff: 工作人员 - more: 更多 - inactive: 不活跃 - suspended: 已封禁 - deleted: 已删除 - normal: 正常 - Moderator: 版主 - Admin: 管理员 - User: 用户 - filter: - placeholder: "按名称筛选,用户:id" - set_new_password: 设置新密码 - edit_profile: 编辑资料 - change_status: 更改状态 - change_role: 更改角色 - show_logs: 显示日志 - add_user: 添加用户 - deactivate_user: - title: 停用用户 - content: 未激活的用户必须重新验证他们的邮箱。 - delete_user: - title: 删除此用户 - content: 确定要删除此用户?此操作无法撤销! - remove: 移除内容 - label: 删除所有问题、 答案、 评论等 - text: 如果你只想删除用户账户,请不要选中此项。 - suspend_user: - title: 挂起此用户 - content: 被封禁的用户将无法登录。 - questions: - page_title: 问题 - unlisted: 已隐藏 - post: 标题 - votes: 得票数 - answers: 回答数 - created: 创建于 - status: 状态 - action: 操作 - change: 更改 - pending: 等待处理 - filter: - placeholder: "按标题过滤,问题:id" - answers: - page_title: 回答 - post: 标题 - votes: 得票数 - created: 创建于 - status: 状态 - action: 操作 - change: 更改 - filter: - placeholder: "按标题筛选,答案:id" - general: - page_title: 一般 - name: - label: 站点名称 - msg: 不能为空 - text: "站点的名称,作为站点的标题。" - site_url: - label: 网站网址 - msg: 网站网址不能为空。 - validate: 请输入一个有效的 URL。 - text: 此网站的地址。 - short_desc: - label: 简短站点描述 - msg: 简短网站描述不能为空。 - text: "简短的标语,作为网站主页的标题(Html 的 title 标签)。" - desc: - label: 站点描述 - msg: 网站描述不能为空。 - text: "使用一句话描述本站,作为网站的描述(Html 的 meta 标签)。" - contact_email: - label: 联系邮箱 - msg: 联系人邮箱不能为空。 - validate: 联系人邮箱无效。 - text: 本网站的主要联系邮箱地址。 - check_update: - label: 软件更新 - text: 自动检查软件更新 - interface: - page_title: 界面 - language: - label: 界面语言 - msg: 不能为空 - text: 设置用户界面语言,在刷新页面后生效。 - time_zone: - label: 时区 - msg: 时区不能为空。 - text: 选择一个与您相同时区的城市。 - smtp: - page_title: SMTP - from_email: - label: 发件人邮箱 - msg: 发件人邮箱不能为空。 - text: 用于发送邮件的地址。 - from_name: - label: 发件人 - msg: 不能为空 - text: 发件人的名字。 - smtp_host: - label: SMTP 主机 - msg: 不能为空 - text: 邮件服务器 - encryption: - label: 加密 - msg: 不能为空 - text: 对于大多数服务器而言,SSL 是推荐开启的。 - ssl: SSL - tls: TLS - none: 无加密 - smtp_port: - label: SMTP 端口 - msg: SMTP 端口必须在 1 ~ 65535 之间。 - text: 邮件服务器的端口号。 - smtp_username: - label: SMTP 用户名 - msg: 不能为空 - smtp_password: - label: SMTP 密码 - msg: 不能为空 - test_email_recipient: - label: 测试收件邮箱 - text: 提供用于接收测试邮件的邮箱地址。 - msg: 测试收件邮箱无效 - smtp_authentication: - label: 启用身份验证 - title: SMTP 身份验证 - msg: 不能为空 - "yes": "是" - "no": "否" - branding: - page_title: 品牌 - logo: - label: 网站标志(Logo) - msg: 图标不能为空。 - text: 在你的网站左上方的Logo图标。使用一个高度为56,长宽比大于3:1的宽长方形图像。如果留空,将显示网站标题文本。 - mobile_logo: - label: 移动端 Logo - text: 在你的网站的移动版上使用的标志。使用一个高度为56的宽矩形图像。如果留空,将使用 "Logo"设置中的图像。 - square_icon: - label: 方形图标 - msg: 方形图标不能为空。 - text: 用作元数据图标的基础的图像。最好是大于512x512。 - favicon: - label: 收藏夹图标 - text: 网站的图标。要在 CDN 正常工作,它必须是 png。 将调整大小到32x32。如果留空,将使用“方形图标”。 - legal: - page_title: 法律条款 - terms_of_service: - label: 服务条款 - text: "您可以在此添加服务内容的条款。如果您已经在别处托管了文档,请在这里提供完整的URL。" - privacy_policy: - label: 隐私政策 - text: "您可以在此添加隐私政策内容。如果您已经在别处托管了文档,请在这里提供完整的URL。" - write: - page_title: 编辑 - restrict_answer: - title: 限制一个回答 - label: 每个用户对于每个问题只能有一个回答 - text: "用户可以使用编辑按钮优化已有的回答" - recommend_tags: - label: 推荐标签 - text: "请在上方输入标签固定链接,每行一个标签。" - required_tag: - title: 必需的标签 - label: 根据需要设置推荐标签 - text: "每个新问题必须至少有一个推荐标签。" - reserved_tags: - label: 保留标签 - text: "保留的标签只能由版主添加到一个帖子中。" - seo: - page_title: 搜索引擎优化 - permalink: - label: 固定链接 - text: 自定义URL结构可以提高可用性,以及你的链接的向前兼容性。 - robots: - label: robots.txt - text: 这将永久覆盖任何相关的网站设置。 - themes: - page_title: 主题 - themes: - label: 主题 - text: 选择一个现有主题。 - color_scheme: - label: 配色方案 - navbar_style: - label: 导航栏样式 - primary_color: - label: 主色调 - text: 修改您主题使用的颜色 - css_and_html: - page_title: CSS 与 HTML - custom_css: - label: 自定义 CSS - text: > - - head: - label: 头部 - text: > - - header: - label: 页眉 - text: > - - footer: - label: 页脚 - text: 这将在 之前插入. - sidebar: - label: 侧边栏 - text: 这将插入侧边栏中。 - login: - page_title: 登录 - membership: - title: 会员 - label: 允许新注册 - text: 关闭以防止任何人创建新账户。 - email_registration: - title: 邮箱注册 - label: 允许邮箱注册 - text: 关闭以阻止任何人通过邮箱创建新账户。 - allowed_email_domains: - title: 允许的邮箱域 - text: 允许注册账户的邮箱域。每行一个域名。留空时忽略。 - private: - title: 非公开的 - label: 需要登录 - text: 只有登录用户才能访问这个社区。 - password_login: - title: 密码登录 - label: 允许使用邮箱和密码登录 - text: "警告:如果您未配置过其他登录方式,关闭密码登录后您则可能无法登录。" - installed_plugins: - title: 已安装插件 - plugin_link: 插件扩展功能。您可以在<1>插件仓库中找到插件。 - filter: - all: 全部 - active: 已启用 - inactive: 未启用 - outdated: 已过期 - plugins: - label: 插件 - text: 选择一个现有的插件。 - name: 名称 - version: 版本 - status: 状态 - action: 操作 - deactivate: 停用 - activate: 启用 - settings: 设置 - settings_users: - title: 用户 - avatar: - label: 默认头像 - text: 没有自定义头像的用户。 - gravatar_base_url: - label: Gravatar 根路径 URL - text: Gravatar 提供商的 API 基础的 URL。当为空时忽略。 - profile_editable: - title: 个人资料可编辑 - allow_update_display_name: - label: 允许用户修改显示名称 - allow_update_username: - label: 允许用户修改用户名 - allow_update_avatar: - label: 允许用户修改个人头像 - allow_update_bio: - label: 允许用户修改个人介绍 - allow_update_website: - label: 允许用户修改个人主页网址 - allow_update_location: - label: 允许用户更改位置 - privilege: - title: 特权 - level: - label: 级别所需声望 - text: 选择特权所需的声望值 - msg: - should_be_number: 输入必须是数字 - number_larger_1: 数字应该大于等于 1 - form: - optional: (选填) - empty: 不能为空 - invalid: 是无效的 - btn_submit: 保存 - not_found_props: "所需属性 {{ key }} 未找到。" - select: 选择 - page_review: - review: 评论 - proposed: 提案 - question_edit: 问题编辑 - answer_edit: 回答编辑 - tag_edit: '标签管理: 编辑标签' - edit_summary: 编辑备注 - edit_question: 编辑问题 - edit_answer: 编辑回答 - edit_tag: 编辑标签 - empty: 没有剩余的审核任务。 - approve_revision_tip: 您是否批准此修订? - approve_flag_tip: 您是否批准此举报? - approve_post_tip: 您是否批准此帖子? - approve_user_tip: 您是否批准此修订? - suggest_edits: 建议的编辑 - flag_post: 举报帖子 - flag_user: 举报用户 - queued_post: 排队的帖子 - queued_user: 排队用户 - filter_label: 类型 - reputation: 声望值 - flag_post_type: 举报这个帖子的类型是 {{ type }} - flag_user_type: 举报这个用户的类型是 {{ type }} - edit_post: 编辑帖子 - list_post: 文章列表 - unlist_post: 隐藏的帖子 - timeline: - undeleted: 取消删除 - deleted: 删除 - downvote: 反对 - upvote: 点赞 - accept: 采纳 - cancelled: 已取消 - commented: '评论:' - rollback: 回滚 - edited: 最后编辑于 - answered: 回答于 - asked: 提问于 - closed: 关闭 - reopened: 重新开启 - created: 创建于 - pin: 已置顶 - unpin: 取消置頂 - show: 已显示 - hide: 已隐藏 - title: "历史记录" - tag_title: "时间线" - show_votes: "显示投票" - n_or_a: N/A - title_for_question: "时间线" - title_for_answer: "{{ title }} 的 {{ author }} 回答时间线" - title_for_tag: "时间线" - datetime: 日期时间 - type: 类型 - by: 由 - comment: 评论 - no_data: "空空如也" - users: - title: 用户 - users_with_the_most_reputation: 本周声望最高的用户 - users_with_the_most_vote: 本周投票最多的用户 - staffs: 我们的社区工作人员 - reputation: 声望值 - votes: 投票 - prompt: - leave_page: 确定要离开此页面? - changes_not_save: 您的更改尚未保存 - draft: - discard_confirm: 您确定要丢弃您的草稿吗? - messages: - post_deleted: 该帖子已被删除。 - post_pin: 该帖子已被置顶。 - post_unpin: 该帖子已被取消置顶。 - post_hide_list: 此帖子已经从列表中隐藏。 - post_show_list: 该帖子已显示到列表中。 - post_reopen: 这个帖子已被重新打开. - post_list: 这个帖子已经被显示 - post_unlist: 这个帖子已经被隐藏 - post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。 From 1d9f88f803cb6427c9eb7d1166436494a2314d83 Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 19 Aug 2024 16:15:21 +0800 Subject: [PATCH 061/129] fix(embed): embed plugin type definition --- docs/docs.go | 90 ++++++++++++++++++------ docs/swagger.json | 92 ++++++++++++++++++------- docs/swagger.yaml | 65 ++++++++++++----- internal/controller/embed_controller.go | 22 ++---- internal/schema/plugin_option_schema.go | 25 ------- plugin/embed.go | 8 +++ 6 files changed, 195 insertions(+), 107 deletions(-) delete mode 100644 internal/schema/plugin_option_schema.go diff --git a/docs/docs.go b/docs/docs.go index cf1264d14..fab0d8e9b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2738,7 +2738,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetEmbedOptionResp" + "$ref": "#/definitions/plugin.EmbedConfig" } } } @@ -4410,7 +4410,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetTagBasicResp" } } } @@ -5380,7 +5380,7 @@ const docTemplate = `{ }, "/answer/api/v1/tags": { "get": { - "description": "get tags list", + "description": "get tags list by slug name", "produces": [ "application/json" ], @@ -5404,7 +5404,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" + } + } + } + } + ] } } } @@ -6571,14 +6586,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", @@ -7004,6 +7019,17 @@ const docTemplate = `{ "list": {} } }, + "plugin.EmbedConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + }, + "platform": { + "type": "string" + } + } + }, "schema.AcceptAnswerReq": { "type": "object", "required": [ @@ -7813,17 +7839,6 @@ const docTemplate = `{ } } }, - "schema.GetEmbedOptionResp": { - "type": "object", - "properties": { - "enable": { - "type": "boolean" - }, - "platform": { - "type": "string" - } - } - }, "schema.GetFollowingTagsResp": { "type": "object", "properties": { @@ -8242,6 +8257,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 +9851,7 @@ const docTemplate = `{ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9828,7 +9860,7 @@ const docTemplate = `{ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9842,7 +9874,7 @@ const docTemplate = `{ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9851,7 +9883,7 @@ const docTemplate = `{ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9859,6 +9891,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..9ea2d2ee4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2708,7 +2708,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetEmbedOptionResp" + "$ref": "#/definitions/plugin.EmbedConfig" } } } @@ -4380,7 +4380,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetTagBasicResp" } } } @@ -5350,7 +5350,7 @@ }, "/answer/api/v1/tags": { "get": { - "description": "get tags list", + "description": "get tags list by slug name", "produces": [ "application/json" ], @@ -5374,7 +5374,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" + } + } + } + } + ] } } } @@ -6541,14 +6556,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", @@ -6974,6 +6989,17 @@ "list": {} } }, + "plugin.EmbedConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + }, + "platform": { + "type": "string" + } + } + }, "schema.AcceptAnswerReq": { "type": "object", "required": [ @@ -7783,17 +7809,6 @@ } } }, - "schema.GetEmbedOptionResp": { - "type": "object", - "properties": { - "enable": { - "type": "boolean" - }, - "platform": { - "type": "string" - } - } - }, "schema.GetFollowingTagsResp": { "type": "object", "properties": { @@ -8212,6 +8227,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 +8841,7 @@ "enum": [ "newest", "active", - "frequent", + "hot", "score", "unanswered" ] @@ -9789,7 +9821,7 @@ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9798,7 +9830,7 @@ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9812,7 +9844,7 @@ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9821,7 +9853,7 @@ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9829,6 +9861,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..1b176960d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -117,6 +117,13 @@ definitions: type: integer list: {} type: object + plugin.EmbedConfig: + properties: + enable: + type: boolean + platform: + type: string + type: object schema.AcceptAnswerReq: properties: answer_id: @@ -687,13 +694,6 @@ definitions: description: website type: string type: object - schema.GetEmbedOptionResp: - properties: - enable: - type: boolean - platform: - type: string - type: object schema.GetFollowingTagsResp: properties: display_name: @@ -983,6 +983,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 +1412,7 @@ definitions: enum: - newest - active - - frequent + - hot - score - unanswered type: string @@ -2071,13 +2082,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 +2097,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: @@ -4353,7 +4373,7 @@ paths: - properties: data: items: - $ref: '#/definitions/schema.GetEmbedOptionResp' + $ref: '#/definitions/plugin.EmbedConfig' type: array type: object summary: GetEmbedConfig @@ -5382,7 +5402,7 @@ paths: - properties: data: items: - $ref: '#/definitions/schema.GetTagResp' + $ref: '#/definitions/schema.GetTagBasicResp' type: array type: object security: @@ -5965,7 +5985,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 +6000,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 @@ -6677,15 +6704,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/embed_controller.go b/internal/controller/embed_controller.go index c9691d299..a61bd68cf 100644 --- a/internal/controller/embed_controller.go +++ b/internal/controller/embed_controller.go @@ -21,7 +21,6 @@ package controller import ( "github.com/apache/incubator-answer/internal/base/handler" - "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/plugin" "github.com/gin-gonic/gin" ) @@ -40,27 +39,14 @@ func NewEmbedController() *EmbedController { // @Accept json // @Produce json // @Router /answer/api/v1/embed/config [get] -// @Success 200 {object} handler.RespBody{data=[]schema.GetEmbedOptionResp} +// @Success 200 {object} handler.RespBody{data=[]plugin.EmbedConfig} func (c *EmbedController) GetEmbedConfig(ctx *gin.Context) { - resp := make([]*schema.GetEmbedOptionResp, 0) - var slugName string + resp := make([]*plugin.EmbedConfig, 0) - _ = plugin.CallEmbed(func(base plugin.Embed) error { - slugName = base.Info().SlugName + _ = plugin.CallEmbed(func(embed plugin.Embed) (err error) { + resp, err = embed.GetEmbedConfigs(ctx) return nil }) - _ = plugin.CallConfig(func(fn plugin.Config) error { - if fn.Info().SlugName == slugName { - for _, field := range fn.ConfigFields() { - resp = append(resp, &schema.GetEmbedOptionResp{ - Platform: field.Name, - Enable: field.Value.(bool), - }) - } - return nil - } - return nil - }) handler.HandleResponse(ctx, nil, resp) } diff --git a/internal/schema/plugin_option_schema.go b/internal/schema/plugin_option_schema.go deleted file mode 100644 index 8db229738..000000000 --- a/internal/schema/plugin_option_schema.go +++ /dev/null @@ -1,25 +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 - -type GetEmbedOptionResp struct { - Platform string `json:"platform"` - Enable bool `json:"enable"` -} diff --git a/plugin/embed.go b/plugin/embed.go index 55149f664..e853c8c27 100644 --- a/plugin/embed.go +++ b/plugin/embed.go @@ -19,8 +19,16 @@ package plugin +import "github.com/gin-gonic/gin" + +type EmbedConfig struct { + Platform string `json:"platform"` + Enable bool `json:"enable"` +} + type Embed interface { Base + GetEmbedConfigs(ctx *gin.Context) (embedConfigs []*EmbedConfig, err error) } var ( From 3d8cf0ef418f57524d4b1403c91453059e307fb7 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 5 Aug 2024 15:00:54 +0800 Subject: [PATCH 062/129] feat: bages ui --- i18n/en_US.yaml | 10 + ui/package.json | 5 +- ui/pnpm-lock.yaml | 1020 ++++++++++++++++- ui/src/components/CardBadge/index.scss | 36 + ui/src/components/CardBadge/index.tsx | 51 + ui/src/components/SideNav/index.tsx | 5 + ui/src/components/index.ts | 2 + ui/src/pages/Badges/Detail/index.tsx | 97 ++ ui/src/pages/Badges/index.tsx | 45 + .../Users/Personal/components/Alert/index.tsx | 23 +- .../Personal/components/Badges/index.tsx | 43 + .../Personal/components/NavBar/index.tsx | 5 + .../Personal/components/Overview/index.tsx | 36 +- .../Personal/components/TopList/index.tsx | 56 +- .../pages/Users/Personal/components/index.ts | 2 + ui/src/pages/Users/Personal/index.tsx | 70 +- ui/src/router/routes.ts | 8 + 17 files changed, 1393 insertions(+), 121 deletions(-) create mode 100644 ui/src/components/CardBadge/index.scss create mode 100644 ui/src/components/CardBadge/index.tsx create mode 100644 ui/src/pages/Badges/Detail/index.tsx create mode 100644 ui/src/pages/Badges/index.tsx create mode 100644 ui/src/pages/Users/Personal/components/Badges/index.tsx diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 4de696589..f9787eda3 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1155,6 +1155,7 @@ ui: question: Questions tag: Tags user: Users + badges: Badges profile: Profile setting: Settings logout: Log out @@ -1572,12 +1573,14 @@ ui: reputation: Reputation comments: Comments votes: Votes + badges: Badges newest: Newest score: Score edit_profile: Edit profile visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined + comma: ',' last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" @@ -1733,6 +1736,13 @@ ui: login: Login qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. + badges: + title: Badges + awarded: Awarded + earned_x: Earned x{{ number }} + x_awarded: "{{ number }} awarded" + can_earn_multiple: You can earn this multiple times. + admin: admin_header: diff --git a/ui/package.json b/ui/package.json index 8a087428c..0ae727e23 100644 --- a/ui/package.json +++ b/ui/package.json @@ -45,7 +45,8 @@ "react-router-dom": "^6.22.3", "semver": "^7.3.8", "swr": "^1.3.0", - "zustand": "^4.1.1" + "zustand": "^4.1.1", + "demo": "workspace:*" }, "devDependencies": { "@commitlint/cli": "^17.0.3", @@ -98,4 +99,4 @@ "pnpm": ">=8" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 77bbee2d4..50fdae8f5 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: dayjs: specifier: ^1.11.5 version: 1.11.5 + demo: + specifier: workspace:* + version: link:src/plugins/demo diff: specifier: ^5.1.0 version: 5.1.0 @@ -229,6 +232,52 @@ importers: specifier: ^0.8.0 version: 0.8.0 + src/plugins/demo: + dependencies: + react: + specifier: ^18.2.0 + version: 18.2.0 + react-bootstrap: + specifier: ^2.10.0 + version: 2.10.0(@types/react@18.0.20)(react-dom@18.2.0)(react@18.2.0) + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-i18next: + specifier: ^11.18.3 + version: 11.18.6(i18next@21.9.2)(react-dom@18.2.0)(react@18.2.0) + devDependencies: + '@modyfi/vite-plugin-yaml': + specifier: ^1.1.0 + version: 1.1.0(rollup@2.79.0)(vite@4.5.3) + '@typescript-eslint/eslint-plugin': + specifier: ^6.0.0 + version: 6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.5.4) + '@typescript-eslint/parser': + specifier: ^6.0.0 + version: 6.11.0(eslint@8.53.0)(typescript@5.5.4) + '@vitejs/plugin-react-swc': + specifier: ^3.3.2 + version: 3.7.0(vite@4.5.3) + eslint: + specifier: ^8.45.0 + version: 8.53.0 + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.0(eslint@8.53.0) + eslint-plugin-react-refresh: + specifier: ^0.4.3 + version: 0.4.9(eslint@8.53.0) + typescript: + specifier: ^5.0.2 + version: 5.5.4 + vite: + specifier: ^4.4.5 + version: 4.5.3(@types/node@16.11.59)(sass@1.54.9) + vite-plugin-dts: + specifier: ^3.9.1 + version: 3.9.1(@types/node@16.11.59)(rollup@2.79.0)(typescript@5.5.4)(vite@4.5.3) + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -549,6 +598,14 @@ packages: dependencies: '@babel/types': 7.19.0 + /@babel/parser@7.25.0: + resolution: {integrity: sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.6 + dev: true + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.19.1): resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} engines: {node: '>=6.9.0'} @@ -2191,6 +2248,204 @@ packages: postcss: 8.4.16 postcss-selector-parser: 6.0.10 + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2495,14 +2750,14 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 /@jridgewell/gen-mapping@0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.17 /@jridgewell/gen-mapping@0.3.3: @@ -2510,7 +2765,7 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.22 dev: true @@ -2545,6 +2800,9 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} dev: true /@jridgewell/trace-mapping@0.3.15: @@ -2563,14 +2821,14 @@ packages: resolution: {integrity: sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==} dependencies: '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 dev: true /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/sourcemap-codec': 1.4.15 /@leichtgewicht/ip-codec@2.0.4: resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} @@ -2702,6 +2960,63 @@ packages: '@lezer/lr': 1.4.0 dev: false + /@microsoft/api-extractor-model@7.28.13(@types/node@16.11.59): + resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==} + dependencies: + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 4.0.2(@types/node@16.11.59) + transitivePeerDependencies: + - '@types/node' + dev: true + + /@microsoft/api-extractor@7.43.0(@types/node@16.11.59): + resolution: {integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==} + hasBin: true + dependencies: + '@microsoft/api-extractor-model': 7.28.13(@types/node@16.11.59) + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 4.0.2(@types/node@16.11.59) + '@rushstack/rig-package': 0.5.2 + '@rushstack/terminal': 0.10.0(@types/node@16.11.59) + '@rushstack/ts-command-line': 4.19.1(@types/node@16.11.59) + lodash: 4.17.21 + minimatch: 3.0.4 + resolve: 1.22.1 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.4.2 + transitivePeerDependencies: + - '@types/node' + dev: true + + /@microsoft/tsdoc-config@0.16.2: + resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} + dependencies: + '@microsoft/tsdoc': 0.14.2 + ajv: 6.12.6 + jju: 1.4.0 + resolve: 1.19.0 + dev: true + + /@microsoft/tsdoc@0.14.2: + resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} + dev: true + + /@modyfi/vite-plugin-yaml@1.1.0(rollup@2.79.0)(vite@4.5.3): + resolution: {integrity: sha512-L26xfzkSo1yamODCAtk/ipVlL6OEw2bcJ92zunyHu8zxi7+meV0zefA9xscRMDCsMY8xL3C3wi3DhMiPxcbxbw==} + peerDependencies: + vite: ^3.2.7 || ^4.0.5 || ^5.0.5 + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@2.79.0) + js-yaml: 4.1.0 + tosource: 2.0.0-alpha.3 + vite: 4.5.3(@types/node@16.11.59)(sass@1.54.9) + transitivePeerDependencies: + - rollup + dev: true + /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} dependencies: @@ -2873,9 +3188,72 @@ packages: picomatch: 2.3.1 rollup: 2.79.0 + /@rollup/pluginutils@5.1.0(rollup@2.79.0): + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 2.79.0 + dev: true + /@rushstack/eslint-patch@1.2.0: resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} + /@rushstack/node-core-library@4.0.2(@types/node@16.11.59): + resolution: {integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + dependencies: + '@types/node': 16.11.59 + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.1 + semver: 7.5.4 + z-schema: 5.0.5 + dev: true + + /@rushstack/rig-package@0.5.2: + resolution: {integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==} + dependencies: + resolve: 1.22.1 + strip-json-comments: 3.1.1 + dev: true + + /@rushstack/terminal@0.10.0(@types/node@16.11.59): + resolution: {integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + dependencies: + '@rushstack/node-core-library': 4.0.2(@types/node@16.11.59) + '@types/node': 16.11.59 + supports-color: 8.1.1 + dev: true + + /@rushstack/ts-command-line@4.19.1(@types/node@16.11.59): + resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==} + dependencies: + '@rushstack/terminal': 0.10.0(@types/node@16.11.59) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + dev: true + /@sinclair/typebox@0.24.42: resolution: {integrity: sha512-d+2AtrHGyWek2u2ITF0lHRIv6Tt7X0dEHW+0rP+5aDCEjC3fiN2RBjrLD0yU0at52BcZbRGxLbAtXiR0hFCjYw==} @@ -2992,12 +3370,141 @@ packages: transitivePeerDependencies: - supports-color + /@swc/core-darwin-arm64@1.7.3: + resolution: {integrity: sha512-CTkHa6MJdov9t41vuV2kmQIMu+Q19LrEHGIR/UiJYH06SC/sOu35ZZH8DyfLp9ZoaCn21gwgWd61ixOGQlwzTw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-darwin-x64@1.7.3: + resolution: {integrity: sha512-mun623y6rCoZ2EFIYfIRqXYRFufJOopoYSJcxYhZUrfTpAvQ1zLngjQpWCUU1krggXR2U0PQj+ls0DfXUTraNg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm-gnueabihf@1.7.3: + resolution: {integrity: sha512-4Jz4UcIcvZNMp9qoHbBx35bo3rjt8hpYLPqnR4FFq6gkAsJIMFC56UhRZwdEQoDuYiOFMBnnrsg31Fyo6YQypA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-gnu@1.7.3: + resolution: {integrity: sha512-p+U/M/oqV7HC4erQ5TVWHhJU1984QD+wQBPxslAYq751bOQGm0R/mXK42GjugqjnR6yYrAiwKKbpq4iWVXNePA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-musl@1.7.3: + resolution: {integrity: sha512-s6VzyaJwaRGTi2mz2h6Ywxfmgpkc69IxhuMzl+sl34plH0V0RgnZDm14HoCGIKIzRk4+a2EcBV1ZLAfWmPACQg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-gnu@1.7.3: + resolution: {integrity: sha512-IrFY48C356Z2dU2pjYg080yvMXzmSV3Lmm/Wna4cfcB1nkVLjWsuYwwRAk9CY7E19c+q8N1sMNggubAUDYoX2g==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-musl@1.7.3: + resolution: {integrity: sha512-qoLgxBlBnnyUEDu5vmRQqX90h9jldU1JXI96e6eh2d1gJyKRA0oSK7xXmTzorv1fGHiHulv9qiJOUG+g6uzJWg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-arm64-msvc@1.7.3: + resolution: {integrity: sha512-OAd7jVVJ7nb0Ev80VAa1aeK+FldPeC4eZ35H4Qn6EICzIz0iqJo2T33qLKkSZiZEBKSoF4KcwrqYfkjLOp5qWg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-ia32-msvc@1.7.3: + resolution: {integrity: sha512-31+Le1NyfSnILFV9+AhxfFOG0DK0272MNhbIlbcv4w/iqpjkhaOnNQnLsYJD1Ow7lTX1MtIZzTjOhRlzSviRWg==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-x64-msvc@1.7.3: + resolution: {integrity: sha512-jVQPbYrwcuueI4QB0fHC29SVrkFOBcfIspYDlgSoHnEz6tmLMqUy+txZUypY/ZH/KaK0HEY74JkzgbRC1S6LFQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core@1.7.3: + resolution: {integrity: sha512-HHAlbXjWI6Kl9JmmUW1LSygT1YbblXgj2UvvDzMkTBPRzYMhW6xchxdO8HbtMPtFYRt/EQq9u1z7j4ttRSrFsA==} + engines: {node: '>=10'} + requiresBuild: true + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.12 + optionalDependencies: + '@swc/core-darwin-arm64': 1.7.3 + '@swc/core-darwin-x64': 1.7.3 + '@swc/core-linux-arm-gnueabihf': 1.7.3 + '@swc/core-linux-arm64-gnu': 1.7.3 + '@swc/core-linux-arm64-musl': 1.7.3 + '@swc/core-linux-x64-gnu': 1.7.3 + '@swc/core-linux-x64-musl': 1.7.3 + '@swc/core-win32-arm64-msvc': 1.7.3 + '@swc/core-win32-ia32-msvc': 1.7.3 + '@swc/core-win32-x64-msvc': 1.7.3 + dev: true + + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: true + /@swc/helpers@0.5.3: resolution: {integrity: sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==} dependencies: tslib: 2.6.2 dev: false + /@swc/types@0.1.12: + resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==} + dependencies: + '@swc/counter': 0.1.3 + dev: true + /@testing-library/dom@8.18.1: resolution: {integrity: sha512-oEvsm2B/WtcHKE+IcEeeCqNU/ltFGaVyGbpcm4g/2ytuT49jrlH9x5qRKL/H3A6yfM4YAbSbC0ceT5+9CEXnLg==} engines: {node: '>=12'} @@ -3071,6 +3578,10 @@ packages: /@tsconfig/node16@1.0.3: resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} + /@types/argparse@1.0.38: + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + dev: true + /@types/aria-query@4.2.2: resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} dev: true @@ -3148,7 +3659,7 @@ packages: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: '@types/eslint': 8.4.6 - '@types/estree': 0.0.51 + '@types/estree': 1.0.5 /@types/eslint-scope@3.7.7: resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -3160,7 +3671,7 @@ packages: /@types/eslint@8.4.6: resolution: {integrity: sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==} dependencies: - '@types/estree': 1.0.0 + '@types/estree': 1.0.5 '@types/json-schema': 7.0.15 /@types/eslint@8.56.2: @@ -3176,12 +3687,8 @@ packages: /@types/estree@0.0.51: resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} - /@types/estree@1.0.0: - resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} - /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - dev: true /@types/express-serve-static-core@4.17.31: resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==} @@ -3397,7 +3904,36 @@ packages: transitivePeerDependencies: - supports-color - /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@4.9.5): + /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@4.9.5): + resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@4.9.5) + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@4.9.5) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@4.9.5) + '@typescript-eslint/visitor-keys': 6.11.0 + debug: 4.3.4 + eslint: 8.53.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.5.4): resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -3409,10 +3945,10 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@4.9.5) + '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.5.4) '@typescript-eslint/scope-manager': 6.11.0 - '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@4.9.5) - '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@4.9.5) + '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.5.4) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.5.4) '@typescript-eslint/visitor-keys': 6.11.0 debug: 4.3.4 eslint: 8.53.0 @@ -3420,8 +3956,8 @@ packages: ignore: 5.2.4 natural-compare: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@4.9.5) - typescript: 4.9.5 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true @@ -3478,6 +4014,27 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.5.4): + resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 6.11.0 + debug: 4.3.4 + eslint: 8.53.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/scope-manager@5.38.0: resolution: {integrity: sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3532,6 +4089,26 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.5.4): + resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.5.4) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.5.4) + debug: 4.3.4 + eslint: 8.53.0 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/types@5.38.0: resolution: {integrity: sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3582,6 +4159,27 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.11.0(typescript@5.5.4): + resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/visitor-keys': 6.11.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils@5.38.0(eslint@8.53.0)(typescript@4.9.5): resolution: {integrity: sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3618,6 +4216,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.5.4): + resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.5 + '@typescript-eslint/scope-manager': 6.11.0 + '@typescript-eslint/types': 6.11.0 + '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.5.4) + eslint: 8.53.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@5.38.0: resolution: {integrity: sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3636,6 +4253,77 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + /@vitejs/plugin-react-swc@3.7.0(vite@4.5.3): + resolution: {integrity: sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==} + peerDependencies: + vite: ^4 || ^5 + dependencies: + '@swc/core': 1.7.3 + vite: 4.5.3(@types/node@16.11.59)(sass@1.54.9) + transitivePeerDependencies: + - '@swc/helpers' + dev: true + + /@volar/language-core@1.11.1: + resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} + dependencies: + '@volar/source-map': 1.11.1 + dev: true + + /@volar/source-map@1.11.1: + resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} + dependencies: + muggle-string: 0.3.1 + dev: true + + /@volar/typescript@1.11.1: + resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} + dependencies: + '@volar/language-core': 1.11.1 + path-browserify: 1.0.1 + dev: true + + /@vue/compiler-core@3.4.34: + resolution: {integrity: sha512-Z0izUf32+wAnQewjHu+pQf1yw00EGOmevl1kE+ljjjMe7oEfpQ+BI3/JNK7yMB4IrUsqLDmPecUrpj3mCP+yJQ==} + dependencies: + '@babel/parser': 7.25.0 + '@vue/shared': 3.4.34 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.0 + dev: true + + /@vue/compiler-dom@3.4.34: + resolution: {integrity: sha512-3PUOTS1h5cskdOJMExCu2TInXuM0j60DRPpSCJDqOCupCfUZCJoyQmKtRmA8EgDNZ5kcEE7vketamRZfrEuVDw==} + dependencies: + '@vue/compiler-core': 3.4.34 + '@vue/shared': 3.4.34 + dev: true + + /@vue/language-core@1.8.27(typescript@5.5.4): + resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@volar/language-core': 1.11.1 + '@volar/source-map': 1.11.1 + '@vue/compiler-dom': 3.4.34 + '@vue/shared': 3.4.34 + computeds: 0.0.1 + minimatch: 9.0.5 + muggle-string: 0.3.1 + path-browserify: 1.0.1 + typescript: 5.5.4 + vue-template-compiler: 2.7.16 + dev: true + + /@vue/shared@3.4.34: + resolution: {integrity: sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==} + dev: true + /@webassemblyjs/ast@1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} dependencies: @@ -3878,12 +4566,12 @@ packages: acorn: 8.11.3 dev: true - /acorn-jsx@5.3.2(acorn@8.10.0): + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.10.0 + acorn: 8.11.3 /acorn-node@1.8.2: resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} @@ -3905,11 +4593,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - /acorn@8.10.0: - resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} - engines: {node: '>=0.4.0'} - hasBin: true - /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} @@ -4802,6 +5485,13 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + requiresBuild: true + dev: true + optional: true + /common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} @@ -4839,6 +5529,10 @@ packages: transitivePeerDependencies: - supports-color + /computeds@0.0.1: + resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + dev: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -5227,6 +5921,10 @@ packages: resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==} dev: false + /de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + dev: true + /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -5596,6 +6294,11 @@ packages: /entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==, tarball: https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz} + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -5727,6 +6430,36 @@ packages: is-date-object: 1.0.5 is-symbol: 1.0.4 + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -6139,6 +6872,14 @@ packages: dependencies: eslint: 8.53.0 + /eslint-plugin-react-refresh@0.4.9(eslint@8.53.0): + resolution: {integrity: sha512-QK49YrBAo5CLNLseZ7sZgvgTy21E6NEw22eZqc4teZfH8pxV3yXc9XXOYfUI6JNpw7mfHNkAeWtBxrTyykB6HA==} + peerDependencies: + eslint: '>=7' + dependencies: + eslint: 8.53.0 + dev: true + /eslint-plugin-react@7.33.2(eslint@8.53.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} @@ -6283,8 +7024,8 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.10.0 - acorn-jsx: 5.3.2(acorn@8.10.0) + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) eslint-visitor-keys: 3.4.3 /esprima@4.0.1: @@ -6315,6 +7056,10 @@ packages: /estree-walker@1.0.1: resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -6633,6 +7378,15 @@ packages: jsonfile: 6.1.0 universalify: 2.0.0 + /fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + /fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -7099,6 +7853,11 @@ packages: parent-module: 1.0.1 resolve-from: 4.0.0 + /import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + dev: true + /import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} @@ -8101,6 +8860,12 @@ packages: engines: {node: '>=6'} hasBin: true + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -8153,6 +8918,10 @@ packages: resolution: {integrity: sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==} engines: {node: '>= 8'} + /kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + dev: true + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} @@ -8271,6 +9040,14 @@ packages: resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==} dev: true + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: true + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: true + /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -8324,6 +9101,12 @@ packages: dependencies: sourcemap-codec: 1.4.8 + /magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + dev: true + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -8465,6 +9248,13 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -8492,6 +9282,10 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /muggle-string@0.3.1: + resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} + dev: true + /multicast-dns@7.2.5: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true @@ -8504,6 +9298,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -8844,6 +9644,10 @@ packages: no-case: 3.0.4 tslib: 2.6.2 + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: true + /path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -8884,6 +9688,10 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + dev: true + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -9621,6 +10429,15 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postcss@8.4.40: + resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + dev: true + /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -10327,6 +11144,13 @@ packages: resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} engines: {node: '>=10'} + /resolve@1.19.0: + resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} + dependencies: + is-core-module: 2.10.0 + path-parse: 1.0.7 + dev: true + /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -10394,6 +11218,14 @@ packages: optionalDependencies: fsevents: 2.3.3 + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + /run-applescript@5.0.0: resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} engines: {node: '>=12'} @@ -10736,6 +11568,11 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: true + /source-map-loader@3.0.1(webpack@5.74.0): resolution: {integrity: sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==} engines: {node: '>= 12.13.0'} @@ -11324,6 +12161,11 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + /tosource@2.0.0-alpha.3: + resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} + engines: {node: '>=10'} + dev: true + /tough-cookie@4.1.2: resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} engines: {node: '>=6'} @@ -11361,6 +12203,15 @@ packages: typescript: 4.9.5 dev: true + /ts-api-utils@1.0.3(typescript@5.5.4): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.5.4 + dev: true + /ts-node@10.9.1(@types/node@16.11.59)(typescript@4.9.5): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -11513,6 +12364,18 @@ packages: engines: {node: '>=4.2.0'} hasBin: true + /typescript@5.4.2: + resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -11566,6 +12429,11 @@ packages: dependencies: crypto-random-string: 2.0.0 + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -11598,7 +12466,7 @@ packages: dependencies: browserslist: 4.22.2 escalade: 3.1.1 - picocolors: 1.0.0 + picocolors: 1.0.1 dev: true /update-browserslist-db@1.0.9(browserslist@4.21.4): @@ -11675,15 +12543,101 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + dev: true + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + /vite-plugin-dts@3.9.1(@types/node@16.11.59)(rollup@2.79.0)(typescript@5.5.4)(vite@4.5.3): + resolution: {integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + dependencies: + '@microsoft/api-extractor': 7.43.0(@types/node@16.11.59) + '@rollup/pluginutils': 5.1.0(rollup@2.79.0) + '@vue/language-core': 1.8.27(typescript@5.5.4) + debug: 4.3.4 + kolorist: 1.8.0 + magic-string: 0.30.11 + typescript: 5.5.4 + vite: 4.5.3(@types/node@16.11.59)(sass@1.54.9) + vue-tsc: 1.8.27(typescript@5.5.4) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + dev: true + + /vite@4.5.3(@types/node@16.11.59)(sass@1.54.9): + resolution: {integrity: sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 16.11.59 + esbuild: 0.18.20 + postcss: 8.4.40 + rollup: 3.29.4 + sass: 1.54.9 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} dev: false + /vue-template-compiler@2.7.16: + resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + dev: true + + /vue-tsc@1.8.27(typescript@5.5.4): + resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} + hasBin: true + peerDependencies: + typescript: '*' + dependencies: + '@volar/typescript': 1.11.1 + '@vue/language-core': 1.8.27(typescript@5.5.4) + semver: 7.5.4 + typescript: 5.5.4 + dev: true + /w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} dependencies: @@ -12320,6 +13274,18 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + /z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.12.0 + optionalDependencies: + commander: 9.5.0 + dev: true + /zustand@4.1.1(react@18.2.0): resolution: {integrity: sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==} engines: {node: '>=12.7.0'} diff --git a/ui/src/components/CardBadge/index.scss b/ui/src/components/CardBadge/index.scss new file mode 100644 index 000000000..30c30cb1a --- /dev/null +++ b/ui/src/components/CardBadge/index.scss @@ -0,0 +1,36 @@ +/* + * 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. + */ + +.badge-card { + width: 194px; + margin: 12px; + + .label { + position: absolute; + top: 1rem; + right: 1rem; + } +} + + +@media screen and (max-width: 768px) { + .badge-card { + width: 163.5px; + } +} diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx new file mode 100644 index 000000000..b8dde5029 --- /dev/null +++ b/ui/src/components/CardBadge/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 { useTranslation } from 'react-i18next'; +import { FC } from 'react'; +import { Card, Badge } from 'react-bootstrap'; + +import { formatCount } from '@/utils'; + +import './index.scss'; + +interface IProps { + data: any; + badgePill: boolean; +} + +const Index: FC = ({ data, badgePill }) => { + const { t } = useTranslation('translation', { keyPrefix: 'badges' }); + console.log(data); + return ( + + + + 0 + + +
Nice Question
+
+ {t('x_awarded', { number: formatCount(16) })} +
+
+
+ ); +}; + +export default Index; diff --git a/ui/src/components/SideNav/index.tsx b/ui/src/components/SideNav/index.tsx index 6c53fe597..327d7b38e 100644 --- a/ui/src/components/SideNav/index.tsx +++ b/ui/src/components/SideNav/index.tsx @@ -73,6 +73,11 @@ const Index: FC = () => { {t('header.nav.user')} + + + {t('header.nav.badges')} + + {can_revision || userInfo?.role_id === 2 ? ( <>
diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index fdf052d39..44dc15160 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -60,6 +60,7 @@ import ImgViewer from './ImgViewer'; import SideNav from './SideNav'; import PluginRender from './PluginRender'; import HighlightText from './HighlightText'; +import CardBadge from './CardBadge'; export { Avatar, @@ -107,5 +108,6 @@ export { SideNav, PluginRender, HighlightText, + CardBadge, }; export type { EditorRef, JSONSchema, UISchema }; diff --git a/ui/src/pages/Badges/Detail/index.tsx b/ui/src/pages/Badges/Detail/index.tsx new file mode 100644 index 000000000..7a192a13a --- /dev/null +++ b/ui/src/pages/Badges/Detail/index.tsx @@ -0,0 +1,97 @@ +/* + * 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 { Card, Badge, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; + +import { Avatar, FormatTime } from '@/components'; +import { usePageTags } from '@/hooks'; +import { formatCount } from '@/utils'; + +const Index = () => { + const { t } = useTranslation('translation', { keyPrefix: 'badges' }); + + usePageTags({ + title: t('title'), + }); + + return ( +
+

{t('title')}

+ + + +
+
Support Expert
+
+ This badge is granted for achieving the expert level of our + community support programme. This certifies that the recipient has + demonstrated a high level of skills and abilities to manage and + support multiple communities/instances. +
+
{t('can_earn_multiple')}
+
+ + {t('x_awarded', { number: formatCount(16) })} + + + {t('earned_x', { number: 2 })} + +
+
+
+
+ + {[0, 1, 2, 3, 4, 5, 6].map((item) => { + return ( + + +
+ + + +
+ + username + +
+ 980 {t('x_reputation', { keyPrefix: 'personal' })} +
+
+
+ + How to `go test` all tests in my project? + + + ); + })} +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Badges/index.tsx b/ui/src/pages/Badges/index.tsx new file mode 100644 index 000000000..84784c53e --- /dev/null +++ b/ui/src/pages/Badges/index.tsx @@ -0,0 +1,45 @@ +/* + * 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 { useTranslation } from 'react-i18next'; + +import { CardBadge } from '@/components'; +import { usePageTags } from '@/hooks'; + +const Index = () => { + const { t } = useTranslation('translation', { keyPrefix: 'badges' }); + + usePageTags({ + title: t('title'), + }); + + return ( +
+

{t('title')}

+
Community Badges
+
+ {[0, 1, 2, 3, 4, 5, 6].map((item) => { + return ; + })} +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Users/Personal/components/Alert/index.tsx b/ui/src/pages/Users/Personal/components/Alert/index.tsx index 65fd157b0..8bde01f28 100644 --- a/ui/src/pages/Users/Personal/components/Alert/index.tsx +++ b/ui/src/pages/Users/Personal/components/Alert/index.tsx @@ -18,7 +18,7 @@ */ import { memo, FC, useState } from 'react'; -import { Alert, Col } from 'react-bootstrap'; +import { Alert } from 'react-bootstrap'; interface Props { data; @@ -27,17 +27,16 @@ const Index: FC = ({ data }) => { const [show, setShow] = useState(Boolean(data)); return ( - - { - setShow(false); - }}> -
- - + { + setShow(false); + }}> +
+ ); }; diff --git a/ui/src/pages/Users/Personal/components/Badges/index.tsx b/ui/src/pages/Users/Personal/components/Badges/index.tsx new file mode 100644 index 000000000..a7bd3e58a --- /dev/null +++ b/ui/src/pages/Users/Personal/components/Badges/index.tsx @@ -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. + */ + +import { FC } from 'react'; + +import { CardBadge } from '@/components'; + +interface IProps { + data: any[]; + visible: boolean; +} + +const Index: FC = ({ data, visible }) => { + console.log(data); + if (!visible) { + return null; + } + return ( +
+ {[0, 1, 2, 3, 4, 5, 6].map((item) => { + return ; + })} +
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Users/Personal/components/NavBar/index.tsx b/ui/src/pages/Users/Personal/components/NavBar/index.tsx index d201a579d..5f4360ed8 100644 --- a/ui/src/pages/Users/Personal/components/NavBar/index.tsx +++ b/ui/src/pages/Users/Personal/components/NavBar/index.tsx @@ -59,6 +59,11 @@ const list = [ path: '/votes', name: 'votes', }, + { + role: 'self', // Only visible to author + path: '/badges', + name: 'badges', + }, ]; const Index: FC = ({ slug, tabName = 'overview', isSelf }) => { const { t } = useTranslation('translation', { keyPrefix: 'personal' }); diff --git a/ui/src/pages/Users/Personal/components/Overview/index.tsx b/ui/src/pages/Users/Personal/components/Overview/index.tsx index 3d5c7360c..4f2280618 100644 --- a/ui/src/pages/Users/Personal/components/Overview/index.tsx +++ b/ui/src/pages/Users/Personal/components/Overview/index.tsx @@ -19,6 +19,7 @@ import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { Row, Col } from 'react-bootstrap'; import TopList from '../TopList'; @@ -37,26 +38,31 @@ const Index: FC = ({ visible, introduction, data }) => {
{t('about_me')}
{introduction ? (
) : ( -
{t('about_me_empty')}
+
{t('about_me_empty')}
)} - {data?.answer?.length > 0 && ( - <> -
{t('top_answers')}
- - - )} - - {data?.question?.length > 0 && ( - <> -
{t('top_questions')}
- - - )} + + + {data?.answer?.length > 0 && ( + <> +
{t('top_answers')}
+ + + )} + + + {data?.question?.length > 0 && ( + <> +
{t('top_questions')}
+ + + )} + +
); }; diff --git a/ui/src/pages/Users/Personal/components/TopList/index.tsx b/ui/src/pages/Users/Personal/components/TopList/index.tsx index ad9044fa6..045534288 100644 --- a/ui/src/pages/Users/Personal/components/TopList/index.tsx +++ b/ui/src/pages/Users/Personal/components/TopList/index.tsx @@ -31,14 +31,13 @@ interface Props { const Index: FC = ({ data, type }) => { const { t } = useTranslation('translation', { keyPrefix: 'personal' }); return ( -
    - {data?.map((item) => { +
      + {data?.map((item, index) => { return (
    1. = ({ data, type }) => { {type === 'answer' ? item.question_info.title : item.title} -
      +
      {item.vote_count} {t('votes', { keyPrefix: 'counts' })} -
      - {type === 'question' && ( -
      0 ? 'text-success' : '' - }`}> - {Number(item.accepted_answer_id) > 0 ? ( - - ) : ( - - )} - - {' '} - {item.answer_count} {t('answers', { keyPrefix: 'counts' })} - -
      - )} + {type === 'question' && ( +
      0 ? 'text-success' : '' + }`}> + {Number(item.accepted_answer_id) > 0 ? ( + + ) : ( + + )} + + + {' '} + {item.answer_count} {t('answers', { keyPrefix: 'counts' })} + +
      + )} - {type === 'answer' && item.accepted === 2 && ( -
      - - {t('accepted')} -
      - )} + {type === 'answer' && item.accepted === 2 && ( +
      + + {t('accepted')} +
      + )} +
    2. ); })} diff --git a/ui/src/pages/Users/Personal/components/index.ts b/ui/src/pages/Users/Personal/components/index.ts index c2c3ec79b..0a9baab1f 100644 --- a/ui/src/pages/Users/Personal/components/index.ts +++ b/ui/src/pages/Users/Personal/components/index.ts @@ -28,6 +28,7 @@ import Reputation from './Reputation'; import Comments from './Comments'; import Votes from './Votes'; import Answers from './Answers'; +import Badges from './Badges'; export { Alert, @@ -41,4 +42,5 @@ export { Comments, Votes, Answers, + Badges, }; diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index 2d205a42a..96704b858 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -43,6 +43,7 @@ import { Comments, Answers, Votes, + Badges, } from './components'; const Personal: FC = () => { @@ -79,36 +80,30 @@ const Personal: FC = () => { return (
      - {userInfo?.status !== 'normal' && userInfo?.status_msg && ( - - )} - - - - - {isSelf && ( -
      - - {t('edit_profile')} - -
      + + {userInfo?.status !== 'normal' && userInfo?.status_msg && ( + )} - -
      - - - +
      + + {isSelf && ( +
      + + {t('edit_profile')} + +
      + )} +
      + + + { + {!list?.length && !isLoading && } {count > 0 && ( @@ -135,20 +131,20 @@ const Personal: FC = () => { />
      )} - - -
      {t('stats')}
      - {userInfo?.created_at && ( + + {tabName === 'overview' && ( <> -
      - -
      -
      - -
      +
      {t('stats')}
      + {userInfo?.created_at && ( +
      + + {t('comma')}{' '} + +
      + )} )} diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index bd4a0c796..aa31266eb 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -217,6 +217,14 @@ const routes: RouteNode[] = [ path: 'review', page: 'pages/Review', }, + { + path: '/badges', + page: 'pages/Badges/index', + }, + { + path: '/badges/:badge_id', + page: 'pages/Badges/Detail/index', + }, ], }, { From 6973a0045e8e7c5e66667449f2969e5d5378d256 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 6 Aug 2024 10:50:58 +0800 Subject: [PATCH 063/129] fix: personal add recent bages ui --- i18n/en_US.yaml | 1 + ui/src/components/CardBadge/index.tsx | 11 +++++++---- .../Users/Personal/components/Overview/index.tsx | 12 +++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index f9787eda3..09596b783 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1598,6 +1598,7 @@ ui: x_votes: votes received x_answers: answers x_questions: questions + recent_badges: Recent Badges install: title: Installation next: Next diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx index b8dde5029..9251b38ce 100644 --- a/ui/src/components/CardBadge/index.tsx +++ b/ui/src/components/CardBadge/index.tsx @@ -27,9 +27,10 @@ import './index.scss'; interface IProps { data: any; badgePill: boolean; + showAwardedCount?: boolean; } -const Index: FC = ({ data, badgePill }) => { +const Index: FC = ({ data, badgePill, showAwardedCount = false }) => { const { t } = useTranslation('translation', { keyPrefix: 'badges' }); console.log(data); return ( @@ -40,9 +41,11 @@ const Index: FC = ({ data, badgePill }) => {
      Nice Question
      -
      - {t('x_awarded', { number: formatCount(16) })} -
      + {showAwardedCount && ( +
      + {t('x_awarded', { number: formatCount(16) })} +
      + )} ); diff --git a/ui/src/pages/Users/Personal/components/Overview/index.tsx b/ui/src/pages/Users/Personal/components/Overview/index.tsx index 4f2280618..384689dcb 100644 --- a/ui/src/pages/Users/Personal/components/Overview/index.tsx +++ b/ui/src/pages/Users/Personal/components/Overview/index.tsx @@ -21,6 +21,7 @@ import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Row, Col } from 'react-bootstrap'; +import { CardBadge } from '@/components'; import TopList from '../TopList'; interface Props { @@ -54,7 +55,7 @@ const Index: FC = ({ visible, introduction, data }) => { )} - + {data?.question?.length > 0 && ( <>
      {t('top_questions')}
      @@ -63,6 +64,15 @@ const Index: FC = ({ visible, introduction, data }) => { )} + +
      +
      {t('recent_badges')}
      +
      + {[0, 1, 2, 3, 4, 5, 6].map((item) => { + return ; + })} +
      +
); }; From 8534079687e6257fed2eb3dae07fa116b1a8c590 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 6 Aug 2024 15:56:30 +0800 Subject: [PATCH 064/129] fix: replace share params shareUserId to share --- ui/src/components/Share/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/Share/index.tsx b/ui/src/components/Share/index.tsx index 203a72d6d..392d1fbdd 100644 --- a/ui/src/components/Share/index.tsx +++ b/ui/src/components/Share/index.tsx @@ -46,7 +46,7 @@ const Index: FC = ({ type, qid, aid, title }) => { ? `${BASE_ORIGIN}/questions/${qid}` : `${BASE_ORIGIN}/questions/${qid}/${aid}`; if (user.id) { - baseUrl = `${baseUrl}?shareUserId=${user.username}`; + baseUrl = `${baseUrl}?share=${user.username}`; } const closeShare = () => { From 3f1f31581f6bb0b9165b607a04ab95234dd09bdd Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 8 Aug 2024 18:28:37 +0800 Subject: [PATCH 065/129] fix: badge list and badge detail docking api --- ui/package.json | 3 +- ui/pnpm-lock.yaml | 988 +----------------- ui/src/common/interface.ts | 32 + ui/src/components/CardBadge/index.tsx | 46 +- ui/src/index.scss | 10 + .../Badges/Detail/components/Badge/index.tsx | 93 ++ .../Detail/components/HeaderLoader/index.tsx | 49 + .../Badges/Detail/components/Loader/index.tsx | 55 + ui/src/pages/Badges/Detail/index.tsx | 131 ++- ui/src/pages/Badges/index.tsx | 21 +- .../Personal/components/Badges/index.tsx | 7 +- .../Personal/components/Overview/index.tsx | 7 +- ui/src/services/client/badges.ts | 64 ++ ui/src/services/client/index.ts | 1 + 14 files changed, 448 insertions(+), 1059 deletions(-) create mode 100644 ui/src/pages/Badges/Detail/components/Badge/index.tsx create mode 100644 ui/src/pages/Badges/Detail/components/HeaderLoader/index.tsx create mode 100644 ui/src/pages/Badges/Detail/components/Loader/index.tsx create mode 100644 ui/src/services/client/badges.ts diff --git a/ui/package.json b/ui/package.json index 0ae727e23..2344125da 100644 --- a/ui/package.json +++ b/ui/package.json @@ -45,8 +45,7 @@ "react-router-dom": "^6.22.3", "semver": "^7.3.8", "swr": "^1.3.0", - "zustand": "^4.1.1", - "demo": "workspace:*" + "zustand": "^4.1.1" }, "devDependencies": { "@commitlint/cli": "^17.0.3", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 50fdae8f5..0fd9c0f1a 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -44,9 +44,6 @@ importers: dayjs: specifier: ^1.11.5 version: 1.11.5 - demo: - specifier: workspace:* - version: link:src/plugins/demo diff: specifier: ^5.1.0 version: 5.1.0 @@ -232,52 +229,6 @@ importers: specifier: ^0.8.0 version: 0.8.0 - src/plugins/demo: - dependencies: - react: - specifier: ^18.2.0 - version: 18.2.0 - react-bootstrap: - specifier: ^2.10.0 - version: 2.10.0(@types/react@18.0.20)(react-dom@18.2.0)(react@18.2.0) - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) - react-i18next: - specifier: ^11.18.3 - version: 11.18.6(i18next@21.9.2)(react-dom@18.2.0)(react@18.2.0) - devDependencies: - '@modyfi/vite-plugin-yaml': - specifier: ^1.1.0 - version: 1.1.0(rollup@2.79.0)(vite@4.5.3) - '@typescript-eslint/eslint-plugin': - specifier: ^6.0.0 - version: 6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.5.4) - '@typescript-eslint/parser': - specifier: ^6.0.0 - version: 6.11.0(eslint@8.53.0)(typescript@5.5.4) - '@vitejs/plugin-react-swc': - specifier: ^3.3.2 - version: 3.7.0(vite@4.5.3) - eslint: - specifier: ^8.45.0 - version: 8.53.0 - eslint-plugin-react-hooks: - specifier: ^4.6.0 - version: 4.6.0(eslint@8.53.0) - eslint-plugin-react-refresh: - specifier: ^0.4.3 - version: 0.4.9(eslint@8.53.0) - typescript: - specifier: ^5.0.2 - version: 5.5.4 - vite: - specifier: ^4.4.5 - version: 4.5.3(@types/node@16.11.59)(sass@1.54.9) - vite-plugin-dts: - specifier: ^3.9.1 - version: 3.9.1(@types/node@16.11.59)(rollup@2.79.0)(typescript@5.5.4)(vite@4.5.3) - packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -598,14 +549,6 @@ packages: dependencies: '@babel/types': 7.19.0 - /@babel/parser@7.25.0: - resolution: {integrity: sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.23.6 - dev: true - /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.19.1): resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} engines: {node: '>=6.9.0'} @@ -2248,204 +2191,6 @@ packages: postcss: 8.4.16 postcss-selector-parser: 6.0.10 - /@esbuild/android-arm64@0.18.20: - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.18.20: - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.18.20: - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.18.20: - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.18.20: - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.18.20: - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.18.20: - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.18.20: - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.18.20: - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.18.20: - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.18.20: - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.18.20: - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.18.20: - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.18.20: - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.18.20: - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.18.20: - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.18.20: - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.18.20: - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.18.20: - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.18.20: - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.18.20: - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.18.20: - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2960,63 +2705,6 @@ packages: '@lezer/lr': 1.4.0 dev: false - /@microsoft/api-extractor-model@7.28.13(@types/node@16.11.59): - resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==} - dependencies: - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@16.11.59) - transitivePeerDependencies: - - '@types/node' - dev: true - - /@microsoft/api-extractor@7.43.0(@types/node@16.11.59): - resolution: {integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==} - hasBin: true - dependencies: - '@microsoft/api-extractor-model': 7.28.13(@types/node@16.11.59) - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.0.2(@types/node@16.11.59) - '@rushstack/rig-package': 0.5.2 - '@rushstack/terminal': 0.10.0(@types/node@16.11.59) - '@rushstack/ts-command-line': 4.19.1(@types/node@16.11.59) - lodash: 4.17.21 - minimatch: 3.0.4 - resolve: 1.22.1 - semver: 7.5.4 - source-map: 0.6.1 - typescript: 5.4.2 - transitivePeerDependencies: - - '@types/node' - dev: true - - /@microsoft/tsdoc-config@0.16.2: - resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} - dependencies: - '@microsoft/tsdoc': 0.14.2 - ajv: 6.12.6 - jju: 1.4.0 - resolve: 1.19.0 - dev: true - - /@microsoft/tsdoc@0.14.2: - resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} - dev: true - - /@modyfi/vite-plugin-yaml@1.1.0(rollup@2.79.0)(vite@4.5.3): - resolution: {integrity: sha512-L26xfzkSo1yamODCAtk/ipVlL6OEw2bcJ92zunyHu8zxi7+meV0zefA9xscRMDCsMY8xL3C3wi3DhMiPxcbxbw==} - peerDependencies: - vite: ^3.2.7 || ^4.0.5 || ^5.0.5 - dependencies: - '@rollup/pluginutils': 5.1.0(rollup@2.79.0) - js-yaml: 4.1.0 - tosource: 2.0.0-alpha.3 - vite: 4.5.3(@types/node@16.11.59)(sass@1.54.9) - transitivePeerDependencies: - - rollup - dev: true - /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} dependencies: @@ -3188,72 +2876,9 @@ packages: picomatch: 2.3.1 rollup: 2.79.0 - /@rollup/pluginutils@5.1.0(rollup@2.79.0): - resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - dependencies: - '@types/estree': 1.0.5 - estree-walker: 2.0.2 - picomatch: 2.3.1 - rollup: 2.79.0 - dev: true - /@rushstack/eslint-patch@1.2.0: resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} - /@rushstack/node-core-library@4.0.2(@types/node@16.11.59): - resolution: {integrity: sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - dependencies: - '@types/node': 16.11.59 - fs-extra: 7.0.1 - import-lazy: 4.0.0 - jju: 1.4.0 - resolve: 1.22.1 - semver: 7.5.4 - z-schema: 5.0.5 - dev: true - - /@rushstack/rig-package@0.5.2: - resolution: {integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==} - dependencies: - resolve: 1.22.1 - strip-json-comments: 3.1.1 - dev: true - - /@rushstack/terminal@0.10.0(@types/node@16.11.59): - resolution: {integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - dependencies: - '@rushstack/node-core-library': 4.0.2(@types/node@16.11.59) - '@types/node': 16.11.59 - supports-color: 8.1.1 - dev: true - - /@rushstack/ts-command-line@4.19.1(@types/node@16.11.59): - resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==} - dependencies: - '@rushstack/terminal': 0.10.0(@types/node@16.11.59) - '@types/argparse': 1.0.38 - argparse: 1.0.10 - string-argv: 0.3.2 - transitivePeerDependencies: - - '@types/node' - dev: true - /@sinclair/typebox@0.24.42: resolution: {integrity: sha512-d+2AtrHGyWek2u2ITF0lHRIv6Tt7X0dEHW+0rP+5aDCEjC3fiN2RBjrLD0yU0at52BcZbRGxLbAtXiR0hFCjYw==} @@ -3370,141 +2995,12 @@ packages: transitivePeerDependencies: - supports-color - /@swc/core-darwin-arm64@1.7.3: - resolution: {integrity: sha512-CTkHa6MJdov9t41vuV2kmQIMu+Q19LrEHGIR/UiJYH06SC/sOu35ZZH8DyfLp9ZoaCn21gwgWd61ixOGQlwzTw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@swc/core-darwin-x64@1.7.3: - resolution: {integrity: sha512-mun623y6rCoZ2EFIYfIRqXYRFufJOopoYSJcxYhZUrfTpAvQ1zLngjQpWCUU1krggXR2U0PQj+ls0DfXUTraNg==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@swc/core-linux-arm-gnueabihf@1.7.3: - resolution: {integrity: sha512-4Jz4UcIcvZNMp9qoHbBx35bo3rjt8hpYLPqnR4FFq6gkAsJIMFC56UhRZwdEQoDuYiOFMBnnrsg31Fyo6YQypA==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@swc/core-linux-arm64-gnu@1.7.3: - resolution: {integrity: sha512-p+U/M/oqV7HC4erQ5TVWHhJU1984QD+wQBPxslAYq751bOQGm0R/mXK42GjugqjnR6yYrAiwKKbpq4iWVXNePA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - requiresBuild: true - dev: true - optional: true - - /@swc/core-linux-arm64-musl@1.7.3: - resolution: {integrity: sha512-s6VzyaJwaRGTi2mz2h6Ywxfmgpkc69IxhuMzl+sl34plH0V0RgnZDm14HoCGIKIzRk4+a2EcBV1ZLAfWmPACQg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [musl] - requiresBuild: true - dev: true - optional: true - - /@swc/core-linux-x64-gnu@1.7.3: - resolution: {integrity: sha512-IrFY48C356Z2dU2pjYg080yvMXzmSV3Lmm/Wna4cfcB1nkVLjWsuYwwRAk9CY7E19c+q8N1sMNggubAUDYoX2g==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [glibc] - requiresBuild: true - dev: true - optional: true - - /@swc/core-linux-x64-musl@1.7.3: - resolution: {integrity: sha512-qoLgxBlBnnyUEDu5vmRQqX90h9jldU1JXI96e6eh2d1gJyKRA0oSK7xXmTzorv1fGHiHulv9qiJOUG+g6uzJWg==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [musl] - requiresBuild: true - dev: true - optional: true - - /@swc/core-win32-arm64-msvc@1.7.3: - resolution: {integrity: sha512-OAd7jVVJ7nb0Ev80VAa1aeK+FldPeC4eZ35H4Qn6EICzIz0iqJo2T33qLKkSZiZEBKSoF4KcwrqYfkjLOp5qWg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@swc/core-win32-ia32-msvc@1.7.3: - resolution: {integrity: sha512-31+Le1NyfSnILFV9+AhxfFOG0DK0272MNhbIlbcv4w/iqpjkhaOnNQnLsYJD1Ow7lTX1MtIZzTjOhRlzSviRWg==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@swc/core-win32-x64-msvc@1.7.3: - resolution: {integrity: sha512-jVQPbYrwcuueI4QB0fHC29SVrkFOBcfIspYDlgSoHnEz6tmLMqUy+txZUypY/ZH/KaK0HEY74JkzgbRC1S6LFQ==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@swc/core@1.7.3: - resolution: {integrity: sha512-HHAlbXjWI6Kl9JmmUW1LSygT1YbblXgj2UvvDzMkTBPRzYMhW6xchxdO8HbtMPtFYRt/EQq9u1z7j4ttRSrFsA==} - engines: {node: '>=10'} - requiresBuild: true - peerDependencies: - '@swc/helpers': '*' - peerDependenciesMeta: - '@swc/helpers': - optional: true - dependencies: - '@swc/counter': 0.1.3 - '@swc/types': 0.1.12 - optionalDependencies: - '@swc/core-darwin-arm64': 1.7.3 - '@swc/core-darwin-x64': 1.7.3 - '@swc/core-linux-arm-gnueabihf': 1.7.3 - '@swc/core-linux-arm64-gnu': 1.7.3 - '@swc/core-linux-arm64-musl': 1.7.3 - '@swc/core-linux-x64-gnu': 1.7.3 - '@swc/core-linux-x64-musl': 1.7.3 - '@swc/core-win32-arm64-msvc': 1.7.3 - '@swc/core-win32-ia32-msvc': 1.7.3 - '@swc/core-win32-x64-msvc': 1.7.3 - dev: true - - /@swc/counter@0.1.3: - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} - dev: true - /@swc/helpers@0.5.3: resolution: {integrity: sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==} dependencies: tslib: 2.6.2 dev: false - /@swc/types@0.1.12: - resolution: {integrity: sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==} - dependencies: - '@swc/counter': 0.1.3 - dev: true - /@testing-library/dom@8.18.1: resolution: {integrity: sha512-oEvsm2B/WtcHKE+IcEeeCqNU/ltFGaVyGbpcm4g/2ytuT49jrlH9x5qRKL/H3A6yfM4YAbSbC0ceT5+9CEXnLg==} engines: {node: '>=12'} @@ -3578,10 +3074,6 @@ packages: /@tsconfig/node16@1.0.3: resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} - /@types/argparse@1.0.38: - resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} - dev: true - /@types/aria-query@4.2.2: resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} dev: true @@ -3892,48 +3384,19 @@ packages: dependencies: '@typescript-eslint/parser': 5.38.0(eslint@8.53.0)(typescript@4.9.5) '@typescript-eslint/scope-manager': 5.38.0 - '@typescript-eslint/type-utils': 5.38.0(eslint@8.53.0)(typescript@4.9.5) - '@typescript-eslint/utils': 5.38.0(eslint@8.53.0)(typescript@4.9.5) - debug: 4.3.4 - eslint: 8.53.0 - ignore: 5.2.4 - regexpp: 3.2.0 - semver: 7.5.4 - tsutils: 3.21.0(typescript@4.9.5) - typescript: 4.9.5 - transitivePeerDependencies: - - supports-color - - /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@4.9.5): - resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@4.9.5) - '@typescript-eslint/scope-manager': 6.11.0 - '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@4.9.5) - '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@4.9.5) - '@typescript-eslint/visitor-keys': 6.11.0 + '@typescript-eslint/type-utils': 5.38.0(eslint@8.53.0)(typescript@4.9.5) + '@typescript-eslint/utils': 5.38.0(eslint@8.53.0)(typescript@4.9.5) debug: 4.3.4 eslint: 8.53.0 - graphemer: 1.4.0 ignore: 5.2.4 - natural-compare: 1.4.0 + regexpp: 3.2.0 semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@4.9.5) + tsutils: 3.21.0(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@5.5.4): + /@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0)(eslint@8.53.0)(typescript@4.9.5): resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -3945,10 +3408,10 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@4.9.5) '@typescript-eslint/scope-manager': 6.11.0 - '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.5.4) - '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.5.4) + '@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@4.9.5) + '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@4.9.5) '@typescript-eslint/visitor-keys': 6.11.0 debug: 4.3.4 eslint: 8.53.0 @@ -3956,8 +3419,8 @@ packages: ignore: 5.2.4 natural-compare: 1.4.0 semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.5.4) - typescript: 5.5.4 + ts-api-utils: 1.0.3(typescript@4.9.5) + typescript: 4.9.5 transitivePeerDependencies: - supports-color dev: true @@ -4014,27 +3477,6 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.5.4): - resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 6.11.0 - '@typescript-eslint/types': 6.11.0 - '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4 - eslint: 8.53.0 - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/scope-manager@5.38.0: resolution: {integrity: sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4089,26 +3531,6 @@ packages: - supports-color dev: true - /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.5.4): - resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.5.4) - '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.5.4) - debug: 4.3.4 - eslint: 8.53.0 - ts-api-utils: 1.0.3(typescript@5.5.4) - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/types@5.38.0: resolution: {integrity: sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4159,27 +3581,6 @@ packages: - supports-color dev: true - /@typescript-eslint/typescript-estree@6.11.0(typescript@5.5.4): - resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 6.11.0 - '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - ts-api-utils: 1.0.3(typescript@5.5.4) - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - dev: true - /@typescript-eslint/utils@5.38.0(eslint@8.53.0)(typescript@4.9.5): resolution: {integrity: sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4216,25 +3617,6 @@ packages: - typescript dev: true - /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.5.4): - resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.5 - '@typescript-eslint/scope-manager': 6.11.0 - '@typescript-eslint/types': 6.11.0 - '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.5.4) - eslint: 8.53.0 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - /@typescript-eslint/visitor-keys@5.38.0: resolution: {integrity: sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4253,77 +3635,6 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - /@vitejs/plugin-react-swc@3.7.0(vite@4.5.3): - resolution: {integrity: sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==} - peerDependencies: - vite: ^4 || ^5 - dependencies: - '@swc/core': 1.7.3 - vite: 4.5.3(@types/node@16.11.59)(sass@1.54.9) - transitivePeerDependencies: - - '@swc/helpers' - dev: true - - /@volar/language-core@1.11.1: - resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==} - dependencies: - '@volar/source-map': 1.11.1 - dev: true - - /@volar/source-map@1.11.1: - resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==} - dependencies: - muggle-string: 0.3.1 - dev: true - - /@volar/typescript@1.11.1: - resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==} - dependencies: - '@volar/language-core': 1.11.1 - path-browserify: 1.0.1 - dev: true - - /@vue/compiler-core@3.4.34: - resolution: {integrity: sha512-Z0izUf32+wAnQewjHu+pQf1yw00EGOmevl1kE+ljjjMe7oEfpQ+BI3/JNK7yMB4IrUsqLDmPecUrpj3mCP+yJQ==} - dependencies: - '@babel/parser': 7.25.0 - '@vue/shared': 3.4.34 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.0 - dev: true - - /@vue/compiler-dom@3.4.34: - resolution: {integrity: sha512-3PUOTS1h5cskdOJMExCu2TInXuM0j60DRPpSCJDqOCupCfUZCJoyQmKtRmA8EgDNZ5kcEE7vketamRZfrEuVDw==} - dependencies: - '@vue/compiler-core': 3.4.34 - '@vue/shared': 3.4.34 - dev: true - - /@vue/language-core@1.8.27(typescript@5.5.4): - resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@volar/language-core': 1.11.1 - '@volar/source-map': 1.11.1 - '@vue/compiler-dom': 3.4.34 - '@vue/shared': 3.4.34 - computeds: 0.0.1 - minimatch: 9.0.5 - muggle-string: 0.3.1 - path-browserify: 1.0.1 - typescript: 5.5.4 - vue-template-compiler: 2.7.16 - dev: true - - /@vue/shared@3.4.34: - resolution: {integrity: sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==} - dev: true - /@webassemblyjs/ast@1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} dependencies: @@ -5485,13 +4796,6 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} - /commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - requiresBuild: true - dev: true - optional: true - /common-path-prefix@3.0.0: resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} @@ -5529,10 +4833,6 @@ packages: transitivePeerDependencies: - supports-color - /computeds@0.0.1: - resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} - dev: true - /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -5921,10 +5221,6 @@ packages: resolution: {integrity: sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==} dev: false - /de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - dev: true - /debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -6294,11 +5590,6 @@ packages: /entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==, tarball: https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz} - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: true - /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -6430,36 +5721,6 @@ packages: is-date-object: 1.0.5 is-symbol: 1.0.4 - /esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - dev: true - /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -6872,14 +6133,6 @@ packages: dependencies: eslint: 8.53.0 - /eslint-plugin-react-refresh@0.4.9(eslint@8.53.0): - resolution: {integrity: sha512-QK49YrBAo5CLNLseZ7sZgvgTy21E6NEw22eZqc4teZfH8pxV3yXc9XXOYfUI6JNpw7mfHNkAeWtBxrTyykB6HA==} - peerDependencies: - eslint: '>=7' - dependencies: - eslint: 8.53.0 - dev: true - /eslint-plugin-react@7.33.2(eslint@8.53.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} @@ -7056,10 +6309,6 @@ packages: /estree-walker@1.0.1: resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} - /estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true - /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -7378,15 +6627,6 @@ packages: jsonfile: 6.1.0 universalify: 2.0.0 - /fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - dev: true - /fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -7853,11 +7093,6 @@ packages: parent-module: 1.0.1 resolve-from: 4.0.0 - /import-lazy@4.0.0: - resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} - engines: {node: '>=8'} - dev: true - /import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} @@ -8860,12 +8095,6 @@ packages: engines: {node: '>=6'} hasBin: true - /jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - optionalDependencies: - graceful-fs: 4.2.11 - dev: true - /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -8918,10 +8147,6 @@ packages: resolution: {integrity: sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==} engines: {node: '>= 8'} - /kolorist@1.8.0: - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - dev: true - /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} @@ -9040,14 +8265,6 @@ packages: resolution: {integrity: sha512-ff3BX/tSioo+XojX4MOsOMhJw0nZoUEF011LX8g8d3gvjVbxd89cCio4BCXronjxcTUIJUoqKEUA+n4CqvvRPw==} dev: true - /lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: true - - /lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - dev: true - /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -9101,12 +8318,6 @@ packages: dependencies: sourcemap-codec: 1.4.8 - /magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - dev: true - /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -9248,13 +8459,6 @@ packages: dependencies: brace-expansion: 2.0.1 - /minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - brace-expansion: 2.0.1 - dev: true - /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -9282,10 +8486,6 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - /muggle-string@0.3.1: - resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} - dev: true - /multicast-dns@7.2.5: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true @@ -9298,12 +8498,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -9644,10 +8838,6 @@ packages: no-case: 3.0.4 tslib: 2.6.2 - /path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - dev: true - /path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -10429,15 +9619,6 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 - /postcss@8.4.40: - resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 - dev: true - /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -11144,13 +10325,6 @@ packages: resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==} engines: {node: '>=10'} - /resolve@1.19.0: - resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} - dependencies: - is-core-module: 2.10.0 - path-parse: 1.0.7 - dev: true - /resolve@1.22.1: resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} hasBin: true @@ -11218,14 +10392,6 @@ packages: optionalDependencies: fsevents: 2.3.3 - /rollup@3.29.4: - resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true - optionalDependencies: - fsevents: 2.3.3 - dev: true - /run-applescript@5.0.0: resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} engines: {node: '>=12'} @@ -11568,11 +10734,6 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - /source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - dev: true - /source-map-loader@3.0.1(webpack@5.74.0): resolution: {integrity: sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==} engines: {node: '>= 12.13.0'} @@ -12161,11 +11322,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - /tosource@2.0.0-alpha.3: - resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} - engines: {node: '>=10'} - dev: true - /tough-cookie@4.1.2: resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} engines: {node: '>=6'} @@ -12203,15 +11359,6 @@ packages: typescript: 4.9.5 dev: true - /ts-api-utils@1.0.3(typescript@5.5.4): - resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} - engines: {node: '>=16.13.0'} - peerDependencies: - typescript: '>=4.2.0' - dependencies: - typescript: 5.5.4 - dev: true - /ts-node@10.9.1(@types/node@16.11.59)(typescript@4.9.5): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -12364,18 +11511,6 @@ packages: engines: {node: '>=4.2.0'} hasBin: true - /typescript@5.4.2: - resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - - /typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -12429,11 +11564,6 @@ packages: dependencies: crypto-random-string: 2.0.0 - /universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - dev: true - /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -12543,101 +11673,15 @@ packages: spdx-expression-parse: 3.0.1 dev: true - /validator@13.12.0: - resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} - engines: {node: '>= 0.10'} - dev: true - /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - /vite-plugin-dts@3.9.1(@types/node@16.11.59)(rollup@2.79.0)(typescript@5.5.4)(vite@4.5.3): - resolution: {integrity: sha512-rVp2KM9Ue22NGWB8dNtWEr+KekN3rIgz1tWD050QnRGlriUCmaDwa7qA5zDEjbXg5lAXhYMSBJtx3q3hQIJZSg==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - typescript: '*' - vite: '*' - peerDependenciesMeta: - vite: - optional: true - dependencies: - '@microsoft/api-extractor': 7.43.0(@types/node@16.11.59) - '@rollup/pluginutils': 5.1.0(rollup@2.79.0) - '@vue/language-core': 1.8.27(typescript@5.5.4) - debug: 4.3.4 - kolorist: 1.8.0 - magic-string: 0.30.11 - typescript: 5.5.4 - vite: 4.5.3(@types/node@16.11.59)(sass@1.54.9) - vue-tsc: 1.8.27(typescript@5.5.4) - transitivePeerDependencies: - - '@types/node' - - rollup - - supports-color - dev: true - - /vite@4.5.3(@types/node@16.11.59)(sass@1.54.9): - resolution: {integrity: sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 16.11.59 - esbuild: 0.18.20 - postcss: 8.4.40 - rollup: 3.29.4 - sass: 1.54.9 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} dev: false - /vue-template-compiler@2.7.16: - resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} - dependencies: - de-indent: 1.0.2 - he: 1.2.0 - dev: true - - /vue-tsc@1.8.27(typescript@5.5.4): - resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==} - hasBin: true - peerDependencies: - typescript: '*' - dependencies: - '@volar/typescript': 1.11.1 - '@vue/language-core': 1.8.27(typescript@5.5.4) - semver: 7.5.4 - typescript: 5.5.4 - dev: true - /w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} dependencies: @@ -13274,18 +12318,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - /z-schema@5.0.5: - resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} - engines: {node: '>=8.0.0'} - hasBin: true - dependencies: - lodash.get: 4.4.2 - lodash.isequal: 4.5.0 - validator: 13.12.0 - optionalDependencies: - commander: 9.5.0 - dev: true - /zustand@4.1.1(react@18.2.0): resolution: {integrity: sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==} engines: {node: '>=12.7.0'} diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 41dc0fef0..789109d2f 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -735,3 +735,35 @@ export interface ReactionItem { tooltip: string; is_active: boolean; } + +export interface BadgeListItem { + id: string; + name: string; + icon: string; + award_count: number; + earned: boolean; + /** 1: bronze 2: silver 3:gold */ + level: number; +} + +export interface BadgeListGroupItem { + badges: BadgeListItem[]; + group_name: string; +} + +export interface BadgeInfo extends BadgeListItem { + description: string; + earned_count: number; + is_single: boolean; +} + +export interface BadgeDetailListReq { + page: number; + page_size: number; + badge_id: string; +} + +export interface BadgeDetailListRes { + count: number; + list: BadgeInfo[]; +} diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx index 9251b38ce..6fb4af7c0 100644 --- a/ui/src/components/CardBadge/index.tsx +++ b/ui/src/components/CardBadge/index.tsx @@ -19,35 +19,59 @@ import { useTranslation } from 'react-i18next'; import { FC } from 'react'; import { Card, Badge } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; +import classnames from 'classnames'; + +import { Icon } from '@/components'; +import * as Type from '@/common/interface'; import { formatCount } from '@/utils'; import './index.scss'; interface IProps { - data: any; - badgePill: boolean; + data: Type.BadgeListItem; showAwardedCount?: boolean; } -const Index: FC = ({ data, badgePill, showAwardedCount = false }) => { +const Index: FC = ({ data, showAwardedCount = false }) => { const { t } = useTranslation('translation', { keyPrefix: 'badges' }); console.log(data); return ( - + - - 0 - - -
Nice Question
+ {data.earned && ( + + {t('earned')} + + )} + + {/* + {showEarned ? t('earned') : data.award_count} + */} + {data.icon.startsWith('http') ? ( + {data.name} + ) : ( + + )} + +
{data.name}
{showAwardedCount && (
- {t('x_awarded', { number: formatCount(16) })} + {t('x_awarded', { number: formatCount(data.award_count) })}
)}
-
+ ); }; diff --git a/ui/src/index.scss b/ui/src/index.scss index b174fbc58..6d3ade051 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -352,3 +352,13 @@ img[src=""] { .alert-exist { color: var(--an-alert-exist-color); } + +.bronze { + color: #CD7F32; +} +.silver { + color: #C0C0C0; +} +.gold { + color: #FFD700; +} diff --git a/ui/src/pages/Badges/Detail/components/Badge/index.tsx b/ui/src/pages/Badges/Detail/components/Badge/index.tsx new file mode 100644 index 000000000..1fc5d43d3 --- /dev/null +++ b/ui/src/pages/Badges/Detail/components/Badge/index.tsx @@ -0,0 +1,93 @@ +/* + * 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 { Card, Badge } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import classnames from 'classnames'; + +import * as Type from '@/common/interface'; +import { Icon } from '@/components'; +import { formatCount } from '@/utils'; + +interface IProps { + data: Type.BadgeInfo; +} + +const Index: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'badges' }); + + if (!data?.id) { + return null; + } + + return ( + + + {data.icon?.startsWith('http') ? ( + {data.name} + ) : ( + + )} +
+
{data.name}
+
+ + {!data.is_single && ( +
{t('can_earn_multiple')}
+ )} + + {data.award_count > 0 && data.earned_count > 0 && ( +
+ {data.award_count > 0 && ( + + {t('x_awarded', { number: formatCount(data.award_count) })} + + )} + + {data.earned_count > 0 && ( + + {t('earned_x', { number: data.earned_count })} + + )} +
+ )} +
+ + + ); +}; + +export default Index; diff --git a/ui/src/pages/Badges/Detail/components/HeaderLoader/index.tsx b/ui/src/pages/Badges/Detail/components/HeaderLoader/index.tsx new file mode 100644 index 000000000..df0c92048 --- /dev/null +++ b/ui/src/pages/Badges/Detail/components/HeaderLoader/index.tsx @@ -0,0 +1,49 @@ +/* + * 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 { Card } from 'react-bootstrap'; + +const Index = () => { + return ( + + +
+ +
+
+
+
+ +
+ +
+ + + +
+
+ + + ); +}; + +export default Index; diff --git a/ui/src/pages/Badges/Detail/components/Loader/index.tsx b/ui/src/pages/Badges/Detail/components/Loader/index.tsx new file mode 100644 index 000000000..515e69af1 --- /dev/null +++ b/ui/src/pages/Badges/Detail/components/Loader/index.tsx @@ -0,0 +1,55 @@ +/* + * 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, memo } from 'react'; +import { Col } from 'react-bootstrap'; + +interface Props { + count?: number; +} + +const Index: FC = ({ count = 12 }) => { + const list = new Array(count).fill(0).map((v, i) => v + i); + return ( + <> + {list.map((v) => ( + +
+
+
+
+
+
+
+
+
+
+ + ))} + + ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Badges/Detail/index.tsx b/ui/src/pages/Badges/Detail/index.tsx index 7a192a13a..3e8513ae6 100644 --- a/ui/src/pages/Badges/Detail/index.tsx +++ b/ui/src/pages/Badges/Detail/index.tsx @@ -17,79 +17,98 @@ * under the License. */ -import { Card, Badge, Row, Col } from 'react-bootstrap'; +import { Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; -import { Avatar, FormatTime } from '@/components'; -import { usePageTags } from '@/hooks'; -import { formatCount } from '@/utils'; +// import classnames from 'classnames'; + +import { Avatar, FormatTime, Pagination, Empty } from '@/components'; +import { usePageTags, useSkeletonControl } from '@/hooks'; +// import { formatCount } from '@/utils'; +import { useGetBadgeInfo, useBadgeDetailList } from '@/services'; + +import BadgeDetail from './components/Badge'; +import Loader from './components/Loader'; +import HeaderLoader from './components/HeaderLoader'; const Index = () => { const { t } = useTranslation('translation', { keyPrefix: 'badges' }); + const { badge_id = '' } = useParams(); + const [urlSearchParams] = useSearchParams(); + + const page = Number(urlSearchParams.get('page')) || 1; + const pageSize = 30; + const { data: badgeInfo, isLoading: isHeaderLoading } = + useGetBadgeInfo(badge_id); + const { data: badges, isLoading: isDetailLoading } = useBadgeDetailList({ + badge_id, + page, + page_size: pageSize, + }); + + const { isSkeletonShow } = useSkeletonControl(isDetailLoading); + usePageTags({ - title: t('title'), + title: badgeInfo?.name || '', }); + if (badgeInfo === undefined) { + return null; + } + + console.log(badges); + return (

{t('title')}

- - - -
-
Support Expert
-
- This badge is granted for achieving the expert level of our - community support programme. This certifies that the recipient has - demonstrated a high level of skills and abilities to manage and - support multiple communities/instances. -
-
{t('can_earn_multiple')}
-
- - {t('x_awarded', { number: formatCount(16) })} - - - {t('earned_x', { number: 2 })} - -
-
-
-
+ {isHeaderLoading ? : } - {[0, 1, 2, 3, 4, 5, 6].map((item) => { - return ( - - -
- - - -
- - username + + {isSkeletonShow ? ( + + ) : ( + badges?.list?.map((item) => { + return ( + + +
+ + -
- 980 {t('x_reputation', { keyPrefix: 'personal' })} +
+ + username + +
+ 980 {t('x_reputation', { keyPrefix: 'personal' })} +
-
- - How to `go test` all tests in my project? - - - ); - })} + + How to `go test` all tests in my project? + + + ); + }) + )} + {Number(badges?.count) <= 0 && !isDetailLoading && } +
+ +
); }; diff --git a/ui/src/pages/Badges/index.tsx b/ui/src/pages/Badges/index.tsx index 84784c53e..396280620 100644 --- a/ui/src/pages/Badges/index.tsx +++ b/ui/src/pages/Badges/index.tsx @@ -21,10 +21,13 @@ import { useTranslation } from 'react-i18next'; import { CardBadge } from '@/components'; import { usePageTags } from '@/hooks'; +import { useGetAllBadges } from '@/services'; const Index = () => { const { t } = useTranslation('translation', { keyPrefix: 'badges' }); + const { data: badgesList } = useGetAllBadges(); + usePageTags({ title: t('title'), }); @@ -32,12 +35,18 @@ const Index = () => { return (

{t('title')}

-
Community Badges
-
- {[0, 1, 2, 3, 4, 5, 6].map((item) => { - return ; - })} -
+ {badgesList?.map((item) => { + return ( +
+
{item.group_name}
+
+ {item.badges?.map((badge) => { + return ; + })} +
+
+ ); + })}
); }; diff --git a/ui/src/pages/Users/Personal/components/Badges/index.tsx b/ui/src/pages/Users/Personal/components/Badges/index.tsx index a7bd3e58a..0ef00f39a 100644 --- a/ui/src/pages/Users/Personal/components/Badges/index.tsx +++ b/ui/src/pages/Users/Personal/components/Badges/index.tsx @@ -19,10 +19,11 @@ import { FC } from 'react'; +import * as Type from '@/common/interface'; import { CardBadge } from '@/components'; interface IProps { - data: any[]; + data: Type.BadgeListItem[]; visible: boolean; } @@ -33,8 +34,8 @@ const Index: FC = ({ data, visible }) => { } return (
- {[0, 1, 2, 3, 4, 5, 6].map((item) => { - return ; + {data.map((item) => { + return ; })}
); diff --git a/ui/src/pages/Users/Personal/components/Overview/index.tsx b/ui/src/pages/Users/Personal/components/Overview/index.tsx index 384689dcb..b4dd548b7 100644 --- a/ui/src/pages/Users/Personal/components/Overview/index.tsx +++ b/ui/src/pages/Users/Personal/components/Overview/index.tsx @@ -21,7 +21,8 @@ import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Row, Col } from 'react-bootstrap'; -import { CardBadge } from '@/components'; +// import * as Type from '@/common/interface'; +// import { CardBadge } from '@/components'; import TopList from '../TopList'; interface Props { @@ -67,11 +68,11 @@ const Index: FC = ({ visible, introduction, data }) => {
{t('recent_badges')}
-
+ {/*
{[0, 1, 2, 3, 4, 5, 6].map((item) => { return ; })} -
+
*/}
); diff --git a/ui/src/services/client/badges.ts b/ui/src/services/client/badges.ts new file mode 100644 index 000000000..c3a7bba23 --- /dev/null +++ b/ui/src/services/client/badges.ts @@ -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. + */ + +import useSWR from 'swr'; +import qs from 'qs'; + +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; + +export const useGetAllBadges = () => { + const apiUrl = '/answer/api/v1/badges'; + const { data, error, mutate } = useSWR, Error>( + apiUrl, + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + mutate, + }; +}; + +export const useGetBadgeInfo = (id: string) => { + const { data, error, mutate } = useSWR( + `/answer/api/v1/badge?id=${id}`, + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + mutate, + }; +}; + +export const useBadgeDetailList = (params: Type.BadgeDetailListReq) => { + const { data, error, mutate } = useSWR( + `/answer/api/v1/badge/awards/page?${qs.stringify(params)}`, + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + mutate, + }; +}; diff --git a/ui/src/services/client/index.ts b/ui/src/services/client/index.ts index 0d7184237..005bc26d7 100644 --- a/ui/src/services/client/index.ts +++ b/ui/src/services/client/index.ts @@ -30,3 +30,4 @@ export * from './revision'; export * from './user'; export * from './Oauth'; export * from './review'; +export * from './badges'; From fce712e6c7a7d907c2d576725fa46b07504a5e01 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 12 Aug 2024 16:26:31 +0800 Subject: [PATCH 066/129] fix: bages detail add userCard conponent --- ui/src/common/interface.ts | 14 ++- .../Detail/components/HeaderLoader/index.tsx | 16 +-- .../Detail/components/UserCard/index.tsx | 97 +++++++++++++++++++ ui/src/pages/Badges/Detail/index.tsx | 28 ++---- ui/src/pages/Badges/index.tsx | 1 - ui/src/services/client/badges.ts | 4 +- ui/src/services/client/personal.ts | 8 +- 7 files changed, 134 insertions(+), 34 deletions(-) create mode 100644 ui/src/pages/Badges/Detail/components/UserCard/index.tsx diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 789109d2f..45f957c2f 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -761,9 +761,21 @@ export interface BadgeDetailListReq { page: number; page_size: number; badge_id: string; + username?: string | null; +} + +export interface BadgeDetailListItem { + created_at: number; + author_user_info: UserInfoBase; + object_type: string; + object_id: string; + url_title: string; + question_id: string; + answer_id: string; + comment_id: string; } export interface BadgeDetailListRes { count: number; - list: BadgeInfo[]; + list: BadgeDetailListItem[]; } diff --git a/ui/src/pages/Badges/Detail/components/HeaderLoader/index.tsx b/ui/src/pages/Badges/Detail/components/HeaderLoader/index.tsx index df0c92048..d56432e81 100644 --- a/ui/src/pages/Badges/Detail/components/HeaderLoader/index.tsx +++ b/ui/src/pages/Badges/Detail/components/HeaderLoader/index.tsx @@ -22,23 +22,23 @@ import { Card } from 'react-bootstrap'; const Index = () => { return ( - +
-
-
-
-
+
+
+
+
- + - +
diff --git a/ui/src/pages/Badges/Detail/components/UserCard/index.tsx b/ui/src/pages/Badges/Detail/components/UserCard/index.tsx new file mode 100644 index 000000000..d874ceb9e --- /dev/null +++ b/ui/src/pages/Badges/Detail/components/UserCard/index.tsx @@ -0,0 +1,97 @@ +/* + * 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 { memo, FC } from 'react'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +import classnames from 'classnames'; + +import { Avatar } from '@/components'; +import { formatCount } from '@/utils'; + +interface Props { + data: any; + className?: string; +} + +const Index: FC = ({ data, className = '' }) => { + const { t } = useTranslation('translation', { keyPrefix: 'badges' }); + return ( +
+ {data?.status !== 'deleted' ? ( + + + + + + ) : ( + <> + + + + + )} +
+
+ {data?.status !== 'deleted' ? ( + + {data?.display_name} + + ) : ( + {data?.display_name} + )} +
+
+ {formatCount(data?.rank)}{' '} + {t('x_reputation', { keyPrefix: 'personal' })} +
+
+
+ ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Badges/Detail/index.tsx b/ui/src/pages/Badges/Detail/index.tsx index 3e8513ae6..42487db30 100644 --- a/ui/src/pages/Badges/Detail/index.tsx +++ b/ui/src/pages/Badges/Detail/index.tsx @@ -23,7 +23,7 @@ import { Link, useParams, useSearchParams } from 'react-router-dom'; // import classnames from 'classnames'; -import { Avatar, FormatTime, Pagination, Empty } from '@/components'; +import { FormatTime, Pagination, Empty } from '@/components'; import { usePageTags, useSkeletonControl } from '@/hooks'; // import { formatCount } from '@/utils'; import { useGetBadgeInfo, useBadgeDetailList } from '@/services'; @@ -31,6 +31,7 @@ import { useGetBadgeInfo, useBadgeDetailList } from '@/services'; import BadgeDetail from './components/Badge'; import Loader from './components/Loader'; import HeaderLoader from './components/HeaderLoader'; +import UserCard from './components/UserCard'; const Index = () => { const { t } = useTranslation('translation', { keyPrefix: 'badges' }); @@ -46,6 +47,7 @@ const Index = () => { badge_id, page, page_size: pageSize, + username: urlSearchParams.get('username') || null, }); const { isSkeletonShow } = useSkeletonControl(isDetailLoading); @@ -65,36 +67,20 @@ const Index = () => {

{t('title')}

{isHeaderLoading ? : } - {isSkeletonShow ? ( ) : ( badges?.list?.map((item) => { return ( - + -
- - - -
- - username - -
- 980 {t('x_reputation', { keyPrefix: 'personal' })} -
-
-
+ - How to `go test` all tests in my project? + {item.url_title} ); diff --git a/ui/src/pages/Badges/index.tsx b/ui/src/pages/Badges/index.tsx index 396280620..d37f5d9a8 100644 --- a/ui/src/pages/Badges/index.tsx +++ b/ui/src/pages/Badges/index.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { useTranslation } from 'react-i18next'; import { CardBadge } from '@/components'; diff --git a/ui/src/services/client/badges.ts b/ui/src/services/client/badges.ts index c3a7bba23..476f85d12 100644 --- a/ui/src/services/client/badges.ts +++ b/ui/src/services/client/badges.ts @@ -52,7 +52,9 @@ export const useGetBadgeInfo = (id: string) => { export const useBadgeDetailList = (params: Type.BadgeDetailListReq) => { const { data, error, mutate } = useSWR( - `/answer/api/v1/badge/awards/page?${qs.stringify(params)}`, + `/answer/api/v1/badge/awards/page?${qs.stringify(params, { + skipNulls: true, + })}`, request.instance.get, ); return { diff --git a/ui/src/services/client/personal.ts b/ui/src/services/client/personal.ts index 339f1e605..fbf389389 100644 --- a/ui/src/services/client/personal.ts +++ b/ui/src/services/client/personal.ts @@ -66,7 +66,7 @@ export const usePersonalTop = (username: string, tabName: string) => { }; export const usePersonalListByTabName = (params: ListReq, tabName: string) => { - let apiUrl = ''; + let apiUrl: string | null = ''; if (tabName === 'answers') { apiUrl = '/answer/api/v1/personal/answer/page'; } @@ -89,10 +89,14 @@ export const usePersonalListByTabName = (params: ListReq, tabName: string) => { delete params.username; apiUrl = '/answer/api/v1/personal/vote/page'; } + if (tabName === 'badges') { + delete params.order; + apiUrl = '/answer/api/v1/badge/user/awards'; + } const queryParams = qs.stringify(params, { skipNulls: true }); const { data, error, mutate } = useSWR( - tabName !== 'overview' ? `${apiUrl}?${queryParams}` : null, + tabName !== 'review' ? `${apiUrl}?${queryParams}` : null, request.instance.get, ); From 3d71bc17c843a78b71f30a94eed55600a1d3f89d Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 12 Aug 2024 17:45:03 +0800 Subject: [PATCH 067/129] fix: badge detail list styles format --- i18n/en_US.yaml | 2 +- ui/src/common/interface.ts | 1 + ui/src/components/CardBadge/index.tsx | 23 +++++++++---- .../Badges/Detail/components/Badge/index.tsx | 6 ++-- .../Detail/components/UserCard/index.tsx | 31 ++++++++--------- .../Personal/components/Badges/index.tsx | 11 +++++-- .../Personal/components/Overview/index.tsx | 33 +++++++++++++------ ui/src/pages/Users/Personal/index.tsx | 7 +++- ui/src/services/client/badges.ts | 16 +++++++++ 9 files changed, 89 insertions(+), 41 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 09596b783..420d25bac 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1743,7 +1743,7 @@ ui: earned_x: Earned x{{ number }} x_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. - + earned: Earned admin: admin_header: diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 45f957c2f..608a2465e 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -744,6 +744,7 @@ export interface BadgeListItem { earned: boolean; /** 1: bronze 2: silver 3:gold */ level: number; + earned_count?: number; } export interface BadgeListGroupItem { diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx index 6fb4af7c0..bb42630c6 100644 --- a/ui/src/components/CardBadge/index.tsx +++ b/ui/src/components/CardBadge/index.tsx @@ -32,23 +32,34 @@ import './index.scss'; interface IProps { data: Type.BadgeListItem; showAwardedCount?: boolean; + urlSearchParams?: string; + badgePillType?: 'earned' | 'count'; } -const Index: FC = ({ data, showAwardedCount = false }) => { +const Index: FC = ({ + data, + badgePillType = 'earned', + showAwardedCount = false, + urlSearchParams, +}) => { const { t } = useTranslation('translation', { keyPrefix: 'badges' }); console.log(data); return ( - + - {data.earned && ( + {data.earned && badgePillType === 'earned' && ( {t('earned')} )} - {/* - {showEarned ? t('earned') : data.award_count} - */} + {badgePillType === 'count' && Number(data?.earned_count) > 0 && ( + + x{data.earned_count} + + )} {data.icon.startsWith('http') ? ( {data.name} ) : ( diff --git a/ui/src/pages/Badges/Detail/components/Badge/index.tsx b/ui/src/pages/Badges/Detail/components/Badge/index.tsx index 1fc5d43d3..affb73e6e 100644 --- a/ui/src/pages/Badges/Detail/components/Badge/index.tsx +++ b/ui/src/pages/Badges/Detail/components/Badge/index.tsx @@ -69,16 +69,16 @@ const Index: FC = ({ data }) => {
{t('can_earn_multiple')}
)} - {data.award_count > 0 && data.earned_count > 0 && ( + {(data.award_count > 0 || data.earned_count > 0) && (
{data.award_count > 0 && ( - + {t('x_awarded', { number: formatCount(data.award_count) })} )} {data.earned_count > 0 && ( - + {t('earned_x', { number: data.earned_count })} )} diff --git a/ui/src/pages/Badges/Detail/components/UserCard/index.tsx b/ui/src/pages/Badges/Detail/components/UserCard/index.tsx index d874ceb9e..116a81b33 100644 --- a/ui/src/pages/Badges/Detail/components/UserCard/index.tsx +++ b/ui/src/pages/Badges/Detail/components/UserCard/index.tsx @@ -21,20 +21,17 @@ import { memo, FC } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import classnames from 'classnames'; - import { Avatar } from '@/components'; import { formatCount } from '@/utils'; interface Props { data: any; - className?: string; } -const Index: FC = ({ data, className = '' }) => { +const Index: FC = ({ data }) => { const { t } = useTranslation('translation', { keyPrefix: 'badges' }); return ( -
+
{data?.status !== 'deleted' ? ( = ({ data, className = '' }) => { /> )} -
-
- {data?.status !== 'deleted' ? ( - - {data?.display_name} - - ) : ( - {data?.display_name} - )} -
+
+ {data?.status !== 'deleted' ? ( + + {data?.display_name} + + ) : ( + {data?.display_name} + )}
{formatCount(data?.rank)}{' '} {t('x_reputation', { keyPrefix: 'personal' })} diff --git a/ui/src/pages/Users/Personal/components/Badges/index.tsx b/ui/src/pages/Users/Personal/components/Badges/index.tsx index 0ef00f39a..cd6830c9a 100644 --- a/ui/src/pages/Users/Personal/components/Badges/index.tsx +++ b/ui/src/pages/Users/Personal/components/Badges/index.tsx @@ -24,10 +24,11 @@ import { CardBadge } from '@/components'; interface IProps { data: Type.BadgeListItem[]; + username: string; visible: boolean; } -const Index: FC = ({ data, visible }) => { +const Index: FC = ({ data, visible, username }) => { console.log(data); if (!visible) { return null; @@ -35,7 +36,13 @@ const Index: FC = ({ data, visible }) => { return (
{data.map((item) => { - return ; + return ( + + ); })}
); diff --git a/ui/src/pages/Users/Personal/components/Overview/index.tsx b/ui/src/pages/Users/Personal/components/Overview/index.tsx index b4dd548b7..6b247b0cc 100644 --- a/ui/src/pages/Users/Personal/components/Overview/index.tsx +++ b/ui/src/pages/Users/Personal/components/Overview/index.tsx @@ -22,16 +22,21 @@ import { useTranslation } from 'react-i18next'; import { Row, Col } from 'react-bootstrap'; // import * as Type from '@/common/interface'; -// import { CardBadge } from '@/components'; +import { CardBadge } from '@/components'; +import { useGetRecentAwardBadges } from '@/services'; import TopList from '../TopList'; interface Props { + username: string; visible: boolean; introduction: string; data; } -const Index: FC = ({ visible, introduction, data }) => { +const Index: FC = ({ visible, introduction, data, username }) => { const { t } = useTranslation('translation', { keyPrefix: 'personal' }); + const { data: recentBadges } = useGetRecentAwardBadges( + visible ? username : null, + ); if (!visible) { return null; } @@ -66,14 +71,22 @@ const Index: FC = ({ visible, introduction, data }) => { -
-
{t('recent_badges')}
- {/*
- {[0, 1, 2, 3, 4, 5, 6].map((item) => { - return ; - })} -
*/} -
+ {Number(recentBadges?.count) > 0 && ( +
+
{t('recent_badges')}
+
+ {recentBadges?.list?.map((item) => { + return ( + + ); + })} +
+
+ )}
); }; diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index 96704b858..e9516acee 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -102,6 +102,7 @@ const Personal: FC = () => { visible={tabName === 'overview'} introduction={userInfo?.bio_html || ''} data={topData} + username={username} /> { - + {!list?.length && !isLoading && } {count > 0 && ( diff --git a/ui/src/services/client/badges.ts b/ui/src/services/client/badges.ts index 476f85d12..7f3f8abd7 100644 --- a/ui/src/services/client/badges.ts +++ b/ui/src/services/client/badges.ts @@ -64,3 +64,19 @@ export const useBadgeDetailList = (params: Type.BadgeDetailListReq) => { mutate, }; }; + +export const useGetRecentAwardBadges = (username) => { + const apiUrl = username + ? `/answer/api/v1/badge/user/awards/recent?username=${username}` + : null; + const { data, error, mutate } = useSWR< + { count: number; list: Array }, + Error + >(apiUrl, request.instance.get); + return { + data, + isLoading: !data && !error, + error, + mutate, + }; +}; From 957c299fc5b4a7c7d69d3bbbf191683cafa149a1 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 13 Aug 2024 10:03:03 +0800 Subject: [PATCH 068/129] fix: delete log --- ui/src/components/CardBadge/index.tsx | 1 - ui/src/pages/Users/Personal/components/Badges/index.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx index bb42630c6..0983f108f 100644 --- a/ui/src/components/CardBadge/index.tsx +++ b/ui/src/components/CardBadge/index.tsx @@ -43,7 +43,6 @@ const Index: FC = ({ urlSearchParams, }) => { const { t } = useTranslation('translation', { keyPrefix: 'badges' }); - console.log(data); return ( = ({ data, visible, username }) => { - console.log(data); if (!visible) { return null; } From 29f2ce3a5630d0426b9fb9be91201dbfa584a487 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 14 Aug 2024 11:23:57 +0800 Subject: [PATCH 069/129] fix: add link url path --- ui/src/pages/Badges/Detail/index.tsx | 18 +++++++++++++++--- ui/src/utils/localize.ts | 4 +--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ui/src/pages/Badges/Detail/index.tsx b/ui/src/pages/Badges/Detail/index.tsx index 42487db30..a388d4963 100644 --- a/ui/src/pages/Badges/Detail/index.tsx +++ b/ui/src/pages/Badges/Detail/index.tsx @@ -71,6 +71,16 @@ const Index = () => { ) : ( badges?.list?.map((item) => { + const linkUrl = + item.object_type === 'question' + ? `/question/${item.question_id}` + : item.object_type === 'answer' + ? `/question/${item.question_id}/${item.answer_id}` + : item.object_type === 'comment' && item.answer_id + ? `/question/${item.question_id}/${item.answer_id}?commentId=${item.comment_id}` + : item.object_type === 'comment' + ? `/question/${item.question_id}?commentId=${item.comment_id}` + : ''; return ( { className="small mb-1 d-block" /> - - {item.url_title} - + {item.url_title && ( + + {item.url_title} + + )} ); }) diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts index b64473a90..eea132a84 100644 --- a/ui/src/utils/localize.ts +++ b/ui/src/utils/localize.ts @@ -133,9 +133,7 @@ const localeDayjs = (langName) => { export const setupAppLanguage = async () => { const lang = getCurrentLang(); - if (!i18next.getDataByLanguage(lang)?.translation) { - await addI18nResource(lang); - } + await addI18nResource(lang); localeDayjs(lang); i18next.changeLanguage(lang); }; From ec31d18c2621a6a06604a14c5ea4bcb2e445d6c7 Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 13 Aug 2024 17:42:12 +0800 Subject: [PATCH 070/129] feat(admin): Improve the badge list --- .../Admin/Badges/components/Action/index.tsx | 15 ++- ui/src/pages/Admin/Badges/index.tsx | 96 +++++++++++++------ ui/src/services/admin/badges.ts | 42 ++++++++ ui/src/services/admin/index.ts | 1 + 4 files changed, 116 insertions(+), 38 deletions(-) create mode 100644 ui/src/services/admin/badges.ts diff --git a/ui/src/pages/Admin/Badges/components/Action/index.tsx b/ui/src/pages/Admin/Badges/components/Action/index.tsx index 77601cbb4..9ec183793 100644 --- a/ui/src/pages/Admin/Badges/components/Action/index.tsx +++ b/ui/src/pages/Admin/Badges/components/Action/index.tsx @@ -23,23 +23,20 @@ import { useTranslation } from 'react-i18next'; import { Icon } from '@/components'; interface Props { - badgeData; + onSelect: (eventKey: string | null) => void; } - -const UserOperation = ({ badgeData }: Props) => { +const BadgeOperation = ({ onSelect }: Props) => { const { t } = useTranslation('translation', { keyPrefix: 'admin.badges' }); - console.log(badgeData); - return ( - + - {t('active')} - {t('deactivate')} + {t('active')} + {t('deactivate')} {t('show_logs')} @@ -48,4 +45,4 @@ const UserOperation = ({ badgeData }: Props) => { ); }; -export default UserOperation; +export default BadgeOperation; diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx index 288540123..6c74f3ce4 100644 --- a/ui/src/pages/Admin/Badges/index.tsx +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -22,33 +22,50 @@ import { Form, Table, Stack } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@/components'; +import classNames from 'classnames'; + +import { Empty, Icon, Pagination, QueryGroup } from '@/components'; import * as Type from '@/common/interface'; +import { useQueryBadges, updateBadgeStatus } from '@/services/admin/badges'; 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 bgMap = { + active: 'text-bg-success', + inactive: 'text-bg-secondary', +}; + +const PAGE_SIZE = 10; const Users: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.badges' }); const [urlSearchParams, setUrlSearchParams] = useSearchParams(); + const curPage = Number(urlSearchParams.get('page') || '1'); const curFilter = urlSearchParams.get('filter') || BadgeFilterKeys[0]; const curQuery = urlSearchParams.get('query') || ''; + const { data, isLoading, mutate } = useQueryBadges({ + page: curPage, + pageSize: PAGE_SIZE, + query: curQuery, + ...(curFilter === 'all' ? {} : { status: curFilter }), + }); + const handleFilter = (e) => { urlSearchParams.set('query', e.target.value); urlSearchParams.delete('page'); setUrlSearchParams(urlSearchParams); }; + const handleBadgeStatus = (badgeId, status) => { + updateBadgeStatus({ id: badgeId, status }).then(() => { + mutate(); + }); + }; + return ( <>

{t('title')}

@@ -85,36 +102,57 @@ const Users: FC = () => { - - - badge ( + + + {badge.icon?.startsWith('http') ? ( + {badge.name} + ) : ( + + )} +
+
{badge.name}
+
{badge.description}
+
+ + + {badge.group_name} + {badge.award_count} + + + {t(badge.status)} + + + handleBadgeStatus(badge.id, status)} /> -
-
Nice Question
-
Question score of 10 or more.
-
- - - Community Badges - 200 - Active - - + + ))} - {/* {Number(data?.count) <= 0 && !isLoading && } */} - {/*
+ {Number(data?.count) <= 0 && !isLoading && } +
-
*/} +
); }; diff --git a/ui/src/services/admin/badges.ts b/ui/src/services/admin/badges.ts new file mode 100644 index 000000000..b984164e2 --- /dev/null +++ b/ui/src/services/admin/badges.ts @@ -0,0 +1,42 @@ +/* + * 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 qs from 'qs'; +import useSWR from 'swr'; + +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; + +export const useQueryBadges = (params) => { + const apiUrl = `/answer/admin/api/badges?${qs.stringify(params)}`; + const { data, error, mutate } = useSWR( + apiUrl, + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + mutate, + }; +}; + +export const updateBadgeStatus = (params) => { + return request.put('/answer/admin/api/badge/status', params); +}; diff --git a/ui/src/services/admin/index.ts b/ui/src/services/admin/index.ts index ce64c5116..af83d365b 100644 --- a/ui/src/services/admin/index.ts +++ b/ui/src/services/admin/index.ts @@ -24,3 +24,4 @@ export * from './settings'; export * from './users'; export * from './dashboard'; export * from './plugins'; +export * from './badges'; From 04065a8dcb56b338c10feb9f5265ee470c92eee0 Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 13 Aug 2024 18:25:52 +0800 Subject: [PATCH 071/129] refactor: Update query parameter name in badge list API call --- ui/src/pages/Admin/Badges/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx index 6c74f3ce4..571f9b836 100644 --- a/ui/src/pages/Admin/Badges/index.tsx +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -50,7 +50,7 @@ const Users: FC = () => { const { data, isLoading, mutate } = useQueryBadges({ page: curPage, pageSize: PAGE_SIZE, - query: curQuery, + q: curQuery, ...(curFilter === 'all' ? {} : { status: curFilter }), }); From 4afa76ba2a8233a949ef05f605416f5983f556c7 Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 13 Aug 2024 18:31:47 +0800 Subject: [PATCH 072/129] refactor: update zh_CN.yaml --- i18n/zh_CN.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 94e6c97f3..5612e1abd 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1407,6 +1407,7 @@ ui: questions: 问题 answers: 回答 users: 用户管理 + badges: 徽章管理 flags: 举报管理 settings: 站点设置 general: 一般 @@ -1834,6 +1835,20 @@ ui: msg: should_be_number: 输入必须是数字 number_larger_1: 数字应该大于等于 1 + badges: + action: 操作 + active: 激活 + all: 全部 + awards: 奖励 + deactivate: 取消激活 + filter: + placeholder: "按名称筛选,徽章:id" + group: 组 + inactive: 未激活 + name: 名称 + show_logs: 显示日志 + status: 状态 + title: 徽章 form: optional: (选填) empty: 不能为空 From 342c31f38cda7c671c4939c547e424284bbda4aa Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 14 Aug 2024 16:41:14 +0800 Subject: [PATCH 073/129] fix: add link url path --- ui/src/pages/Badges/Detail/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/pages/Badges/Detail/index.tsx b/ui/src/pages/Badges/Detail/index.tsx index a388d4963..46f49ce20 100644 --- a/ui/src/pages/Badges/Detail/index.tsx +++ b/ui/src/pages/Badges/Detail/index.tsx @@ -60,8 +60,6 @@ const Index = () => { return null; } - console.log(badges); - return (

{t('title')}

From 3cb1be88f6fa6c5b4bb7e476e1dada0c8e0e429b Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 14 Aug 2024 14:13:28 +0800 Subject: [PATCH 074/129] refactor: Update badge description styling in Admin Badges page --- ui/src/pages/Admin/Badges/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx index 571f9b836..f6b8cb0d0 100644 --- a/ui/src/pages/Admin/Badges/index.tsx +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -127,7 +127,7 @@ const Users: FC = () => { )}
{badge.name}
-
{badge.description}
+
{badge.description}
From db1a527f955343c936b261a2832e7b071c8316d9 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 14 Aug 2024 14:19:18 +0800 Subject: [PATCH 075/129] refactor: Update page_size parameter in badge list API call --- ui/src/pages/Admin/Badges/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx index f6b8cb0d0..448040a4a 100644 --- a/ui/src/pages/Admin/Badges/index.tsx +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -49,7 +49,7 @@ const Users: FC = () => { const { data, isLoading, mutate } = useQueryBadges({ page: curPage, - pageSize: PAGE_SIZE, + page_size: PAGE_SIZE, q: curQuery, ...(curFilter === 'all' ? {} : { status: curFilter }), }); From 45f410ca4c9b06e8f8fd3041cfce46af67698ba7 Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 16 Aug 2024 11:39:17 +0800 Subject: [PATCH 076/129] chore: Update tsconfig.json to exclude src/plugins directory --- ui/tsconfig.json | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ui/tsconfig.json b/ui/tsconfig.json index b16f340ca..ac6a549eb 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -20,9 +24,20 @@ "jsx": "react-jsx", "baseUrl": "./", "paths": { - "@/*": ["src/*"], - "@i18n/*": ["../i18n/*"] + "@/*": [ + "src/*" + ], + "@i18n/*": [ + "../i18n/*" + ] } }, - "include": ["src", "node_modules/@testing-library/jest-dom", "scripts" ] -} + "include": [ + "src", + "node_modules/@testing-library/jest-dom", + "scripts" + ], + "exclude": [ + "src/plugins" + ] +} \ No newline at end of file From ac28c8871251c28fcdf9711922bc549c93ee7f10 Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 16 Aug 2024 11:41:11 +0800 Subject: [PATCH 077/129] refactor: Update badge description styling in Admin Badges page --- i18n/en_US.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 420d25bac..8b137d373 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1580,7 +1580,7 @@ ui: visited_x_days: "Visited {{ count }} days" viewed: Viewed joined: Joined - comma: ',' + comma: "," last_login: Seen about_me: About Me about_me_empty: "// Hello, World !" @@ -1738,6 +1738,11 @@ ui: qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. login_failed_email_tip: Login failed, please allow this app to access your email information before try again. badges: + modal: + title: Congratulations + content: You've earned a new badge. + close: Close + confirm: View badges title: Badges awarded: Awarded earned_x: Earned x{{ number }} From 90cf954d8fc28103f15079c102ae4ac0a6b9cab7 Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 16 Aug 2024 11:41:57 +0800 Subject: [PATCH 078/129] refactor: Update badge description styling in Admin Badges page --- ui/src/common/interface.ts | 15 +++- ui/src/components/Modal/BadgeModal.tsx | 70 +++++++++++++++++++ ui/src/components/Modal/index.tsx | 3 +- ui/src/pages/Admin/Badges/index.tsx | 6 +- ui/src/pages/Layout/index.tsx | 8 ++- .../components/Achievements/index.scss | 5 +- .../components/Achievements/index.tsx | 27 +++++-- ui/src/services/admin/badges.ts | 8 +-- 8 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 ui/src/components/Modal/BadgeModal.tsx diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 608a2465e..a06fec280 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -220,11 +220,19 @@ export interface SetNoticeReq { notice_switch: boolean; } +export interface NotificationBadgeAward { + notification_id: string; + badge_id: string; + name: string; + icon: string; + level: number; +} export interface NotificationStatus { inbox: number; achievement: number; revision: number; can_revision: boolean; + badge_award: NotificationBadgeAward | null; } export interface QuestionDetailRes { @@ -758,13 +766,18 @@ export interface BadgeInfo extends BadgeListItem { is_single: boolean; } +export interface AdminBadgeListItem extends BadgeListItem { + group_name: string; + status: string; + description: string; +} + export interface BadgeDetailListReq { page: number; page_size: number; badge_id: string; username?: string | null; } - export interface BadgeDetailListItem { created_at: number; author_user_info: UserInfoBase; diff --git a/ui/src/components/Modal/BadgeModal.tsx b/ui/src/components/Modal/BadgeModal.tsx new file mode 100644 index 000000000..214c1d67f --- /dev/null +++ b/ui/src/components/Modal/BadgeModal.tsx @@ -0,0 +1,70 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import classNames from 'classnames'; + +import type * as Type from '@/common/interface'; +import { loggedUserInfoStore } from '@/stores'; +import { readNotification, useQueryNotificationStatus } from '@/services'; +import Icon from '../Icon'; + +import Modal from './Modal'; + +interface BadgeModalProps { + badge?: Type.NotificationBadgeAward | null; + visible: boolean; +} +const BadgeModal: FC = ({ badge, visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'badges.modal' }); + const { user } = loggedUserInfoStore(); + const navigate = useNavigate(); + const { data } = useQueryNotificationStatus(); + + const handleCancel = async () => { + if (!data) return; + await readNotification(badge?.notification_id); + }; + const handleConfirm = async () => { + await readNotification(badge?.notification_id); + + const url = `/badges/${badge?.badge_id}?username=${user.username}`; + navigate(url); + }; + + return ( + + {badge && ( +
+ {badge.icon?.startsWith('http') ? ( + {badge.name} + ) : ( + + )} +
{badge?.name}
+

{t('content')}

+
+ )} +
+ ); +}; + +export default BadgeModal; diff --git a/ui/src/components/Modal/index.tsx b/ui/src/components/Modal/index.tsx index 404591966..75d968421 100644 --- a/ui/src/components/Modal/index.tsx +++ b/ui/src/components/Modal/index.tsx @@ -20,6 +20,7 @@ import DefaultModal from './Modal'; import confirm, { Config } from './Confirm'; import LoginToContinueModal from './LoginToContinueModal'; +import BadgeModal from './BadgeModal'; type ModalType = typeof DefaultModal & { confirm: (config: Config) => void; @@ -32,4 +33,4 @@ Modal.confirm = function (props: Config) { export default Modal; -export { LoginToContinueModal }; +export { LoginToContinueModal, BadgeModal }; diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx index 448040a4a..e76f90eba 100644 --- a/ui/src/pages/Admin/Badges/index.tsx +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -39,7 +39,7 @@ const bgMap = { const PAGE_SIZE = 10; -const Users: FC = () => { +const Badges: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.badges' }); const [urlSearchParams, setUrlSearchParams] = useSearchParams(); @@ -103,7 +103,7 @@ const Users: FC = () => { {data?.list.map((badge) => ( - + {badge.icon?.startsWith('http') ? ( { ); }; -export default Users; +export default Badges; diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 2029b0fa4..e8bab2799 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -33,8 +33,9 @@ import { PageTags, HttpErrorContent, } from '@/components'; -import { LoginToContinueModal } from '@/components/Modal'; +import { LoginToContinueModal, BadgeModal } from '@/components/Modal'; import { changeTheme } from '@/utils'; +import { useQueryNotificationStatus } from '@/services'; const Layout: FC = () => { const location = useLocation(); @@ -44,6 +45,7 @@ const Layout: FC = () => { }; const { code: httpStatusCode, reset: httpStatusReset } = errorCodeStore(); const { show: showLoginToContinueModal } = loginToContinueStore(); + const { data: notificationData } = useQueryNotificationStatus(); useEffect(() => { httpStatusReset(); @@ -86,6 +88,10 @@ const Layout: FC = () => {
+ diff --git a/ui/src/pages/Users/Notifications/components/Achievements/index.scss b/ui/src/pages/Users/Notifications/components/Achievements/index.scss index fb91193fb..9a2249215 100644 --- a/ui/src/pages/Users/Notifications/components/Achievements/index.scss +++ b/ui/src/pages/Users/Notifications/components/Achievements/index.scss @@ -18,8 +18,9 @@ */ .achievement-wrap { - .num { + .num, + .icon { width: 60px; flex: none; } -} +} \ No newline at end of file diff --git a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx index 5f2e8f58c..c171a4273 100644 --- a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx +++ b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx @@ -24,10 +24,13 @@ import classNames from 'classnames'; import isEmpty from 'lodash/isEmpty'; import { Empty } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; import './index.scss'; const Achievements = ({ data, handleReadNotification }) => { + const { user } = loggedUserInfoStore(); + if (!data) { return null; } @@ -50,6 +53,9 @@ const Achievements = ({ data, handleReadNotification }) => { case 'comment': url = `/questions/${question}/${answer}?commentId=${comment}`; break; + case 'badge_award': + url = `/badges/${item.object_info.object_map.badge_id}?username=${user.username}`; + break; default: url = ''; } @@ -60,13 +66,22 @@ const Achievements = ({ data, handleReadNotification }) => { 'd-flex border-start-0 border-end-0 py-3', !item.is_read && 'warning', )}> - {item.rank > 0 && ( -
{`+${item.rank}`}
- )} - {item.rank === 0 &&
{item.rank}
} - {item.rank < 0 && ( -
{`${item.rank}`}
+ {item.object_info.object_type === 'badge_award' ? ( +
👏
+ ) : ( + <> + {item.rank > 0 && ( +
{`+${item.rank}`}
+ )} + {item.rank === 0 && ( +
{item.rank}
+ )} + {item.rank < 0 && ( +
{`${item.rank}`}
+ )} + )} +
handleReadNotification(item.id)}> {item.object_info.title} diff --git a/ui/src/services/admin/badges.ts b/ui/src/services/admin/badges.ts index b984164e2..64685bc5e 100644 --- a/ui/src/services/admin/badges.ts +++ b/ui/src/services/admin/badges.ts @@ -25,10 +25,10 @@ import type * as Type from '@/common/interface'; export const useQueryBadges = (params) => { const apiUrl = `/answer/admin/api/badges?${qs.stringify(params)}`; - const { data, error, mutate } = useSWR( - apiUrl, - request.instance.get, - ); + const { data, error, mutate } = useSWR< + Type.ListResult, + Error + >(apiUrl, request.instance.get); return { data, isLoading: !data && !error, From 85516530ca1d98710f05194da0305a227318244e Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 13 Aug 2024 15:02:29 +0800 Subject: [PATCH 079/129] =?UTF-8?q?feat=EF=BC=9ASupport=20Render=20type=20?= =?UTF-8?q?plugins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/utils/pluginKit/index.ts | 3 ++- ui/src/utils/pluginKit/interface.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/src/utils/pluginKit/index.ts b/ui/src/utils/pluginKit/index.ts index 1f7efacab..26e3a2883 100644 --- a/ui/src/utils/pluginKit/index.ts +++ b/ui/src/utils/pluginKit/index.ts @@ -221,7 +221,8 @@ const useRenderHtmlPlugin = ( return ( plugin.activated && plugin.hooks?.useRender && - plugin.info.type === PluginType.Editor + (plugin.info.type === PluginType.Editor || + plugin.info.type === PluginType.Render) ); }) .forEach((plugin) => { diff --git a/ui/src/utils/pluginKit/interface.ts b/ui/src/utils/pluginKit/interface.ts index e1b8ab0c9..462ae47b2 100644 --- a/ui/src/utils/pluginKit/interface.ts +++ b/ui/src/utils/pluginKit/interface.ts @@ -27,6 +27,7 @@ export enum PluginType { Editor = 'editor', Route = 'route', Captcha = 'captcha', + Render = 'render', } export interface PluginInfo { @@ -45,7 +46,7 @@ export interface Plugin { useRender?: Array< (element: HTMLElement | RefObject | null) => void >; - useCaptcha?: (props: { captchaKey: Type.CaptchaKey; commonProps: any }) => { + useCaptcha?: (props: { captchaKey: Type.CaptchaKey; commonProps: any; }) => { getCaptcha: () => Record; check: (t: () => void) => void; handleCaptchaError: (error) => any; From 7c59d3f3cb167abdaadf0a570dec24c11efc9cc3 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 15 Aug 2024 10:57:33 +0800 Subject: [PATCH 080/129] feat: Add useRenderPlugin to Editor component --- ui/src/components/Editor/index.tsx | 4 +++- ui/src/utils/pluginKit/index.ts | 28 +++++++++++++++++++++++++++- ui/src/utils/pluginKit/interface.ts | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/ui/src/components/Editor/index.tsx b/ui/src/components/Editor/index.tsx index dd0040219..ead37653d 100644 --- a/ui/src/components/Editor/index.tsx +++ b/ui/src/components/Editor/index.tsx @@ -27,7 +27,7 @@ import { import classNames from 'classnames'; -import { PluginType } from '@/utils/pluginKit'; +import { PluginType, useRenderPlugin } from '@/utils/pluginKit'; import PluginRender from '../PluginRender'; import { @@ -84,6 +84,8 @@ const MDEditor: ForwardRefRenderFunction = ( const editorRef = useRef(null); const previewRef = useRef<{ getHtml; element } | null>(null); + useRenderPlugin(previewRef.current?.element); + const editor = useEditor({ editorRef, onChange, diff --git a/ui/src/utils/pluginKit/index.ts b/ui/src/utils/pluginKit/index.ts index 26e3a2883..6d0e9878a 100644 --- a/ui/src/utils/pluginKit/index.ts +++ b/ui/src/utils/pluginKit/index.ts @@ -232,6 +232,26 @@ const useRenderHtmlPlugin = ( }); }; +// Only for render type plugins +const useRenderPlugin = ( + element: HTMLElement | RefObject | null, +) => { + return plugins + .getPlugins() + .filter((plugin) => { + return ( + plugin.activated && + plugin.hooks?.useRender && + plugin.info.type === PluginType.Render + ); + }) + .forEach((plugin) => { + plugin.hooks?.useRender?.forEach((hook) => { + hook(element); + }); + }); +}; + // Only one captcha type plug-in can be enabled at the same time const useCaptchaPlugin = (key: Type.CaptchaKey) => { const captcha = plugins @@ -248,5 +268,11 @@ const useCaptchaPlugin = (key: Type.CaptchaKey) => { export type { Plugin, PluginInfo }; -export { useRenderHtmlPlugin, mergeRoutePlugins, useCaptchaPlugin, PluginType }; +export { + useRenderHtmlPlugin, + mergeRoutePlugins, + useCaptchaPlugin, + useRenderPlugin, + PluginType, +}; export default plugins; diff --git a/ui/src/utils/pluginKit/interface.ts b/ui/src/utils/pluginKit/interface.ts index 462ae47b2..880a524a5 100644 --- a/ui/src/utils/pluginKit/interface.ts +++ b/ui/src/utils/pluginKit/interface.ts @@ -46,7 +46,7 @@ export interface Plugin { useRender?: Array< (element: HTMLElement | RefObject | null) => void >; - useCaptcha?: (props: { captchaKey: Type.CaptchaKey; commonProps: any; }) => { + useCaptcha?: (props: { captchaKey: Type.CaptchaKey; commonProps: any }) => { getCaptcha: () => Record; check: (t: () => void) => void; handleCaptchaError: (error) => any; From e48a13fed51f9f14a28d914ea3ca75920dc49333 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 22 Aug 2024 10:04:47 +0800 Subject: [PATCH 081/129] refactor: Update Chinese translation for badge activation --- i18n/zh_CN.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 5612e1abd..207825167 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1840,7 +1840,7 @@ ui: active: 激活 all: 全部 awards: 奖励 - deactivate: 取消激活 + deactivate: 停用 filter: placeholder: "按名称筛选,徽章:id" group: 组 From e4d0322a1694b026a205f0ddc3221e249fe6280a Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 22 Aug 2024 10:04:52 +0800 Subject: [PATCH 082/129] refactor: Remove unused dropdown item in BadgeOperation component --- ui/src/pages/Admin/Badges/components/Action/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/pages/Admin/Badges/components/Action/index.tsx b/ui/src/pages/Admin/Badges/components/Action/index.tsx index 9ec183793..b5bf8ddd7 100644 --- a/ui/src/pages/Admin/Badges/components/Action/index.tsx +++ b/ui/src/pages/Admin/Badges/components/Action/index.tsx @@ -37,8 +37,6 @@ const BadgeOperation = ({ onSelect }: Props) => { {t('active')} {t('deactivate')} - - {t('show_logs')} From e6aa178062c65cd0f79671efd6ebda90219f3843 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 22 Aug 2024 10:45:24 +0800 Subject: [PATCH 083/129] refactor: Update key prop in BadgeDetail component --- ui/src/pages/Badges/Detail/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/Badges/Detail/index.tsx b/ui/src/pages/Badges/Detail/index.tsx index 46f49ce20..f8a9327af 100644 --- a/ui/src/pages/Badges/Detail/index.tsx +++ b/ui/src/pages/Badges/Detail/index.tsx @@ -80,7 +80,12 @@ const Index = () => { ? `/question/${item.question_id}?commentId=${item.comment_id}` : ''; return ( - + Date: Thu, 22 Aug 2024 10:47:44 +0800 Subject: [PATCH 084/129] refactor: Update BadgeModal component to handle badge cancellation and confirmation --- ui/src/components/Modal/BadgeModal.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ui/src/components/Modal/BadgeModal.tsx b/ui/src/components/Modal/BadgeModal.tsx index 214c1d67f..32049de6d 100644 --- a/ui/src/components/Modal/BadgeModal.tsx +++ b/ui/src/components/Modal/BadgeModal.tsx @@ -19,14 +19,21 @@ const BadgeModal: FC = ({ badge, visible }) => { const { t } = useTranslation('translation', { keyPrefix: 'badges.modal' }); const { user } = loggedUserInfoStore(); const navigate = useNavigate(); - const { data } = useQueryNotificationStatus(); + const { data, mutate } = useQueryNotificationStatus(); - const handleCancel = async () => { + const handle = async () => { if (!data) return; await readNotification(badge?.notification_id); + await mutate({ + ...data, + badge_award: null, + }); + }; + const handleCancel = async () => { + await handle(); }; const handleConfirm = async () => { - await readNotification(badge?.notification_id); + await handle(); const url = `/badges/${badge?.badge_id}?username=${user.username}`; navigate(url); From 7d8e679d9c969c036ea8697d225c25c843d88c15 Mon Sep 17 00:00:00 2001 From: Fen Date: Tue, 13 Aug 2024 10:29:55 +0800 Subject: [PATCH 085/129] docs: enable wiki --- .asf.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.asf.yaml b/.asf.yaml index a8baeb21c..7505cd7f9 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -30,7 +30,7 @@ github: - q-and-a - hacktoberfest features: - wiki: false + wiki: true issues: true projects: true discussions: false From 15531f42231471509b2a0513737546263b1a0e25 Mon Sep 17 00:00:00 2001 From: Jan Ernsting Date: Thu, 15 Aug 2024 17:31:49 +0200 Subject: [PATCH 086/129] build swagger documentation on go generate --- cmd/answer/main.go | 7 +++ docs/docs.go | 87 ++++++++++++++++++++----------- docs/swagger.json | 68 ++++++++++++++++++++---- docs/swagger.yaml | 64 ++++++++++++++--------- go.mod | 5 +- go.sum | 6 +++ internal/router/swagger_router.go | 4 -- 7 files changed, 172 insertions(+), 69 deletions(-) diff --git a/cmd/answer/main.go b/cmd/answer/main.go index ae1147090..a14031eca 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -17,12 +17,19 @@ * under the License. */ +//go:generate go run github.com/swaggo/swag/cmd/swag init -g ./cmd/answer/main.go -d ../../ -o ../../docs + package main import ( answercmd "github.com/apache/incubator-answer/cmd" ) +// main godoc +// @title "answer" +// @description = "answer api" +// @version = "v0.0.1" +// @BasePath = "/" // @securityDefinitions.apikey ApiKeyAuth // @in header // @name Authorization diff --git a/docs/docs.go b/docs/docs.go index cf1264d14..fee91459c 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 @@ -4410,7 +4391,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetTagBasicResp" } } } @@ -5380,7 +5361,7 @@ const docTemplate = `{ }, "/answer/api/v1/tags": { "get": { - "description": "get tags list", + "description": "get tags list by slug name", "produces": [ "application/json" ], @@ -5404,7 +5385,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" + } + } + } + } + ] } } } @@ -8242,6 +8238,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 +9832,7 @@ const docTemplate = `{ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9828,7 +9841,7 @@ const docTemplate = `{ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9842,7 +9855,7 @@ const docTemplate = `{ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9851,7 +9864,7 @@ const docTemplate = `{ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9859,6 +9872,20 @@ const docTemplate = `{ } } }, + "schema.SiteWriteTag": { + "type": "object", + "required": [ + "slug_name" + ], + "properties": { + "display_name": { + "type": "string" + }, + "slug_name": { + "type": "string" + } + } + }, "schema.TagItem": { "type": "object", "properties": { @@ -10783,12 +10810,12 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "", + Version: "= \"v0.0.1\"", Host: "", - BasePath: "", + BasePath: "= \"/\"", Schemes: []string{}, - Title: "", - Description: "", + Title: "\"answer\"", + Description: "= \"answer api\"", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index 1e93d2b08..7c103856c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,8 +1,12 @@ { "swagger": "2.0", "info": { - "contact": {} + "description": "= \"answer api\"", + "title": "\"answer\"", + "contact": {}, + "version": "= \"v0.0.1\"" }, + "basePath": "= \"/\"", "paths": { "/": { "get": { @@ -4380,7 +4384,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/schema.GetTagResp" + "$ref": "#/definitions/schema.GetTagBasicResp" } } } @@ -5350,7 +5354,7 @@ }, "/answer/api/v1/tags": { "get": { - "description": "get tags list", + "description": "get tags list by slug name", "produces": [ "application/json" ], @@ -5374,7 +5378,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" + } + } + } + } + ] } } } @@ -8212,6 +8231,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 +8845,7 @@ "enum": [ "newest", "active", - "frequent", + "hot", "score", "unanswered" ] @@ -9789,7 +9825,7 @@ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9798,7 +9834,7 @@ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9812,7 +9848,7 @@ "recommend_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "required_tag": { @@ -9821,7 +9857,7 @@ "reserved_tags": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/schema.SiteWriteTag" } }, "restrict_answer": { @@ -9829,6 +9865,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..231b271e8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,20 +1,4 @@ -# 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. - +basePath: = "/" definitions: constant.NotificationChannelKey: enum: @@ -983,6 +967,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 +1396,7 @@ definitions: enum: - newest - active - - frequent + - hot - score - unanswered type: string @@ -2071,13 +2066,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 +2081,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: @@ -2736,6 +2740,9 @@ definitions: type: object info: contact: {} + description: = "answer api" + title: '"answer"' + version: = "v0.0.1" paths: /: get: @@ -5382,7 +5389,7 @@ paths: - properties: data: items: - $ref: '#/definitions/schema.GetTagResp' + $ref: '#/definitions/schema.GetTagBasicResp' type: array type: object security: @@ -5965,7 +5972,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 +5987,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/go.mod b/go.mod index e1b78ad2c..036a2b42f 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/swaggo/files v1.0.0 github.com/swaggo/gin-swagger v1.5.3 - github.com/swaggo/swag v1.16.1 + github.com/swaggo/swag v1.16.3 github.com/tidwall/gjson v1.14.4 github.com/yuin/goldmark v1.4.13 golang.org/x/crypto v0.21.0 @@ -79,6 +79,7 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/containerd/continuity v0.4.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v24.0.6+incompatible // indirect github.com/docker/docker v24.0.6+incompatible // indirect @@ -128,6 +129,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect @@ -140,6 +142,7 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + github.com/urfave/cli/v2 v2.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index 501d1e941..fb49b88b6 100644 --- a/go.sum +++ b/go.sum @@ -128,6 +128,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -615,6 +616,7 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= @@ -692,6 +694,8 @@ github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89 github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= @@ -710,7 +714,9 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95 github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= diff --git a/internal/router/swagger_router.go b/internal/router/swagger_router.go index 795e88025..55f2d644a 100644 --- a/internal/router/swagger_router.go +++ b/internal/router/swagger_router.go @@ -51,9 +51,5 @@ func (a *SwaggerRouter) Register(r *gin.RouterGroup) { // InitSwaggerDocs init swagger docs func (a *SwaggerRouter) InitSwaggerDocs() { - docs.SwaggerInfo.Title = "answer" - docs.SwaggerInfo.Description = "answer api" - docs.SwaggerInfo.Version = "v0.0.1" docs.SwaggerInfo.Host = fmt.Sprintf("%s%s", a.config.Host, a.config.Address) - docs.SwaggerInfo.BasePath = "/" } From 493d5c0768b1e21415e6c2f416e9febcf35be738 Mon Sep 17 00:00:00 2001 From: Jan Ernsting Date: Fri, 16 Aug 2024 08:41:04 +0200 Subject: [PATCH 087/129] requested changes --- cmd/answer/main.go | 4 ++-- docs/docs.go | 19 +++++++++++++++++++ docs/swagger.yaml | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/cmd/answer/main.go b/cmd/answer/main.go index a14031eca..fb91f5942 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -26,8 +26,8 @@ import ( ) // main godoc -// @title "answer" -// @description = "answer api" +// @title "apache answer" +// @description = "apache answer api" // @version = "v0.0.1" // @BasePath = "/" // @securityDefinitions.apikey ApiKeyAuth diff --git a/docs/docs.go b/docs/docs.go index fee91459c..376cdf072 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/docs/swagger.yaml b/docs/swagger.yaml index 231b271e8..88709bed1 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,3 +1,20 @@ +# 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. + basePath: = "/" definitions: constant.NotificationChannelKey: From b20e656c30b3a3dfead266c36397da78bdff4c69 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Thu, 22 Aug 2024 14:21:18 +0800 Subject: [PATCH 088/129] feat(config): add deactivate plugin config cli --- cmd/command.go | 11 ++++++++--- internal/cli/config.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/cmd/command.go b/cmd/command.go index b8d9c5611..65710e6b4 100644 --- a/cmd/command.go +++ b/cmd/command.go @@ -263,12 +263,17 @@ To run answer, use: } field := &cli.ConfigField{} - for _, f := range configFields { - switch f { + fmt.Println(configFields) + if len(configFields) > 0 { + switch configFields[0] { case "allow_password_login": field.AllowPasswordLogin = true + case "deactivate_plugin": + if len(configFields) > 1 { + field.DeactivatePluginSlugName = configFields[1] + } default: - fmt.Printf("field %s not support\n", f) + fmt.Printf("field %s not support\n", configFields[0]) } } err = cli.SetDefaultConfig(c.Data.Database, c.Data.Cache, field) diff --git a/internal/cli/config.go b/internal/cli/config.go index 06764e41a..9ebefa1da 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -31,6 +31,8 @@ import ( type ConfigField struct { AllowPasswordLogin bool `json:"allow_password_login"` + // The slug name of plugin that you want to deactivate + DeactivatePluginSlugName string `json:"deactivate_plugin_slug_name"` } // SetDefaultConfig set default config @@ -55,6 +57,9 @@ func SetDefaultConfig(dbConf *data.Database, cacheConf *data.CacheConf, field *C if field.AllowPasswordLogin { return defaultLoginConfig(db) } + if len(field.DeactivatePluginSlugName) > 0 { + return deactivatePlugin(db, field.DeactivatePluginSlugName) + } return nil } @@ -82,3 +87,37 @@ func defaultLoginConfig(x *xorm.Engine) (err error) { } return nil } + +func deactivatePlugin(x *xorm.Engine, pluginSlugName string) (err error) { + fmt.Printf("try to deactivate plugin: %s\n", pluginSlugName) + + item := &entity.Config{Key: constant.PluginStatus} + exist, err := x.Get(item) + if err != nil { + return fmt.Errorf("get config failed: %w", err) + } + if !exist { + return nil + } + + pluginStatusMapping := make(map[string]bool) + _ = json.Unmarshal([]byte(item.Value), &pluginStatusMapping) + status, ok := pluginStatusMapping[pluginSlugName] + if !ok { + fmt.Printf("plugin %s not exist\n", pluginSlugName) + return nil + } + if !status { + fmt.Printf("plugin %s already deactivated\n", pluginSlugName) + return nil + } + + pluginStatusMapping[pluginSlugName] = false + dataByte, _ := json.Marshal(pluginStatusMapping) + item.Value = string(dataByte) + _, err = x.ID(item.ID).Cols("value").Update(item) + if err != nil { + return fmt.Errorf("update plugin status failed: %w", err) + } + return nil +} From 76d455e6de6907ff49fea25071113b4cf4782c53 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 22 Aug 2024 14:21:47 +0800 Subject: [PATCH 089/129] refactor: Restore zh_CN.yaml --- i18n/zh_CN.yaml | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 207825167..396c2f9b3 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1407,7 +1407,6 @@ ui: questions: 问题 answers: 回答 users: 用户管理 - badges: 徽章管理 flags: 举报管理 settings: 站点设置 general: 一般 @@ -1835,20 +1834,6 @@ ui: msg: should_be_number: 输入必须是数字 number_larger_1: 数字应该大于等于 1 - badges: - action: 操作 - active: 激活 - all: 全部 - awards: 奖励 - deactivate: 停用 - filter: - placeholder: "按名称筛选,徽章:id" - group: 组 - inactive: 未激活 - name: 名称 - show_logs: 显示日志 - status: 状态 - title: 徽章 form: optional: (选填) empty: 不能为空 @@ -1935,4 +1920,4 @@ ui: post_reopen: 这个帖子已被重新打开. post_list: 这个帖子已经被显示 post_unlist: 这个帖子已经被隐藏 - post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。 + post_pending: 您的帖子正在等待审核。它将在它获得批准后可见。 \ No newline at end of file From 1e8072ba21951890a24c2c448638beadedf2112c Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 22 Aug 2024 14:37:49 +0800 Subject: [PATCH 090/129] refactor: Update translation for achievements --- i18n/en_US.yaml | 3 +++ .../Users/Notifications/components/Achievements/index.tsx | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 8b137d373..032ad1417 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -853,6 +853,9 @@ ui: posts: Posts invites: Invites votes: Votes + answer: Answer + question: Question + badge_award: Badge suspended: title: Your Account has been Suspended until_time: "Your account was suspended until {{ time }}." diff --git a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx index c171a4273..824bb1788 100644 --- a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx +++ b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx @@ -19,6 +19,7 @@ import { ListGroup } from 'react-bootstrap'; import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import isEmpty from 'lodash/isEmpty'; @@ -30,6 +31,7 @@ import './index.scss'; const Achievements = ({ data, handleReadNotification }) => { const { user } = loggedUserInfoStore(); + const { t } = useTranslation('translation', { keyPrefix: 'notifications' }); if (!data) { return null; @@ -87,7 +89,7 @@ const Achievements = ({ data, handleReadNotification }) => { {item.object_info.title} - {item.object_info.object_type} + {t(item.object_info.object_type)}
From 065bf980d34ea50574b713aff4e4d2f1b4d136c4 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 22 Aug 2024 15:50:40 +0800 Subject: [PATCH 091/129] chore: Add License --- ui/src/components/CardBadge/index.tsx | 1 + ui/src/components/Modal/BadgeModal.tsx | 19 +++++++++++++++++++ ui/src/pages/Badges/index.tsx | 1 + 3 files changed, 21 insertions(+) diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx index 0983f108f..dd8c18e61 100644 --- a/ui/src/components/CardBadge/index.tsx +++ b/ui/src/components/CardBadge/index.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { useTranslation } from 'react-i18next'; import { FC } from 'react'; import { Card, Badge } from 'react-bootstrap'; diff --git a/ui/src/components/Modal/BadgeModal.tsx b/ui/src/components/Modal/BadgeModal.tsx index 32049de6d..468c7f892 100644 --- a/ui/src/components/Modal/BadgeModal.tsx +++ b/ui/src/components/Modal/BadgeModal.tsx @@ -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. + */ + import { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; diff --git a/ui/src/pages/Badges/index.tsx b/ui/src/pages/Badges/index.tsx index d37f5d9a8..396280620 100644 --- a/ui/src/pages/Badges/index.tsx +++ b/ui/src/pages/Badges/index.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + import { useTranslation } from 'react-i18next'; import { CardBadge } from '@/components'; From 82d7445e62321a6cc859df15007a486b5a70b5ad Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 22 Aug 2024 16:13:44 +0800 Subject: [PATCH 092/129] fix: embed controller add error return --- internal/controller/embed_controller.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/controller/embed_controller.go b/internal/controller/embed_controller.go index a61bd68cf..eb54f1a67 100644 --- a/internal/controller/embed_controller.go +++ b/internal/controller/embed_controller.go @@ -43,10 +43,10 @@ func NewEmbedController() *EmbedController { func (c *EmbedController) GetEmbedConfig(ctx *gin.Context) { resp := make([]*plugin.EmbedConfig, 0) - _ = plugin.CallEmbed(func(embed plugin.Embed) (err error) { + err := plugin.CallEmbed(func(embed plugin.Embed) (err error) { resp, err = embed.GetEmbedConfigs(ctx) - return nil + return err }) - handler.HandleResponse(ctx, nil, resp) + handler.HandleResponse(ctx, err, resp) } From 1af1f77a60afb2adec3034e06118c0d70610fffd Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 23 Aug 2024 11:10:31 +0800 Subject: [PATCH 093/129] feat: Add RenderController to handle rendering configuration --- cmd/wire_gen.go | 3 +- internal/controller/controller.go | 1 + internal/controller/render_controller.go | 52 ++++++++++++++++++++++++ internal/router/plugin_api_router.go | 4 ++ plugin/plugin.go | 4 ++ plugin/render.go | 39 ++++++++++++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 internal/controller/render_controller.go create mode 100644 plugin/render.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index cc245aea5..940c0dc10 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -282,7 +282,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, userCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService) captchaController := controller.NewCaptchaController() embedController := controller.NewEmbedController() - pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController, captchaController, embedController) + renderController := controller.NewRenderController() + pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController, captchaController, embedController, renderController) ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, shortIDMiddleware, templateRouter, pluginAPIRouter, uiConf) scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService) application := newApplication(serverConf, ginEngine, scheduledTaskManager) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 8fad918a1..cbf80f7fa 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -52,4 +52,5 @@ var ProviderSetController = wire.NewSet( NewMetaController, NewEmbedController, NewBadgeController, + NewRenderController, ) diff --git a/internal/controller/render_controller.go b/internal/controller/render_controller.go new file mode 100644 index 000000000..05e49d0b7 --- /dev/null +++ b/internal/controller/render_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/plugin" + "github.com/gin-gonic/gin" +) + +type RenderController struct { +} + +func NewRenderController() *RenderController { + return &RenderController{} +} + +// GetRenderConfig godoc +// @Summary GetRenderConfig +// @Description GetRenderConfig +// @Tags PluginRender +// @Accept json +// @Produce json +// @Router /answer/api/v1/render/config [get] +// @Success 200 {object} handler.RespBody{data=plugin.RenderConfig} +func (c *RenderController) GetRenderConfig(ctx *gin.Context) { + var resp *plugin.RenderConfig + + _ = plugin.CallRender(func(render plugin.Render) (err error) { + resp = render.GetRenderConfig(ctx) + return nil + }) + + handler.HandleResponse(ctx, nil, resp) +} diff --git a/internal/router/plugin_api_router.go b/internal/router/plugin_api_router.go index e4a85a154..e69e0b6b0 100644 --- a/internal/router/plugin_api_router.go +++ b/internal/router/plugin_api_router.go @@ -29,6 +29,7 @@ type PluginAPIRouter struct { userCenterController *controller.UserCenterController captchaController *controller.CaptchaController embedController *controller.EmbedController + renderController *controller.RenderController } func NewPluginAPIRouter( @@ -36,12 +37,14 @@ func NewPluginAPIRouter( userCenterController *controller.UserCenterController, captchaController *controller.CaptchaController, embedController *controller.EmbedController, + renderController *controller.RenderController, ) *PluginAPIRouter { return &PluginAPIRouter{ connectorController: connectorController, userCenterController: userCenterController, captchaController: captchaController, embedController: embedController, + renderController: renderController, } } @@ -64,6 +67,7 @@ func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) { // captcha plugin r.GET("/captcha/config", pr.captchaController.GetCaptchaConfig) r.GET("/embed/config", pr.embedController.GetEmbedConfig) + r.GET("/render/config", pr.renderController.GetRenderConfig) } func (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) { diff --git a/plugin/plugin.go b/plugin/plugin.go index 852868557..9c144346a 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -103,6 +103,10 @@ func Register(p Base) { registerEmbed(p.(Embed)) } + if _, ok := p.(Render); ok { + registerRender(p.(Render)) + } + if _, ok := p.(CDN); ok { registerCDN(p.(CDN)) } diff --git a/plugin/render.go b/plugin/render.go new file mode 100644 index 000000000..4fc2edf6f --- /dev/null +++ b/plugin/render.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 plugin + +import "github.com/gin-gonic/gin" + +type RenderConfig struct { + SelectTheme string `json:"select_theme"` +} + +// select_theme + +type Render interface { + Base + GetRenderConfig(ctx *gin.Context) (renderConfig *RenderConfig) +} + +var ( + // CallReviewer is a function that calls all registered parsers + CallRender, + registerRender = MakePlugin[Render](false) +) From 6214fed8ae75ac01df5ec937348e295b032e29ed Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 23 Aug 2024 11:24:41 +0800 Subject: [PATCH 094/129] docs: Add render api --- docs/docs.go | 53 +++++++++++++++++++++++++++++++++++++++++++++-- docs/swagger.json | 53 +++++++++++++++++++++++++++++++++++++++++++++-- docs/swagger.yaml | 31 ++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 5 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index daef68513..389f3bdfa 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4854,6 +4854,41 @@ const docTemplate = `{ } } }, + "/answer/api/v1/render/config": { + "get": { + "description": "GetRenderConfig", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PluginRender" + ], + "summary": "GetRenderConfig", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plugin.RenderConfig" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/report": { "post": { "security": [ @@ -7420,6 +7455,14 @@ const docTemplate = `{ } } }, + "plugin.RenderConfig": { + "type": "object", + "properties": { + "select_theme": { + "type": "string" + } + } + }, "schema.AcceptAnswerReq": { "type": "object", "required": [ @@ -9257,10 +9300,16 @@ const docTemplate = `{ }, "schema.NotificationClearRequest": { "type": "object", + "required": [ + "type" + ], "properties": { "type": { - "description": "inbox achievement", - "type": "string" + "type": "string", + "enum": [ + "inbox", + "achievement" + ] } } }, diff --git a/docs/swagger.json b/docs/swagger.json index c7ad452e2..3d6f07c8d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4828,6 +4828,41 @@ } } }, + "/answer/api/v1/render/config": { + "get": { + "description": "GetRenderConfig", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "PluginRender" + ], + "summary": "GetRenderConfig", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/plugin.RenderConfig" + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/report": { "post": { "security": [ @@ -7394,6 +7429,14 @@ } } }, + "plugin.RenderConfig": { + "type": "object", + "properties": { + "select_theme": { + "type": "string" + } + } + }, "schema.AcceptAnswerReq": { "type": "object", "required": [ @@ -9231,10 +9274,16 @@ }, "schema.NotificationClearRequest": { "type": "object", + "required": [ + "type" + ], "properties": { "type": { - "description": "inbox achievement", - "type": "string" + "type": "string", + "enum": [ + "inbox", + "achievement" + ] } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1cf298627..d2d435041 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -135,6 +135,11 @@ definitions: platform: type: string type: object + plugin.RenderConfig: + properties: + select_theme: + type: string + type: object schema.AcceptAnswerReq: properties: answer_id: @@ -1414,8 +1419,12 @@ definitions: schema.NotificationClearRequest: properties: type: - description: inbox achievement + enum: + - inbox + - achievement type: string + required: + - type type: object schema.OnCompleteAction: properties: @@ -5817,6 +5826,26 @@ paths: summary: get reasons by object type and action tags: - reason + /answer/api/v1/render/config: + get: + consumes: + - application/json + description: GetRenderConfig + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + $ref: '#/definitions/plugin.RenderConfig' + type: object + summary: GetRenderConfig + tags: + - PluginRender /answer/api/v1/report: post: consumes: From 701343e82f6b0d934f934a018332ef1df5f7f59f Mon Sep 17 00:00:00 2001 From: Jan Ernsting Date: Fri, 23 Aug 2024 09:18:56 +0200 Subject: [PATCH 095/129] Fix error on multiple make generate runs (#1087) As discussed in #1074 --------- Co-authored-by: Fen --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 9dc8c983e..bcacf3e46 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,8 @@ universal: generate generate: @$(GO) get github.com/google/wire/cmd/wire@v0.5.0 @$(GO) get github.com/golang/mock/mockgen@v1.6.0 + @$(GO) get github.com/swaggo/swag/cmd/swag@v1.16.3 + @$(GO) install github.com/swaggo/swag/cmd/swag@v1.16.3 @$(GO) install github.com/google/wire/cmd/wire@v0.5.0 @$(GO) install github.com/golang/mock/mockgen@v1.6.0 @$(GO) generate ./... From 16605eafd0008ad42537ca4aacbf6e4a250c9c7e Mon Sep 17 00:00:00 2001 From: Jan Ernsting Date: Fri, 23 Aug 2024 09:18:56 +0200 Subject: [PATCH 096/129] Fix error on multiple make generate runs (#1087) As discussed in #1074 --------- Co-authored-by: Fen --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 9dc8c983e..bcacf3e46 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,8 @@ universal: generate generate: @$(GO) get github.com/google/wire/cmd/wire@v0.5.0 @$(GO) get github.com/golang/mock/mockgen@v1.6.0 + @$(GO) get github.com/swaggo/swag/cmd/swag@v1.16.3 + @$(GO) install github.com/swaggo/swag/cmd/swag@v1.16.3 @$(GO) install github.com/google/wire/cmd/wire@v0.5.0 @$(GO) install github.com/golang/mock/mockgen@v1.6.0 @$(GO) generate ./... From 9082623c49a6616306260c84f99b77c1659dbb8b Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Tue, 27 Aug 2024 15:54:20 +0800 Subject: [PATCH 097/129] fix(badge): remove duplicate default badges --- internal/migrations/init.go | 21 --------------------- internal/migrations/v22.go | 5 ++++- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/internal/migrations/init.go b/internal/migrations/init.go index b74e36886..4558ef60b 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -65,7 +65,6 @@ 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) @@ -128,26 +127,6 @@ 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, new(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/v22.go b/internal/migrations/v22.go index ab7185e32..1543c6add 100644 --- a/internal/migrations/v22.go +++ b/internal/migrations/v22.go @@ -52,11 +52,14 @@ func addBadges(ctx context.Context, x *xorm.Engine) (err error) { } for _, badge := range defaultBadgeTable { - exist, err := x.Context(ctx).Get(&entity.Badge{Name: badge.Name}) + beans := &entity.Badge{Name: badge.Name} + exist, err := x.Context(ctx).Get(beans) if err != nil { return err } if exist { + badge.ID = beans.ID + _, err = x.Context(ctx).ID(beans.ID).Update(badge) continue } badge.ID, err = uniqueIDRepo.GenUniqueIDStr(ctx, new(entity.Badge).TableName()) From 17c69c3cc55774f531ab4477e2c022b4d1d197eb Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 27 Aug 2024 16:15:10 +0800 Subject: [PATCH 098/129] fix: badges link url --- ui/src/pages/Badges/Detail/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/Badges/Detail/index.tsx b/ui/src/pages/Badges/Detail/index.tsx index f8a9327af..27fe9b7ed 100644 --- a/ui/src/pages/Badges/Detail/index.tsx +++ b/ui/src/pages/Badges/Detail/index.tsx @@ -71,13 +71,13 @@ const Index = () => { badges?.list?.map((item) => { const linkUrl = item.object_type === 'question' - ? `/question/${item.question_id}` + ? `/questions/${item.question_id}` : item.object_type === 'answer' - ? `/question/${item.question_id}/${item.answer_id}` + ? `/questions/${item.question_id}/${item.answer_id}` : item.object_type === 'comment' && item.answer_id - ? `/question/${item.question_id}/${item.answer_id}?commentId=${item.comment_id}` + ? `/questions/${item.question_id}/${item.answer_id}?commentId=${item.comment_id}` : item.object_type === 'comment' - ? `/question/${item.question_id}?commentId=${item.comment_id}` + ? `/questions/${item.question_id}?commentId=${item.comment_id}` : ''; return ( Date: Wed, 28 Aug 2024 15:34:40 +0800 Subject: [PATCH 099/129] refactor(badge): remove unused code --- internal/service/badge/badge_award_service.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index 02cdcf1c8..0bc7be0fe 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -273,13 +273,6 @@ func (bs *BadgeAwardService) GetUserRecentBadgeAwardList(ctx *gin.Context, req * return } -// validate user - -type userReq struct { - UserID string - Username string -} - func (bs *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) { var ( userInfo *schema.UserBasicInfo From 341b0ab8da56f3a5e4e6cf600e86ec71d8324f32 Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 30 Aug 2024 15:28:21 +0800 Subject: [PATCH 100/129] refactor: Remove unnecessary CSS property in Editor utils --- ui/src/components/Editor/utils/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/components/Editor/utils/index.ts b/ui/src/components/Editor/utils/index.ts index 0624418a8..759033cca 100644 --- a/ui/src/components/Editor/utils/index.ts +++ b/ui/src/components/Editor/utils/index.ts @@ -98,7 +98,6 @@ export const useEditor = ({ '.cm-line': { whiteSpace: 'pre-wrap', wordWrap: 'break-word', - wordBreak: 'break-all', }, '.ͼ7, .ͼ6': { textDecoration: 'none', From 23323d9346e6189a36621b32a7792b5932352964 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 30 Aug 2024 15:55:56 +0800 Subject: [PATCH 101/129] fix: When using base_url, some links are missing the base path --- ui/src/components/AccordionNav/index.tsx | 70 +++++++++++++------ .../Header/components/NavItems/index.tsx | 48 +++++++------ ui/src/components/QueryGroup/index.tsx | 8 ++- ui/src/components/SideNav/index.tsx | 18 ++--- 4 files changed, 86 insertions(+), 58 deletions(-) diff --git a/ui/src/components/AccordionNav/index.tsx b/ui/src/components/AccordionNav/index.tsx index 91c4d1c57..a0e666e17 100644 --- a/ui/src/components/AccordionNav/index.tsx +++ b/ui/src/components/AccordionNav/index.tsx @@ -20,7 +20,7 @@ import React, { FC, useEffect, useState } from 'react'; import { Accordion, Nav } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useMatch } from 'react-router-dom'; +import { useNavigate, useMatch, NavLink } from 'react-router-dom'; import classNames from 'classnames'; @@ -38,30 +38,54 @@ function MenuNode({ const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); const isLeaf = !menu.children.length; const href = isLeaf ? `${path}${menu.path}` : '#'; - return ( - { - callback(evt, menu, href, isLeaf); - }} - href={href} - className={classNames( - 'text-nowrap d-flex flex-nowrap align-items-center w-100', - { expanding, 'link-dark': activeKey !== menu.path }, - )}> - - {menu.displayName ? menu.displayName : t(menu.name)} - - {menu.badgeContent ? ( - {menu.badgeContent} - ) : null} - {!isLeaf && ( - - )} - + {isLeaf ? ( + { + callback(evt, menu, href, isLeaf); + }} + className={classNames( + 'text-nowrap d-flex flex-nowrap align-items-center w-100', + { expanding, 'link-dark': activeKey !== menu.path }, + )}> + + {menu.displayName ? menu.displayName : t(menu.name)} + + {menu.badgeContent ? ( + {menu.badgeContent} + ) : null} + {!isLeaf && ( + + )} + + ) : ( + { + callback(evt, menu, href, isLeaf); + }} + className={classNames( + 'text-nowrap d-flex flex-nowrap align-items-center w-100', + { expanding, 'link-dark': activeKey !== menu.path }, + )}> + + {menu.displayName ? menu.displayName : t(menu.name)} + + {menu.badgeContent ? ( + {menu.badgeContent} + ) : null} + {!isLeaf && ( + + )} + + )} + {menu.children.length ? ( <> diff --git a/ui/src/components/Header/components/NavItems/index.tsx b/ui/src/components/Header/components/NavItems/index.tsx index f3368b0f5..bd80729d3 100644 --- a/ui/src/components/Header/components/NavItems/index.tsx +++ b/ui/src/components/Header/components/NavItems/index.tsx @@ -35,24 +35,25 @@ interface Props { const Index: FC = ({ redDot, userInfo, logOut }) => { const { t } = useTranslation(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const navigate = useNavigate(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { agent: ucAgent } = userCenterStore(); const handleLinkClick = (evt) => { if (floppyNavigation.shouldProcessLinkClick(evt)) { evt.preventDefault(); const href = evt.currentTarget.getAttribute('href'); - navigate(href); + floppyNavigation.navigate(href, { + handler: navigate, + }); } }; return ( <> @@ -95,25 +95,31 @@ const Index: FC = ({ redDot, userInfo, logOut }) => { - 'dropdown-item'} onClick={handleLinkClick}> {t('header.nav.profile')} - - + 'dropdown-item'} onClick={handleLinkClick}> {t('header.nav.bookmark')} - - + 'dropdown-item'} onClick={handleLinkClick}> {t('header.nav.setting')} - + - logOut(e)}> + 'dropdown-item'} + onClick={(e) => logOut(e)}> {t('header.nav.logout')} - + {/* Dropdown for user center agent info */} diff --git a/ui/src/components/QueryGroup/index.tsx b/ui/src/components/QueryGroup/index.tsx index a201ae7aa..9b37c6e9c 100644 --- a/ui/src/components/QueryGroup/index.tsx +++ b/ui/src/components/QueryGroup/index.tsx @@ -24,6 +24,7 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; +import { REACT_BASE_PATH } from '@/router/alias'; import { floppyNavigation } from '@/utils'; interface Props { @@ -82,7 +83,6 @@ const Index: FC = ({ const name = typeof btn === 'string' ? btn : btn.name; return (
From 292dfe6da70de516ca67ef3cdc592043018353d7 Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 2 Sep 2024 15:32:03 +0800 Subject: [PATCH 103/129] feat(badge): feat badge description url --- i18n/en_US.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 4de696589..1a27d804c 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -539,7 +539,7 @@ backend: name: other: Autobiographer desc: - other: Filled out [profile] information. + other: Filled out profile information. certified: name: other: Certified From 90faa87e52bfee67d87e86a2426232efb906b1aa Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Wed, 28 Aug 2024 15:34:40 +0800 Subject: [PATCH 104/129] refactor(badge): remove unused code --- internal/service/badge/badge_award_service.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index 02cdcf1c8..0bc7be0fe 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -273,13 +273,6 @@ func (bs *BadgeAwardService) GetUserRecentBadgeAwardList(ctx *gin.Context, req * return } -// validate user - -type userReq struct { - UserID string - Username string -} - func (bs *BadgeAwardService) validateUserByUsername(ctx context.Context, userName string) (userID string, err error) { var ( userInfo *schema.UserBasicInfo From 6aa64092b625b9e8f0edb503207bd88a77335ddf Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 2 Sep 2024 15:32:03 +0800 Subject: [PATCH 105/129] feat(badge): feat badge description url --- i18n/en_US.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 032ad1417..2b0a47144 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -539,7 +539,7 @@ backend: name: other: Autobiographer desc: - other: Filled out [profile] information. + other: Filled out profile information. certified: name: other: Certified From 4f80f9e97b02a7329d2c24beefe869c8a8d571aa Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 2 Sep 2024 18:12:04 +0800 Subject: [PATCH 106/129] feat(badge): badge list earned return earned total --- docs/docs.go | 6 +++--- docs/swagger.json | 6 +++--- docs/swagger.yaml | 6 +++--- internal/schema/badge_schema.go | 2 +- internal/service/badge/badge_service.go | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 389f3bdfa..e5cc6f166 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -7795,7 +7795,7 @@ const docTemplate = `{ }, "earned": { "description": "badge earned count", - "type": "boolean" + "type": "integer" }, "icon": { "description": "badge icon", @@ -11472,8 +11472,8 @@ var SwaggerInfo = &swag.Spec{ Host: "", BasePath: "= \"/\"", Schemes: []string{}, - Title: "\"answer\"", - Description: "= \"answer api\"", + Title: "\"apache answer\"", + Description: "= \"apache answer api\"", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index 3d6f07c8d..5fd369073 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,8 +1,8 @@ { "swagger": "2.0", "info": { - "description": "= \"answer api\"", - "title": "\"answer\"", + "description": "= \"apache answer api\"", + "title": "\"apache answer\"", "contact": {}, "version": "= \"v0.0.1\"" }, @@ -7769,7 +7769,7 @@ }, "earned": { "description": "badge earned count", - "type": "boolean" + "type": "integer" }, "icon": { "description": "badge icon", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d2d435041..72871edcd 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -372,7 +372,7 @@ definitions: type: integer earned: description: badge earned count - type: boolean + type: integer icon: description: badge icon type: string @@ -2909,8 +2909,8 @@ definitions: type: object info: contact: {} - description: = "answer api" - title: '"answer"' + description: = "apache answer api" + title: '"apache answer"' version: = "v0.0.1" paths: /: diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go index efbcd37ad..1dd2e76f0 100644 --- a/internal/schema/badge_schema.go +++ b/internal/schema/badge_schema.go @@ -49,7 +49,7 @@ type BadgeListInfo struct { // badge award count AwardCount int `json:"award_count" ` // badge earned count - Earned bool `json:"earned" ` + Earned int64 `json:"earned" ` // badge level Level entity.BadgeLevel `json:"level" ` } diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index 031f0f152..49591adc8 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -99,11 +99,11 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* for _, badge := range badges { // check is earned - earned := false + var earned int64 = 0 if len(earnedCounts) > 0 { for _, earnedCount := range earnedCounts { if badge.ID == earnedCount.BadgeID && earnedCount.EarnedCount > 0 { - earned = true + earned = earnedCount.EarnedCount break } } From 1666b9413514fc19bc24e6629927589058ea46d8 Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 2 Sep 2024 18:26:24 +0800 Subject: [PATCH 107/129] feat(badge): badge list earned return earned total --- docs/docs.go | 2 +- docs/swagger.json | 2 +- docs/swagger.yaml | 2 +- internal/schema/badge_schema.go | 2 +- internal/service/badge/badge_service.go | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index e5cc6f166..a9d56ab97 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -7793,7 +7793,7 @@ const docTemplate = `{ "description": "badge award count", "type": "integer" }, - "earned": { + "earned_count": { "description": "badge earned count", "type": "integer" }, diff --git a/docs/swagger.json b/docs/swagger.json index 5fd369073..82ec1a6ff 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -7767,7 +7767,7 @@ "description": "badge award count", "type": "integer" }, - "earned": { + "earned_count": { "description": "badge earned count", "type": "integer" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 72871edcd..2af2900fb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -370,7 +370,7 @@ definitions: award_count: description: badge award count type: integer - earned: + earned_count: description: badge earned count type: integer icon: diff --git a/internal/schema/badge_schema.go b/internal/schema/badge_schema.go index 1dd2e76f0..a33fb480d 100644 --- a/internal/schema/badge_schema.go +++ b/internal/schema/badge_schema.go @@ -49,7 +49,7 @@ type BadgeListInfo struct { // badge award count AwardCount int `json:"award_count" ` // badge earned count - Earned int64 `json:"earned" ` + EarnedCount int64 `json:"earned_count" ` // badge level Level entity.BadgeLevel `json:"level" ` } diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index 49591adc8..3ec7b5bfb 100644 --- a/internal/service/badge/badge_service.go +++ b/internal/service/badge/badge_service.go @@ -110,12 +110,12 @@ func (b *BadgeService) ListByGroup(ctx context.Context, userID string) (resp []* } 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, - AwardCount: badge.AwardCount, - Earned: earned, - Level: badge.Level, + ID: uid.EnShortID(badge.ID), + Name: translator.Tr(handler.GetLangByCtx(ctx), badge.Name), + Icon: badge.Icon, + AwardCount: badge.AwardCount, + EarnedCount: earned, + Level: badge.Level, }) } From 8b9f5d4448f9e4a4498631d5c6c5131f4da557d4 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 2 Sep 2024 16:35:33 +0800 Subject: [PATCH 108/129] refactor: Add links to badge names and award counts in Admin Badges page --- ui/src/pages/Admin/Badges/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx index e76f90eba..4cadc1c3d 100644 --- a/ui/src/pages/Admin/Badges/index.tsx +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -19,7 +19,7 @@ import { FC } from 'react'; import { Form, Table, Stack } from 'react-bootstrap'; -import { useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; @@ -126,13 +126,15 @@ const Badges: FC = () => { /> )}
-
{badge.name}
+ {badge.name}
{badge.description}
{badge.group_name} - {badge.award_count} + + {badge.award_count} + {t(badge.status)} From afaf11ffb64af31836d4d6e6f59ca2a7fb977420 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 2 Sep 2024 16:59:28 +0800 Subject: [PATCH 109/129] refactor: Update badge labels to use multiplication symbol instead of 'x' --- i18n/en_US.yaml | 4 ++-- ui/src/components/CardBadge/index.tsx | 4 ++-- ui/src/pages/Badges/Detail/components/Badge/index.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 2b0a47144..f7c16e641 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1748,8 +1748,8 @@ ui: confirm: View badges title: Badges awarded: Awarded - earned_x: Earned x{{ number }} - x_awarded: "{{ number }} awarded" + earned_×: Earned ×{{ number }} + ×_awarded: "{{ number }} awarded" can_earn_multiple: You can earn this multiple times. earned: Earned diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx index dd8c18e61..fcb1bb9db 100644 --- a/ui/src/components/CardBadge/index.tsx +++ b/ui/src/components/CardBadge/index.tsx @@ -57,7 +57,7 @@ const Index: FC = ({ {badgePillType === 'count' && Number(data?.earned_count) > 0 && ( - x{data.earned_count} + ×{data.earned_count} )} {data.icon.startsWith('http') ? ( @@ -78,7 +78,7 @@ const Index: FC = ({
{data.name}
{showAwardedCount && (
- {t('x_awarded', { number: formatCount(data.award_count) })} + {t('×_awarded', { number: formatCount(data.award_count) })}
)} diff --git a/ui/src/pages/Badges/Detail/components/Badge/index.tsx b/ui/src/pages/Badges/Detail/components/Badge/index.tsx index affb73e6e..29da1af6a 100644 --- a/ui/src/pages/Badges/Detail/components/Badge/index.tsx +++ b/ui/src/pages/Badges/Detail/components/Badge/index.tsx @@ -73,13 +73,13 @@ const Index: FC = ({ data }) => {
{data.award_count > 0 && ( - {t('x_awarded', { number: formatCount(data.award_count) })} + {t('×_awarded', { number: formatCount(data.award_count) })} )} {data.earned_count > 0 && ( - {t('earned_x', { number: data.earned_count })} + {t('earned_×', { number: data.earned_count })} )}
From 63e1da69a1b1dea1a026f7714a2b26e8356e9fbf Mon Sep 17 00:00:00 2001 From: alvin Date: Sun, 1 Sep 2024 21:32:19 +0800 Subject: [PATCH 110/129] refactor: add err handling for activeUser --- internal/service/content/user_service.go | 2 ++ .../user_external_login/user_center_login_service.go | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/service/content/user_service.go b/internal/service/content/user_service.go index 11f3bb63b..974151a5b 100644 --- a/internal/service/content/user_service.go +++ b/internal/service/content/user_service.go @@ -527,6 +527,7 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri } if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil { log.Error(err) + return nil, err } // In the case of three-party login, the associated users are bound @@ -660,6 +661,7 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string if userInfo.MailStatus == entity.EmailStatusToBeVerified { if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil { log.Error(err) + return nil, err } } diff --git a/internal/service/user_external_login/user_center_login_service.go b/internal/service/user_external_login/user_center_login_service.go index 6089d6268..5ff47bce4 100644 --- a/internal/service/user_external_login/user_center_login_service.go +++ b/internal/service/user_external_login/user_center_login_service.go @@ -129,7 +129,9 @@ func (us *UserCenterLoginService) ExternalLogin( return nil, err } - us.activeUser(ctx, oldUserInfo) + if err := us.activeUser(ctx, oldUserInfo); err != nil { + return nil, err + } accessToken, _, err := us.userCommonService.CacheLoginUserInfo( ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) @@ -181,10 +183,12 @@ func (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider return userInfo, nil } -func (us *UserCenterLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User) { +func (us *UserCenterLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User) error { if err := us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil { log.Error(err) + return err } + return nil } func (us *UserCenterLoginService) UserCenterUserSettings(ctx context.Context, userID string) ( From ca8c4b40c653858dbcd3ad70e6c1790577bf755d Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 3 Sep 2024 11:05:03 +0800 Subject: [PATCH 111/129] refactor: Update badge labels to use multiplication symbol instead of 'x' --- ui/src/components/CardBadge/index.tsx | 10 ++++++---- ui/src/pages/Badges/Detail/components/Badge/index.tsx | 5 ++++- .../pages/Users/Personal/components/NavBar/index.tsx | 1 - 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx index fcb1bb9db..a2a0bd7da 100644 --- a/ui/src/components/CardBadge/index.tsx +++ b/ui/src/components/CardBadge/index.tsx @@ -49,14 +49,16 @@ const Index: FC = ({ className="card text-center badge-card" to={`/badges/${data.id}${urlSearchParams ? `?${urlSearchParams}` : ''}`}> - {data.earned && badgePillType === 'earned' && ( + {Number(data?.earned_count) > 0 && badgePillType === 'earned' && ( - {t('earned')} + {`${t('earned')}${ + Number(data?.earned_count) > 1 ? ` ×${data.earned_count}` : '' + }`} )} - {badgePillType === 'count' && Number(data?.earned_count) > 0 && ( - + {badgePillType === 'count' && Number(data?.earned_count) > 1 && ( + ×{data.earned_count} )} diff --git a/ui/src/pages/Badges/Detail/components/Badge/index.tsx b/ui/src/pages/Badges/Detail/components/Badge/index.tsx index 29da1af6a..a554a6bca 100644 --- a/ui/src/pages/Badges/Detail/components/Badge/index.tsx +++ b/ui/src/pages/Badges/Detail/components/Badge/index.tsx @@ -77,11 +77,14 @@ const Index: FC = ({ data }) => {
)} - {data.earned_count > 0 && ( + {data.earned_count > 1 && ( {t('earned_×', { number: data.earned_count })} )} + {data.earned_count === 1 && ( + {t('earned')} + )}
)}
diff --git a/ui/src/pages/Users/Personal/components/NavBar/index.tsx b/ui/src/pages/Users/Personal/components/NavBar/index.tsx index 5f4360ed8..9c6d157a4 100644 --- a/ui/src/pages/Users/Personal/components/NavBar/index.tsx +++ b/ui/src/pages/Users/Personal/components/NavBar/index.tsx @@ -60,7 +60,6 @@ const list = [ name: 'votes', }, { - role: 'self', // Only visible to author path: '/badges', name: 'badges', }, From d72cf56b8c4d2827977547cbc3f19fed9b1401f0 Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 3 Sep 2024 11:58:57 +0800 Subject: [PATCH 112/129] refactor: Add text truncation to TopList component link --- ui/src/pages/Users/Personal/components/TopList/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/pages/Users/Personal/components/TopList/index.tsx b/ui/src/pages/Users/Personal/components/TopList/index.tsx index 045534288..aa896caf1 100644 --- a/ui/src/pages/Users/Personal/components/TopList/index.tsx +++ b/ui/src/pages/Users/Personal/components/TopList/index.tsx @@ -38,6 +38,7 @@ const Index: FC = ({ data, type }) => { className={`${index === data.length - 1 ? '' : 'mb-2'}`} key={type === 'answer' ? item.answer_id : item.question_id}> Date: Tue, 3 Sep 2024 12:28:01 +0800 Subject: [PATCH 113/129] refactor: Update badge description rendering in Admin Badges page --- ui/src/pages/Admin/Badges/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx index 4cadc1c3d..29661714c 100644 --- a/ui/src/pages/Admin/Badges/index.tsx +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -127,7 +127,12 @@ const Badges: FC = () => { )}
{badge.name} -
{badge.description}
+
From 41d63bd245cd59b3866c94bcd67c9dab6e02a192 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Tue, 3 Sep 2024 14:16:17 +0800 Subject: [PATCH 114/129] feat(badge): delete red dot when no unread notification --- .../repo/notification/notification_repo.go | 8 ++++++ .../notification/notification_service.go | 25 ++++++++++++++----- .../notification_common/notification.go | 15 +++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/internal/repo/notification/notification_repo.go b/internal/repo/notification/notification_repo.go index bd325ef27..5a2a6cc7a 100644 --- a/internal/repo/notification/notification_repo.go +++ b/internal/repo/notification/notification_repo.go @@ -129,3 +129,11 @@ func (nr *notificationRepo) GetNotificationPage(ctx context.Context, searchCond } return } + +func (nr *notificationRepo) CountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error) { + count, err := nr.data.DB.Context(ctx).Count(cond) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return count, err +} diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index b73d4fdad..be2963fe8 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -23,12 +23,6 @@ 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" - "github.com/apache/incubator-answer/pkg/converter" - "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/data" "github.com/apache/incubator-answer/internal/base/handler" @@ -36,8 +30,13 @@ 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" notficationcommon "github.com/apache/incubator-answer/internal/service/notification_common" + "github.com/apache/incubator-answer/internal/service/report_common" + "github.com/apache/incubator-answer/internal/service/review" "github.com/apache/incubator-answer/internal/service/revision_common" + usercommon "github.com/apache/incubator-answer/internal/service/user_common" + "github.com/apache/incubator-answer/pkg/converter" "github.com/apache/incubator-answer/pkg/uid" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/log" @@ -203,6 +202,20 @@ func (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string, if err != nil { log.Errorf("remove badge award alert cache failed: %v", err) } + + amount, err := ns.notificationRepo.CountNotificationByUser(ctx, &entity.Notification{ + UserID: userID, + Type: notificationInfo.Type, + IsRead: schema.NotificationNotRead, + Status: schema.NotificationStatusNormal, + }) + if err != nil { + log.Errorf("count notification failed: %v", err) + return nil + } + if amount == 0 { + _ = ns.notificationCommon.DeleteRedDot(ctx, userID, notificationInfo.Type) + } return nil } diff --git a/internal/service/notification_common/notification.go b/internal/service/notification_common/notification.go index a3129b3a4..f3732fcfa 100644 --- a/internal/service/notification_common/notification.go +++ b/internal/service/notification_common/notification.go @@ -54,6 +54,7 @@ type NotificationRepo interface { GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error) UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error) GetById(ctx context.Context, id string) (*entity.Notification, bool, error) + CountNotificationByUser(ctx context.Context, cond *entity.Notification) (int64, error) } type NotificationCommon struct { @@ -226,6 +227,20 @@ func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, noti return nil } +func (ns *NotificationCommon) DeleteRedDot(ctx context.Context, userID string, notificationType int) error { + var key string + if notificationType == schema.NotificationTypeInbox { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) + } else { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) + } + err := ns.data.Cache.Del(ctx, key) + 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) From 8a31d29ce7ddea8a256f92dbf2cef2b0da20413f Mon Sep 17 00:00:00 2001 From: lhpqaq <657407891@qq.com> Date: Tue, 3 Sep 2024 12:48:47 +0800 Subject: [PATCH 115/129] Fix ListNewestEarned in high version MySQL --- internal/repo/badge_award/badge_award_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 377677d19..3a04fd22b 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -139,7 +139,7 @@ func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeI 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"). + Select("any_value(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"). From 70acbe38e053863e02ab033ea7b896a7636f0352 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 3 Sep 2024 14:34:58 +0800 Subject: [PATCH 116/129] fix: styles --- ui/src/components/CardBadge/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/CardBadge/index.tsx b/ui/src/components/CardBadge/index.tsx index a2a0bd7da..214e3f5d0 100644 --- a/ui/src/components/CardBadge/index.tsx +++ b/ui/src/components/CardBadge/index.tsx @@ -79,7 +79,7 @@ const Index: FC = ({
{data.name}
{showAwardedCount && ( -
+
{t('×_awarded', { number: formatCount(data.award_count) })}
)} From fec34ae974f30b4707dce507a13d307d8333096a Mon Sep 17 00:00:00 2001 From: kumfo Date: Tue, 3 Sep 2024 14:43:35 +0800 Subject: [PATCH 117/129] feat(badge): delete useless field --- internal/entity/badge_award_entity.go | 1 - internal/repo/badge_award/badge_award_repo.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index 5ba475cd9..f18138bee 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -57,7 +57,6 @@ func (BadgeEarnedCount) TableName() string { 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"` diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 3a04fd22b..64615d6d8 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -139,7 +139,7 @@ func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeI 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("any_value(user_id), badge_id, max(created_at) created,count(*) earned_count"). + Select("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"). From 86cde0e983da102f6bdb4be4ced05967ce40a668 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 3 Sep 2024 14:45:13 +0800 Subject: [PATCH 118/129] fix: style --- ui/src/pages/Questions/Detail/components/Reactions/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/Questions/Detail/components/Reactions/index.tsx b/ui/src/pages/Questions/Detail/components/Reactions/index.tsx index a98f2fd03..8fa09509f 100644 --- a/ui/src/pages/Questions/Detail/components/Reactions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Reactions/index.tsx @@ -110,7 +110,7 @@ const Index: FC = ({ return (
{showAddCommentBtn && ( From a36d2e810d84835ae17c9b01787b756814fe188d Mon Sep 17 00:00:00 2001 From: kumfo Date: Tue, 3 Sep 2024 14:43:35 +0800 Subject: [PATCH 119/129] feat(badge): delete useless field --- internal/entity/badge_award_entity.go | 1 - internal/repo/badge_award/badge_award_repo.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index 5ba475cd9..f18138bee 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -57,7 +57,6 @@ func (BadgeEarnedCount) TableName() string { 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"` diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 3a04fd22b..64615d6d8 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -139,7 +139,7 @@ func (r *badgeAwardRepo) ListPagedByBadgeIdAndUserId(ctx context.Context, badgeI 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("any_value(user_id), badge_id, max(created_at) created,count(*) earned_count"). + Select("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"). From a2c725abbbbd6d17639efd7b42c9fe2c1cd14a27 Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 3 Sep 2024 14:30:49 +0800 Subject: [PATCH 120/129] refactor: Update BadgeOperation component to conditionally render dropdown items based on badge status --- ui/src/pages/Admin/Badges/components/Action/index.tsx | 11 ++++++++--- ui/src/pages/Admin/Badges/index.tsx | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/Admin/Badges/components/Action/index.tsx b/ui/src/pages/Admin/Badges/components/Action/index.tsx index b5bf8ddd7..023e626fa 100644 --- a/ui/src/pages/Admin/Badges/components/Action/index.tsx +++ b/ui/src/pages/Admin/Badges/components/Action/index.tsx @@ -23,9 +23,10 @@ import { useTranslation } from 'react-i18next'; import { Icon } from '@/components'; interface Props { + status: string; onSelect: (eventKey: string | null) => void; } -const BadgeOperation = ({ onSelect }: Props) => { +const BadgeOperation = ({ onSelect, status }: Props) => { const { t } = useTranslation('translation', { keyPrefix: 'admin.badges' }); return ( @@ -35,8 +36,12 @@ const BadgeOperation = ({ onSelect }: Props) => { - {t('active')} - {t('deactivate')} + {status === 'inactive' && ( + {t('active')} + )} + {status === 'active' && ( + {t('deactivate')} + )} diff --git a/ui/src/pages/Admin/Badges/index.tsx b/ui/src/pages/Admin/Badges/index.tsx index 29661714c..9a86eed92 100644 --- a/ui/src/pages/Admin/Badges/index.tsx +++ b/ui/src/pages/Admin/Badges/index.tsx @@ -146,6 +146,7 @@ const Badges: FC = () => { handleBadgeStatus(badge.id, status)} /> From daab3eebee5e4bd8776a9424ae5f651ed7c38cae Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 3 Sep 2024 15:03:28 +0800 Subject: [PATCH 121/129] feat: Add animation to badgeModal --- ui/src/components/Modal/BadgeModal.tsx | 40 +++++- ui/src/components/Modal/Modal.tsx | 3 + ui/src/utils/animateGift.ts | 183 +++++++++++++++++++++++++ ui/src/utils/progress.ts | 77 +++++++++++ 4 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 ui/src/utils/animateGift.ts create mode 100644 ui/src/utils/progress.ts diff --git a/ui/src/components/Modal/BadgeModal.tsx b/ui/src/components/Modal/BadgeModal.tsx index 468c7f892..303c96778 100644 --- a/ui/src/components/Modal/BadgeModal.tsx +++ b/ui/src/components/Modal/BadgeModal.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { FC } from 'react'; +import { FC, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -26,6 +26,7 @@ import classNames from 'classnames'; import type * as Type from '@/common/interface'; import { loggedUserInfoStore } from '@/stores'; import { readNotification, useQueryNotificationStatus } from '@/services'; +import AnimateGift from '@/utils/animateGift'; import Icon from '../Icon'; import Modal from './Modal'; @@ -34,6 +35,10 @@ interface BadgeModalProps { badge?: Type.NotificationBadgeAward | null; visible: boolean; } + +let bg1: AnimateGift; +let bg2: AnimateGift; +let timeout: NodeJS.Timeout; const BadgeModal: FC = ({ badge, visible }) => { const { t } = useTranslation('translation', { keyPrefix: 'badges.modal' }); const { user } = loggedUserInfoStore(); @@ -47,6 +52,9 @@ const BadgeModal: FC = ({ badge, visible }) => { ...data, badge_award: null, }); + clearTimeout(timeout); + bg1?.destroy(); + bg2?.destroy(); }; const handleCancel = async () => { await handle(); @@ -58,8 +66,38 @@ const BadgeModal: FC = ({ badge, visible }) => { navigate(url); }; + useEffect(() => { + const DURATION = 8000; + const LENGTH = 200; + const bgNode = document.documentElement || document.body; + + if (visible) { + const paranetNode = document.getElementById('badgeModal')?.parentNode; + + bg1 = new AnimateGift({ + elm: paranetNode, + width: bgNode.clientWidth, + height: bgNode.clientHeight, + length: LENGTH, + duration: DURATION, + isLoop: true, + }); + + timeout = setTimeout(() => { + bg2 = new AnimateGift({ + elm: paranetNode, + width: window.innerWidth, + height: window.innerHeight, + length: LENGTH, + duration: DURATION, + }); + }, DURATION / 2); + } + }, [visible]); + return ( = ({ + id = '', title = 'title', visible = false, centered = true, @@ -63,6 +65,7 @@ const Index: FC = ({ const { t } = useTranslation(); return ( = 1 && this.isLoop) { + this.start(now); + } + + return this.progress; + } + return 0; + } +} + +export default Progress; From 052b78be28622fb3452698e91137aab52b24923f Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Wed, 4 Sep 2024 10:08:07 +0800 Subject: [PATCH 122/129] fix(badge): remove red dot only when the badge notification has been read --- .../notification/notification_service.go | 18 ++-------- .../notification_common/notification.go | 36 ++++++++++++++++++- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index be2963fe8..0860c894f 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -163,9 +163,7 @@ func (ns *NotificationService) countAllReviewAmount(ctx context.Context, req *sc } func (ns *NotificationService) ClearRedDot(ctx context.Context, req *schema.NotificationClearRequest) (*schema.RedDot, error) { - key := fmt.Sprintf(constant.RedDotCacheKey, req.NotificationType, req.UserID) - _ = ns.data.Cache.Del(ctx, key) - + _ = ns.notificationCommon.DeleteRedDot(ctx, req.UserID, schema.NotificationType[req.NotificationType]) resp := &schema.GetRedDot{} _ = copier.Copy(resp, req) return ns.GetRedDot(ctx, resp) @@ -203,19 +201,7 @@ func (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string, log.Errorf("remove badge award alert cache failed: %v", err) } - amount, err := ns.notificationRepo.CountNotificationByUser(ctx, &entity.Notification{ - UserID: userID, - Type: notificationInfo.Type, - IsRead: schema.NotificationNotRead, - Status: schema.NotificationStatusNormal, - }) - if err != nil { - log.Errorf("count notification failed: %v", err) - return nil - } - if amount == 0 { - _ = ns.notificationCommon.DeleteRedDot(ctx, userID, notificationInfo.Type) - } + _ = ns.notificationCommon.DecreaseRedDot(ctx, userID, notificationInfo.Type) return nil } diff --git a/internal/service/notification_common/notification.go b/internal/service/notification_common/notification.go index f3732fcfa..9d08e2582 100644 --- a/internal/service/notification_common/notification.go +++ b/internal/service/notification_common/notification.go @@ -220,10 +220,44 @@ func (ns *NotificationCommon) addRedDot(ctx context.Context, userID string, noti } else { key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) } - err := ns.data.Cache.SetInt64(ctx, key, 1, constant.RedDotCacheTime) + _, exist, err := ns.data.Cache.GetInt64(ctx, key) if err != nil { return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } + if exist { + if _, err := ns.data.Cache.Increase(ctx, key, 1); err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + return nil + } + err = ns.data.Cache.SetInt64(ctx, key, 1, constant.RedDotCacheTime) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + return nil +} + +func (ns *NotificationCommon) DecreaseRedDot(ctx context.Context, userID string, notificationType int) error { + var key string + if notificationType == schema.NotificationTypeInbox { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeInbox, userID) + } else { + key = fmt.Sprintf(constant.RedDotCacheKey, constant.NotificationTypeAchievement, userID) + } + _, exist, err := ns.data.Cache.GetInt64(ctx, key) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if !exist { + return nil + } + res, err := ns.data.Cache.Decrease(ctx, key, 1) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + if res <= 0 { + return ns.DeleteRedDot(ctx, userID, notificationType) + } return nil } From eacb79e5120f46267b78c4713358ab7bef9e8d79 Mon Sep 17 00:00:00 2001 From: ferrischi201 Date: Thu, 18 Jul 2024 14:40:03 -0400 Subject: [PATCH 123/129] feat(recommend): add tag-based recommendations in question page Filter out deleted or hidden questions and support multi databases and filter out deleted or hidden questions. For issue:1003. --- cmd/wire_gen.go | 4 +- go.mod | 4 +- go.sum | 9 +- i18n/en_US.yaml | 1 + i18n/no_NO.yaml | 1 + internal/controller/question_controller.go | 15 ++ .../repo/activity_common/activity_repo.go | 13 ++ internal/repo/question/question_repo.go | 51 +++++ internal/repo/repo_test/recommend_test.go | 215 ++++++++++++++++++ internal/router/answer_api_router.go | 1 + internal/schema/question_schema.go | 2 +- internal/service/activity_common/activity.go | 1 + internal/service/content/question_service.go | 53 ++++- internal/service/question_common/question.go | 1 + ui/src/common/interface.ts | 1 + ui/src/components/QuestionList/index.tsx | 6 +- ui/src/pages/Questions/index.tsx | 12 +- ui/src/pages/Tags/Detail/index.tsx | 1 + ui/src/services/client/question.ts | 15 ++ 19 files changed, 391 insertions(+), 15 deletions(-) create mode 100644 internal/repo/repo_test/recommend_test.go diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 4928a122b..31e6d94e1 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -189,12 +189,13 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, rateLimitMiddleware := middleware.NewRateLimitMiddleware(limitRepo) commentController := controller.NewCommentController(commentService, rankService, captchaService, rateLimitMiddleware) reportRepo := report.NewReportRepo(dataData, uniqueIDRepo) + tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService) answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService) answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService) 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) + questionService := content.NewQuestionService(activityRepo, questionRepo, answerRepo, tagCommonService, tagService, 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) reportHandle := report_handle.NewReportHandle(questionService, answerService, commentService) reportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService) @@ -202,7 +203,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, contentVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService) voteService := content.NewVoteService(contentVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService) voteController := controller.NewVoteController(voteService, rankService, captchaService) - tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService) tagController := controller.NewTagController(tagService, tagCommonService, rankService) followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo) diff --git a/go.mod b/go.mod index 036a2b42f..8b886a09f 100644 --- a/go.mod +++ b/go.mod @@ -147,8 +147,8 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.uber.org/atomic v1.10.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.23.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.24.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/sys v0.18.0 // indirect diff --git a/go.sum b/go.sum index fb49b88b6..ead9468d5 100644 --- a/go.sum +++ b/go.sum @@ -752,21 +752,20 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= -go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index a123d70d7..eebfde2a4 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1290,6 +1290,7 @@ ui: newest: Newest active: Active hot: Hot + recommend: Recommend score: Score unanswered: Unanswered modified: modified diff --git a/i18n/no_NO.yaml b/i18n/no_NO.yaml index c91f56b05..859f1ceaf 100644 --- a/i18n/no_NO.yaml +++ b/i18n/no_NO.yaml @@ -859,6 +859,7 @@ ui: newest: Newest active: Active hot: Hot + recommend: Recommend score: Score unanswered: Unanswered modified: modified diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 1b0dee92a..725a24afc 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -340,6 +340,21 @@ func (qc *QuestionController) QuestionPage(ctx *gin.Context) { handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) } +func (qc *QuestionController) QuestionRecommendPage(ctx *gin.Context) { + req := &schema.QuestionPageReq{} + if handler.BindAndCheck(ctx, req) { + return + } + req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + + questions, total, err := qc.questionService.GetRecommendQuestionPage(ctx, req) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) +} + // AddQuestion add question // @Summary add question // @Description add question diff --git a/internal/repo/activity_common/activity_repo.go b/internal/repo/activity_common/activity_repo.go index 70f49962c..9f0a59e4d 100644 --- a/internal/repo/activity_common/activity_repo.go +++ b/internal/repo/activity_common/activity_repo.go @@ -107,6 +107,19 @@ func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session, return } +func (ar *ActivityRepo) GetUserActivitysByActivityType(ctx context.Context, userID string, activityType int) ( + activityList []*entity.Activity, err error) { + activityList = make([]*entity.Activity, 0) + err = ar.data.DB.Context(ctx).Where("user_id = ?", userID). + And("activity_type = ?", activityType). + And("cancelled = 0"). + Find(&activityList) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + func (ar *ActivityRepo) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) { sum := &entity.ActivityRankSum{} _, err := ar.data.DB.Context(ctx).Table(entity.Activity{}.TableName()). diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 83a841573..aa3694d04 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -401,6 +401,57 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, return questionList, total, err } +// GetRecommendQuestionPageByTags get recommend question page by tags +func (qr *questionRepo) GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) ( + questionList []*entity.Question, total int64, err error) { + questionList = make([]*entity.Question, 0) + orderBySQL := "question.pin DESC, question.created_at DESC" + + // Please Make sure every question has at least one tag + session := qr.data.DB.Context(ctx).Select(entity.Question{}.TableName() + ".*") + + if len(tagIDs) > 0 { + session.Where("question.user_id != ?", userID). + And("question.id NOT IN (SELECT question_id FROM answer WHERE user_id = ?)", userID). + Join("INNER", "tag_rel", "question.id = tag_rel.object_id"). + And("tag_rel.status = ?", entity.TagRelStatusAvailable). + Join("INNER", "tag", "tag.id = tag_rel.tag_id"). + In("tag.id", tagIDs) + } else if len(followedQuestionIDs) == 0 { + return questionList, 0, nil + } + + if len(followedQuestionIDs) > 0 { + idStr := "'" + strings.Join(followedQuestionIDs, "','") + "'" + orderBySQL = fmt.Sprintf("CASE WHEN question.id IN (%s) THEN 0 ELSE 1 END, ", idStr) + orderBySQL + if len(tagIDs) > 0 { + // if tags provided, show followed questions and tag questions + session.Or(builder.In("question.id", followedQuestionIDs)) + } else { + // if no tags, only show followed questions + session.Where(builder.In("question.id", followedQuestionIDs)) + } + } + + session. + And("question.show = ? and question.status = ?", entity.QuestionShow, entity.QuestionStatusAvailable). + Distinct("question.id"). + OrderBy(orderBySQL) + + total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + + if handler.GetEnableShortID(ctx) { + for _, item := range questionList { + item.ID = uid.EnShortID(item.ID) + } + } + + return questionList, total, err +} + func (qr *questionRepo) AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) { var ( count int64 diff --git a/internal/repo/repo_test/recommend_test.go b/internal/repo/repo_test/recommend_test.go new file mode 100644 index 000000000..6c87dfe74 --- /dev/null +++ b/internal/repo/repo_test/recommend_test.go @@ -0,0 +1,215 @@ +/* + * 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 repo_test + +import ( + "context" + "testing" + + "github.com/apache/incubator-answer/internal/entity" + "github.com/apache/incubator-answer/internal/repo/activity" + "github.com/apache/incubator-answer/internal/repo/activity_common" + "github.com/apache/incubator-answer/internal/repo/config" + "github.com/apache/incubator-answer/internal/repo/question" + "github.com/apache/incubator-answer/internal/repo/tag" + "github.com/apache/incubator-answer/internal/repo/tag_common" + "github.com/apache/incubator-answer/internal/repo/unique" + "github.com/apache/incubator-answer/internal/repo/user" + config2 "github.com/apache/incubator-answer/internal/service/config" + "github.com/stretchr/testify/assert" +) + +func Test_questionRepo_GetRecommend(t *testing.T) { + var ( + uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) + questionRepo = question.NewQuestionRepo(testDataSource, uniqueIDRepo) + userRepo = user.NewUserRepo(testDataSource) + tagRelRepo = tag.NewTagRelRepo(testDataSource, uniqueIDRepo) + tagRepo = tag.NewTagRepo(testDataSource, uniqueIDRepo) + tagCommenRepo = tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo) + configRepo = config.NewConfigRepo(testDataSource) + configService = config2.NewConfigService(configRepo) + activityCommonRepo = activity_common.NewActivityRepo(testDataSource, uniqueIDRepo, configService) + followRepo = activity.NewFollowRepo(testDataSource, uniqueIDRepo, activityCommonRepo) + ) + + // create question and user + user := &entity.User{ + Username: "ferrischi201", + Pass: "ferrischi201", + EMail: "ferrischi201@example.com", + MailStatus: entity.EmailStatusAvailable, + Status: entity.UserStatusAvailable, + DisplayName: "ferrischi201", + IsAdmin: false, + } + err := userRepo.AddUser(context.TODO(), user) + assert.NoError(t, err) + assert.NotEqual(t, "", user.ID) + + questions := make([]*entity.Question, 0) + // tag, unjoin, unfollow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Valid recommendation 1", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // tag, unjoin, follow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Valid recommendation 2", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // tag, join, unfollow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Invalid recommendation 1", + OriginalText: "A go question 1", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // tag, join, follow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Valid recommendation 3", + OriginalText: "A java question", + ParsedText: "Java question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, unjoin, unfollow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Invalid recommendation 2", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, unjoin, follow + questions = append(questions, &entity.Question{ + UserID: "1", + Title: "Valid recommendation 4", + OriginalText: "A go question", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, join, unfollow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Invalid recommendation 3", + OriginalText: "A go question 1", + ParsedText: "Go question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + // untag, join, follow + questions = append(questions, &entity.Question{ + UserID: user.ID, + Title: "Valid recommendation 5", + OriginalText: "A java question", + ParsedText: "Java question", + Status: entity.QuestionStatusAvailable, + Show: entity.QuestionShow, + }) + + for _, question := range questions { + err = questionRepo.AddQuestion(context.TODO(), question) + assert.NoError(t, err) + assert.NotEqual(t, "", question.ID) + } + + tags := []*entity.Tag{ + { + SlugName: "go", + DisplayName: "Golang", + OriginalText: "golang", + ParsedText: "

golang

", + Status: entity.TagStatusAvailable, + }, + { + SlugName: "java", + DisplayName: "Java", + OriginalText: "java", + ParsedText: "

java

", + Status: entity.TagStatusAvailable, + }, + } + err = tagCommenRepo.AddTagList(context.TODO(), tags) + assert.NoError(t, err) + + tagRels := make([]*entity.TagRel, 0) + for i, question := range questions { + tagRel := &entity.TagRel{ + TagID: tags[i/4].ID, + ObjectID: question.ID, + Status: entity.TagRelStatusAvailable, + } + tagRels = append(tagRels, tagRel) + } + err = tagRelRepo.AddTagRelList(context.TODO(), tagRels) + assert.NoError(t, err) + + followQuestionIDs := make([]string, 0) + for i := range questions { + if i%2 == 0 { + continue + } + err = followRepo.Follow(context.TODO(), questions[i].ID, user.ID) + assert.NoError(t, err) + followQuestionIDs = append(followQuestionIDs, questions[i].ID) + } + + // get recommend + questionList, total, err := questionRepo.GetRecommendQuestionPageByTags(context.TODO(), user.ID, []string{tags[0].ID}, followQuestionIDs, 1, 20) + assert.NoError(t, err) + assert.Equal(t, int64(5), total) + assert.Equal(t, 5, len(questionList)) + + // recovery + t.Cleanup(func() { + tagRelIDs := make([]int64, 0) + for i, tagRel := range tagRels { + if i%2 == 1 { + err = followRepo.FollowCancel(context.TODO(), questions[i].ID, user.ID) + assert.NoError(t, err) + } + tagRelIDs = append(tagRelIDs, tagRel.ID) + } + err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), tagRelIDs) + assert.NoError(t, err) + for _, tag := range tags { + err = tagRepo.RemoveTag(context.TODO(), tag.ID) + assert.NoError(t, err) + } + for _, q := range questions { + err = questionRepo.RemoveQuestion(context.TODO(), q.ID) + assert.NoError(t, err) + } + }) +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 328541868..4434a8d63 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -159,6 +159,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/question/info", a.questionController.GetQuestion) r.GET("/question/invite", a.questionController.GetQuestionInviteUserInfo) r.GET("/question/page", a.questionController.QuestionPage) + r.GET("/question/recommend/page", a.questionController.QuestionRecommendPage) r.GET("/question/similar/tag", a.questionController.SimilarQuestion) r.GET("/personal/qa/top", a.questionController.UserTop) r.GET("/personal/question/page", a.questionController.PersonalQuestionPage) diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index a4c6ee8c1..2a6e5f122 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -355,7 +355,7 @@ const ( type QuestionPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` PageSize int `validate:"omitempty,min=1" form:"page_size"` - OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered" form:"order"` + OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend" form:"order"` Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"` Username string `validate:"omitempty,gt=0,lte=100" form:"username"` InDays int `validate:"omitempty,min=1" form:"in_days"` diff --git a/internal/service/activity_common/activity.go b/internal/service/activity_common/activity.go index db565f1e0..6b3557c0d 100644 --- a/internal/service/activity_common/activity.go +++ b/internal/service/activity_common/activity.go @@ -37,6 +37,7 @@ type ActivityRepo interface { GetActivityTypeByObjectType(ctx context.Context, objectKey, action string) (activityType int, err error) GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) ( existsActivity *entity.Activity, exist bool, err error) + GetUserActivitysByActivityType(ctx context.Context, userID string, activityType int) (activityList []*entity.Activity, err error) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) AddActivity(ctx context.Context, activity *entity.Activity) (err error) diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go index 6d8b37ba9..83bd7bedc 100644 --- a/internal/service/content/question_service.go +++ b/internal/service/content/question_service.go @@ -22,7 +22,6 @@ package content import ( "encoding/json" "fmt" - answercommon "github.com/apache/incubator-answer/internal/service/answer_common" "strings" "time" @@ -35,11 +34,13 @@ import ( "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/internal/schema" "github.com/apache/incubator-answer/internal/service/activity" + "github.com/apache/incubator-answer/internal/service/activity_common" "github.com/apache/incubator-answer/internal/service/activity_queue" + answercommon "github.com/apache/incubator-answer/internal/service/answer_common" collectioncommon "github.com/apache/incubator-answer/internal/service/collection_common" "github.com/apache/incubator-answer/internal/service/config" "github.com/apache/incubator-answer/internal/service/export" - "github.com/apache/incubator-answer/internal/service/meta_common" + metacommon "github.com/apache/incubator-answer/internal/service/meta_common" "github.com/apache/incubator-answer/internal/service/notice_queue" "github.com/apache/incubator-answer/internal/service/notification" "github.com/apache/incubator-answer/internal/service/permission" @@ -48,6 +49,7 @@ import ( "github.com/apache/incubator-answer/internal/service/revision_common" "github.com/apache/incubator-answer/internal/service/role" "github.com/apache/incubator-answer/internal/service/siteinfo_common" + "github.com/apache/incubator-answer/internal/service/tag" tagcommon "github.com/apache/incubator-answer/internal/service/tag_common" usercommon "github.com/apache/incubator-answer/internal/service/user_common" "github.com/apache/incubator-answer/pkg/checker" @@ -65,9 +67,11 @@ import ( // QuestionService user service type QuestionService struct { + activityRepo activity_common.ActivityRepo questionRepo questioncommon.QuestionRepo answerRepo answercommon.AnswerRepo tagCommon *tagcommon.TagCommonService + tagService *tag.TagService questioncommon *questioncommon.QuestionCommon userCommon *usercommon.UserCommon userRepo usercommon.UserRepo @@ -87,9 +91,11 @@ type QuestionService struct { } func NewQuestionService( + activityRepo activity_common.ActivityRepo, questionRepo questioncommon.QuestionRepo, answerRepo answercommon.AnswerRepo, tagCommon *tagcommon.TagCommonService, + tagService *tag.TagService, questioncommon *questioncommon.QuestionCommon, userCommon *usercommon.UserCommon, userRepo usercommon.UserRepo, @@ -108,9 +114,11 @@ func NewQuestionService( configService *config.ConfigService, ) *QuestionService { return &QuestionService{ + activityRepo: activityRepo, questionRepo: questionRepo, answerRepo: answerRepo, tagCommon: tagCommon, + tagService: tagService, questioncommon: questioncommon, userCommon: userCommon, userRepo: userRepo, @@ -1348,6 +1356,47 @@ func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.Ques return questions, total, nil } +// GetRecommendQuestionPage retrieves recommended question page based on following tags and questions. +func (qs *QuestionService) GetRecommendQuestionPage(ctx context.Context, req *schema.QuestionPageReq) ( + questions []*schema.QuestionPageResp, total int64, err error) { + followingTagsResp, err := qs.tagService.GetFollowingTags(ctx, req.LoginUserID) + if err != nil { + return nil, 0, err + } + tagIDs := make([]string, 0, len(followingTagsResp)) + for _, tag := range followingTagsResp { + tagIDs = append(tagIDs, tag.TagID) + } + + activityType, err := qs.activityRepo.GetActivityTypeByObjectType(ctx, constant.QuestionObjectType, "follow") + if err != nil { + return nil, 0, err + } + activities, err := qs.activityRepo.GetUserActivitysByActivityType(ctx, req.LoginUserID, activityType) + if err != nil { + return nil, 0, err + } + + followedQuestionIDs := make([]string, 0, len(activities)) + for _, activity := range activities { + if activity.Cancelled == entity.ActivityCancelled { + continue + } + followedQuestionIDs = append(followedQuestionIDs, activity.ObjectID) + } + questionList, total, err := qs.questionRepo.GetRecommendQuestionPageByTags(ctx, req.LoginUserID, tagIDs, followedQuestionIDs, req.Page, req.PageSize) + if err != nil { + return nil, 0, err + } + + questions, err = qs.questioncommon.FormatQuestionsPage(ctx, questionList, req.LoginUserID, "frequent") + if err != nil { + return nil, 0, err + } + + return questions, total, nil +} + func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, req *schema.AdminUpdateQuestionStatusReq) error { setStatus, ok := entity.AdminQuestionSearchStatus[req.Status] if !ok { diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index b9f2fd539..5dab12144 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -57,6 +57,7 @@ type QuestionRepo interface { GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error) GetQuestionPage(ctx context.Context, page, pageSize int, tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) ( questionList []*entity.Question, total int64, err error) + GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (questionList []*entity.Question, total int64, err error) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) RecoverQuestion(ctx context.Context, questionID string) (err error) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index ff7c09027..093680e05 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -288,6 +288,7 @@ export interface LangsType { * @description interface for Question */ export type QuestionOrderBy = + | 'recommend' | 'newest' | 'active' | 'hot' diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index d9b46dd26..c41f4b9dc 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -43,11 +43,13 @@ export const QUESTION_ORDER_KEYS: Type.QuestionOrderBy[] = [ 'hot', 'score', 'unanswered', + 'recommend', ]; interface Props { source: 'questions' | 'tag'; order?: Type.QuestionOrderBy; data; + orderList?: Type.QuestionOrderBy[]; isLoading: boolean; } @@ -55,6 +57,7 @@ const QuestionList: FC = ({ source, order, data, + orderList, isLoading = false, }) => { const { t } = useTranslation('translation', { keyPrefix: 'question' }); @@ -65,6 +68,7 @@ const QuestionList: FC = ({ const curPage = Number(urlSearchParams.get('page')) || 1; const pageSize = 20; const count = data?.count || 0; + const orderKeys = orderList || QUESTION_ORDER_KEYS; return (
@@ -75,7 +79,7 @@ const QuestionList: FC = ({ : t('x_questions', { count })} { page: curPage, order: curOrder as Type.QuestionOrderBy, }; - const { data: listData, isLoading: listLoading } = useQuestionList(reqParams); + const { data: listData, isLoading: listLoading } = + curOrder === 'recommend' + ? useQuestionRecommendList(reqParams) + : useQuestionList(reqParams); const isIndexPage = useMatch('/'); let pageTitle = t('questions', { keyPrefix: 'page_title' }); let slogan = ''; @@ -71,6 +74,11 @@ const Questions: FC = () => { source="questions" data={listData} order={curOrder} + orderList={ + loggedUser.username + ? QUESTION_ORDER_KEYS + : QUESTION_ORDER_KEYS.filter((key) => key !== 'recommend') + } isLoading={listLoading} /> diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index 42b89d5d4..ac2fa937e 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -176,6 +176,7 @@ const Index: FC = () => { source="tag" data={listData} order={curOrder} + orderList={QUESTION_ORDER_KEYS.slice(0, 4)} isLoading={listLoading} /> diff --git a/ui/src/services/client/question.ts b/ui/src/services/client/question.ts index 0e382c26f..b62fbf1a7 100644 --- a/ui/src/services/client/question.ts +++ b/ui/src/services/client/question.ts @@ -36,6 +36,21 @@ export const useQuestionList = (params: Type.QueryQuestionsReq) => { }; }; +export const useQuestionRecommendList = (params: Type.QueryQuestionsReq) => { + const apiUrl = `/answer/api/v1/question/recommend/page?${qs.stringify( + params, + )}`; + const { data, error } = useSWR( + [apiUrl], + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + }; +}; + export const useHotQuestions = ( params: Type.QueryQuestionsReq = { page: 1, From cf7b2f0c1a2c8b8f9de7c0d7a692940db702eb62 Mon Sep 17 00:00:00 2001 From: ferrischi201 Date: Tue, 27 Aug 2024 14:07:08 -0400 Subject: [PATCH 124/129] fix(recommend): fix for all database backend --- internal/repo/question/question_repo.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index aa3694d04..2c6e595ab 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -408,7 +408,13 @@ func (qr *questionRepo) GetRecommendQuestionPageByTags(ctx context.Context, user orderBySQL := "question.pin DESC, question.created_at DESC" // Please Make sure every question has at least one tag - session := qr.data.DB.Context(ctx).Select(entity.Question{}.TableName() + ".*") + selectSQL := entity.Question{}.TableName() + ".*" + if len(followedQuestionIDs) > 0 { + idStr := "'" + strings.Join(followedQuestionIDs, "','") + "'" + selectSQL += fmt.Sprintf(", CASE WHEN question.id IN (%s) THEN 0 ELSE 1 END AS order_priority", idStr) + orderBySQL = "order_priority, " + orderBySQL + } + session := qr.data.DB.Context(ctx).Select(selectSQL) if len(tagIDs) > 0 { session.Where("question.user_id != ?", userID). @@ -422,8 +428,6 @@ func (qr *questionRepo) GetRecommendQuestionPageByTags(ctx context.Context, user } if len(followedQuestionIDs) > 0 { - idStr := "'" + strings.Join(followedQuestionIDs, "','") + "'" - orderBySQL = fmt.Sprintf("CASE WHEN question.id IN (%s) THEN 0 ELSE 1 END, ", idStr) + orderBySQL if len(tagIDs) > 0 { // if tags provided, show followed questions and tag questions session.Or(builder.In("question.id", followedQuestionIDs)) From 8142d78e107149401a4d507d9df44630bd704ffd Mon Sep 17 00:00:00 2001 From: ferrischi201 Date: Tue, 3 Sep 2024 16:02:52 -0400 Subject: [PATCH 125/129] fix(recommend): redirect unauth users from recommend page --- internal/controller/question_controller.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 725a24afc..5ddc33f30 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -340,6 +340,15 @@ func (qc *QuestionController) QuestionPage(ctx *gin.Context) { handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions)) } +// QuestionRecommendPage get recommend questions by page +// @Summary get recommend questions by page +// @Description get recommend questions by page +// @Tags Question +// @Accept json +// @Produce json +// @Param data body schema.QuestionPageReq true "QuestionPageReq" +// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.QuestionPageResp}} +// @Router /answer/api/v1/question/recommend/page [get] func (qc *QuestionController) QuestionRecommendPage(ctx *gin.Context) { req := &schema.QuestionPageReq{} if handler.BindAndCheck(ctx, req) { @@ -347,6 +356,11 @@ func (qc *QuestionController) QuestionRecommendPage(ctx *gin.Context) { } req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx) + if req.LoginUserID == "" { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + return + } + questions, total, err := qc.questionService.GetRecommendQuestionPage(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) From cb5a0b7045998d8fcc583b06c7a3f65b0b6cbb59 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Wed, 4 Sep 2024 10:26:57 +0800 Subject: [PATCH 126/129] chore(lint): lint code and regenerate docs --- docs/docs.go | 74 +++++++++++++++++++++++++++++++++++++++++++---- docs/swagger.json | 74 +++++++++++++++++++++++++++++++++++++++++++---- docs/swagger.yaml | 45 ++++++++++++++++++++++++---- go.mod | 3 -- go.sum | 6 ---- 5 files changed, 176 insertions(+), 26 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 376cdf072..cf218a566 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4185,6 +4185,67 @@ const docTemplate = `{ } } }, + "/answer/api/v1/question/recommend/page": { + "get": { + "description": "get recommend questions by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get recommend questions by page", + "parameters": [ + { + "description": "QuestionPageReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionPageReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/question/recover": { "post": { "security": [ @@ -6586,14 +6647,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", @@ -8873,7 +8934,8 @@ const docTemplate = `{ "active", "hot", "score", - "unanswered" + "unanswered", + "recommend" ] }, "page": { @@ -10833,8 +10895,8 @@ var SwaggerInfo = &swag.Spec{ Host: "", BasePath: "= \"/\"", Schemes: []string{}, - Title: "\"answer\"", - Description: "= \"answer api\"", + Title: "\"apache answer\"", + Description: "= \"apache answer api\"", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/docs/swagger.json b/docs/swagger.json index 7c103856c..0b24c60c6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1,8 +1,8 @@ { "swagger": "2.0", "info": { - "description": "= \"answer api\"", - "title": "\"answer\"", + "description": "= \"apache answer api\"", + "title": "\"apache answer\"", "contact": {}, "version": "= \"v0.0.1\"" }, @@ -4159,6 +4159,67 @@ } } }, + "/answer/api/v1/question/recommend/page": { + "get": { + "description": "get recommend questions by page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Question" + ], + "summary": "get recommend questions by page", + "parameters": [ + { + "description": "QuestionPageReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.QuestionPageReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.RespBody" + }, + { + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/definitions/pager.PageModel" + }, + { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/schema.QuestionPageResp" + } + } + } + } + ] + } + } + } + ] + } + } + } + } + }, "/answer/api/v1/question/recover": { "post": { "security": [ @@ -6560,14 +6621,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", @@ -8847,7 +8908,8 @@ "active", "hot", "score", - "unanswered" + "unanswered", + "recommend" ] }, "page": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 88709bed1..eabc45bb9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1416,6 +1416,7 @@ definitions: - hot - score - unanswered + - recommend type: string page: minimum: 1 @@ -2757,8 +2758,8 @@ definitions: type: object info: contact: {} - description: = "answer api" - title: '"answer"' + description: = "apache answer api" + title: '"apache answer"' version: = "v0.0.1" paths: /: @@ -5269,6 +5270,40 @@ paths: summary: get questions by page tags: - Question + /answer/api/v1/question/recommend/page: + get: + consumes: + - application/json + description: get recommend questions by page + parameters: + - description: QuestionPageReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.QuestionPageReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.RespBody' + - properties: + data: + allOf: + - $ref: '#/definitions/pager.PageModel' + - properties: + list: + items: + $ref: '#/definitions/schema.QuestionPageResp' + type: array + type: object + type: object + summary: get recommend questions by page + tags: + - Question /answer/api/v1/question/recover: post: consumes: @@ -6708,15 +6743,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/go.mod b/go.mod index 8b886a09f..e14b01b91 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,6 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/containerd/continuity v0.4.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v24.0.6+incompatible // indirect github.com/docker/docker v24.0.6+incompatible // indirect @@ -129,7 +128,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect @@ -142,7 +140,6 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect - github.com/urfave/cli/v2 v2.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index ead9468d5..4bf5cc08b 100644 --- a/go.sum +++ b/go.sum @@ -128,7 +128,6 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -616,7 +615,6 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= @@ -692,8 +690,6 @@ github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= -github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= @@ -714,9 +710,7 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95 github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= From b5401915fdc8a17a419d516ac7902541b6f71933 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 4 Sep 2024 11:06:44 +0800 Subject: [PATCH 127/129] fix: badge detial not show empty --- ui/src/pages/Badges/Detail/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/src/pages/Badges/Detail/index.tsx b/ui/src/pages/Badges/Detail/index.tsx index 27fe9b7ed..cb85a8b85 100644 --- a/ui/src/pages/Badges/Detail/index.tsx +++ b/ui/src/pages/Badges/Detail/index.tsx @@ -23,7 +23,7 @@ import { Link, useParams, useSearchParams } from 'react-router-dom'; // import classnames from 'classnames'; -import { FormatTime, Pagination, Empty } from '@/components'; +import { FormatTime, Pagination } from '@/components'; import { usePageTags, useSkeletonControl } from '@/hooks'; // import { formatCount } from '@/utils'; import { useGetBadgeInfo, useBadgeDetailList } from '@/services'; @@ -102,7 +102,6 @@ const Index = () => { }) )} - {Number(badges?.count) <= 0 && !isDetailLoading && }
Date: Wed, 4 Sep 2024 11:58:05 +0800 Subject: [PATCH 128/129] feat(badge): add trigger object id for all badge award event --- internal/controller/template_controller.go | 3 +- internal/entity/badge_award_entity.go | 2 +- internal/repo/badge/badge_event_rule.go | 9 ++--- internal/repo/badge/badge_repo.go | 9 +++++ internal/repo/badge_award/badge_award_repo.go | 20 ++++++++-- internal/schema/event_schema.go | 24 +++++++++-- internal/service/badge/badge_award_service.go | 1 + internal/service/badge/badge_service.go | 40 ++++++++++++------- internal/service/comment/comment_service.go | 9 +++-- internal/service/content/answer_service.go | 8 ++-- internal/service/content/question_service.go | 6 +-- internal/service/content/vote_service.go | 6 +-- internal/service/meta/meta_service.go | 4 +- internal/service/report/report_service.go | 6 +-- 14 files changed, 100 insertions(+), 47 deletions(-) diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index a786f646d..6b6a20f87 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -304,7 +304,8 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) { userInfo, err := tc.userService.GetOtherUserInfoByUsername( ctx, &schema.GetOtherUserInfoByUsernameReq{Username: shareUsername}) if err == nil { - tc.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserShare, userInfo.ID)) + tc.eventQueueService.Send(ctx, schema.NewEvent(constant.EventUserShare, userInfo.ID). + QID(id, detail.UserID).AID(answerid, "")) } } encodeTitle := htmltext.UrlTitle(detail.Title) diff --git a/internal/entity/badge_award_entity.go b/internal/entity/badge_award_entity.go index f18138bee..339b9ca06 100644 --- a/internal/entity/badge_award_entity.go +++ b/internal/entity/badge_award_entity.go @@ -25,7 +25,7 @@ const ( IsBadgeNotDeleted = 0 IsBadgeDeleted = 1 - BadgeOnceAwardKey = "0" + BadgeEmptyAwardKey = "0" ) // BadgeAward badge_award diff --git a/internal/repo/badge/badge_event_rule.go b/internal/repo/badge/badge_event_rule.go index f107203e8..11ce38a3e 100644 --- a/internal/repo/badge/badge_event_rule.go +++ b/internal/repo/badge/badge_event_rule.go @@ -72,8 +72,8 @@ func NewEventRuleRepo(data *data.Data) badge.EventRuleRepo { 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) + for _, handler := range handlers { + t, err := handler(ctx, msg) if err != nil { log.Errorf("error handling badge event %+v: %v", msg, err) } else { @@ -97,7 +97,7 @@ func (br *eventRuleRepo) FirstUpdateUserProfile(ctx context.Context, continue } if len(bean.Bio) > 0 { - awards = append(awards, br.createBadgeAward(event.UserID, "", b)) + awards = append(awards, br.createBadgeAward(event.UserID, entity.BadgeEmptyAwardKey, b)) } } return awards, nil @@ -245,9 +245,6 @@ func (br *eventRuleRepo) getBadgesByHandler(ctx context.Context, handler string) } 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: badge.ID, diff --git a/internal/repo/badge/badge_repo.go b/internal/repo/badge/badge_repo.go index 80689541f..d52fc1229 100644 --- a/internal/repo/badge/badge_repo.go +++ b/internal/repo/badge/badge_repo.go @@ -140,3 +140,12 @@ func (r *badgeRepo) UpdateStatus(ctx context.Context, id string, status int8) (e return } + +// UpdateAwardCount updates the award count of a badge +func (r *badgeRepo) UpdateAwardCount(ctx context.Context, badgeID string, awardCount int) (err error) { + _, err = r.data.DB.Context(ctx).ID(badgeID).Cols("award_count").Update(&entity.Badge{AwardCount: awardCount}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/badge_award/badge_award_repo.go b/internal/repo/badge_award/badge_award_repo.go index 64615d6d8..c1ce1e584 100644 --- a/internal/repo/badge_award/badge_award_repo.go +++ b/internal/repo/badge_award/badge_award_repo.go @@ -63,9 +63,15 @@ func (r *badgeAwardRepo) AwardBadgeForUser(ctx context.Context, badgeAward *enti return nil, fmt.Errorf("badge not exist") } - 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) + old := &entity.BadgeAward{ + UserID: badgeAward.UserID, + BadgeID: badgeAward.BadgeID, + IsBadgeDeleted: entity.IsBadgeNotDeleted, + } + if badgeInfo.Single != entity.BadgeSingleAward { + old.AwardKey = badgeAward.AwardKey + } + exist, err = session.Get(old) if err != nil { return nil, err } @@ -108,6 +114,14 @@ func (r *badgeAwardRepo) CountByUserIdAndBadgeId(ctx context.Context, userID str return } +func (r *badgeAwardRepo) CountByBadgeID(ctx context.Context, badgeID string) (awardCount int64, err error) { + awardCount, err = r.data.DB.Context(ctx).Count(&entity.BadgeAward{BadgeID: badgeID}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + 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 diff --git a/internal/schema/event_schema.go b/internal/schema/event_schema.go index a507f2699..701d5d88f 100644 --- a/internal/schema/event_schema.go +++ b/internal/schema/event_schema.go @@ -19,13 +19,18 @@ package schema -import "github.com/apache/incubator-answer/internal/base/constant" +import ( + "github.com/apache/incubator-answer/internal/base/constant" + "github.com/apache/incubator-answer/pkg/uid" +) // EventMsg event message type EventMsg struct { EventType constant.EventType UserID string + TriggerObjectID string + QuestionID string QuestionUserID string @@ -49,14 +54,18 @@ func NewEvent(e constant.EventType, userID string) *EventMsg { // QID get question id func (e *EventMsg) QID(questionID, userID string) *EventMsg { - e.QuestionID = questionID + if len(questionID) > 0 { + e.QuestionID = uid.DeShortID(questionID) + } e.QuestionUserID = userID return e } // AID get answer id func (e *EventMsg) AID(answerID, userID string) *EventMsg { - e.AnswerID = answerID + if len(answerID) > 0 { + e.AnswerID = uid.DeShortID(answerID) + } e.AnswerUserID = userID return e } @@ -68,6 +77,12 @@ func (e *EventMsg) CID(comment, userID string) *EventMsg { return e } +// TID get trigger object id +func (e *EventMsg) TID(triggerObjectID string) *EventMsg { + e.TriggerObjectID = triggerObjectID + return e +} + // AddExtra add extra info func (e *EventMsg) AddExtra(key, value string) *EventMsg { e.ExtraInfo[key] = value @@ -84,6 +99,9 @@ func (e *EventMsg) GetExtra(key string) string { // GetObjectID get object id func (e *EventMsg) GetObjectID() string { + if len(e.TriggerObjectID) > 0 { + return e.TriggerObjectID + } if len(e.CommentID) > 0 { return e.CommentID } diff --git a/internal/service/badge/badge_award_service.go b/internal/service/badge/badge_award_service.go index 0bc7be0fe..feb95a657 100644 --- a/internal/service/badge/badge_award_service.go +++ b/internal/service/badge/badge_award_service.go @@ -42,6 +42,7 @@ type BadgeAwardRepo interface { AwardBadgeForUser(ctx context.Context, badgeAward *entity.BadgeAward) (err error) CountByUserIdAndBadgeId(ctx context.Context, userID string, badgeID string) (awardCount int64) + CountByBadgeID(ctx context.Context, badgeID string) (awardCount int64, err error) SumUserEarnedGroupByBadgeID(ctx context.Context, userID string) (earnedCounts []*entity.BadgeEarnedCount, err error) diff --git a/internal/service/badge/badge_service.go b/internal/service/badge/badge_service.go index 3ec7b5bfb..faf9b628f 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" + "github.com/segmentfault/pacman/log" "strings" ) @@ -42,6 +43,7 @@ type BadgeRepo interface { ListInactivated(ctx context.Context, page int, pageSize int) (badges []*entity.Badge, total int64, err error) UpdateStatus(ctx context.Context, id string, status int8) (err error) + UpdateAwardCount(ctx context.Context, badgeID string, awardCount int) (err error) } type BadgeService struct { @@ -266,32 +268,42 @@ func (b *BadgeService) GetBadgeInfo(ctx *gin.Context, id string, userID string) // 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) + badge, exists, err := b.badgeRepo.GetByID(ctx, req.ID) if err != nil { - return + return err } if !exists { - err = errors.BadRequest(reason.BadgeObjectNotFound) - return + return errors.BadRequest(reason.BadgeObjectNotFound) } - status, ok := schema.BadgeStatusEMap[req.Status] // check duplicate action - if badge.Status == status { - return - } - + status, ok := schema.BadgeStatusEMap[req.Status] if !ok { err = errors.BadRequest(reason.StatusInvalid) return } + if badge.Status == status { + return + } err = b.badgeRepo.UpdateStatus(ctx, req.ID, status) - return + if err != nil { + return err + } + + if status == entity.BadgeStatusActive { + count, err := b.badgeAwardRepo.CountByBadgeID(ctx, badge.ID) + if err != nil { + log.Errorf("count badge award failed: %v", err) + return nil + } + err = b.badgeRepo.UpdateAwardCount(ctx, badge.ID, int(count)) + if err != nil { + log.Errorf("update badge award count failed: %v", err) + return nil + } + } + return nil } diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index 75d30f9da..012149e73 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -192,11 +192,11 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment switch objInfo.ObjectType { case constant.QuestionObjectType: activityMsg.ActivityTypeKey = constant.ActQuestionCommented - event = schema.NewEvent(constant.EventCommentCreate, req.UserID). + event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID). CID(comment.ID, comment.UserID).QID(objInfo.QuestionID, objInfo.ObjectCreatorUserID) case constant.AnswerObjectType: activityMsg.ActivityTypeKey = constant.ActAnswerCommented - event = schema.NewEvent(constant.EventCommentCreate, req.UserID). + event = schema.NewEvent(constant.EventCommentCreate, req.UserID).TID(comment.ID). CID(comment.ID, comment.UserID).AID(objInfo.AnswerID, objInfo.ObjectCreatorUserID) } cs.activityQueueService.Send(ctx, activityMsg) @@ -255,7 +255,8 @@ func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveC if err != nil { return err } - cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentDelete, req.UserID).CID(req.CommentID, req.UserID)) + cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentDelete, req.UserID). + TID(req.CommentID).CID(req.CommentID, req.UserID)) return nil } @@ -288,7 +289,7 @@ 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). + cs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventCommentUpdate, req.UserID).TID(old.ID). 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 496f7546b..c34e9081a 100644 --- a/internal/service/content/answer_service.go +++ b/internal/service/content/answer_service.go @@ -179,7 +179,7 @@ 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). + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerDelete, req.UserID).TID(answerInfo.ID). AID(answerInfo.ID, answerInfo.UserID)) return } @@ -301,7 +301,7 @@ 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). + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerCreate, req.UserID).TID(insertData.ID). AID(insertData.ID, insertData.UserID)) return insertData.ID, nil } @@ -391,7 +391,7 @@ 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). + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventAnswerUpdate, req.UserID).TID(insertData.ID). AID(insertData.ID, insertData.UserID)) } @@ -447,7 +447,7 @@ func (as *AnswerService) AcceptAnswer(ctx context.Context, req *schema.AcceptAns } if acceptedAnswerInfo != nil { - as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID). + as.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionAccept, req.UserID).TID(acceptedAnswerInfo.ID). QID(questionInfo.ID, questionInfo.UserID).AID(acceptedAnswerInfo.ID, acceptedAnswerInfo.UserID)) } diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go index d3e952b78..9a6dd2805 100644 --- a/internal/service/content/question_service.go +++ b/internal/service/content/question_service.go @@ -397,7 +397,7 @@ 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). + qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionCreate, req.UserID).TID(question.ID). QID(question.ID, question.UserID)) questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, req.QuestionPermission) @@ -560,7 +560,7 @@ 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). + qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionDelete, req.UserID).TID(questionInfo.ID). QID(questionInfo.ID, questionInfo.UserID)) return nil } @@ -953,7 +953,7 @@ 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). + qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionUpdate, req.UserID).TID(question.ID). QID(question.ID, question.UserID)) } diff --git a/internal/service/content/vote_service.go b/internal/service/content/vote_service.go index c83011a9c..b4b92ddd4 100644 --- a/internal/service/content/vote_service.go +++ b/internal/service/content/vote_service.go @@ -305,13 +305,13 @@ func (vs *VoteService) sendEvent(ctx context.Context, var event *schema.EventMsg switch objectInfo.ObjectType { case constant.QuestionObjectType: - event = schema.NewEvent(constant.EventQuestionVote, req.UserID). + event = schema.NewEvent(constant.EventQuestionVote, req.UserID).TID(objectInfo.QuestionID). QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID) case constant.AnswerObjectType: - event = schema.NewEvent(constant.EventAnswerVote, req.UserID). + event = schema.NewEvent(constant.EventAnswerVote, req.UserID).TID(objectInfo.AnswerID). AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID) case constant.CommentObjectType: - event = schema.NewEvent(constant.EventCommentVote, req.UserID). + event = schema.NewEvent(constant.EventCommentVote, req.UserID).TID(objectInfo.CommentID). CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID) default: return diff --git a/internal/service/meta/meta_service.go b/internal/service/meta/meta_service.go index 778c6ca66..b85aecf6d 100644 --- a/internal/service/meta/meta_service.go +++ b/internal/service/meta/meta_service.go @@ -104,7 +104,7 @@ func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.Upda if !exist { return nil, myErrors.BadRequest(reason.AnswerNotFound) } - event = schema.NewEvent(constant.EventAnswerReact, req.UserID). + event = schema.NewEvent(constant.EventAnswerReact, req.UserID).TID(answerInfo.ID). AID(answerInfo.ID, answerInfo.UserID) } else if objectType == constant.QuestionObjectType { questionInfo, exist, err := ms.questionRepo.GetQuestion(ctx, req.ObjectID) @@ -114,7 +114,7 @@ func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.Upda if !exist { return nil, myErrors.BadRequest(reason.QuestionNotFound) } - event = schema.NewEvent(constant.EventQuestionReact, req.UserID). + event = schema.NewEvent(constant.EventQuestionReact, req.UserID).TID(questionInfo.ID). QID(questionInfo.ID, questionInfo.UserID) } else { return nil, myErrors.BadRequest(reason.ObjectNotFound) diff --git a/internal/service/report/report_service.go b/internal/service/report/report_service.go index 218423e13..2ca44855f 100644 --- a/internal/service/report/report_service.go +++ b/internal/service/report/report_service.go @@ -233,13 +233,13 @@ func (rs *ReportService) sendEvent(ctx context.Context, var event *schema.EventMsg switch objectInfo.ObjectType { case constant.QuestionObjectType: - event = schema.NewEvent(constant.EventQuestionFlag, report.UserID). + event = schema.NewEvent(constant.EventQuestionFlag, report.UserID).TID(objectInfo.QuestionID). QID(objectInfo.QuestionID, objectInfo.ObjectCreatorUserID) case constant.AnswerObjectType: - event = schema.NewEvent(constant.EventAnswerFlag, report.UserID). + event = schema.NewEvent(constant.EventAnswerFlag, report.UserID).TID(objectInfo.AnswerID). AID(objectInfo.AnswerID, objectInfo.ObjectCreatorUserID) case constant.CommentObjectType: - event = schema.NewEvent(constant.EventCommentFlag, report.UserID). + event = schema.NewEvent(constant.EventCommentFlag, report.UserID).TID(objectInfo.CommentID). CID(objectInfo.CommentID, objectInfo.ObjectCreatorUserID) default: return From 323ee0e36d6e85d15f21391a920a26aca3fa2a84 Mon Sep 17 00:00:00 2001 From: LinkinStars Date: Wed, 4 Sep 2024 15:53:29 +0800 Subject: [PATCH 129/129] docs(makefile): upgrade version --- Makefile | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bcacf3e46..8279d17a4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build clean ui -VERSION=1.3.6 +VERSION=1.4.0 BIN=answer DIR_SRC=./cmd/answer DOCKER_CMD=docker diff --git a/README.md b/README.md index 0894a413e..7d7538050 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ To learn more about the project, visit [answer.apache.org](https://answer.apache ### Running with docker ```bash -docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.3.6 +docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.4.0 ``` For more information, see [Installation](https://answer.apache.org/docs/installation).