chore: import upstream snapshot with attribution
CI / Check and lint (push) Failing after 1s
Lint / Lint (ubuntu-latest) (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:32 +08:00
commit 39dbe3a57d
1131 changed files with 272709 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
/*
* 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 activity
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/activity"
"github.com/apache/answer/internal/service/activity_type"
"github.com/apache/answer/internal/service/config"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// activityRepo activity repository
type activityRepo struct {
data *data.Data
configService *config.ConfigService
}
// NewActivityRepo new repository
func NewActivityRepo(
data *data.Data,
configService *config.ConfigService,
) activity.ActivityRepo {
return &activityRepo{
data: data,
configService: configService,
}
}
func (ar *activityRepo) GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (
activityList []*entity.Activity, err error) {
activityList = make([]*entity.Activity, 0)
session := ar.data.DB.Context(ctx).Desc("id")
if !showVote {
activityTypeNotShown := ar.getAllActivityType(ctx)
session.NotIn("activity_type", activityTypeNotShown)
}
err = session.Find(&activityList, &entity.Activity{OriginalObjectID: objectID})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return activityList, nil
}
func (ar *activityRepo) getAllActivityType(ctx context.Context) (activityTypes []int) {
var activityTypeNotShown []int
for _, key := range activity_type.VoteActivityTypeList {
id, err := ar.configService.GetIDByKey(ctx, key)
if err != nil {
log.Errorf("get config id by key [%s] error: %v", key, err)
} else {
activityTypeNotShown = append(activityTypeNotShown, id)
}
}
return activityTypeNotShown
}
+349
View File
@@ -0,0 +1,349 @@
/*
* 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 activity
import (
"context"
"fmt"
"time"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/activity"
"github.com/apache/answer/internal/service/activity_common"
"github.com/apache/answer/internal/service/noticequeue"
"github.com/apache/answer/internal/service/rank"
"github.com/apache/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// AnswerActivityRepo answer accepted
type AnswerActivityRepo struct {
data *data.Data
activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo
notificationQueueService noticequeue.Service
}
// NewAnswerActivityRepo new repository
func NewAnswerActivityRepo(
data *data.Data,
activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo,
notificationQueueService noticequeue.Service,
) activity.AnswerActivityRepo {
return &AnswerActivityRepo{
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
notificationQueueService: notificationQueueService,
}
}
func (ar *AnswerActivityRepo) SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (
err error) {
// save activity
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
userInfoMapping, err := ar.acquireUserInfo(session, op.GetUserIDs())
if err != nil {
return nil, err
}
err = ar.saveActivitiesAvailable(session, op)
if err != nil {
return nil, err
}
err = ar.changeUserRank(ctx, session, op, userInfoMapping)
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// notification
ar.sendAcceptAnswerNotification(ctx, op)
return nil
}
func (ar *AnswerActivityRepo) SaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (
err error) {
// pre check
activities, err := ar.getExistActivity(ctx, op)
if err != nil {
return err
}
var userIDs []string
for _, act := range activities {
if act.Cancelled == entity.ActivityCancelled {
continue
}
userIDs = append(userIDs, act.UserID)
}
if len(userIDs) == 0 {
return nil
}
// save activity
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
userInfoMapping, err := ar.acquireUserInfo(session, userIDs)
if err != nil {
return nil, err
}
err = ar.cancelActivities(session, activities)
if err != nil {
return nil, err
}
err = ar.rollbackUserRank(ctx, session, activities, userInfoMapping)
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// notification
ar.sendCancelAcceptAnswerNotification(ctx, op)
return nil
}
func (ar *AnswerActivityRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) {
us := make([]*entity.User, 0)
err := session.In("id", userIDs).ForUpdate().Find(&us)
if err != nil {
log.Error(err)
return nil, err
}
users := make(map[string]*entity.User, 0)
for _, u := range us {
users[u.ID] = u
}
return users, nil
}
// saveActivitiesAvailable save activities
// If activity not exist it will be created or else will be updated
// If this activity is already exist, set activity rank to 0
// So after this function, the activity rank will be correct for update user rank
func (ar *AnswerActivityRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.AcceptAnswerOperationInfo) (
err error) {
for _, act := range op.Activities {
existsActivity := &entity.Activity{}
exist, err := session.
Where(builder.Eq{"object_id": op.AnswerObjectID}).
And(builder.Eq{"user_id": act.ActivityUserID}).
And(builder.Eq{"trigger_user_id": act.TriggerUserID}).
And(builder.Eq{"activity_type": act.ActivityType}).
Get(existsActivity)
if err != nil {
return err
}
if exist && existsActivity.Cancelled == entity.ActivityAvailable {
act.Rank = 0
continue
}
if exist {
bean := &entity.Activity{
Cancelled: entity.ActivityAvailable,
Rank: act.Rank,
HasRank: act.HasRank(),
}
session.Where("id = ?", existsActivity.ID)
if _, err = session.Cols("`cancelled`", "`rank`", "`has_rank`").Update(bean); err != nil {
return err
}
} else {
insertActivity := entity.Activity{
ObjectID: op.AnswerObjectID,
OriginalObjectID: act.OriginalObjectID,
UserID: act.ActivityUserID,
TriggerUserID: converter.StringToInt64(act.TriggerUserID),
ActivityType: act.ActivityType,
Rank: act.Rank,
HasRank: act.HasRank(),
Cancelled: entity.ActivityAvailable,
}
_, err = session.Insert(&insertActivity)
if err != nil {
return err
}
}
}
return nil
}
// cancelActivities cancel activities
// If this activity is already cancelled, set activity rank to 0
// So after this function, the activity rank will be correct for update user rank
func (ar *AnswerActivityRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) {
for _, act := range activities {
t := &entity.Activity{}
exist, err := session.ID(act.ID).Get(t)
if err != nil {
log.Error(err)
return err
}
if !exist {
log.Error(fmt.Errorf("%s activity not exist", act.ID))
return fmt.Errorf("%s activity not exist", act.ID)
}
// If this activity is already cancelled, set activity rank to 0
if t.Cancelled == entity.ActivityCancelled {
act.Rank = 0
}
if _, err = session.ID(act.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{
Cancelled: entity.ActivityCancelled,
CancelledAt: time.Now(),
}); err != nil {
log.Error(err)
return err
}
}
return nil
}
func (ar *AnswerActivityRepo) changeUserRank(ctx context.Context, session *xorm.Session,
op *schema.AcceptAnswerOperationInfo,
userInfoMapping map[string]*entity.User) (err error) {
for _, act := range op.Activities {
if act.Rank == 0 {
continue
}
user := userInfoMapping[act.ActivityUserID]
if user == nil {
continue
}
if err = ar.userRankRepo.ChangeUserRank(ctx, session,
act.ActivityUserID, user.Rank, act.Rank); err != nil {
log.Error(err)
return err
}
}
return nil
}
func (ar *AnswerActivityRepo) rollbackUserRank(ctx context.Context, session *xorm.Session,
activities []*entity.Activity,
userInfoMapping map[string]*entity.User) (err error) {
for _, act := range activities {
if act.Rank == 0 {
continue
}
user := userInfoMapping[act.UserID]
if user == nil {
continue
}
if err = ar.userRankRepo.ChangeUserRank(ctx, session,
act.UserID, user.Rank, -act.Rank); err != nil {
log.Error(err)
return err
}
}
return nil
}
func (ar *AnswerActivityRepo) getExistActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ([]*entity.Activity, error) {
var activities []*entity.Activity
for _, action := range op.Activities {
var t []*entity.Activity
err := ar.data.DB.Context(ctx).
Where(builder.Eq{"user_id": action.ActivityUserID}).
And(builder.Eq{"activity_type": action.ActivityType}).
And(builder.Eq{"object_id": op.AnswerObjectID}).
Find(&t)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if len(t) > 0 {
activities = append(activities, t...)
}
}
return activities, nil
}
func (ar *AnswerActivityRepo) sendAcceptAnswerNotification(
ctx context.Context, op *schema.AcceptAnswerOperationInfo) {
for _, act := range op.Activities {
msg := &schema.NotificationMsg{
Type: schema.NotificationTypeAchievement,
ObjectID: op.AnswerObjectID,
ReceiverUserID: act.ActivityUserID,
TriggerUserID: act.TriggerUserID,
}
msg.ObjectType = constant.AnswerObjectType
if msg.TriggerUserID != msg.ReceiverUserID {
ar.notificationQueueService.Send(ctx, msg)
}
}
for _, act := range op.Activities {
msg := &schema.NotificationMsg{
ReceiverUserID: act.ActivityUserID,
Type: schema.NotificationTypeInbox,
ObjectID: op.AnswerObjectID,
TriggerUserID: op.TriggerUserID,
}
if act.ActivityUserID != op.QuestionUserID {
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.NotificationAcceptAnswer
ar.notificationQueueService.Send(ctx, msg)
}
}
}
func (ar *AnswerActivityRepo) sendCancelAcceptAnswerNotification(
ctx context.Context, op *schema.AcceptAnswerOperationInfo) {
for _, act := range op.Activities {
msg := &schema.NotificationMsg{
TriggerUserID: act.TriggerUserID,
ReceiverUserID: act.ActivityUserID,
Type: schema.NotificationTypeAchievement,
ObjectID: op.AnswerObjectID,
}
if act.ActivityUserID == op.QuestionObjectID {
msg.ObjectType = constant.QuestionObjectType
} else {
msg.ObjectType = constant.AnswerObjectType
}
if msg.TriggerUserID != msg.ReceiverUserID {
ar.notificationQueueService.Send(ctx, msg)
}
}
}
+187
View File
@@ -0,0 +1,187 @@
/*
* 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 activity
import (
"context"
"time"
"github.com/apache/answer/internal/service/activity_common"
"github.com/apache/answer/internal/service/follow"
"github.com/apache/answer/pkg/obj"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// FollowRepo activity repository
type FollowRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
activityRepo activity_common.ActivityRepo
}
// NewFollowRepo new repository
func NewFollowRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
activityRepo activity_common.ActivityRepo,
) follow.FollowRepo {
return &FollowRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
activityRepo: activityRepo,
}
}
func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error {
objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
if err != nil {
return err
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
var (
existsActivity entity.Activity
has bool
)
result = nil
has, err = session.Where(builder.Eq{"activity_type": activityType}).
And(builder.Eq{"user_id": userID}).
And(builder.Eq{"object_id": objectID}).
Get(&existsActivity)
if err != nil {
return
}
if has && existsActivity.Cancelled == entity.ActivityAvailable {
return
}
if has {
_, err = session.Where(builder.Eq{"id": existsActivity.ID}).
Cols(`cancelled`).
Update(&entity.Activity{
Cancelled: entity.ActivityAvailable,
})
} else {
// update existing activity with new user id and u object id
_, err = session.Insert(&entity.Activity{
UserID: userID,
ObjectID: objectID,
OriginalObjectID: objectID,
ActivityType: activityType,
Cancelled: entity.ActivityAvailable,
Rank: 0,
HasRank: 0,
})
}
if err != nil {
log.Error(err)
return
}
// start update followers when everything is fine
err = ar.updateFollows(ctx, session, objectID, 1)
if err != nil {
log.Error(err)
}
return
})
return err
}
func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string) error {
objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
if err != nil {
return err
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
var (
existsActivity entity.Activity
has bool
)
result = nil
has, err = session.Where(builder.Eq{"activity_type": activityType}).
And(builder.Eq{"user_id": userID}).
And(builder.Eq{"object_id": objectID}).
Get(&existsActivity)
if err != nil || !has {
return
}
if has && existsActivity.Cancelled == entity.ActivityCancelled {
return
}
if _, err = session.Where("id = ?", existsActivity.ID).
Cols("cancelled").
Update(&entity.Activity{
Cancelled: entity.ActivityCancelled,
CancelledAt: time.Now(),
}); err != nil {
return
}
err = ar.updateFollows(ctx, session, objectID, -1)
return
})
return err
}
func (ar *FollowRepo) updateFollows(_ context.Context, session *xorm.Session, objectID string, follows int) error {
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return err
}
switch objectType {
case "question":
_, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.Question{})
case "user":
_, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.User{})
case "tag":
_, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.Tag{})
default:
err = errors.InternalServer(reason.DisallowFollow).WithMsg("this object can't be followed")
}
return err
}
+125
View File
@@ -0,0 +1,125 @@
/*
* 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 activity
import (
"context"
"fmt"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/pkg/converter"
"xorm.io/builder"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/activity"
"github.com/apache/answer/internal/service/activity_common"
"github.com/apache/answer/internal/service/config"
"github.com/apache/answer/internal/service/rank"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// ReviewActivityRepo answer accepted
type ReviewActivityRepo struct {
data *data.Data
activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo
configService *config.ConfigService
}
const (
EditAccepted = "edit.accepted"
)
// NewReviewActivityRepo new repository
func NewReviewActivityRepo(
data *data.Data,
activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo,
configService *config.ConfigService,
) activity.ReviewActivityRepo {
return &ReviewActivityRepo{
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
configService: configService,
}
}
// Review user active
func (ar *ReviewActivityRepo) Review(ctx context.Context, act *schema.PassReviewActivity) (err error) {
cfg, err := ar.configService.GetConfigByKey(ctx, EditAccepted)
if err != nil {
return err
}
addActivity := &entity.Activity{
UserID: act.UserID,
TriggerUserID: converter.StringToInt64(act.TriggerUserID),
ObjectID: act.ObjectID,
OriginalObjectID: act.OriginalObjectID,
ActivityType: cfg.ID,
Rank: cfg.GetIntValue(),
HasRank: 1,
RevisionID: converter.StringToInt64(act.RevisionID),
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
user := &entity.User{}
exist, err := session.ID(addActivity.UserID).ForUpdate().Get(user)
if err != nil {
return nil, err
}
if !exist {
return nil, fmt.Errorf("user not exist")
}
existsActivity := &entity.Activity{}
exist, err = session.
And(builder.Eq{"user_id": addActivity.UserID}).
And(builder.Eq{"activity_type": addActivity.ActivityType}).
And(builder.Eq{"revision_id": addActivity.RevisionID}).
Get(existsActivity)
if err != nil {
return nil, err
}
if exist {
return nil, nil
}
err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank)
if err != nil {
return nil, err
}
_, err = session.Insert(addActivity)
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
+120
View File
@@ -0,0 +1,120 @@
/*
* 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 activity
import (
"context"
"fmt"
"xorm.io/builder"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/activity"
"github.com/apache/answer/internal/service/activity_common"
"github.com/apache/answer/internal/service/config"
"github.com/apache/answer/internal/service/rank"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// UserActiveActivityRepo answer accepted
type UserActiveActivityRepo struct {
data *data.Data
activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo
configService *config.ConfigService
}
const (
UserActivated = "user.activated"
)
// NewUserActiveActivityRepo new repository
func NewUserActiveActivityRepo(
data *data.Data,
activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo,
configService *config.ConfigService,
) activity.UserActiveActivityRepo {
return &UserActiveActivityRepo{
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
configService: configService,
}
}
// UserActive user active
func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string) (err error) {
cfg, err := ar.configService.GetConfigByKey(ctx, UserActivated)
if err != nil {
return err
}
addActivity := &entity.Activity{
UserID: userID,
ObjectID: "0",
OriginalObjectID: "0",
ActivityType: cfg.ID,
Rank: cfg.GetIntValue(),
HasRank: 1,
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
user := &entity.User{}
exist, err := session.ID(userID).ForUpdate().Get(user)
if err != nil {
return nil, err
}
if !exist {
return nil, fmt.Errorf("user not exist")
}
existsActivity := &entity.Activity{}
exist, err = session.
And(builder.Eq{"user_id": addActivity.UserID}).
And(builder.Eq{"activity_type": addActivity.ActivityType}).
Get(existsActivity)
if err != nil {
return nil, err
}
if exist {
return nil, nil
}
err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank)
if err != nil {
return nil, err
}
_, err = session.Insert(addActivity)
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
+507
View File
@@ -0,0 +1,507 @@
/*
* 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 activity
import (
"context"
"fmt"
"time"
"github.com/apache/answer/internal/service/content"
"github.com/segmentfault/pacman/log"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/service/noticequeue"
"github.com/apache/answer/pkg/converter"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/service/rank"
"github.com/apache/answer/pkg/obj"
"xorm.io/builder"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/activity_common"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// VoteRepo activity repository
type VoteRepo struct {
data *data.Data
activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo
notificationQueueService noticequeue.Service
}
// NewVoteRepo new repository
func NewVoteRepo(
data *data.Data,
activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo,
notificationQueueService noticequeue.Service,
) content.VoteRepo {
return &VoteRepo{
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
notificationQueueService: notificationQueueService,
}
}
func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
noNeedToVote, err := vr.votePreCheck(ctx, op)
if err != nil {
return err
}
if noNeedToVote {
return nil
}
sendInboxNotification := false
maxDailyRank, err := vr.userRankRepo.GetMaxDailyRank(ctx)
if err != nil {
return err
}
var userIDs []string
for _, activity := range op.Activities {
userIDs = append(userIDs, activity.ActivityUserID)
}
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
if err != nil {
return nil, err
}
err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, userInfoMapping, maxDailyRank)
if err != nil {
return nil, err
}
sendInboxNotification, err = vr.saveActivitiesAvailable(session, op)
if err != nil {
return nil, err
}
err = vr.changeUserRank(ctx, session, op, userInfoMapping)
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
return err
}
for _, activity := range op.Activities {
if activity.Rank == 0 {
continue
}
vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID)
}
if sendInboxNotification {
vr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp)
}
return nil
}
func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
// Pre-Check
// 1. check if the activity exist
// 2. check if the activity is not cancelled
// 3. if all activities are cancelled, return directly
activities, err := vr.getExistActivity(ctx, op)
if err != nil {
return err
}
var userIDs []string
for _, activity := range activities {
if activity.Cancelled == entity.ActivityCancelled {
continue
}
userIDs = append(userIDs, activity.UserID)
}
if len(userIDs) == 0 {
return nil
}
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
if err != nil {
return nil, err
}
err = vr.cancelActivities(session, activities)
if err != nil {
return nil, err
}
err = vr.rollbackUserRank(ctx, session, activities, userInfoMapping)
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
return err
}
for _, activity := range activities {
if activity.Rank == 0 {
continue
}
vr.sendAchievementNotification(ctx, activity.UserID, op.ObjectCreatorUserID, op.ObjectID)
}
return nil
}
func (vr *VoteRepo) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (
up, down int64, err error) {
up = vr.countVoteUp(ctx, objectID, objectType)
down = vr.countVoteDown(ctx, objectID, objectType)
err = vr.updateVotes(ctx, objectID, objectType, int(up-down))
return
}
func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) {
session := vr.data.DB.Context(ctx)
cond := builder.
And(
builder.Eq{"user_id": userID},
builder.Eq{"cancelled": 0},
builder.In("activity_type", activityTypes),
)
session.Where(cond).Desc("updated_at")
total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) {
activities, err := vr.getExistActivity(ctx, op)
if err != nil {
return false, err
}
done := 0
for _, activity := range activities {
if activity.Cancelled == entity.ActivityAvailable {
done++
}
}
return done == len(op.Activities), nil
}
func (vr *VoteRepo) acquireUserInfo(session *xorm.Session, userIDs []string) (map[string]*entity.User, error) {
us := make([]*entity.User, 0)
err := session.In("id", userIDs).ForUpdate().Find(&us)
if err != nil {
log.Error(err)
return nil, err
}
users := make(map[string]*entity.User, 0)
for _, u := range us {
users[u.ID] = u
}
return users, nil
}
func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, session *xorm.Session,
op *schema.VoteOperationInfo, userInfoMapping map[string]*entity.User, maxDailyRank int) (err error) {
// check if user reach daily rank limit
for _, activity := range op.Activities {
if userInfoMapping[activity.ActivityUserID] == nil {
continue
}
if activity.Rank > 0 {
// check if reach max daily rank
reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank)
if err != nil {
log.Error(err)
return err
}
if reach {
activity.Rank = 0
continue
}
} else {
// If user rank is lower than 1 after this action, then user rank will be set to 1 only.
userCurrentScore := userInfoMapping[activity.ActivityUserID].Rank
if userCurrentScore+activity.Rank < 1 {
activity.Rank = 1 - userCurrentScore
}
}
}
return nil
}
func (vr *VoteRepo) changeUserRank(ctx context.Context, session *xorm.Session,
op *schema.VoteOperationInfo,
userInfoMapping map[string]*entity.User) (err error) {
for _, activity := range op.Activities {
if activity.Rank == 0 {
continue
}
user := userInfoMapping[activity.ActivityUserID]
if user == nil {
continue
}
if err = vr.userRankRepo.ChangeUserRank(ctx, session,
activity.ActivityUserID, user.Rank, activity.Rank); err != nil {
log.Error(err)
return err
}
}
return nil
}
func (vr *VoteRepo) rollbackUserRank(ctx context.Context, session *xorm.Session,
activities []*entity.Activity,
userInfoMapping map[string]*entity.User) (err error) {
for _, activity := range activities {
if activity.Rank == 0 {
continue
}
user := userInfoMapping[activity.UserID]
if user == nil {
continue
}
if err = vr.userRankRepo.ChangeUserRank(ctx, session,
activity.UserID, user.Rank, -activity.Rank); err != nil {
log.Error(err)
return err
}
}
return nil
}
// saveActivitiesAvailable save activities
// If activity not exist it will be created or else will be updated
// If this activity is already exist, set activity rank to 0
// So after this function, the activity rank will be correct for update user rank
func (vr *VoteRepo) saveActivitiesAvailable(session *xorm.Session, op *schema.VoteOperationInfo) (newAct bool, err error) {
for _, activity := range op.Activities {
existsActivity := &entity.Activity{}
exist, err := session.
Where(builder.Eq{"object_id": op.ObjectID}).
And(builder.Eq{"user_id": activity.ActivityUserID}).
And(builder.Eq{"trigger_user_id": activity.TriggerUserID}).
And(builder.Eq{"activity_type": activity.ActivityType}).
Get(existsActivity)
if err != nil {
return false, err
}
if exist && existsActivity.Cancelled == entity.ActivityAvailable {
activity.Rank = 0
continue
}
if exist {
bean := &entity.Activity{
Cancelled: entity.ActivityAvailable,
Rank: activity.Rank,
HasRank: activity.HasRank(),
}
session.Where("id = ?", existsActivity.ID)
if _, err = session.Cols("`cancelled`", "`rank`", "`has_rank`").
Update(bean); err != nil {
return false, err
}
} else {
insertActivity := entity.Activity{
ObjectID: op.ObjectID,
OriginalObjectID: op.ObjectID,
UserID: activity.ActivityUserID,
TriggerUserID: converter.StringToInt64(activity.TriggerUserID),
ActivityType: activity.ActivityType,
Rank: activity.Rank,
HasRank: activity.HasRank(),
Cancelled: entity.ActivityAvailable,
}
_, err = session.Insert(&insertActivity)
if err != nil {
return false, err
}
newAct = true
}
}
return newAct, nil
}
// cancelActivities cancel activities
// If this activity is already cancelled, set activity rank to 0
// So after this function, the activity rank will be correct for update user rank
func (vr *VoteRepo) cancelActivities(session *xorm.Session, activities []*entity.Activity) (err error) {
for _, activity := range activities {
t := &entity.Activity{}
exist, err := session.ID(activity.ID).Get(t)
if err != nil {
log.Error(err)
return err
}
if !exist {
log.Error(fmt.Errorf("%s activity not exist", activity.ID))
return fmt.Errorf("%s activity not exist", activity.ID)
}
// If this activity is already cancelled, set activity rank to 0
if t.Cancelled == entity.ActivityCancelled {
activity.Rank = 0
}
if _, err = session.ID(activity.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{
Cancelled: entity.ActivityCancelled,
CancelledAt: time.Now(),
}); err != nil {
log.Error(err)
return err
}
}
return nil
}
func (vr *VoteRepo) getExistActivity(ctx context.Context, op *schema.VoteOperationInfo) ([]*entity.Activity, error) {
var activities []*entity.Activity
for _, action := range op.Activities {
t := &entity.Activity{}
exist, err := vr.data.DB.Context(ctx).
Where(builder.Eq{"user_id": action.ActivityUserID}).
And(builder.Eq{"trigger_user_id": action.TriggerUserID}).
And(builder.Eq{"activity_type": action.ActivityType}).
And(builder.Eq{"object_id": op.ObjectID}).
Get(t)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
activities = append(activities, t)
}
}
return activities, nil
}
func (vr *VoteRepo) countVoteUp(ctx context.Context, objectID, objectType string) (count int64) {
count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteUp)
if err != nil {
log.Errorf("get vote up count error: %v", err)
}
return count
}
func (vr *VoteRepo) countVoteDown(ctx context.Context, objectID, objectType string) (count int64) {
count, err := vr.countVote(ctx, objectID, objectType, constant.ActVoteDown)
if err != nil {
log.Errorf("get vote down count error: %v", err)
}
return count
}
func (vr *VoteRepo) countVote(ctx context.Context, objectID, objectType, action string) (count int64, err error) {
activity := &entity.Activity{}
activityType, _ := vr.activityRepo.GetActivityTypeByObjectType(ctx, objectType, action)
count, err = vr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}).
And(builder.Eq{"activity_type": activityType}).
And(builder.Eq{"cancelled": 0}).
Count(activity)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, err
}
func (vr *VoteRepo) updateVotes(ctx context.Context, objectID, objectType string, voteCount int) (err error) {
session := vr.data.DB.Context(ctx)
switch objectType {
case constant.QuestionObjectType:
_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount})
case constant.AnswerObjectType:
_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount})
case constant.CommentObjectType:
_, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount})
}
if err != nil {
log.Error(err)
}
return
}
func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return
}
msg := &schema.NotificationMsg{
ReceiverUserID: activityUserID,
TriggerUserID: objectUserID,
Type: schema.NotificationTypeAchievement,
ObjectID: objectID,
ObjectType: objectType,
}
vr.notificationQueueService.Send(ctx, msg)
}
func (vr *VoteRepo) sendVoteInboxNotification(ctx context.Context, triggerUserID, receiverUserID, objectID string, upvote bool) {
if triggerUserID == receiverUserID {
return
}
objectType, _ := obj.GetObjectTypeStrByObjectID(objectID)
msg := &schema.NotificationMsg{
TriggerUserID: triggerUserID,
ReceiverUserID: receiverUserID,
Type: schema.NotificationTypeInbox,
ObjectID: objectID,
ObjectType: objectType,
}
if objectType == constant.QuestionObjectType {
if upvote {
msg.NotificationAction = constant.NotificationUpVotedTheQuestion
} else {
msg.NotificationAction = constant.NotificationDownVotedTheQuestion
}
}
if objectType == constant.AnswerObjectType {
if upvote {
msg.NotificationAction = constant.NotificationUpVotedTheAnswer
} else {
msg.NotificationAction = constant.NotificationDownVotedTheAnswer
}
}
if objectType == constant.CommentObjectType {
if upvote {
msg.NotificationAction = constant.NotificationUpVotedTheComment
}
}
if len(msg.NotificationAction) > 0 {
vr.notificationQueueService.Send(ctx, msg)
}
}
@@ -0,0 +1,191 @@
/*
* 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 activity_common
import (
"context"
"fmt"
"time"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/activity_common"
"github.com/apache/answer/internal/service/activity_type"
"github.com/apache/answer/pkg/obj"
"xorm.io/builder"
"xorm.io/xorm"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/service/config"
"github.com/apache/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
)
// ActivityRepo activity repository
type ActivityRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
configService *config.ConfigService
}
// NewActivityRepo new repository
func NewActivityRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
configService *config.ConfigService,
) activity_common.ActivityRepo {
return &ActivityRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
configService: configService,
}
}
func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) (
activityType, rank, hasRank int, err error) {
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return
}
confKey := fmt.Sprintf("%s.%s", objectType, action)
cfg, err := ar.configService.GetConfigByKey(ctx, confKey)
if err != nil {
return
}
activityType, rank = cfg.ID, cfg.GetIntValue()
hasRank = 0
if rank != 0 {
hasRank = 1
}
return
}
func (ar *ActivityRepo) GetActivityTypeByObjectType(ctx context.Context, objectType, action string) (activityType int, err error) {
configKey := fmt.Sprintf("%s.%s", objectType, action)
cfg, err := ar.configService.GetConfigByKey(ctx, configKey)
if err != nil {
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return cfg.ID, nil
}
func (ar *ActivityRepo) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) {
cfg, err := ar.configService.GetConfigByKey(ctx, configKey)
if err != nil {
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return cfg.ID, nil
}
func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session,
objectID, userID string, activityType int,
) (existsActivity *entity.Activity, exist bool, err error) {
existsActivity = &entity.Activity{}
exist, err = session.
Where(builder.Eq{"object_id": objectID}).
And(builder.Eq{"user_id": userID}).
And(builder.Eq{"activity_type": activityType}).
Get(existsActivity)
return
}
func (ar *ActivityRepo) GetUserActivitiesByActivityType(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()).
Select("sum(`rank`) as `rank`").
Where("user_id =?", userID).
And("object_id = ?", objectID).
And("cancelled =0").
Get(sum)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return 0, err
}
return sum.Rank, nil
}
// AddActivity add activity
func (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activity) (err error) {
_, err = ar.data.DB.Context(ctx).Insert(activity)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUsersWhoHasGainedTheMostReputation get users who has gained the most reputation over a period of time
func (ar *ActivityRepo) GetUsersWhoHasGainedTheMostReputation(
ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) {
rankStat = make([]*entity.ActivityUserRankStat, 0)
session := ar.data.DB.Context(ctx).Select("user_id, SUM(`rank`) AS rank_amount").Table("activity")
session.Where("has_rank = 1 AND cancelled = 0")
session.Where("created_at >= ?", startTime)
session.Where("created_at <= ?", endTime)
session.GroupBy("user_id")
session.Desc("rank_amount")
session.Limit(limit)
err = session.Find(&rankStat)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUsersWhoHasVoteMost get users who has vote most
func (ar *ActivityRepo) GetUsersWhoHasVoteMost(
ctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error) {
voteStat = make([]*entity.ActivityUserVoteStat, 0)
actIDs := make([]int, 0)
for _, act := range activity_type.ActivityTypeList {
cfg, err := ar.configService.GetConfigByKey(ctx, act)
if err == nil {
actIDs = append(actIDs, cfg.ID)
}
}
session := ar.data.DB.Context(ctx).Select("user_id, COUNT(*) AS vote_count").Table("activity")
session.Where("cancelled = 0")
session.In("activity_type", actIDs)
session.Where("created_at >= ?", startTime)
session.Where("created_at <= ?", endTime)
session.GroupBy("user_id")
session.Desc("vote_count")
session.Limit(limit)
err = session.Find(&voteStat)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+265
View File
@@ -0,0 +1,265 @@
/*
* 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 activity_common
import (
"context"
"time"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/activity_common"
"github.com/apache/answer/internal/service/unique"
"github.com/apache/answer/pkg/obj"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
"xorm.io/xorm"
)
// FollowRepo follow repository
type FollowRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
activityRepo activity_common.ActivityRepo
}
// NewFollowRepo new repository
func NewFollowRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
activityRepo activity_common.ActivityRepo,
) activity_common.FollowRepo {
return &FollowRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
activityRepo: activityRepo,
}
}
// GetFollowAmount get object id's follows
func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectID string) (follows int, err error) {
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return 0, err
}
switch objectType {
case "question":
model := &entity.Question{}
_, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model)
if err == nil {
follows = model.FollowCount
}
case "user":
model := &entity.User{}
_, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model)
if err == nil {
follows = model.FollowCount
}
case "tag":
model := &entity.Tag{}
_, err = ar.data.DB.Context(ctx).Where("id = ?", objectID).Cols("`follow_count`").Get(model)
if err == nil {
follows = model.FollowCount
}
default:
err = errors.InternalServer(reason.DisallowFollow).WithMsg("this object can't be followed")
}
if err != nil {
return 0, err
}
return follows, nil
}
// GetFollowUserIDs get follow userID by objectID
func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) {
objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return nil, err
}
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
if err != nil {
log.Errorf("can't get activity type by object key: %s", objectTypeStr)
return nil, err
}
userIDs = make([]string, 0)
session := ar.data.DB.Context(ctx).Select("user_id")
session.Table(entity.Activity{}.TableName())
session.Where("object_id = ?", objectID)
session.Where("activity_type = ?", activityType)
session.Where("cancelled = 0")
err = session.Find(&userIDs)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return userIDs, nil
}
// GetFollowIDs get all follow id list
func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) {
followIDs = make([]string, 0)
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
session := ar.data.DB.Context(ctx).Select("object_id")
session.Table(entity.Activity{}.TableName())
session.Where("user_id = ? AND activity_type = ?", userID, activityType)
session.Where("cancelled = 0")
err = session.Find(&followIDs)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return followIDs, nil
}
// IsFollowed check user if follow object or not
func (ar *FollowRepo) IsFollowed(ctx context.Context, userID, objectID string) (followed bool, err error) {
objectKey, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return false, err
}
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
if err != nil {
return false, err
}
at := &entity.Activity{}
has, err := ar.data.DB.Context(ctx).Where("user_id = ? AND object_id = ? AND activity_type = ?", userID, objectID, activityType).Get(at)
if err != nil {
return false, err
}
if !has {
return false, nil
}
if at.Cancelled == entity.ActivityCancelled {
return false, nil
} else {
return true, nil
}
}
// MigrateFollowers migrate followers from source object to target object
func (ar *FollowRepo) MigrateFollowers(ctx context.Context, sourceObjectID, targetObjectID, action string) error {
// if source object id and target object id are same type
sourceObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(sourceObjectID)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
targetObjectTypeStr, err := obj.GetObjectTypeStrByObjectID(targetObjectID)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if sourceObjectTypeStr != targetObjectTypeStr {
return errors.InternalServer(reason.DisallowFollow).WithMsg("not same object type")
}
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, sourceObjectTypeStr, action)
if err != nil {
return err
}
// 1. get all user ids who follow the source object
userIDs, err := ar.GetFollowUserIDs(ctx, sourceObjectID)
if err != nil {
log.Errorf("MigrateFollowers: failed to get user ids who follow %s: %v", sourceObjectID, err)
return err
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
// 1. delete all follows of the source object
_, err = session.Table(entity.Activity{}.TableName()).
Where(builder.Eq{
"object_id": sourceObjectID,
"activity_type": activityType,
}).
Delete(&entity.Activity{})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// 2. update cancel status to active for target tag if source tag followers is active
_, err = session.Table(entity.Activity{}.TableName()).
Where(builder.Eq{
"object_id": targetObjectID,
"activity_type": activityType,
}).
And(builder.In("user_id", userIDs)).
Cols("cancelled").
Update(&entity.Activity{
Cancelled: entity.ActivityAvailable,
})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// 3. get existing follows of the target object
targetFollowers := make([]string, 0)
err = session.Table(entity.Activity{}.TableName()).
Where(builder.Eq{
"object_id": targetObjectID,
"activity_type": activityType,
"cancelled": entity.ActivityAvailable,
}).
Cols("user_id").
Find(&targetFollowers)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// 4. filter out user ids that already follow the target object and create new activity
// Create a map for faster lookup of existing followers
existingFollowers := make(map[string]bool)
for _, uid := range targetFollowers {
existingFollowers[uid] = true
}
// Filter out users who already follow the target
newFollowers := make([]string, 0)
for _, uid := range userIDs {
if !existingFollowers[uid] {
newFollowers = append(newFollowers, uid)
}
}
// Create new activities for the filtered users
for _, uid := range newFollowers {
activity := &entity.Activity{
UserID: uid,
ObjectID: targetObjectID,
OriginalObjectID: targetObjectID,
ActivityType: activityType,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Cancelled: entity.ActivityAvailable,
}
if _, err = session.Insert(activity); err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
}
return nil, nil
})
return err
}
+83
View File
@@ -0,0 +1,83 @@
/*
* 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 activity_common
import (
"context"
"github.com/apache/answer/pkg/uid"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/activity_common"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// VoteRepo activity repository
type VoteRepo struct {
data *data.Data
activityRepo activity_common.ActivityRepo
}
// NewVoteRepo new repository
func NewVoteRepo(data *data.Data, activityRepo activity_common.ActivityRepo) activity_common.VoteRepo {
return &VoteRepo{
data: data,
activityRepo: activityRepo,
}
}
func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string) (status string) {
if len(userID) == 0 {
return ""
}
objectID = uid.DeShortID(objectID)
if len(objectID) == 0 || objectID == "0" {
return ""
}
for _, action := range []string{"vote_up", "vote_down"} {
activityType, _, _, err := vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)
if err != nil {
return ""
}
at := &entity.Activity{}
has, err := vr.data.DB.Context(ctx).Where("object_id = ? AND cancelled = 0 AND activity_type = ? AND user_id = ?",
objectID, activityType, userID).Get(at)
if err != nil {
log.Error(err)
return ""
}
if has {
return action
}
}
return ""
}
func (vr *VoteRepo) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) {
list := make([]*entity.Activity, 0)
count, err = vr.data.DB.Context(ctx).Where("cancelled =0").In("activity_type", activityTypes).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
@@ -0,0 +1,205 @@
/*
* 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 ai_conversation
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
"xorm.io/xorm"
)
// AIConversationRepo
type AIConversationRepo interface {
CreateConversation(ctx context.Context, conversation *entity.AIConversation) error
GetConversation(ctx context.Context, conversationID string) (*entity.AIConversation, bool, error)
UpdateConversation(ctx context.Context, conversation *entity.AIConversation) error
GetConversationsPage(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error)
CreateRecord(ctx context.Context, record *entity.AIConversationRecord) error
GetRecordsByConversationID(ctx context.Context, conversationID string) ([]*entity.AIConversationRecord, error)
UpdateRecordVote(ctx context.Context, cond *entity.AIConversationRecord) error
GetRecord(ctx context.Context, recordID int) (*entity.AIConversationRecord, bool, error)
GetRecordByChatCompletionID(ctx context.Context, role, chatCompletionID string) (*entity.AIConversationRecord, bool, error)
GetConversationsForAdmin(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error)
GetConversationWithVoteStats(ctx context.Context, conversationID string) (helpful, unhelpful int64, err error)
DeleteConversation(ctx context.Context, conversationID string) error
}
type aiConversationRepo struct {
data *data.Data
}
// NewAIConversationRepo new AIConversationRepo
func NewAIConversationRepo(data *data.Data) AIConversationRepo {
return &aiConversationRepo{
data: data,
}
}
// CreateConversation creates a conversation
func (r *aiConversationRepo) CreateConversation(ctx context.Context, conversation *entity.AIConversation) error {
_, err := r.data.DB.Context(ctx).Insert(conversation)
if err != nil {
log.Errorf("create ai conversation failed: %v", err)
return err
}
return nil
}
// GetConversation gets a conversation
func (r *aiConversationRepo) GetConversation(ctx context.Context, conversationID string) (*entity.AIConversation, bool, error) {
conversation := &entity.AIConversation{}
exist, err := r.data.DB.Context(ctx).Where(builder.Eq{"conversation_id": conversationID}).Get(conversation)
if err != nil {
log.Errorf("get ai conversation failed: %v", err)
return nil, false, err
}
return conversation, exist, nil
}
// UpdateConversation updates a conversation
func (r *aiConversationRepo) UpdateConversation(ctx context.Context, conversation *entity.AIConversation) error {
_, err := r.data.DB.Context(ctx).ID(conversation.ID).Update(conversation)
if err != nil {
log.Errorf("update ai conversation failed: %v", err)
return err
}
return nil
}
// GetConversationsPage get conversations by user ID
func (r *aiConversationRepo) GetConversationsPage(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error) {
list = make([]*entity.AIConversation, 0)
total, err = pager.Help(page, pageSize, &list, cond, r.data.DB.Context(ctx).Desc("id"))
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return list, total, err
}
// CreateRecord creates a conversation record
func (r *aiConversationRepo) CreateRecord(ctx context.Context, record *entity.AIConversationRecord) error {
_, err := r.data.DB.Context(ctx).Insert(record)
if err != nil {
log.Errorf("create ai conversation record failed: %v", err)
return err
}
return nil
}
// GetRecordsByConversationID get records by conversation ID
func (r *aiConversationRepo) GetRecordsByConversationID(ctx context.Context, conversationID string) ([]*entity.AIConversationRecord, error) {
records := make([]*entity.AIConversationRecord, 0)
err := r.data.DB.Context(ctx).
Where(builder.Eq{"conversation_id": conversationID}).
OrderBy("created_at ASC").
Find(&records)
if err != nil {
log.Errorf("get ai conversation records failed: %v", err)
return nil, err
}
return records, nil
}
// UpdateRecordVote update record vote
func (r *aiConversationRepo) UpdateRecordVote(ctx context.Context, cond *entity.AIConversationRecord) (err error) {
_, err = r.data.DB.Context(ctx).ID(cond.ID).MustCols("helpful", "unhelpful").Update(cond)
if err != nil {
log.Errorf("update ai conversation record vote failed: %v", err)
return err
}
return nil
}
// GetRecord get record
func (r *aiConversationRepo) GetRecord(ctx context.Context, recordID int) (*entity.AIConversationRecord, bool, error) {
record := &entity.AIConversationRecord{}
exist, err := r.data.DB.Context(ctx).ID(recordID).Get(record)
if err != nil {
log.Errorf("get ai conversation record failed: %v", err)
return nil, false, err
}
return record, exist, nil
}
// GetRecordByChatCompletionID gets record by chat completion ID
func (r *aiConversationRepo) GetRecordByChatCompletionID(ctx context.Context, role, chatCompletionID string) (*entity.AIConversationRecord, bool, error) {
record := &entity.AIConversationRecord{}
exist, err := r.data.DB.Context(ctx).Where(builder.Eq{"role": role}).
Where(builder.Eq{"chat_completion_id": chatCompletionID}).Get(record)
if err != nil {
log.Errorf("get ai conversation record by chat completion id failed: %v", err)
return nil, false, err
}
return record, exist, nil
}
// GetConversationsForAdmin gets conversation list for admin
func (r *aiConversationRepo) GetConversationsForAdmin(ctx context.Context, page, pageSize int, cond *entity.AIConversation) (list []*entity.AIConversation, total int64, err error) {
list = make([]*entity.AIConversation, 0)
total, err = pager.Help(page, pageSize, &list, cond, r.data.DB.Context(ctx).Desc("id"))
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return list, total, err
}
// GetConversationWithVoteStats gets conversation vote statistics
func (r *aiConversationRepo) GetConversationWithVoteStats(ctx context.Context, conversationID string) (helpful, unhelpful int64, err error) {
res, err := r.data.DB.Context(ctx).SumsInt(&entity.AIConversationRecord{ConversationID: conversationID}, "helpful", "unhelpful")
if err != nil {
log.Errorf("get ai conversation vote stats failed: %v", err)
return 0, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if len(res) < 2 {
log.Errorf("get ai conversation vote stats failed: invalid result length %d", len(res))
return 0, 0, nil
}
return res[0], res[1], nil
}
// DeleteConversation deletes a conversation and its related records
func (r *aiConversationRepo) DeleteConversation(ctx context.Context, conversationID string) error {
_, err := r.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
if _, err := session.Context(ctx).Where("conversation_id = ?", conversationID).Delete(&entity.AIConversationRecord{}); err != nil {
log.Errorf("delete ai conversation records failed: %v", err)
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if _, err := session.Context(ctx).Where("conversation_id = ?", conversationID).Delete(&entity.AIConversation{}); err != nil {
log.Errorf("delete ai conversation failed: %v", err)
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil, nil
})
if err != nil {
return err
}
return nil
}
+559
View File
@@ -0,0 +1,559 @@
/*
* 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 answer
import (
"context"
"time"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/handler"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/activity_common"
answercommon "github.com/apache/answer/internal/service/answer_common"
"github.com/apache/answer/internal/service/rank"
"github.com/apache/answer/internal/service/unique"
"github.com/apache/answer/pkg/uid"
"github.com/apache/answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// answerRepo answer repository
type answerRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
userRankRepo rank.UserRankRepo
activityRepo activity_common.ActivityRepo
}
// NewAnswerRepo new repository
func NewAnswerRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
userRankRepo rank.UserRankRepo,
activityRepo activity_common.ActivityRepo,
) answercommon.AnswerRepo {
return &answerRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
userRankRepo: userRankRepo,
activityRepo: activityRepo,
}
}
// AddAnswer add answer
func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err error) {
answer.QuestionID = uid.DeShortID(answer.QuestionID)
ID, err := ar.uniqueIDRepo.GenUniqueIDStr(ctx, answer.TableName())
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
answer.ID = ID
_, err = ar.data.DB.Context(ctx).Insert(answer)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
answer.ID = uid.EnShortID(answer.ID)
answer.QuestionID = uid.EnShortID(answer.QuestionID)
}
_ = ar.updateSearch(ctx, answer.ID)
return nil
}
// RemoveAnswer delete answer
func (ar *answerRepo) RemoveAnswer(ctx context.Context, answerID string) (err error) {
answerID = uid.DeShortID(answerID)
_, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{
Status: entity.AnswerStatusDeleted,
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = ar.updateSearch(ctx, answerID)
return nil
}
// RecoverAnswer recover answer
func (ar *answerRepo) RecoverAnswer(ctx context.Context, answerID string) (err error) {
answerID = uid.DeShortID(answerID)
_, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{
Status: entity.AnswerStatusAvailable,
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = ar.updateSearch(ctx, answerID)
return nil
}
// RemoveAllUserAnswer remove all user answer
func (ar *answerRepo) RemoveAllUserAnswer(ctx context.Context, userID string) (err error) {
// find all answer id that need to be deleted
answerIDs := make([]string, 0)
session := ar.data.DB.Context(ctx).Where("user_id = ?", userID)
session.Where("status != ?", entity.AnswerStatusDeleted)
err = session.Select("id").Table("answer").Find(&answerIDs)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if len(answerIDs) == 0 {
return nil
}
log.Infof("find %d answers need to be deleted for user %s", len(answerIDs), userID)
// delete all question
session = ar.data.DB.Context(ctx).Where("user_id = ?", userID)
session.Where("status != ?", entity.AnswerStatusDeleted)
_, err = session.Cols("status", "updated_at").Update(&entity.Answer{
UpdatedAt: time.Now(),
Status: entity.AnswerStatusDeleted,
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// update search content
for _, id := range answerIDs {
_ = ar.updateSearch(ctx, id)
}
return nil
}
// UpdateAnswer update answer
func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, cols []string) (err error) {
answer.ID = uid.DeShortID(answer.ID)
answer.QuestionID = uid.DeShortID(answer.QuestionID)
_, err = ar.data.DB.Context(ctx).ID(answer.ID).Cols(cols...).Update(answer)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = ar.updateSearch(ctx, answer.ID)
return err
}
func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answerID string, status int) (err error) {
answerID = uid.DeShortID(answerID)
_, err = ar.data.DB.Context(ctx).ID(answerID).Cols("status").Update(&entity.Answer{Status: status})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = ar.updateSearch(ctx, answerID)
return
}
// GetAnswer get answer one
func (ar *answerRepo) GetAnswer(ctx context.Context, id string) (
answer *entity.Answer, exist bool, err error,
) {
id = uid.DeShortID(id)
answer = &entity.Answer{}
exist, err = ar.data.DB.Context(ctx).ID(id).Get(answer)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
answer.ID = uid.EnShortID(answer.ID)
answer.QuestionID = uid.EnShortID(answer.QuestionID)
}
return
}
// GetAnswerCount count answer
func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) {
var resp = new(entity.Answer)
count, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusAvailable).Count(resp)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetAnswerList get answer list all
func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) {
answerList = make([]*entity.Answer, 0)
if len(answer.ID) > 0 {
answer.ID = uid.DeShortID(answer.ID)
}
if len(answer.QuestionID) > 0 {
answer.QuestionID = uid.DeShortID(answer.QuestionID)
}
err = ar.data.DB.Context(ctx).Find(&answerList, answer)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
for _, item := range answerList {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
}
return
}
// GetAnswerPage get answer page
func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) {
answer.ID = uid.DeShortID(answer.ID)
answer.QuestionID = uid.DeShortID(answer.QuestionID)
answerList = make([]*entity.Answer, 0)
total, err = pager.Help(page, pageSize, &answerList, answer, ar.data.DB.Context(ctx))
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
for _, item := range answerList {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
}
return
}
// UpdateAcceptedStatus update all accepted status of this question's answers
func (ar *answerRepo) UpdateAcceptedStatus(ctx context.Context, acceptedAnswerID string, questionID string) error {
acceptedAnswerID = uid.DeShortID(acceptedAnswerID)
questionID = uid.DeShortID(questionID)
// update all this question's answer accepted status to false
_, err := ar.data.DB.Context(ctx).Where("question_id = ?", questionID).Cols("adopted").Update(&entity.Answer{
Accepted: schema.AnswerAcceptedFailed,
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// if acceptedAnswerID is not empty, update accepted status to true
if len(acceptedAnswerID) > 0 && acceptedAnswerID != "0" {
_, err = ar.data.DB.Context(ctx).Where("id = ?", acceptedAnswerID).Cols("adopted").Update(&entity.Answer{
Accepted: schema.AnswerAcceptedEnable,
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
}
_ = ar.updateSearch(ctx, acceptedAnswerID)
return nil
}
// GetByID
func (ar *answerRepo) GetByID(ctx context.Context, answerID string) (*entity.Answer, bool, error) {
var resp entity.Answer
answerID = uid.DeShortID(answerID)
has, err := ar.data.DB.Context(ctx).ID(answerID).Get(&resp)
if err != nil {
return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
resp.ID = uid.EnShortID(resp.ID)
resp.QuestionID = uid.EnShortID(resp.QuestionID)
}
return &resp, has, nil
}
func (ar *answerRepo) GetByIDs(ctx context.Context, answerIDs ...string) ([]*entity.Answer, error) {
for idx, answerID := range answerIDs {
answerIDs[idx] = uid.DeShortID(answerID)
}
var resp = make([]*entity.Answer, 0)
err := ar.data.DB.Context(ctx).In("id", answerIDs).Find(&resp)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
for _, item := range resp {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
}
return resp, nil
}
func (ar *answerRepo) GetCountByQuestionID(ctx context.Context, questionID string) (int64, error) {
questionID = uid.DeShortID(questionID)
var resp = new(entity.Answer)
count, err := ar.data.DB.Context(ctx).Where("question_id =? and status = ?", questionID, entity.AnswerStatusAvailable).Count(resp)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, nil
}
func (ar *answerRepo) GetCountByUserID(ctx context.Context, userID string) (int64, error) {
var resp = new(entity.Answer)
count, err := ar.data.DB.Context(ctx).Where(" user_id = ? and status = ?", userID, entity.AnswerStatusAvailable).Count(resp)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, nil
}
func (ar *answerRepo) GetIDsByUserIDAndQuestionID(ctx context.Context, userID string, questionID string) ([]string, error) {
questionID = uid.DeShortID(questionID)
var ids []string
resp := make([]string, 0)
err := ar.data.DB.Context(ctx).Table(entity.Answer{}.TableName()).Where("question_id =? and user_id = ? and status = ?", questionID, userID, entity.AnswerStatusAvailable).OrderBy("created_at ASC").Cols("id").Find(&ids)
if err != nil {
return resp, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
for _, id := range ids {
resp = append(resp, uid.EnShortID(id))
}
} else {
resp = ids
}
return resp, nil
}
// SearchList
func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) {
if search.QuestionID != "" {
search.QuestionID = uid.DeShortID(search.QuestionID)
}
search.ID = uid.DeShortID(search.ID)
var count int64
var err error
rows := make([]*entity.Answer, 0)
if search.Page > 0 {
search.Page--
} else {
search.Page = 0
}
if search.PageSize == 0 {
search.PageSize = constant.DefaultPageSize
}
offset := search.Page * search.PageSize
session := ar.data.DB.Context(ctx)
if search.QuestionID != "" {
session = session.And("question_id = ?", search.QuestionID)
}
if len(search.UserID) > 0 {
session = session.And("user_id = ?", search.UserID)
}
switch search.Order {
case entity.AnswerSearchOrderByTime:
session = session.OrderBy("created_at desc")
case entity.AnswerSearchOrderByTimeAsc:
session = session.OrderBy("created_at asc")
case entity.AnswerSearchOrderByVote:
session = session.OrderBy("vote_count desc")
default:
session = session.OrderBy("adopted desc,vote_count desc,created_at asc")
}
if !search.IncludeDeleted {
if search.LoginUserID == "" {
session = session.And("status = ? ", entity.AnswerStatusAvailable)
} else {
session = session.And("status = ? OR user_id = ?", entity.AnswerStatusAvailable, search.LoginUserID)
}
}
session = session.Limit(search.PageSize, offset)
count, err = session.FindAndCount(&rows)
if err != nil {
return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
for _, item := range rows {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
}
return rows, count, nil
}
// GetPersonalAnswerPage personal answer page
func (ar *answerRepo) GetPersonalAnswerPage(ctx context.Context, req *entity.PersonalAnswerPageQueryCond) (
resp []*entity.Answer, total int64, err error) {
cond := &entity.Answer{
UserID: req.UserID,
}
session := ar.data.DB.Context(ctx)
switch req.Order {
case entity.AnswerSearchOrderByTime:
session = session.OrderBy("created_at desc")
case entity.AnswerSearchOrderByTimeAsc:
session = session.OrderBy("created_at asc")
case entity.AnswerSearchOrderByVote:
session = session.OrderBy("vote_count desc")
default:
session = session.OrderBy("adopted desc,vote_count desc,created_at asc")
}
if req.ShowPending {
session = session.And("status != ?", entity.AnswerStatusDeleted)
} else {
session = session.And("status = ?", entity.AnswerStatusAvailable)
}
resp = make([]*entity.Answer, 0)
total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session)
if err != nil {
return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
for _, item := range resp {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
}
return resp, total, nil
}
func (ar *answerRepo) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) (
resp []*entity.Answer, total int64, err error) {
cond := &entity.Answer{}
session := ar.data.DB.Context(ctx)
if len(req.QuestionID) == 0 && len(req.AnswerID) == 0 {
session.Join("INNER", "question", "answer.question_id = question.id")
if len(req.QuestionTitle) > 0 {
session.Where("question.title like ?", "%"+req.QuestionTitle+"%")
}
}
if len(req.AnswerID) > 0 {
cond.ID = req.AnswerID
}
if len(req.QuestionID) > 0 {
session.Where("answer.question_id = ?", req.QuestionID)
}
if req.Status > 0 {
cond.Status = req.Status
}
session.Desc("answer.created_at")
resp = make([]*entity.Answer, 0)
total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session)
if err != nil {
return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return resp, total, nil
}
// SumVotesByQuestionID sum votes by question id
func (ar *answerRepo) SumVotesByQuestionID(ctx context.Context, questionID string) (float64, error) {
questionID = uid.DeShortID(questionID)
var resp entity.Answer
count, err := ar.data.DB.Context(ctx).Where("question_id = ? and status = ?", questionID, entity.AnswerStatusAvailable).Sum(&resp, "vote_count")
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, nil
}
// updateSearch update search, if search plugin not enable, do nothing
func (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err error) {
answerID = uid.DeShortID(answerID)
// check search plugin
var (
s plugin.Search
)
_ = plugin.CallSearch(func(search plugin.Search) error {
s = search
return nil
})
if s == nil {
return
}
answer, exist, err := ar.GetAnswer(ctx, answerID)
if !exist {
return
}
if err != nil {
return err
}
// get question
var (
question = new(entity.Question)
)
exist, err = ar.data.DB.Context(ctx).Where("id = ?", answer.QuestionID).Get(&question)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return
}
// get tags
var (
tagListList = make([]*entity.TagRel, 0)
tags = make([]string, 0)
)
st := ar.data.DB.Context(ctx).Where("object_id = ?", uid.DeShortID(question.ID))
st.Where("status = ?", entity.TagRelStatusAvailable)
err = st.Find(&tagListList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
for _, tag := range tagListList {
tags = append(tags, tag.TagID)
}
content := &plugin.SearchContent{
ObjectID: answerID,
Title: question.Title,
Type: constant.AnswerObjectType,
Content: answer.OriginalText,
Answers: 0,
Status: plugin.SearchContentStatus(answer.Status),
Tags: tags,
QuestionID: answer.QuestionID,
UserID: answer.UserID,
Views: int64(question.ViewCount),
Created: answer.CreatedAt.Unix(),
Active: answer.UpdatedAt.Unix(),
Score: int64(answer.VoteCount),
HasAccepted: answer.Accepted == schema.AnswerAcceptedEnable,
}
err = s.UpdateContent(ctx, content)
return
}
func (ar *answerRepo) DeletePermanentlyAnswers(ctx context.Context) error {
// get all deleted answers ids
ids := make([]string, 0)
err := ar.data.DB.Context(ctx).Select("id").Table(new(entity.Answer).TableName()).
Where("status = ?", entity.AnswerStatusDeleted).Find(&ids)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if len(ids) == 0 {
return nil
}
// delete all revisions permanently
_, err = ar.data.DB.Context(ctx).In("object_id", ids).Delete(&entity.Revision{})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusDeleted).Delete(&entity.Answer{})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
+91
View File
@@ -0,0 +1,91 @@
/*
* 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 api_key
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/apikey"
"github.com/segmentfault/pacman/errors"
)
type apiKeyRepo struct {
data *data.Data
}
// NewAPIKeyRepo creates a new apiKey repository
func NewAPIKeyRepo(data *data.Data) apikey.APIKeyRepo {
return &apiKeyRepo{
data: data,
}
}
func (ar *apiKeyRepo) GetAPIKeyList(ctx context.Context) (keys []*entity.APIKey, err error) {
keys = make([]*entity.APIKey, 0)
err = ar.data.DB.Context(ctx).Where("hidden = ?", 0).Find(&keys)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (ar *apiKeyRepo) GetAPIKey(ctx context.Context, apiKey string) (key *entity.APIKey, exist bool, err error) {
key = &entity.APIKey{}
exist, err = ar.data.DB.Context(ctx).Where("access_key = ?", apiKey).Get(key)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (ar *apiKeyRepo) UpdateAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) {
_, err = ar.data.DB.Context(ctx).ID(apiKey.ID).Update(&apiKey)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (ar *apiKeyRepo) AddAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) {
_, err = ar.data.DB.Context(ctx).Insert(&apiKey)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (ar *apiKeyRepo) DeleteAPIKey(ctx context.Context, id int) (err error) {
_, err = ar.data.DB.Context(ctx).ID(id).Delete(&entity.APIKey{})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (ar *apiKeyRepo) DeleteAPIKeysByUserID(ctx context.Context, userID string) (err error) {
_, err = ar.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.APIKey{})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+239
View File
@@ -0,0 +1,239 @@
/*
* 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 auth
import (
"context"
"encoding/json"
"github.com/apache/answer/internal/service/auth"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// authRepo auth repository
type authRepo struct {
data *data.Data
}
// NewAuthRepo new repository
func NewAuthRepo(data *data.Data) auth.AuthRepo {
return &authRepo{
data: data,
}
}
// GetUserCacheInfo get user cache info
func (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserTokenCacheKey+accessToken)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return nil, nil
}
userInfo = &entity.UserCacheInfo{}
_ = json.Unmarshal([]byte(userInfoCache), userInfo)
return userInfo, nil
}
// SetUserCacheInfo set user cache info
func (ar *authRepo) SetUserCacheInfo(ctx context.Context,
accessToken, visitToken string, userInfo *entity.UserCacheInfo) (err error) {
userInfo.VisitToken = visitToken
userInfoCache, err := json.Marshal(userInfo)
if err != nil {
return err
}
err = ar.data.Cache.SetString(ctx, constant.UserTokenCacheKey+accessToken,
string(userInfoCache), constant.UserTokenCacheTime)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if err := ar.AddUserTokenMapping(ctx, userInfo.UserID, accessToken); err != nil {
log.Error(err)
}
if len(visitToken) == 0 {
return nil
}
if err := ar.data.Cache.SetString(ctx, constant.UserVisitTokenCacheKey+visitToken,
accessToken, constant.UserTokenCacheTime); err != nil {
log.Error(err)
}
return nil
}
// GetUserVisitCacheInfo get user visit cache info
func (ar *authRepo) GetUserVisitCacheInfo(ctx context.Context, visitToken string) (accessToken string, err error) {
accessToken, exist, err := ar.data.Cache.GetString(ctx, constant.UserVisitTokenCacheKey+visitToken)
if err != nil {
return "", errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return "", nil
}
return accessToken, nil
}
// RemoveUserCacheInfo remove user cache info
func (ar *authRepo) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) {
err = ar.data.Cache.Del(ctx, constant.UserTokenCacheKey+accessToken)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// RemoveUserVisitCacheInfo remove visit token cache
func (ar *authRepo) RemoveUserVisitCacheInfo(ctx context.Context, visitToken string) (err error) {
err = ar.data.Cache.Del(ctx, constant.UserVisitTokenCacheKey+visitToken)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// SetUserStatus set user status
func (ar *authRepo) SetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error) {
userInfoCache, err := json.Marshal(userInfo)
if err != nil {
return err
}
err = ar.data.Cache.SetString(ctx, constant.UserStatusChangedCacheKey+userID,
string(userInfoCache), constant.UserStatusChangedCacheTime)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// GetUserStatus get user status
func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) {
userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return nil, nil
}
userInfo = &entity.UserCacheInfo{}
_ = json.Unmarshal([]byte(userInfoCache), userInfo)
return userInfo, nil
}
// RemoveUserStatus remove user status
func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err error) {
err = ar.data.Cache.Del(ctx, constant.UserStatusChangedCacheKey+userID)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// GetAdminUserCacheInfo get admin user cache info
func (ar *authRepo) GetAdminUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) {
userInfoCache, exist, err := ar.data.Cache.GetString(ctx, constant.AdminTokenCacheKey+accessToken)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
if !exist {
return nil, nil
}
userInfo = &entity.UserCacheInfo{}
_ = json.Unmarshal([]byte(userInfoCache), userInfo)
return userInfo, nil
}
// SetAdminUserCacheInfo set admin user cache info
func (ar *authRepo) SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) {
userInfoCache, err := json.Marshal(userInfo)
if err != nil {
return err
}
err = ar.data.Cache.SetString(ctx, constant.AdminTokenCacheKey+accessToken, string(userInfoCache),
constant.AdminTokenCacheTime)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// RemoveAdminUserCacheInfo remove admin user cache info
func (ar *authRepo) RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error) {
err = ar.data.Cache.Del(ctx, constant.AdminTokenCacheKey+accessToken)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// AddUserTokenMapping add user token mapping
func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) {
key := constant.UserTokenMappingCacheKey + userID
resp, _, err := ar.data.Cache.GetString(ctx, key)
if err != nil {
return err
}
mapping := make(map[string]bool, 0)
if len(resp) > 0 {
_ = json.Unmarshal([]byte(resp), &mapping)
}
mapping[accessToken] = true
content, _ := json.Marshal(mapping)
return ar.data.Cache.SetString(ctx, key, string(content), constant.UserTokenCacheTime)
}
// RemoveUserTokens Log out all users under this user id
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) {
key := constant.UserTokenMappingCacheKey + userID
resp, _, err := ar.data.Cache.GetString(ctx, key)
if err != nil {
return
}
mapping := make(map[string]bool, 0)
if len(resp) > 0 {
_ = json.Unmarshal([]byte(resp), &mapping)
log.Debugf("find %d user tokens by user id %s", len(mapping), userID)
}
for token := range mapping {
if token == remainToken {
continue
}
if err := ar.RemoveUserCacheInfo(ctx, token); err != nil {
log.Error(err)
} else {
log.Debugf("del user %s token success", userID)
}
}
if err := ar.RemoveUserStatus(ctx, userID); err != nil {
log.Error(err)
}
if err := ar.data.Cache.Del(ctx, key); err != nil {
log.Error(err)
}
}
+254
View File
@@ -0,0 +1,254 @@
/*
* 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"
"strconv"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/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 _, handler := range handlers {
t, err := handler(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) {
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, entity.BadgeEmptyAwardKey, b))
}
}
return awards, nil
}
// FirstPostEdit first post edit
func (br *eventRuleRepo) FirstPostEdit(ctx context.Context,
event *schema.EventMsg) (awards []*entity.BadgeAward, err error) {
badges := br.getBadgesByHandler(ctx, "FirstPostEdit")
for _, b := range badges {
awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))
}
return awards, nil
}
// FirstFlaggedPost first flagged post.
func (br *eventRuleRepo) FirstFlaggedPost(ctx context.Context,
event *schema.EventMsg) (awards []*entity.BadgeAward, err error) {
badges := br.getBadgesByHandler(ctx, "FirstFlaggedPost")
for _, b := range badges {
awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))
}
return awards, nil
}
// FirstVotedPost first voted post
func (br *eventRuleRepo) FirstVotedPost(ctx context.Context,
event *schema.EventMsg) (awards []*entity.BadgeAward, err error) {
badges := br.getBadgesByHandler(ctx, "FirstVotedPost")
for _, b := range badges {
awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))
}
return awards, nil
}
// FirstReactedPost first reacted post
func (br *eventRuleRepo) FirstReactedPost(ctx context.Context,
event *schema.EventMsg) (awards []*entity.BadgeAward, err error) {
badges := br.getBadgesByHandler(ctx, "FirstReactedPost")
for _, b := range badges {
awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))
}
return awards, nil
}
// FirstSharedPost first shared post
func (br *eventRuleRepo) FirstSharedPost(ctx context.Context,
event *schema.EventMsg) (awards []*entity.BadgeAward, err error) {
badges := br.getBadgesByHandler(ctx, "FirstSharedPost")
for _, b := range badges {
awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))
}
return awards, nil
}
// FirstAcceptAnswer user first accept answer
func (br *eventRuleRepo) FirstAcceptAnswer(ctx context.Context,
event *schema.EventMsg) (awards []*entity.BadgeAward, err error) {
badges := br.getBadgesByHandler(ctx, "FirstAcceptAnswer")
for _, b := range badges {
awards = append(awards, br.createBadgeAward(event.UserID, event.GetObjectID(), b))
}
return awards, nil
}
// ReachAnswerAcceptedAmount reach answer accepted amount
func (br *eventRuleRepo) ReachAnswerAcceptedAmount(ctx context.Context,
event *schema.EventMsg) (awards []*entity.BadgeAward, err error) {
badges := br.getBadgesByHandler(ctx, "ReachAnswerAcceptedAmount")
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()
}
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 awards, nil
}
// ReachAnswerVote reach answer vote
func (br *eventRuleRepo) ReachAnswerVote(ctx context.Context,
event *schema.EventMsg) (awards []*entity.BadgeAward, err error) {
badges := br.getBadgesByHandler(ctx, "ReachAnswerVote")
// get vote amount
amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount"))
if amount == 0 {
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 awards, nil
}
// ReachQuestionVote reach question vote
func (br *eventRuleRepo) ReachQuestionVote(ctx context.Context,
event *schema.EventMsg) (awards []*entity.BadgeAward, err error) {
badges := br.getBadgesByHandler(ctx, "ReachQuestionVote")
// get vote amount
amount, _ := strconv.Atoi(event.GetExtra("vote_up_amount"))
if amount == 0 {
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 awards, nil
}
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
}
return badges
}
func (br *eventRuleRepo) createBadgeAward(userID, awardKey string, badge *entity.Badge) (awards *entity.BadgeAward) {
return &entity.BadgeAward{
UserID: userID,
BadgeID: badge.ID,
AwardKey: awardKey,
}
}
+152
View File
@@ -0,0 +1,152 @@
/*
* 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/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/badge"
"github.com/apache/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
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,
}
}
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
}
// 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)
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()
}
return
}
// ListActivated returns a list of activated badges
func (r *badgeRepo) ListActivated(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.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()
}
return
}
// ListInactivated returns a list of inactivated badges
func (r *badgeRepo) ListInactivated(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.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()
}
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
}
// 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
}
@@ -0,0 +1,196 @@
/*
* 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"
"fmt"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/badge"
"github.com/apache/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
type badgeAwardRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
func NewBadgeAwardRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.BadgeAwardRepo {
return &badgeAwardRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// 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 err
}
_, 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 {
return nil, err
}
if !exist {
return nil, fmt.Errorf("badge not exist")
}
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
}
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
}
// 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) 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) 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
}
// 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.BadgeAward{}, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// 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
}
// 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("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
}
// 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) {
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
}
// 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
}
// DeleteUserBadgeAward delete user badge award
func (r *badgeAwardRepo) DeleteUserBadgeAward(ctx context.Context, userID string) (err error) {
_, err = r.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.BadgeAward{})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
@@ -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.
*/
package badge_group
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/badge"
"github.com/apache/answer/internal/service/unique"
)
type badgeGroupRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
func NewBadgeGroupRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) badge.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
}
+117
View File
@@ -0,0 +1,117 @@
/*
* 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 captcha
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/action"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// captchaRepo captcha repository
type captchaRepo struct {
data *data.Data
}
// NewCaptchaRepo new repository
func NewCaptchaRepo(data *data.Data) action.CaptchaRepo {
return &captchaRepo{
data: data,
}
}
func (cr *captchaRepo) SetActionType(ctx context.Context, unit, actionType, config string, amount int) (err error) {
now := time.Now()
cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02"))
value := &entity.ActionRecordInfo{}
value.LastTime = now.Unix()
value.Num = amount
valueStr, err := json.Marshal(value)
if err != nil {
return nil
}
err = cr.data.Cache.SetString(ctx, cacheKey, string(valueStr), 6*time.Minute)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (cr *captchaRepo) GetActionType(ctx context.Context, unit, actionType string) (actionInfo *entity.ActionRecordInfo, err error) {
now := time.Now()
cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02"))
res, exist, err := cr.data.Cache.GetString(ctx, cacheKey)
if err != nil {
return nil, err
}
if !exist {
return nil, nil
}
actionInfo = &entity.ActionRecordInfo{}
_ = json.Unmarshal([]byte(res), actionInfo)
return actionInfo, nil
}
func (cr *captchaRepo) DelActionType(ctx context.Context, unit, actionType string) (err error) {
now := time.Now()
cacheKey := fmt.Sprintf("ActionRecord:%s@%s@%s", unit, actionType, now.Format("2006-1-02"))
err = cr.data.Cache.Del(ctx, cacheKey)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// SetCaptcha set captcha to cache
func (cr *captchaRepo) SetCaptcha(ctx context.Context, key, captcha string) (err error) {
err = cr.data.Cache.SetString(ctx, key, captcha, 6*time.Minute)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetCaptcha get captcha from cache
func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha string, err error) {
captcha, exist, err := cr.data.Cache.GetString(ctx, key)
if err != nil {
return "", err
}
if !exist {
return "", fmt.Errorf("captcha not exist")
}
return captcha, nil
}
func (cr *captchaRepo) DelCaptcha(ctx context.Context, key string) (err error) {
err = cr.data.Cache.Del(ctx, key)
if err != nil {
log.Debug(err)
}
return nil
}
@@ -0,0 +1,153 @@
/*
* 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 collection
import (
"context"
"github.com/apache/answer/internal/service/collection"
"xorm.io/xorm"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/segmentfault/pacman/errors"
)
// collectionGroupRepo collectionGroup repository
type collectionGroupRepo struct {
data *data.Data
}
// NewCollectionGroupRepo new repository
func NewCollectionGroupRepo(data *data.Data) collection.CollectionGroupRepo {
return &collectionGroupRepo{
data: data,
}
}
// AddCollectionGroup add collection group
func (cr *collectionGroupRepo) AddCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup) (err error) {
_, err = cr.data.DB.Context(ctx).Insert(collectionGroup)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// AddCollectionDefaultGroup add collection group
func (cr *collectionGroupRepo) AddCollectionDefaultGroup(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error) {
defaultGroup := &entity.CollectionGroup{
Name: "default",
DefaultGroup: schema.CGDefault,
UserID: userID,
}
_, err = cr.data.DB.Context(ctx).Insert(defaultGroup)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
collectionGroup = defaultGroup
return
}
// CreateDefaultGroupIfNotExist create default group if not exist
func (cr *collectionGroupRepo) CreateDefaultGroupIfNotExist(ctx context.Context, userID string) (
collectionGroup *entity.CollectionGroup, err error) {
_, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
old := &entity.CollectionGroup{
UserID: userID,
DefaultGroup: schema.CGDefault,
}
exist, err := session.ForUpdate().Get(old)
if err != nil {
return nil, err
}
if exist {
collectionGroup = old
return old, nil
}
defaultGroup := &entity.CollectionGroup{
Name: "default",
DefaultGroup: schema.CGDefault,
UserID: userID,
}
_, err = session.Insert(defaultGroup)
if err != nil {
return nil, err
}
collectionGroup = defaultGroup
return nil, nil
})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return collectionGroup, nil
}
// UpdateCollectionGroup update collection group
func (cr *collectionGroupRepo) UpdateCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup, cols []string) (err error) {
_, err = cr.data.DB.Context(ctx).ID(collectionGroup.ID).Cols(cols...).Update(collectionGroup)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetCollectionGroup get collection group one
func (cr *collectionGroupRepo) GetCollectionGroup(ctx context.Context, id string) (
collectionGroup *entity.CollectionGroup, exist bool, err error,
) {
collectionGroup = &entity.CollectionGroup{}
exist, err = cr.data.DB.Context(ctx).ID(id).Get(collectionGroup)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetCollectionGroupPage get collection group page
func (cr *collectionGroupRepo) GetCollectionGroupPage(ctx context.Context, page, pageSize int, collectionGroup *entity.CollectionGroup) (collectionGroupList []*entity.CollectionGroup, total int64, err error) {
collectionGroupList = make([]*entity.CollectionGroup, 0)
session := cr.data.DB.Context(ctx)
if collectionGroup.UserID != "" && collectionGroup.UserID != "0" {
session = session.Where("user_id = ?", collectionGroup.UserID)
}
session = session.OrderBy("update_time desc")
total, err = pager.Help(page, pageSize, collectionGroupList, collectionGroup, session)
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
func (cr *collectionGroupRepo) GetDefaultID(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, has bool, err error) {
collectionGroup = &entity.CollectionGroup{}
has, err = cr.data.DB.Context(ctx).Where("user_id =? and default_group = ?", userID, schema.CGDefault).Get(collectionGroup)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
return
}
+216
View File
@@ -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 collection
import (
"context"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/handler"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
collectioncommon "github.com/apache/answer/internal/service/collection_common"
"github.com/apache/answer/internal/service/unique"
"github.com/apache/answer/pkg/uid"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// collectionRepo collection repository
type collectionRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewCollectionRepo new repository
func NewCollectionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) collectioncommon.CollectionRepo {
return &collectionRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// AddCollection add collection
func (cr *collectionRepo) AddCollection(ctx context.Context, collection *entity.Collection) (err error) {
collection.ID, err = cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName())
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
old := &entity.Collection{
UserID: collection.UserID,
ObjectID: collection.ObjectID,
}
exist, err := session.ForUpdate().Get(old)
if err != nil {
return nil, err
}
if exist {
return nil, nil
}
_, err = session.Insert(collection)
if err != nil {
return nil, err
}
return
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// RemoveCollection delete collection
func (cr *collectionRepo) RemoveCollection(ctx context.Context, id string) (err error) {
_, err = cr.data.DB.Context(ctx).Where("id = ?", id).Delete(&entity.Collection{})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// UpdateCollection update collection
func (cr *collectionRepo) UpdateCollection(ctx context.Context, collection *entity.Collection, cols []string) (err error) {
_, err = cr.data.DB.Context(ctx).ID(collection.ID).Cols(cols...).Update(collection)
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// GetCollection get collection one
func (cr *collectionRepo) GetCollection(ctx context.Context, id int) (collection *entity.Collection, exist bool, err error) {
collection = &entity.Collection{}
exist, err = cr.data.DB.Context(ctx).ID(id).Get(collection)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetCollectionList get collection list all
func (cr *collectionRepo) GetCollectionList(ctx context.Context, collection *entity.Collection) (collectionList []*entity.Collection, err error) {
collectionList = make([]*entity.Collection, 0)
err = cr.data.DB.Context(ctx).Find(&collectionList, collection)
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
// GetOneByObjectIDAndUser get one by object TagID and user
func (cr *collectionRepo) GetOneByObjectIDAndUser(ctx context.Context, userID string, objectID string) (collection *entity.Collection, exist bool, err error) {
collection = &entity.Collection{}
exist, err = cr.data.DB.Context(ctx).Where("user_id = ? and object_id = ?", userID, objectID).Get(collection)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// SearchByObjectIDsAndUser search by object IDs and user
func (cr *collectionRepo) SearchByObjectIDsAndUser(ctx context.Context, userID string, objectIDs []string) ([]*entity.Collection, error) {
collectionList := make([]*entity.Collection, 0)
err := cr.data.DB.Context(ctx).Where("user_id = ?", userID).In("object_id", objectIDs).Find(&collectionList)
if err != nil {
return collectionList, err
}
return collectionList, nil
}
// CountByObjectID count by object TagID
func (cr *collectionRepo) CountByObjectID(ctx context.Context, objectID string) (total int64, err error) {
collection := &entity.Collection{}
total, err = cr.data.DB.Context(ctx).Where("object_id = ?", objectID).Count(collection)
if err != nil {
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetCollectionPage get collection page
func (cr *collectionRepo) GetCollectionPage(ctx context.Context, page, pageSize int, collection *entity.Collection) (collectionList []*entity.Collection, total int64, err error) {
collectionList = make([]*entity.Collection, 0)
session := cr.data.DB.Context(ctx)
if collection.UserID != "" && collection.UserID != "0" {
session = session.Where("user_id = ?", collection.UserID)
}
if collection.UserCollectionGroupID != "" && collection.UserCollectionGroupID != "0" {
session = session.Where("user_collection_group_id = ?", collection.UserCollectionGroupID)
}
session = session.OrderBy("update_time desc")
total, err = pager.Help(page, pageSize, collectionList, collection, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// SearchObjectCollected check object is collected or not
func (cr *collectionRepo) SearchObjectCollected(ctx context.Context, userID string, objectIds []string) (map[string]bool, error) {
for i := range objectIds {
objectIds[i] = uid.DeShortID(objectIds[i])
}
list, err := cr.SearchByObjectIDsAndUser(ctx, userID, objectIds)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
collectedMap := make(map[string]bool)
short := handler.GetEnableShortID(ctx)
for _, item := range list {
if short {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
collectedMap[item.ObjectID] = true
}
return collectedMap, nil
}
// SearchList
func (cr *collectionRepo) SearchList(ctx context.Context, search *entity.CollectionSearch) ([]*entity.Collection, int64, error) {
var count int64
var err error
rows := make([]*entity.Collection, 0)
if search.Page > 0 {
search.Page--
} else {
search.Page = 0
}
if search.PageSize == 0 {
search.PageSize = constant.DefaultPageSize
}
offset := search.Page * search.PageSize
session := cr.data.DB.Context(ctx).Where("")
if len(search.UserID) > 0 {
session = session.And("user_id = ?", search.UserID)
} else {
return rows, count, nil
}
session = session.Limit(search.PageSize, offset)
count, err = session.OrderBy("updated_at desc").FindAndCount(&rows)
if err != nil {
return rows, count, err
}
return rows, count, nil
}
+172
View File
@@ -0,0 +1,172 @@
/*
* 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 comment
import (
"context"
"github.com/segmentfault/pacman/log"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/comment"
"github.com/apache/answer/internal/service/comment_common"
"github.com/apache/answer/internal/service/unique"
"github.com/apache/answer/pkg/uid"
"github.com/segmentfault/pacman/errors"
)
// commentRepo comment repository
type commentRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewCommentRepo new repository
func NewCommentRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) comment.CommentRepo {
return &commentRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// NewCommentCommonRepo new repository
func NewCommentCommonRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) comment_common.CommentCommonRepo {
return &commentRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// AddComment add comment
func (cr *commentRepo) AddComment(ctx context.Context, comment *entity.Comment) (err error) {
comment.ID, err = cr.uniqueIDRepo.GenUniqueIDStr(ctx, comment.TableName())
if err != nil {
return err
}
_, err = cr.data.DB.Context(ctx).Insert(comment)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// RemoveComment delete comment
func (cr *commentRepo) RemoveComment(ctx context.Context, commentID string) (err error) {
session := cr.data.DB.Context(ctx).ID(commentID)
_, err = session.Update(&entity.Comment{Status: entity.CommentStatusDeleted})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateCommentContent update comment
func (cr *commentRepo) UpdateCommentContent(
ctx context.Context, commentID string, originalText string, parsedText string) (err error) {
_, err = cr.data.DB.Context(ctx).ID(commentID).Update(&entity.Comment{
OriginalText: originalText,
ParsedText: parsedText,
})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateCommentStatus update comment status
func (cr *commentRepo) UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error) {
_, err = cr.data.DB.Context(ctx).ID(commentID).Update(&entity.Comment{
Status: status,
})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetComment get comment one
func (cr *commentRepo) GetComment(ctx context.Context, commentID string) (
comment *entity.Comment, exist bool, err error) {
comment = &entity.Comment{}
if !uid.IsValidNumericID(commentID) {
return comment, false, nil
}
exist, err = cr.data.DB.Context(ctx).Where("status = ?", entity.CommentStatusAvailable).ID(commentID).Get(comment)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetCommentWithoutStatus get comment one without status
func (cr *commentRepo) GetCommentWithoutStatus(ctx context.Context, commentID string) (
comment *entity.Comment, exist bool, err error) {
comment = &entity.Comment{}
if !uid.IsValidNumericID(commentID) {
return comment, false, nil
}
exist, err = cr.data.DB.Context(ctx).ID(commentID).Get(comment)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (cr *commentRepo) GetCommentCount(ctx context.Context) (count int64, err error) {
list := make([]*entity.Comment, 0)
count, err = cr.data.DB.Context(ctx).Where("status = ?", entity.CommentStatusAvailable).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetCommentPage get comment page
func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) (
commentList []*entity.Comment, total int64, err error,
) {
commentList = make([]*entity.Comment, 0)
session := cr.data.DB.Context(ctx)
session.OrderBy(commentQuery.GetOrderBy())
session.Where("status = ?", entity.CommentStatusAvailable)
cond := &entity.Comment{ObjectID: commentQuery.ObjectID, UserID: commentQuery.UserID}
total, err = pager.Help(commentQuery.Page, commentQuery.PageSize, &commentList, cond, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// RemoveAllUserComment remove all user comment
func (cr *commentRepo) RemoveAllUserComment(ctx context.Context, userID string) (err error) {
session := cr.data.DB.Context(ctx).Where("user_id = ?", userID)
session.Where("status != ?", entity.CommentStatusDeleted)
affected, err := session.Update(&entity.Comment{Status: entity.CommentStatusDeleted})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
log.Infof("delete user comment, userID: %s, affected: %d", userID, affected)
return
}
+143
View File
@@ -0,0 +1,143 @@
/*
* 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 config
import (
"context"
"fmt"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/config"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// configRepo config repository
type configRepo struct {
data *data.Data
}
// NewConfigRepo new repository
func NewConfigRepo(data *data.Data) config.ConfigRepo {
repo := &configRepo{
data: data,
}
return repo
}
func (cr configRepo) GetConfigByID(ctx context.Context, id int) (c *entity.Config, err error) {
cacheKey := fmt.Sprintf("%s%d", constant.ConfigID2KEYCacheKeyPrefix, id)
cacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey)
if err == nil && exist && len(cacheData) > 0 {
c = &entity.Config{}
c.BuildByJSON([]byte(cacheData))
if c.ID > 0 {
return c, nil
}
}
c = &entity.Config{}
exist, err = cr.data.DB.Context(ctx).ID(id).Get(c)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return nil, fmt.Errorf("config not found by id: %d", id)
}
// update cache
if err := cr.data.Cache.SetString(ctx, cacheKey, c.JsonString(), constant.ConfigCacheTime); err != nil {
log.Error(err)
}
return c, nil
}
func (cr configRepo) GetConfigByKey(ctx context.Context, key string) (c *entity.Config, err error) {
cacheKey := constant.ConfigKEY2ContentCacheKeyPrefix + key
cacheData, exist, err := cr.data.Cache.GetString(ctx, cacheKey)
if err == nil && exist && len(cacheData) > 0 {
c = &entity.Config{}
c.BuildByJSON([]byte(cacheData))
if c.ID > 0 {
return c, nil
}
}
c = &entity.Config{Key: key}
exist, err = cr.data.DB.Context(ctx).Get(c)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return nil, fmt.Errorf("config not found by key: %s", key)
}
// update cache
if err := cr.data.Cache.SetString(ctx, cacheKey, c.JsonString(), constant.ConfigCacheTime); err != nil {
log.Error(err)
}
return c, nil
}
func (cr configRepo) GetConfigByKeyFromDB(ctx context.Context, key string) (c *entity.Config, err error) {
c = &entity.Config{Key: key}
exist, err := cr.data.DB.Context(ctx).Get(c)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return nil, fmt.Errorf("config not found by key: %s", key)
}
return c, nil
}
func (cr configRepo) UpdateConfig(ctx context.Context, key string, value string) (err error) {
// check if key exists
oldConfig := &entity.Config{Key: key}
exist, err := cr.data.DB.Context(ctx).Get(oldConfig)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return errors.BadRequest(reason.ObjectNotFound)
}
// update database
_, err = cr.data.DB.Context(ctx).ID(oldConfig.ID).Update(&entity.Config{Value: value})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
oldConfig.Value = value
cacheVal := oldConfig.JsonString()
// update cache
if err := cr.data.Cache.SetString(ctx,
constant.ConfigKEY2ContentCacheKeyPrefix+key, cacheVal, constant.ConfigCacheTime); err != nil {
log.Error(err)
}
if err := cr.data.Cache.SetString(ctx,
fmt.Sprintf("%s%d", constant.ConfigID2KEYCacheKeyPrefix, oldConfig.ID), cacheVal, constant.ConfigCacheTime); err != nil {
log.Error(err)
}
return
}
+99
View File
@@ -0,0 +1,99 @@
/*
* 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 export
import (
"context"
"time"
"github.com/apache/answer/internal/base/constant"
"github.com/tidwall/gjson"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/service/export"
"github.com/segmentfault/pacman/errors"
)
// emailRepo email repository
type emailRepo struct {
data *data.Data
}
// NewEmailRepo new repository
func NewEmailRepo(data *data.Data) export.EmailRepo {
return &emailRepo{
data: data,
}
}
// SetCode The email code is used to verify that the link in the message is out of date
func (e *emailRepo) SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error {
// Setting the latest code is to help ensure that only one link is active at a time.
// Set userID -> latest code
if err := e.data.Cache.SetString(ctx, constant.UserLatestEmailCodeCacheKey+userID, code, duration); err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// Set latest code -> content
if err := e.data.Cache.SetString(ctx, constant.UserEmailCodeCacheKey+code, content, duration); err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// VerifyCode verify the code if out of date
func (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) {
// Get latest code -> content
codeCacheKey := constant.UserEmailCodeCacheKey + code
content, exist, err := e.data.Cache.GetString(ctx, codeCacheKey)
if err != nil {
return "", err
}
if !exist {
return "", nil
}
// Delete the code after verification
_ = e.data.Cache.Del(ctx, codeCacheKey)
// If some email content does not need to verify the latest code is the same as the code, skip it.
// For example, some unsubscribe email content does not need to verify the latest code.
// This link always works before the code is out of date.
if skipValidationLatestCode := gjson.Get(content, "skip_validation_latest_code").Bool(); skipValidationLatestCode {
return content, nil
}
userID := gjson.Get(content, "user_id").String()
// Get userID -> latest code
latestCode, exist, err := e.data.Cache.GetString(ctx, constant.UserLatestEmailCodeCacheKey+userID)
if err != nil {
return "", err
}
if !exist {
return "", nil
}
// Check if the latest code is the same as the code, if not, means the code is out of date
if latestCode != code {
return "", nil
}
return content, nil
}
@@ -0,0 +1,99 @@
/*
* 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 file_record
import (
"context"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/service/file_record"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/segmentfault/pacman/errors"
)
// fileRecordRepo fileRecord repository
type fileRecordRepo struct {
data *data.Data
}
// NewFileRecordRepo new repository
func NewFileRecordRepo(data *data.Data) file_record.FileRecordRepo {
return &fileRecordRepo{
data: data,
}
}
// AddFileRecord add file record
func (fr *fileRecordRepo) AddFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) {
_, err = fr.data.DB.Context(ctx).Insert(fileRecord)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetFileRecordPage get fileRecord page
func (fr *fileRecordRepo) GetFileRecordPage(ctx context.Context, page, pageSize int, cond *entity.FileRecord) (
fileRecordList []*entity.FileRecord, total int64, err error) {
fileRecordList = make([]*entity.FileRecord, 0)
session := fr.data.DB.Context(ctx)
total, err = pager.Help(page, pageSize, &fileRecordList, cond, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// DeleteFileRecord delete file record
func (fr *fileRecordRepo) DeleteFileRecord(ctx context.Context, id int) (err error) {
_, err = fr.data.DB.Context(ctx).ID(id).Cols("status").Update(&entity.FileRecord{Status: entity.FileRecordStatusDeleted})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateFileRecord update file record
func (fr *fileRecordRepo) UpdateFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) {
_, err = fr.data.DB.Context(ctx).ID(fileRecord.ID).Update(fileRecord)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetFileRecordByURL gets a file record by its url
func (fr *fileRecordRepo) GetFileRecordByURL(ctx context.Context, fileURL string) (record *entity.FileRecord, err error) {
record = &entity.FileRecord{}
session := fr.data.DB.Context(ctx)
exists, err := session.Where("file_url = ? AND status = ?", fileURL, entity.FileRecordStatusAvailable).Get(record)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
if !exists {
return
}
return record, nil
}
+65
View File
@@ -0,0 +1,65 @@
/*
* 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 limit
import (
"context"
"fmt"
"time"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/segmentfault/pacman/errors"
)
// LimitRepo auth repository
type LimitRepo struct {
data *data.Data
}
// NewRateLimitRepo new repository
func NewRateLimitRepo(data *data.Data) *LimitRepo {
return &LimitRepo{
data: data,
}
}
// CheckAndRecord check
func (lr *LimitRepo) CheckAndRecord(ctx context.Context, key string) (limit bool, err error) {
_, exist, err := lr.data.Cache.GetString(ctx, constant.RateLimitCacheKeyPrefix+key)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
return true, nil
}
err = lr.data.Cache.SetString(ctx, constant.RateLimitCacheKeyPrefix+key,
fmt.Sprintf("%d", time.Now().Unix()), constant.RateLimitCacheTime)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return false, nil
}
// ClearRecord clear
func (lr *LimitRepo) ClearRecord(ctx context.Context, key string) error {
return lr.data.Cache.Del(ctx, constant.RateLimitCacheKeyPrefix+key)
}
+121
View File
@@ -0,0 +1,121 @@
/*
* 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 meta
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
metacommon "github.com/apache/answer/internal/service/meta_common"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
"xorm.io/xorm"
)
// metaRepo meta repository
type metaRepo struct {
data *data.Data
}
// NewMetaRepo new repository
func NewMetaRepo(data *data.Data) metacommon.MetaRepo {
return &metaRepo{
data: data,
}
}
// AddMeta add meta
func (mr *metaRepo) AddMeta(ctx context.Context, meta *entity.Meta) (err error) {
_, err = mr.data.DB.Context(ctx).Insert(meta)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// RemoveMeta delete meta
func (mr *metaRepo) RemoveMeta(ctx context.Context, id int) (err error) {
_, err = mr.data.DB.Context(ctx).ID(id).Delete(&entity.Meta{})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateMeta update meta
func (mr *metaRepo) UpdateMeta(ctx context.Context, meta *entity.Meta) (err error) {
_, err = mr.data.DB.Context(ctx).ID(meta.ID).Update(meta)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// AddOrUpdateMetaByObjectIdAndKey if exist record with same objectID and key, update it. Or create a new one
func (mr *metaRepo) AddOrUpdateMetaByObjectIdAndKey(ctx context.Context, objectId, key string, f func(*entity.Meta, bool) (*entity.Meta, error)) error {
_, err := mr.data.DB.Transaction(func(session *xorm.Session) (any, error) {
session = session.Context(ctx)
// 1. acquire meta entity with target object id and key
metaEntity := &entity.Meta{}
exist, err := session.Where(builder.Eq{"object_id": objectId}.And(builder.Eq{"`key`": key})).ForUpdate().Get(metaEntity)
if err != nil {
return nil, err
}
meta, err := f(metaEntity, exist)
if err != nil {
return nil, err
}
// return entity.Meta
if exist {
_, err = session.ID(metaEntity.ID).Update(meta)
} else {
_, err = session.Insert(meta)
}
return nil, err
})
return err
}
// GetMetaByObjectIdAndKey get meta one
func (mr *metaRepo) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key string) (
meta *entity.Meta, exist bool, err error) {
meta = &entity.Meta{}
exist, err = mr.data.DB.Context(ctx).Where(builder.Eq{"object_id": objectID}.And(builder.Eq{"`key`": key})).Desc("created_at").Get(meta)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetMetaList get meta list all
func (mr *metaRepo) GetMetaList(ctx context.Context, meta *entity.Meta) (metaList []*entity.Meta, err error) {
metaList = make([]*entity.Meta, 0)
err = mr.data.DB.Context(ctx).Find(&metaList, meta)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
@@ -0,0 +1,155 @@
/*
* 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 notification
import (
"context"
"time"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
notficationcommon "github.com/apache/answer/internal/service/notification_common"
"github.com/apache/answer/pkg/uid"
"github.com/segmentfault/pacman/errors"
)
// notificationRepo notification repository
type notificationRepo struct {
data *data.Data
}
// NewNotificationRepo new repository
func NewNotificationRepo(data *data.Data) notficationcommon.NotificationRepo {
return &notificationRepo{
data: data,
}
}
// AddNotification add notification
func (nr *notificationRepo) AddNotification(ctx context.Context, notification *entity.Notification) (err error) {
notification.ObjectID = uid.DeShortID(notification.ObjectID)
_, err = nr.data.DB.Context(ctx).Insert(notification)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (nr *notificationRepo) UpdateNotificationContent(ctx context.Context, notification *entity.Notification) (err error) {
now := time.Now()
notification.UpdatedAt = now
notification.ObjectID = uid.DeShortID(notification.ObjectID)
_, err = nr.data.DB.Context(ctx).Where("id =?", notification.ID).Cols("content", "updated_at").Update(notification)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
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)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
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)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (nr *notificationRepo) GetById(ctx context.Context, id string) (*entity.Notification, bool, error) {
info := &entity.Notification{}
exist, err := nr.data.DB.Context(ctx).Where("id = ? ", id).Get(info)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return info, false, err
}
return info, exist, nil
}
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)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return info, false, err
}
return info, exist, nil
}
func (nr *notificationRepo) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) (
notificationList []*entity.Notification, total int64, err error) {
notificationList = make([]*entity.Notification, 0)
if searchCond.UserID == "" {
return notificationList, 0, nil
}
session := nr.data.DB.Context(ctx)
session = session.Desc("updated_at")
cond := &entity.Notification{
UserID: searchCond.UserID,
Type: searchCond.Type,
}
if searchCond.InboxType > 0 {
cond.MsgType = searchCond.InboxType
}
total, err = pager.Help(searchCond.Page, searchCond.PageSize, &notificationList, cond, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
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
}
func (nr *notificationRepo) DeleteNotification(ctx context.Context, userID string) (err error) {
_, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.Notification{})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (nr *notificationRepo) DeleteUserNotificationConfig(ctx context.Context, userID string) (err error) {
_, err = nr.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.UserNotificationConfig{})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
@@ -0,0 +1,68 @@
/*
* 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_config
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/plugin_common"
"github.com/segmentfault/pacman/errors"
)
type pluginConfigRepo struct {
data *data.Data
}
// NewPluginConfigRepo new repository
func NewPluginConfigRepo(data *data.Data) plugin_common.PluginConfigRepo {
return &pluginConfigRepo{
data: data,
}
}
func (ur *pluginConfigRepo) SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error) {
old := &entity.PluginConfig{PluginSlugName: pluginSlugName}
exist, err := ur.data.DB.Context(ctx).Get(old)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
old.Value = configValue
_, err = ur.data.DB.Context(ctx).ID(old.ID).Update(old)
} else {
_, err = ur.data.DB.Context(ctx).Insert(&entity.PluginConfig{PluginSlugName: pluginSlugName, Value: configValue})
}
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *pluginConfigRepo) GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error) {
pluginConfigs = make([]*entity.PluginConfig, 0)
err = ur.data.DB.Context(ctx).Find(&pluginConfigs)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return pluginConfigs, err
}
@@ -0,0 +1,108 @@
/*
* 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_config
import (
"context"
"github.com/apache/answer/internal/base/pager"
"xorm.io/xorm"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/plugin_common"
"github.com/segmentfault/pacman/errors"
)
type pluginUserConfigRepo struct {
data *data.Data
}
// NewPluginUserConfigRepo new repository
func NewPluginUserConfigRepo(data *data.Data) plugin_common.PluginUserConfigRepo {
return &pluginUserConfigRepo{
data: data,
}
}
func (ur *pluginUserConfigRepo) SaveUserPluginConfig(ctx context.Context, userID string,
pluginSlugName, configValue string) (err error) {
_, err = ur.data.DB.Transaction(func(session *xorm.Session) (any, error) {
session = session.Context(ctx)
old := &entity.PluginUserConfig{
UserID: userID,
PluginSlugName: pluginSlugName,
}
exist, err := session.Get(old)
if err != nil {
return nil, err
}
if exist {
old.Value = configValue
_, err = session.ID(old.ID).Update(old)
} else {
_, err = session.Insert(&entity.PluginUserConfig{
UserID: userID,
PluginSlugName: pluginSlugName,
Value: configValue,
})
}
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *pluginUserConfigRepo) GetPluginUserConfig(ctx context.Context, userID, pluginSlugName string) (
pluginUserConfig *entity.PluginUserConfig, exist bool, err error) {
pluginUserConfig = &entity.PluginUserConfig{
UserID: userID,
PluginSlugName: pluginSlugName,
}
exist, err = ur.data.DB.Context(ctx).Get(pluginUserConfig)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return pluginUserConfig, exist, err
}
func (ur *pluginUserConfigRepo) GetPluginUserConfigPage(ctx context.Context, page, pageSize int) (
pluginUserConfigs []*entity.PluginUserConfig, total int64, err error) {
pluginUserConfigs = make([]*entity.PluginUserConfig, 0)
total, err = pager.Help(page, pageSize, &pluginUserConfigs, &entity.PluginUserConfig{}, ur.data.DB.Context(ctx))
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (ur *pluginUserConfigRepo) DeleteUserPluginConfig(ctx context.Context, userID string) (err error) {
_, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.PluginUserConfig{})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+116
View File
@@ -0,0 +1,116 @@
/*
* 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
import (
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/repo/activity"
"github.com/apache/answer/internal/repo/activity_common"
"github.com/apache/answer/internal/repo/ai_conversation"
"github.com/apache/answer/internal/repo/answer"
"github.com/apache/answer/internal/repo/api_key"
"github.com/apache/answer/internal/repo/auth"
"github.com/apache/answer/internal/repo/badge"
"github.com/apache/answer/internal/repo/badge_award"
"github.com/apache/answer/internal/repo/badge_group"
"github.com/apache/answer/internal/repo/captcha"
"github.com/apache/answer/internal/repo/collection"
"github.com/apache/answer/internal/repo/comment"
"github.com/apache/answer/internal/repo/config"
"github.com/apache/answer/internal/repo/export"
"github.com/apache/answer/internal/repo/file_record"
"github.com/apache/answer/internal/repo/limit"
"github.com/apache/answer/internal/repo/meta"
"github.com/apache/answer/internal/repo/notification"
"github.com/apache/answer/internal/repo/plugin_config"
"github.com/apache/answer/internal/repo/question"
"github.com/apache/answer/internal/repo/rank"
"github.com/apache/answer/internal/repo/reason"
"github.com/apache/answer/internal/repo/report"
"github.com/apache/answer/internal/repo/review"
"github.com/apache/answer/internal/repo/revision"
"github.com/apache/answer/internal/repo/role"
"github.com/apache/answer/internal/repo/search_common"
"github.com/apache/answer/internal/repo/site_info"
"github.com/apache/answer/internal/repo/tag"
"github.com/apache/answer/internal/repo/tag_common"
"github.com/apache/answer/internal/repo/unique"
"github.com/apache/answer/internal/repo/user"
"github.com/apache/answer/internal/repo/user_external_login"
"github.com/apache/answer/internal/repo/user_notification_config"
"github.com/google/wire"
)
// ProviderSetRepo is data providers.
var ProviderSetRepo = wire.NewSet(
data.NewData,
data.NewDB,
data.NewCache,
comment.NewCommentRepo,
comment.NewCommentCommonRepo,
captcha.NewCaptchaRepo,
unique.NewUniqueIDRepo,
report.NewReportRepo,
activity_common.NewFollowRepo,
activity_common.NewVoteRepo,
config.NewConfigRepo,
user.NewUserRepo,
user.NewUserAdminRepo,
rank.NewUserRankRepo,
question.NewQuestionRepo,
answer.NewAnswerRepo,
activity_common.NewActivityRepo,
activity.NewVoteRepo,
activity.NewFollowRepo,
activity.NewAnswerActivityRepo,
activity.NewUserActiveActivityRepo,
activity.NewActivityRepo,
activity.NewReviewActivityRepo,
tag.NewTagRepo,
tag_common.NewTagCommonRepo,
tag.NewTagRelRepo,
collection.NewCollectionRepo,
collection.NewCollectionGroupRepo,
auth.NewAuthRepo,
revision.NewRevisionRepo,
search_common.NewSearchRepo,
meta.NewMetaRepo,
export.NewEmailRepo,
reason.NewReasonRepo,
site_info.NewSiteInfo,
notification.NewNotificationRepo,
role.NewRoleRepo,
role.NewUserRoleRelRepo,
role.NewRolePowerRelRepo,
role.NewPowerRepo,
user_external_login.NewUserExternalLoginRepo,
plugin_config.NewPluginConfigRepo,
user_notification_config.NewUserNotificationConfigRepo,
limit.NewRateLimitRepo,
plugin_config.NewPluginUserConfigRepo,
review.NewReviewRepo,
badge.NewBadgeRepo,
badge.NewEventRuleRepo,
badge_group.NewBadgeGroupRepo,
badge_award.NewBadgeAwardRepo,
file_record.NewFileRecordRepo,
api_key.NewAPIKeyRepo,
ai_conversation.NewAIConversationRepo,
)
+873
View File
@@ -0,0 +1,873 @@
/*
* 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 question
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"unicode"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/handler"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
questioncommon "github.com/apache/answer/internal/service/question_common"
"github.com/apache/answer/internal/service/unique"
"github.com/apache/answer/pkg/htmltext"
"github.com/apache/answer/pkg/uid"
"github.com/apache/answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
"xorm.io/xorm"
)
// questionRepo question repository
type questionRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewQuestionRepo new repository
func NewQuestionRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
) questioncommon.QuestionRepo {
return &questionRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// AddQuestion add question
func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Question) (err error) {
question.ID, err = qr.uniqueIDRepo.GenUniqueIDStr(ctx, question.TableName())
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_, err = qr.data.DB.Context(ctx).Insert(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
question.ID = uid.EnShortID(question.ID)
}
return
}
// RemoveQuestion delete question
func (qr *questionRepo) RemoveQuestion(ctx context.Context, id string) (err error) {
id = uid.DeShortID(id)
_, err = qr.data.DB.Context(ctx).Where("id =?", id).Delete(&entity.Question{})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateQuestion update question
func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Question, cols []string) (err error) {
question.ID = uid.DeShortID(question.ID)
_, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols(cols...).Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
question.ID = uid.EnShortID(question.ID)
}
_ = qr.UpdateSearch(ctx, question.ID)
return
}
func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (err error) {
questionID = uid.DeShortID(questionID)
question := &entity.Question{}
_, err = qr.data.DB.Context(ctx).Where("id =?", questionID).Incr("view_count", 1).Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error) {
questionID = uid.DeShortID(questionID)
question := &entity.Question{}
question.AnswerCount = num
_, err = qr.data.DB.Context(ctx).Where("id =?", questionID).Cols("answer_count").Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
func (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionID string) (count int64, err error) {
questionID = uid.DeShortID(questionID)
_, err = qr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
count, err = session.Count(&entity.Collection{ObjectID: questionID})
if err != nil {
return nil, err
}
question := &entity.Question{CollectionCount: int(count)}
_, err = session.ID(questionID).MustCols("collection_count").Update(question)
if err != nil {
return nil, err
}
return
})
if err != nil {
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, nil
}
func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) {
questionID = uid.DeShortID(questionID)
_, err = qr.data.DB.Context(ctx).ID(questionID).Cols("status").Update(&entity.Question{Status: status})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.UpdateSearch(ctx, questionID)
return nil
}
func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) {
question.ID = uid.DeShortID(question.ID)
_, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("status").Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
func (qr *questionRepo) DeletePermanentlyQuestions(ctx context.Context) (err error) {
// get all deleted question ids
ids := make([]string, 0)
err = qr.data.DB.Context(ctx).Select("id").Table(new(entity.Question).TableName()).
Where("status = ?", entity.QuestionStatusDeleted).Find(&ids)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if len(ids) == 0 {
return nil
}
// delete all revisions permanently
_, err = qr.data.DB.Context(ctx).In("object_id", ids).Delete(&entity.Revision{})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_, err = qr.data.DB.Context(ctx).Where("status = ?", entity.QuestionStatusDeleted).Delete(&entity.Question{})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (qr *questionRepo) RecoverQuestion(ctx context.Context, questionID string) (err error) {
questionID = uid.DeShortID(questionID)
_, err = qr.data.DB.Context(ctx).ID(questionID).Cols("status").Update(&entity.Question{Status: entity.QuestionStatusAvailable})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.UpdateSearch(ctx, questionID)
return nil
}
func (qr *questionRepo) UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) {
question.ID = uid.DeShortID(question.ID)
_, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("pin", "show").Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) {
question.ID = uid.DeShortID(question.ID)
_, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("accepted_answer_id").Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) {
question.ID = uid.DeShortID(question.ID)
_, err = qr.data.DB.Context(ctx).Where("id =?", question.ID).Cols("last_answer_id").Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_ = qr.UpdateSearch(ctx, question.ID)
return nil
}
// GetQuestion get question one
func (qr *questionRepo) GetQuestion(ctx context.Context, id string) (
question *entity.Question, exist bool, err error,
) {
id = uid.DeShortID(id)
question = &entity.Question{}
question.ID = id
exist, err = qr.data.DB.Context(ctx).Where("id = ?", id).Get(question)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
question.ID = uid.EnShortID(question.ID)
}
return
}
// GetQuestionsByTitle get question list by title
func (qr *questionRepo) GetQuestionsByTitle(ctx context.Context, title string, pageSize int) (
questionList []*entity.Question, err error) {
questionList = make([]*entity.Question, 0)
session := qr.data.DB.Context(ctx)
session.Where("status != ?", entity.QuestionStatusDeleted)
session.Where("title like ?", "%"+title+"%")
session.Limit(pageSize)
err = session.Find(&questionList)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
}
}
return
}
func (qr *questionRepo) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) {
for key, itemID := range id {
id[key] = uid.DeShortID(itemID)
}
questionList = make([]*entity.Question, 0)
err = qr.data.DB.Context(ctx).Table("question").In("id", id).Find(&questionList)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
}
}
return
}
// GetQuestionList get question list all
func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Question) (questionList []*entity.Question, err error) {
question.ID = uid.DeShortID(question.ID)
questionList = make([]*entity.Question, 0)
err = qr.data.DB.Context(ctx).Find(&questionList, question)
if err != nil {
return questionList, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, item := range questionList {
item.ID = uid.DeShortID(item.ID)
}
return
}
func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) {
session := qr.data.DB.Context(ctx)
session.Where(builder.Lt{"status": entity.QuestionStatusDeleted})
count, err = session.Count(&entity.Question{Show: entity.QuestionShow})
if err != nil {
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, nil
}
func (qr *questionRepo) GetUnansweredQuestionCount(ctx context.Context) (count int64, err error) {
session := qr.data.DB.Context(ctx)
session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}).
And(builder.Eq{"answer_count": 0})
count, err = session.Count(&entity.Question{Show: entity.QuestionShow})
if err != nil {
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, nil
}
func (qr *questionRepo) GetResolvedQuestionCount(ctx context.Context) (count int64, err error) {
session := qr.data.DB.Context(ctx)
session.Where(builder.Lt{"status": entity.QuestionStatusDeleted}).
And(builder.Neq{"answer_count": 0}).
And(builder.Neq{"accepted_answer_id": 0})
count, err = session.Count(&entity.Question{Show: entity.QuestionShow})
if err != nil {
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, nil
}
func (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error) {
session := qr.data.DB.Context(ctx)
session.Where(builder.Lt{"status": entity.QuestionStatusDeleted})
count, err = session.Count(&entity.Question{UserID: userID, Show: show})
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (qr *questionRepo) SitemapQuestions(ctx context.Context, page, pageSize int) (
questionIDList []*schema.SiteMapQuestionInfo, err error) {
page--
questionIDList = make([]*schema.SiteMapQuestionInfo, 0)
// try to get sitemap data from cache
cacheKey := fmt.Sprintf(constant.SiteMapQuestionCacheKeyPrefix, page)
cacheData, exist, err := qr.data.Cache.GetString(ctx, cacheKey)
if err == nil && exist {
_ = json.Unmarshal([]byte(cacheData), &questionIDList)
return questionIDList, nil
}
// get sitemap data from db
rows := make([]*entity.Question, 0)
session := qr.data.DB.Context(ctx)
session.Select("id,title,created_at,post_update_time")
session.Where("`show` = ?", entity.QuestionShow)
session.Where("status = ? OR status = ?", entity.QuestionStatusAvailable, entity.QuestionStatusClosed)
session.Limit(pageSize, page*pageSize)
session.Asc("created_at")
err = session.Find(&rows)
if err != nil {
return questionIDList, err
}
// warp data
for _, question := range rows {
item := &schema.SiteMapQuestionInfo{ID: question.ID}
if handler.GetEnableShortID(ctx) {
item.ID = uid.EnShortID(question.ID)
}
item.Title = htmltext.UrlTitle(question.Title)
if question.PostUpdateTime.IsZero() {
item.UpdateTime = question.CreatedAt.Format(time.RFC3339)
} else {
item.UpdateTime = question.PostUpdateTime.Format(time.RFC3339)
}
questionIDList = append(questionIDList, item)
}
// set sitemap data to cache
cacheDataByte, _ := json.Marshal(questionIDList)
if err := qr.data.Cache.SetString(ctx, cacheKey, string(cacheDataByte), constant.SiteMapQuestionCacheTime); err != nil {
log.Error(err)
}
return questionIDList, nil
}
// GetQuestionPage query question page
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
tagIDs []string, userID, orderCond string, inDays int, showHidden, showPending bool) (
questionList []*entity.Question, total int64, err error) {
questionList = make([]*entity.Question, 0)
session := qr.data.DB.Context(ctx)
status := []int{entity.QuestionStatusAvailable}
if orderCond != "unanswered" {
status = append(status, entity.QuestionStatusClosed)
}
if showPending {
status = append(status, entity.QuestionStatusPending)
}
session.Select("question.*")
session.In("question.status", status)
if len(tagIDs) > 0 {
session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id")
session.In("tag_rel.tag_id", tagIDs)
session.And("tag_rel.status = ?", entity.TagRelStatusAvailable)
}
if len(userID) > 0 {
session.And("question.user_id = ?", userID)
if !showHidden {
session.And("question.show = ?", entity.QuestionShow)
}
} else {
session.And("question.show = ?", entity.QuestionShow)
}
if inDays > 0 {
session.And("question.created_at > ?", time.Now().AddDate(0, 0, -inDays))
}
switch orderCond {
case "newest":
session.OrderBy("question.pin desc,question.created_at DESC")
case "active":
if inDays == 0 {
session.And("question.created_at > ?", time.Now().AddDate(0, 0, -180))
}
session.And("question.post_update_time > ?", time.Now().AddDate(0, 0, -90))
session.OrderBy("question.pin desc,question.post_update_time DESC, question.updated_at DESC")
case "hot":
session.OrderBy("question.pin desc,question.hot_score DESC")
case "score":
session.OrderBy("question.pin desc,question.vote_count DESC, question.view_count DESC")
case "unanswered":
session.Where("question.answer_count = 0")
session.OrderBy("question.pin desc,question.created_at DESC")
case "frequent":
session.OrderBy("question.pin DESC, question.linked_count DESC, question.updated_at DESC")
}
session.GroupBy("question.id")
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
}
// 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
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).
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 {
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
err error
session = qr.data.DB.Context(ctx).Table("question")
)
session.Where(builder.Eq{
"status": search.Status,
})
rows := make([]*entity.Question, 0)
if search.Page > 0 {
search.Page--
} else {
search.Page = 0
}
if search.PageSize == 0 {
search.PageSize = constant.DefaultPageSize
}
// search by question title like or question id
if len(search.Query) > 0 {
// check id search
var (
idSearch = false
id = ""
)
if strings.Contains(search.Query, "question:") {
idSearch = true
id = strings.TrimSpace(strings.TrimPrefix(search.Query, "question:"))
id = uid.DeShortID(id)
for _, r := range id {
if !unicode.IsDigit(r) {
idSearch = false
break
}
}
}
if idSearch {
session.And(builder.Eq{
"id": id,
})
} else {
session.And(builder.Like{
"title", search.Query,
})
}
}
offset := search.Page * search.PageSize
session.OrderBy("created_at desc").
Limit(search.PageSize, offset)
count, err = session.FindAndCount(&rows)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return rows, count, err
}
if handler.GetEnableShortID(ctx) {
for _, item := range rows {
item.ID = uid.EnShortID(item.ID)
}
}
return rows, count, nil
}
// UpdateSearch update search, if search plugin not enable, do nothing
func (qr *questionRepo) UpdateSearch(ctx context.Context, questionID string) (err error) {
// check search plugin
var s plugin.Search
_ = plugin.CallSearch(func(search plugin.Search) error {
s = search
return nil
})
if s == nil {
return
}
questionID = uid.DeShortID(questionID)
question, exist, err := qr.GetQuestion(ctx, questionID)
if !exist {
return
}
if err != nil {
return err
}
// get tags
var (
tagListList = make([]*entity.TagRel, 0)
tags = make([]string, 0)
)
session := qr.data.DB.Context(ctx).Where("object_id = ?", questionID)
session.Where("status = ?", entity.TagRelStatusAvailable)
err = session.Find(&tagListList)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, tag := range tagListList {
tags = append(tags, tag.TagID)
}
content := &plugin.SearchContent{
ObjectID: questionID,
Title: question.Title,
Type: constant.QuestionObjectType,
Content: question.OriginalText,
Answers: int64(question.AnswerCount),
Status: plugin.SearchContentStatus(question.Status),
Tags: tags,
QuestionID: questionID,
UserID: question.UserID,
Views: int64(question.ViewCount),
Created: question.CreatedAt.Unix(),
Active: question.UpdatedAt.Unix(),
Score: int64(question.VoteCount),
HasAccepted: question.AcceptedAnswerID != "" && question.AcceptedAnswerID != "0",
}
err = s.UpdateContent(ctx, content)
return
}
func (qr *questionRepo) RemoveAllUserQuestion(ctx context.Context, userID string) (err error) {
// get all question id that need to be deleted
questionIDs := make([]string, 0)
session := qr.data.DB.Context(ctx).Where("user_id = ?", userID)
session.Where("status != ?", entity.QuestionStatusDeleted)
err = session.Select("id").Table("question").Find(&questionIDs)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if len(questionIDs) == 0 {
return nil
}
log.Infof("find %d questions need to be deleted for user %s", len(questionIDs), userID)
// delete all question
session = qr.data.DB.Context(ctx).Where("user_id = ?", userID)
session.Where("status != ?", entity.QuestionStatusDeleted)
_, err = session.Cols("status", "updated_at").Update(&entity.Question{
UpdatedAt: time.Now(),
Status: entity.QuestionStatusDeleted,
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// update search content
for _, id := range questionIDs {
_ = qr.UpdateSearch(ctx, id)
}
return nil
}
// LinkQuestion batch insert question link
func (qr *questionRepo) LinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error) {
// Batch retrieve all links
var links []*entity.QuestionLink
for _, l := range link {
l.FromQuestionID = uid.DeShortID(l.FromQuestionID)
l.ToQuestionID = uid.DeShortID(l.ToQuestionID)
l.FromAnswerID = uid.DeShortID(l.FromAnswerID)
l.ToAnswerID = uid.DeShortID(l.ToAnswerID)
links = append(links, l)
}
// Retrieve existing records from the database
var existLinks []*entity.QuestionLink
session := qr.data.DB.Context(ctx)
for _, link := range links {
session = session.Or(builder.Eq{
"from_question_id": link.FromQuestionID,
"to_question_id": link.ToQuestionID,
"from_answer_id": link.FromAnswerID,
"to_answer_id": link.ToAnswerID,
})
}
err = session.Find(&existLinks)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// Optimize separation of records that need to be updated or inserted using a map
existMap := make(map[string]*entity.QuestionLink)
for _, el := range existLinks {
key := fmt.Sprintf("%s:%s:%s:%s", el.FromQuestionID, el.ToQuestionID, el.FromAnswerID, el.ToAnswerID)
existMap[key] = el
}
var updateLinks []*entity.QuestionLink
var insertLinks []*entity.QuestionLink
for _, link := range links {
key := fmt.Sprintf("%s:%s:%s:%s", link.FromQuestionID, link.ToQuestionID, link.FromAnswerID, link.ToAnswerID)
if el, exist := existMap[key]; exist {
if el.Status == entity.QuestionLinkStatusDeleted {
el.Status = entity.QuestionLinkStatusAvailable
el.UpdatedAt = time.Now()
updateLinks = append(updateLinks, el)
}
} else {
link.Status = entity.QuestionLinkStatusAvailable
link.CreatedAt = time.Now()
link.UpdatedAt = time.Now()
insertLinks = append(insertLinks, link)
}
}
// Batch update
if len(updateLinks) > 0 {
for _, link := range updateLinks {
_, err = qr.data.DB.Context(ctx).ID(link.ID).Cols("status").Update(&entity.QuestionLink{Status: entity.QuestionLinkStatusAvailable})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
}
}
// Batch insert
if len(insertLinks) > 0 {
_, err = qr.data.DB.Context(ctx).Insert(insertLinks)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
}
return
}
// UpdateQuestionLinkCount update question link count
func (qr *questionRepo) UpdateQuestionLinkCount(ctx context.Context, questionID string) (err error) {
// count the number of links
count, err := qr.data.DB.Context(ctx).
Where("to_question_id = ?", questionID).
Where("status = ?", entity.QuestionLinkStatusAvailable).
Count(&entity.QuestionLink{})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// update the number of links
_, err = qr.data.DB.Context(ctx).ID(questionID).
Cols("linked_count").Update(&entity.Question{LinkedCount: int(count)})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetLinkedQuestionIDs get linked question ids
func (qr *questionRepo) GetLinkedQuestionIDs(ctx context.Context, questionID string, status int) (
questionIDs []string, err error) {
questionIDs = make([]string, 0)
err = qr.data.DB.Context(ctx).
Select("to_question_id").
Table(new(entity.QuestionLink).TableName()).
Where("from_question_id = ?", questionID).
Where("status = ?", status).
Find(&questionIDs)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return questionIDs, nil
}
// RecoverQuestionLink batch recover question link
func (qr *questionRepo) RecoverQuestionLink(ctx context.Context, links ...*entity.QuestionLink) (err error) {
return qr.UpdateQuestionLinkStatus(ctx, entity.QuestionLinkStatusAvailable, links...)
}
// RemoveQuestionLink batch remove question link
func (qr *questionRepo) RemoveQuestionLink(ctx context.Context, links ...*entity.QuestionLink) (err error) {
return qr.UpdateQuestionLinkStatus(ctx, entity.QuestionLinkStatusDeleted, links...)
}
// UpdateQuestionLinkStatus update question link status
func (qr *questionRepo) UpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error) {
if len(links) == 0 {
return nil
}
session := qr.data.DB.Context(ctx).Cols("status")
for _, link := range links {
eq := builder.Eq{}
if link.FromQuestionID != "" {
eq["from_question_id"] = uid.DeShortID(link.FromQuestionID)
}
if link.FromAnswerID != "" {
eq["from_answer_id"] = uid.DeShortID(link.FromAnswerID)
}
if link.ToQuestionID != "" {
eq["to_question_id"] = uid.DeShortID(link.ToQuestionID)
}
if link.ToAnswerID != "" {
eq["to_answer_id"] = uid.DeShortID(link.ToAnswerID)
}
session = session.Or(eq)
}
_, err = session.Update(&entity.QuestionLink{Status: status})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetQuestionLink get linked question to questionID
func (qr *questionRepo) GetQuestionLink(ctx context.Context, page, pageSize int, questionID string, orderCond string, inDays int) (questionList []*entity.Question, total int64, err error) {
questionList = make([]*entity.Question, 0)
questionID = uid.DeShortID(questionID)
questionStatus := []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed, entity.QuestionStatusPending}
if questionID == "0" {
return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(
fmt.Errorf("questionID is empty"),
).WithStack()
}
session := qr.data.DB.Context(ctx).
Table("question_link").
Join("INNER", "question", "question_link.from_question_id = question.id").
Where("question_link.to_question_id = ? AND question.show = ?", questionID, entity.QuestionShow).
Distinct("question.id").
Where("question_link.status = ?", entity.QuestionLinkStatusAvailable).
Select("question.*").
In("question.status", questionStatus)
switch orderCond {
case "newest":
session.OrderBy("question.pin desc,question.created_at DESC")
case "active":
if inDays == 0 {
session.And("question.created_at > ?", time.Now().AddDate(0, 0, -180))
}
session.And("question.post_update_time > ?", time.Now().AddDate(0, 0, -90))
session.OrderBy("question.pin desc,question.post_update_time DESC, question.updated_at DESC")
case "hot":
session.OrderBy("question.pin desc,question.hot_score DESC")
case "score":
session.OrderBy("question.pin desc,question.vote_count DESC, question.view_count DESC")
case "unanswered":
session.Where("question.answer_count = 0")
session.OrderBy("question.pin desc,question.created_at DESC")
case "frequent":
session.OrderBy("question.pin DESC, question.linked_count DESC, question.updated_at DESC")
}
if page > 0 && pageSize > 0 {
session.Limit(pageSize, (page-1)*pageSize)
}
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
}
+215
View File
@@ -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 rank
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/config"
"github.com/apache/answer/internal/service/rank"
"github.com/apache/answer/plugin"
"github.com/jinzhu/now"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
"xorm.io/xorm"
)
// UserRankRepo user rank repository
type UserRankRepo struct {
data *data.Data
configService *config.ConfigService
}
// NewUserRankRepo new repository
func NewUserRankRepo(data *data.Data, configService *config.ConfigService) rank.UserRankRepo {
return &UserRankRepo{
data: data,
configService: configService,
}
}
func (ur *UserRankRepo) GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error) {
maxDailyRank, err = ur.configService.GetIntValue(ctx, "daily_rank_limit")
if err != nil {
return 0, err
}
return maxDailyRank, nil
}
func (ur *UserRankRepo) CheckReachLimit(ctx context.Context, session *xorm.Session,
userID string, maxDailyRank int) (
reach bool, err error) {
session.Where(builder.Eq{"user_id": userID})
session.Where(builder.Eq{"cancelled": 0})
session.Where(builder.Between{
Col: "updated_at",
LessVal: now.BeginningOfDay(),
MoreVal: now.EndOfDay(),
})
earned, err := session.SumInt(&entity.Activity{}, "`rank`")
if err != nil {
return false, err
}
if int(earned) < maxDailyRank {
return false, nil
}
log.Infof("user %s today has rank %d is reach stand %d", userID, earned, maxDailyRank)
return true, nil
}
// ChangeUserRank change user rank
func (ur *UserRankRepo) ChangeUserRank(
ctx context.Context, session *xorm.Session, userID string, userCurrentScore, deltaRank int) (err error) {
// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
if plugin.RankAgentEnabled() || deltaRank == 0 {
return nil
}
// If user rank is lower than 1 after this action, then user rank will be set to 1 only.
if deltaRank < 0 && userCurrentScore+deltaRank < 1 {
deltaRank = 1 - userCurrentScore
}
_, err = session.ID(userID).Incr("`rank`", deltaRank).Update(&entity.User{})
if err != nil {
return err
}
return nil
}
// TriggerUserRank trigger user rank change
// session is need provider, it means this action must be success or failure
// if outer action is failed then this action is need rollback
func (ur *UserRankRepo) TriggerUserRank(ctx context.Context,
session *xorm.Session, userID string, deltaRank int, activityType int,
) (isReachStandard bool, err error) {
// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
if plugin.RankAgentEnabled() || deltaRank == 0 {
return false, nil
}
if deltaRank < 0 {
// if user rank is lower than 1 after this action, then user rank will be set to 1 only.
var isReachMin bool
isReachMin, err = ur.checkUserMinRank(ctx, session, userID, deltaRank)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if isReachMin {
_, err = session.Where(builder.Eq{"id": userID}).Update(&entity.User{Rank: 1})
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return true, nil
}
} else {
isReachStandard, err = ur.checkUserTodayRank(ctx, session, userID, activityType)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if isReachStandard {
return isReachStandard, nil
}
}
_, err = session.Where(builder.Eq{"id": userID}).Incr("`rank`", deltaRank).Update(&entity.User{})
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return false, nil
}
func (ur *UserRankRepo) checkUserMinRank(_ context.Context, session *xorm.Session, userID string, deltaRank int) (
isReachStandard bool, err error,
) {
bean := &entity.User{ID: userID}
_, err = session.Select("`rank`").Get(bean)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if bean.Rank+deltaRank < 1 {
log.Infof("user %s is rank %d out of range before rank operation", userID, deltaRank)
return true, nil
}
return
}
func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context,
session *xorm.Session, userID string, activityType int,
) (isReachStandard bool, err error) {
// exclude daily rank
exclude, _ := ur.configService.GetArrayStringValue(ctx, "daily_rank_limit.exclude")
for _, item := range exclude {
cfg, err := ur.configService.GetConfigByKey(ctx, item)
if err != nil {
return false, err
}
if activityType == cfg.ID {
return false, nil
}
}
// get user
start, end := now.BeginningOfDay(), now.EndOfDay()
session.Where(builder.Eq{"user_id": userID})
session.Where(builder.Eq{"cancelled": 0})
session.Where(builder.Between{
Col: "updated_at",
LessVal: start,
MoreVal: end,
})
earned, err := session.SumInt(&entity.Activity{}, "`rank`")
if err != nil {
return false, err
}
// max rank
maxDailyRank, err := ur.configService.GetIntValue(ctx, "daily_rank_limit")
if err != nil {
return false, err
}
if int(earned) < maxDailyRank {
return false, nil
}
log.Infof("user %s today has rank %d is reach stand %d", userID, earned, maxDailyRank)
return true, nil
}
func (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, pageSize int) (
rankPage []*entity.Activity, total int64, err error,
) {
rankPage = make([]*entity.Activity, 0)
session := ur.data.DB.Context(ctx).Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0})).And(builder.Gt{"`rank`": 0})
session.Desc("created_at")
cond := &entity.Activity{UserID: userID}
total, err = pager.Help(page, pageSize, &rankPage, cond, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+71
View File
@@ -0,0 +1,71 @@
/*
* 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 reason
import (
"context"
"encoding/json"
"fmt"
"github.com/apache/answer/internal/base/handler"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/config"
"github.com/apache/answer/internal/service/reason_common"
"github.com/segmentfault/pacman/log"
)
type reasonRepo struct {
configService *config.ConfigService
}
func NewReasonRepo(configService *config.ConfigService) reason_common.ReasonRepo {
return &reasonRepo{
configService: configService,
}
}
func (rr *reasonRepo) ListReasons(ctx context.Context, objectType, action string) (resp []*schema.ReasonItem, err error) {
lang := handler.GetLangByCtx(ctx)
reasonAction := fmt.Sprintf("%s.%s.reasons", objectType, action)
resp = make([]*schema.ReasonItem, 0)
reasonKeys, err := rr.configService.GetArrayStringValue(ctx, reasonAction)
if err != nil {
return nil, err
}
for _, reasonKey := range reasonKeys {
cfg, err := rr.configService.GetConfigByKey(ctx, reasonKey)
if err != nil {
log.Error(err)
continue
}
reason := &schema.ReasonItem{}
err = json.Unmarshal(cfg.GetByteValue(), reason)
if err != nil {
log.Error(err)
continue
}
reason.Translate(reasonKey, lang)
reason.ReasonType = cfg.ID
resp = append(resp, reason)
}
return resp, nil
}
+110
View File
@@ -0,0 +1,110 @@
/*
* 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/answer/internal/entity"
"github.com/apache/answer/internal/repo/auth"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
accessToken = "token"
visitToken = "visitToken"
userID = "1"
)
func Test_authRepo_SetUserCacheInfo(t *testing.T) {
authRepo := auth.NewAuthRepo(testDataSource)
err := authRepo.SetUserCacheInfo(context.TODO(), accessToken, visitToken, &entity.UserCacheInfo{UserID: userID})
require.NoError(t, err)
cacheInfo, err := authRepo.GetUserCacheInfo(context.TODO(), accessToken)
require.NoError(t, err)
assert.Equal(t, userID, cacheInfo.UserID)
}
func Test_authRepo_RemoveUserCacheInfo(t *testing.T) {
authRepo := auth.NewAuthRepo(testDataSource)
err := authRepo.SetUserCacheInfo(context.TODO(), accessToken, visitToken, &entity.UserCacheInfo{UserID: userID})
require.NoError(t, err)
err = authRepo.RemoveUserCacheInfo(context.TODO(), accessToken)
require.NoError(t, err)
userInfo, err := authRepo.GetUserCacheInfo(context.TODO(), accessToken)
require.NoError(t, err)
assert.Nil(t, userInfo)
}
func Test_authRepo_SetUserStatus(t *testing.T) {
authRepo := auth.NewAuthRepo(testDataSource)
err := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID})
require.NoError(t, err)
cacheInfo, err := authRepo.GetUserStatus(context.TODO(), userID)
require.NoError(t, err)
assert.Equal(t, userID, cacheInfo.UserID)
}
func Test_authRepo_RemoveUserStatus(t *testing.T) {
authRepo := auth.NewAuthRepo(testDataSource)
err := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID})
require.NoError(t, err)
err = authRepo.RemoveUserStatus(context.TODO(), userID)
require.NoError(t, err)
userInfo, err := authRepo.GetUserStatus(context.TODO(), userID)
require.NoError(t, err)
assert.Nil(t, userInfo)
}
func Test_authRepo_SetAdminUserCacheInfo(t *testing.T) {
authRepo := auth.NewAuthRepo(testDataSource)
err := authRepo.SetAdminUserCacheInfo(context.TODO(), accessToken, &entity.UserCacheInfo{UserID: userID})
require.NoError(t, err)
cacheInfo, err := authRepo.GetAdminUserCacheInfo(context.TODO(), accessToken)
require.NoError(t, err)
assert.Equal(t, userID, cacheInfo.UserID)
}
func Test_authRepo_RemoveAdminUserCacheInfo(t *testing.T) {
authRepo := auth.NewAuthRepo(testDataSource)
err := authRepo.SetAdminUserCacheInfo(context.TODO(), accessToken, &entity.UserCacheInfo{UserID: userID})
require.NoError(t, err)
err = authRepo.RemoveAdminUserCacheInfo(context.TODO(), accessToken)
require.NoError(t, err)
userInfo, err := authRepo.GetAdminUserCacheInfo(context.TODO(), accessToken)
require.NoError(t, err)
assert.Nil(t, userInfo)
}
+59
View File
@@ -0,0 +1,59 @@
/*
* 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/answer/internal/repo/captcha"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
ip = "127.0.0.1"
actionType = "actionType"
amount = 1
)
func Test_captchaRepo_DelActionType(t *testing.T) {
captchaRepo := captcha.NewCaptchaRepo(testDataSource)
err := captchaRepo.SetActionType(context.TODO(), ip, actionType, "", amount)
require.NoError(t, err)
actionInfo, err := captchaRepo.GetActionType(context.TODO(), ip, actionType)
require.NoError(t, err)
assert.Equal(t, amount, actionInfo.Num)
err = captchaRepo.DelActionType(context.TODO(), ip, actionType)
require.NoError(t, err)
}
func Test_captchaRepo_SetCaptcha(t *testing.T) {
captchaRepo := captcha.NewCaptchaRepo(testDataSource)
key, capt := "key", "1234"
err := captchaRepo.SetCaptcha(context.TODO(), key, capt)
require.NoError(t, err)
gotCaptcha, err := captchaRepo.GetCaptcha(context.TODO(), key)
require.NoError(t, err)
assert.Equal(t, capt, gotCaptcha)
}
@@ -0,0 +1,114 @@
/*
* 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/answer/internal/base/pager"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/repo/comment"
"github.com/apache/answer/internal/repo/unique"
commentService "github.com/apache/answer/internal/service/comment"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func buildCommentEntity() *entity.Comment {
return &entity.Comment{
UserID: "1",
ObjectID: "1",
QuestionID: "1",
VoteCount: 1,
Status: entity.CommentStatusAvailable,
OriginalText: "# title",
ParsedText: "<h1>Title</h1>",
}
}
func Test_commentRepo_AddComment(t *testing.T) {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo)
testCommentEntity := buildCommentEntity()
err := commentRepo.AddComment(context.TODO(), testCommentEntity)
require.NoError(t, err)
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
require.NoError(t, err)
}
func Test_commentRepo_GetCommentPage(t *testing.T) {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo)
testCommentEntity := buildCommentEntity()
err := commentRepo.AddComment(context.TODO(), testCommentEntity)
require.NoError(t, err)
resp, total, err := commentRepo.GetCommentPage(context.TODO(), &commentService.CommentQuery{
PageCond: pager.PageCond{
Page: 1,
PageSize: 10,
},
})
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Equal(t, resp[0].ID, testCommentEntity.ID)
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
require.NoError(t, err)
}
func Test_commentRepo_UpdateComment(t *testing.T) {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo)
commonCommentRepo := comment.NewCommentCommonRepo(testDataSource, uniqueIDRepo)
testCommentEntity := buildCommentEntity()
err := commentRepo.AddComment(context.TODO(), testCommentEntity)
require.NoError(t, err)
testCommentEntity.ParsedText = "test"
err = commentRepo.UpdateCommentContent(context.TODO(), testCommentEntity.ID, "test", "test")
require.NoError(t, err)
newComment, exist, err := commonCommentRepo.GetComment(context.TODO(), testCommentEntity.ID)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testCommentEntity.ParsedText, newComment.ParsedText)
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
require.NoError(t, err)
}
func Test_commentRepo_CannotGetDeletedComment(t *testing.T) {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo)
testCommentEntity := buildCommentEntity()
err := commentRepo.AddComment(context.TODO(), testCommentEntity)
require.NoError(t, err)
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
require.NoError(t, err)
_, exist, err := commentRepo.GetComment(context.TODO(), testCommentEntity.ID)
require.NoError(t, err)
assert.False(t, exist)
}
@@ -0,0 +1,41 @@
/*
* 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"
"time"
"github.com/apache/answer/internal/repo/export"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_emailRepo_VerifyCode(t *testing.T) {
emailRepo := export.NewEmailRepo(testDataSource)
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)
require.NoError(t, err)
verifyContent, err := emailRepo.VerifyCode(context.TODO(), code)
require.NoError(t, err)
assert.Equal(t, content, verifyContent)
}
+106
View File
@@ -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 repo_test
import (
"context"
"testing"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/repo/meta"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func buildMetaEntity() *entity.Meta {
return &entity.Meta{
ObjectID: "1",
Key: "1",
Value: "1",
}
}
func Test_metaRepo_GetMetaByObjectIdAndKey(t *testing.T) {
metaRepo := meta.NewMetaRepo(testDataSource)
metaEnt := buildMetaEntity()
err := metaRepo.AddMeta(context.TODO(), metaEnt)
require.NoError(t, err)
gotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, metaEnt.ID, gotMeta.ID)
err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID)
require.NoError(t, err)
}
func Test_metaRepo_GetMetaList(t *testing.T) {
metaRepo := meta.NewMetaRepo(testDataSource)
metaEnt := buildMetaEntity()
err := metaRepo.AddMeta(context.TODO(), metaEnt)
require.NoError(t, err)
gotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt)
require.NoError(t, err)
assert.Len(t, gotMetaList, 1)
assert.Equal(t, gotMetaList[0].ID, metaEnt.ID)
err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID)
require.NoError(t, err)
}
func Test_metaRepo_GetMetaPage(t *testing.T) {
metaRepo := meta.NewMetaRepo(testDataSource)
metaEnt := buildMetaEntity()
err := metaRepo.AddMeta(context.TODO(), metaEnt)
require.NoError(t, err)
gotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt)
require.NoError(t, err)
assert.Len(t, gotMetaList, 1)
assert.Equal(t, gotMetaList[0].ID, metaEnt.ID)
err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID)
require.NoError(t, err)
}
func Test_metaRepo_UpdateMeta(t *testing.T) {
metaRepo := meta.NewMetaRepo(testDataSource)
metaEnt := buildMetaEntity()
err := metaRepo.AddMeta(context.TODO(), metaEnt)
require.NoError(t, err)
metaEnt.Value = "testing"
err = metaRepo.UpdateMeta(context.TODO(), metaEnt)
require.NoError(t, err)
gotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, gotMeta.Value, metaEnt.Value)
err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID)
require.NoError(t, err)
}
@@ -0,0 +1,124 @@
/*
* 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/answer/internal/entity"
"github.com/apache/answer/internal/repo/notification"
"github.com/apache/answer/internal/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func buildNotificationEntity() *entity.Notification {
return &entity.Notification{
UserID: "1",
ObjectID: "1",
Content: "1",
Type: schema.NotificationTypeInbox,
IsRead: schema.NotificationNotRead,
Status: schema.NotificationStatusNormal,
}
}
func Test_notificationRepo_ClearIDUnRead(t *testing.T) {
notificationRepo := notification.NewNotificationRepo(testDataSource)
ent := buildNotificationEntity()
err := notificationRepo.AddNotification(context.TODO(), ent)
require.NoError(t, err)
err = notificationRepo.ClearIDUnRead(context.TODO(), ent.UserID, ent.ID)
require.NoError(t, err)
got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID)
require.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, schema.NotificationRead, got.IsRead)
}
func Test_notificationRepo_ClearUnRead(t *testing.T) {
notificationRepo := notification.NewNotificationRepo(testDataSource)
ent := buildNotificationEntity()
err := notificationRepo.AddNotification(context.TODO(), ent)
require.NoError(t, err)
err = notificationRepo.ClearUnRead(context.TODO(), ent.UserID, ent.Type)
require.NoError(t, err)
got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID)
require.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, schema.NotificationRead, got.IsRead)
}
func Test_notificationRepo_GetById(t *testing.T) {
notificationRepo := notification.NewNotificationRepo(testDataSource)
ent := buildNotificationEntity()
err := notificationRepo.AddNotification(context.TODO(), ent)
require.NoError(t, err)
got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID)
require.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, got.ID, ent.ID)
}
func Test_notificationRepo_GetByUserIdObjectIdTypeId(t *testing.T) {
notificationRepo := notification.NewNotificationRepo(testDataSource)
ent := buildNotificationEntity()
err := notificationRepo.AddNotification(context.TODO(), ent)
require.NoError(t, err)
got, exists, err := notificationRepo.GetByUserIdObjectIdTypeId(context.TODO(), ent.UserID, ent.ObjectID, ent.Type)
require.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, got.ObjectID, ent.ObjectID)
}
func Test_notificationRepo_GetNotificationPage(t *testing.T) {
notificationRepo := notification.NewNotificationRepo(testDataSource)
ent := buildNotificationEntity()
err := notificationRepo.AddNotification(context.TODO(), ent)
require.NoError(t, err)
notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: ent.UserID})
require.NoError(t, err)
assert.Positive(t, total)
assert.Equal(t, notificationPage[0].UserID, ent.UserID)
}
func Test_notificationRepo_UpdateNotificationContent(t *testing.T) {
notificationRepo := notification.NewNotificationRepo(testDataSource)
ent := buildNotificationEntity()
err := notificationRepo.AddNotification(context.TODO(), ent)
require.NoError(t, err)
ent.Content = "test"
err = notificationRepo.UpdateNotificationContent(context.TODO(), ent)
require.NoError(t, err)
got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID)
require.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, got.Content, ent.Content)
}
@@ -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 repo_test
import (
"context"
"testing"
"github.com/apache/answer/internal/repo/config"
serviceconfig "github.com/apache/answer/internal/service/config"
"github.com/apache/answer/internal/repo/reason"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_reasonRepo_ListReasons(t *testing.T) {
configRepo := config.NewConfigRepo(testDataSource)
reasonRepo := reason.NewReasonRepo(serviceconfig.NewConfigService(configRepo))
reasonItems, err := reasonRepo.ListReasons(context.TODO(), "question", "close")
require.NoError(t, err)
assert.Len(t, reasonItems, 4)
}
+216
View File
@@ -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 repo_test
import (
"context"
"testing"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/repo/activity"
"github.com/apache/answer/internal/repo/activity_common"
"github.com/apache/answer/internal/repo/config"
"github.com/apache/answer/internal/repo/question"
"github.com/apache/answer/internal/repo/tag"
"github.com/apache/answer/internal/repo/tag_common"
"github.com/apache/answer/internal/repo/unique"
"github.com/apache/answer/internal/repo/user"
config2 "github.com/apache/answer/internal/service/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
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)
require.NoError(t, err)
assert.NotEmpty(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)
require.NoError(t, err)
assert.NotEmpty(t, question.ID)
}
tags := []*entity.Tag{
{
SlugName: "go",
DisplayName: "Golang",
OriginalText: "golang",
ParsedText: "<p>golang</p>",
Status: entity.TagStatusAvailable,
},
{
SlugName: "java",
DisplayName: "Java",
OriginalText: "java",
ParsedText: "<p>java</p>",
Status: entity.TagStatusAvailable,
},
}
err = tagCommenRepo.AddTagList(context.TODO(), tags)
require.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)
require.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)
require.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)
require.NoError(t, err)
assert.Equal(t, int64(5), total)
assert.Len(t, questionList, 5)
// 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)
require.NoError(t, err)
}
tagRelIDs = append(tagRelIDs, tagRel.ID)
}
err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), tagRelIDs)
require.NoError(t, err)
for _, tag := range tags {
err = tagRepo.RemoveTag(context.TODO(), tag.ID)
require.NoError(t, err)
}
for _, q := range questions {
err = questionRepo.RemoveQuestion(context.TODO(), q.ID)
require.NoError(t, err)
}
})
}
+205
View File
@@ -0,0 +1,205 @@
/*
* 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"
"database/sql"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/migrations"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/segmentfault/pacman/cache"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
var (
mysqlDBSetting = TestDBSetting{
Driver: string(schemas.MYSQL),
ImageName: "mariadb",
ImageVersion: "10.4.7",
ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=answer", "MYSQL_ROOT_HOST=%"},
PortID: "3306/tcp",
Connection: "root:root@(localhost:%s)/answer?parseTime=true", // port is not fixed, it will be got by port id
}
postgresDBSetting = TestDBSetting{
Driver: string(schemas.POSTGRES),
ImageName: "postgres",
ImageVersion: "14",
ENV: []string{"POSTGRES_USER=root", "POSTGRES_PASSWORD=root", "POSTGRES_DB=answer", "LISTEN_ADDRESSES='*'"},
PortID: "5432/tcp",
Connection: "host=localhost port=%s user=root password=root dbname=answer sslmode=disable",
}
sqlite3DBSetting = TestDBSetting{
Driver: string(schemas.SQLITE),
Connection: filepath.Join(os.TempDir(), "answer-test-data.db"),
}
dbSettingMapping = map[string]TestDBSetting{
mysqlDBSetting.Driver: mysqlDBSetting,
sqlite3DBSetting.Driver: sqlite3DBSetting,
postgresDBSetting.Driver: postgresDBSetting,
}
// after all test down will execute tearDown function to clean-up
tearDown func()
// testDataSource used for repo testing
testDataSource *data.Data
)
func TestMain(t *testing.M) {
dbSetting, ok := dbSettingMapping[os.Getenv("TEST_DB_DRIVER")]
if !ok {
// Use sqlite3 to test.
dbSetting = dbSettingMapping[string(schemas.SQLITE)]
}
if dbSetting.Driver == string(schemas.SQLITE) {
_ = os.RemoveAll(dbSetting.Connection)
}
defer func() {
if tearDown != nil {
tearDown()
}
}()
if err := initTestDataSource(dbSetting); err != nil {
panic(err)
}
log.Info("init test database successfully")
if ret := t.Run(); ret != 0 {
panic(ret)
}
}
type TestDBSetting struct {
Driver string
ImageName string
ImageVersion string
ENV []string
PortID string
Connection string
}
func initTestDataSource(dbSetting TestDBSetting) error {
connection, imageCleanUp, err := initDatabaseImage(dbSetting)
if err != nil {
return err
}
dbSetting.Connection = connection
dbEngine, err := initDatabase(dbSetting)
if err != nil {
return err
}
newCache, err := initCache()
if err != nil {
return err
}
newData, dbCleanUp, err := data.NewData(dbEngine, newCache)
if err != nil {
return err
}
testDataSource = newData
tearDown = func() {
dbCleanUp()
log.Info("cleanup test database successfully")
imageCleanUp()
log.Info("cleanup test database image successfully")
}
return nil
}
func initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func(), err error) {
// sqlite3 don't need to set up image
if dbSetting.Driver == string(schemas.SQLITE) {
return dbSetting.Connection, func() {
log.Info("remove database", dbSetting.Connection)
err = os.Remove(dbSetting.Connection)
if err != nil {
log.Error(err)
}
}, nil
}
pool, err := dockertest.NewPool("")
pool.MaxWait = time.Minute * 5
if err != nil {
return "", nil, fmt.Errorf("could not connect to docker: %s", err)
}
// resource, err := pool.Run(dbSetting.ImageName, dbSetting.ImageVersion, dbSetting.ENV)
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
Repository: dbSetting.ImageName,
Tag: dbSetting.ImageVersion,
Env: dbSetting.ENV,
}, func(config *docker.HostConfig) {
config.AutoRemove = true
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
})
if err != nil {
return "", nil, fmt.Errorf("could not pull resource: %s", err)
}
connection = fmt.Sprintf(dbSetting.Connection, resource.GetPort(dbSetting.PortID))
if err := pool.Retry(func() error {
db, err := sql.Open(dbSetting.Driver, connection)
if err != nil {
return err
}
return db.Ping()
}); err != nil {
return "", nil, fmt.Errorf("could not connect to database: %s", err)
}
return connection, func() { _ = pool.Purge(resource) }, nil
}
func initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) {
dataConf := &data.Database{Driver: dbSetting.Driver, Connection: dbSetting.Connection}
dbEngine, err = data.NewDB(true, dataConf)
if err != nil {
return nil, fmt.Errorf("connection to database failed: %s", err)
}
if err := migrations.NewMentor(context.TODO(), dbEngine, &migrations.InitNeedUserInputData{
Language: "en_US",
SiteName: "ANSWER",
SiteURL: "http://127.0.0.1:8080/",
ContactEmail: "answer@answer.com",
AdminName: "admin",
AdminPassword: "admin",
AdminEmail: "answer@answer.com",
}).InitDB(); err != nil {
return nil, fmt.Errorf("migrations init database failed: %s", err)
}
return dbEngine, nil
}
func initCache() (newCache cache.Cache, err error) {
newCache, _, err = data.NewCache(&data.CacheConf{})
return newCache, err
}
@@ -0,0 +1,121 @@
/*
* 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"
"encoding/json"
"testing"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/repo/question"
"github.com/apache/answer/internal/repo/revision"
"github.com/apache/answer/internal/repo/unique"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var q = &entity.Question{
ID: "",
UserID: "1",
Title: "test",
OriginalText: "test",
ParsedText: "test",
Status: entity.QuestionStatusAvailable,
ViewCount: 0,
UniqueViewCount: 0,
VoteCount: 0,
AnswerCount: 0,
CollectionCount: 0,
FollowCount: 0,
AcceptedAnswerID: "",
LastAnswerID: "",
RevisionID: "0",
}
func getRev(objID, title, content string) *entity.Revision {
return &entity.Revision{
ID: "",
UserID: "1",
ObjectID: objID,
Title: title,
Content: content,
Log: "add rev",
}
}
func Test_revisionRepo_AddRevision(t *testing.T) {
var (
uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource)
revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo)
questionRepo = question.NewQuestionRepo(testDataSource, uniqueIDRepo)
)
// create question
err := questionRepo.AddQuestion(context.TODO(), q)
require.NoError(t, err)
assert.NotEmpty(t, q.ID)
content, err := json.Marshal(q)
require.NoError(t, err)
// auto update false
rev := getRev(q.ID, q.Title, string(content))
err = revisionRepo.AddRevision(context.TODO(), rev, false)
require.NoError(t, err)
qr, _, _ := questionRepo.GetQuestion(context.TODO(), q.ID)
assert.NotEqual(t, rev.ID, qr.RevisionID)
// auto update false
rev = getRev(q.ID, q.Title, string(content))
err = revisionRepo.AddRevision(context.TODO(), rev, true)
require.NoError(t, err)
qr, _, _ = questionRepo.GetQuestion(context.TODO(), q.ID)
assert.Equal(t, rev.ID, qr.RevisionID)
// recovery
t.Cleanup(func() {
err = questionRepo.RemoveQuestion(context.TODO(), q.ID)
require.NoError(t, err)
})
}
func Test_revisionRepo_GetLastRevisionByObjectID(t *testing.T) {
var (
uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource)
revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo)
)
Test_revisionRepo_AddRevision(t)
rev, exists, err := revisionRepo.GetLastRevisionByObjectID(context.TODO(), q.ID)
require.NoError(t, err)
assert.True(t, exists)
assert.NotNil(t, rev)
}
func Test_revisionRepo_GetRevisionList(t *testing.T) {
var (
uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource)
revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo)
)
Test_revisionRepo_AddRevision(t)
revs, err := revisionRepo.GetRevisionList(context.TODO(), &entity.Revision{ObjectID: q.ID})
require.NoError(t, err)
assert.GreaterOrEqual(t, len(revs), 1)
}
@@ -0,0 +1,53 @@
/*
* 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/answer/internal/entity"
"github.com/apache/answer/internal/repo/site_info"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_siteInfoRepo_SaveByType(t *testing.T) {
siteInfoRepo := site_info.NewSiteInfo(testDataSource)
data := &entity.SiteInfo{Content: "site_info", Type: "test"}
err := siteInfoRepo.SaveByType(context.TODO(), data.Type, data)
require.NoError(t, err)
got, exist, err := siteInfoRepo.GetByType(context.TODO(), data.Type)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, data.Content, got.Content)
data.Content = "new site_info"
err = siteInfoRepo.SaveByType(context.TODO(), data.Type, data)
require.NoError(t, err)
got, exist, err = siteInfoRepo.GetByType(context.TODO(), data.Type)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, data.Content, got.Content)
}
@@ -0,0 +1,114 @@
/*
* 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"
"log"
"sync"
"testing"
"github.com/apache/answer/internal/repo/unique"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/repo/tag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
tagRelOnce sync.Once
testTagRelList = []*entity.TagRel{
{
ObjectID: "10010000000000101",
TagID: "10030000000000101",
Status: entity.TagRelStatusAvailable,
},
{
ObjectID: "10010000000000202",
TagID: "10030000000000202",
Status: entity.TagRelStatusAvailable,
},
}
)
func addTagRelList() {
tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
err := tagRelRepo.AddTagRelList(context.TODO(), testTagRelList)
if err != nil {
log.Fatalf("%+v", err)
}
}
func Test_tagListRepo_BatchGetObjectTagRelList(t *testing.T) {
tagRelOnce.Do(addTagRelList)
tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
relList, err :=
tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID})
require.NoError(t, err)
assert.Len(t, relList, 2)
}
func Test_tagListRepo_CountTagRelByTagID(t *testing.T) {
tagRelOnce.Do(addTagRelList)
tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101")
require.NoError(t, err)
assert.Equal(t, int64(1), count)
}
func Test_tagListRepo_GetObjectTagRelList(t *testing.T) {
tagRelOnce.Do(addTagRelList)
tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
relList, err :=
tagRelRepo.GetObjectTagRelList(context.TODO(), testTagRelList[0].ObjectID)
require.NoError(t, err)
assert.Len(t, relList, 1)
}
func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) {
tagRelOnce.Do(addTagRelList)
tagRelRepo := tag.NewTagRelRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
relList, err :=
tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID})
require.NoError(t, err)
assert.Len(t, relList, 2)
ids := []int64{relList[0].ID, relList[1].ID}
err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), ids)
require.NoError(t, err)
count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101")
require.NoError(t, err)
assert.Equal(t, int64(0), count)
_, exist, err := tagRelRepo.GetObjectTagRelWithoutStatus(context.TODO(), relList[0].ObjectID, relList[0].TagID)
require.NoError(t, err)
assert.True(t, exist)
err = tagRelRepo.EnableTagRelByIDs(context.TODO(), ids, false)
require.NoError(t, err)
count, err = tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101")
require.NoError(t, err)
assert.Equal(t, int64(1), count)
}
+200
View File
@@ -0,0 +1,200 @@
/*
* 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"
"fmt"
"log"
"sync"
"testing"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/repo/tag"
"github.com/apache/answer/internal/repo/tag_common"
"github.com/apache/answer/internal/repo/unique"
"github.com/apache/answer/pkg/converter"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
tagOnce sync.Once
testTagList = []*entity.Tag{
{
SlugName: "go",
DisplayName: "Golang",
OriginalText: "golang",
ParsedText: "<p>golang</p>",
Status: entity.TagStatusAvailable,
},
{
SlugName: "js",
DisplayName: "javascript",
OriginalText: "javascript",
ParsedText: "<p>javascript</p>",
Status: entity.TagStatusAvailable,
},
{
SlugName: "go2",
DisplayName: "Golang2",
OriginalText: "golang2",
ParsedText: "<p>golang2</p>",
Status: entity.TagStatusAvailable,
},
}
)
func addTagList() {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo)
err := tagCommonRepo.AddTagList(context.TODO(), testTagList)
if err != nil {
log.Fatalf("%+v", err)
}
}
func Test_tagRepo_GetTagByID(t *testing.T) {
tagOnce.Do(addTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
}
func Test_tagRepo_GetTagBySlugName(t *testing.T) {
tagOnce.Do(addTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
}
func Test_tagRepo_GetTagList(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagRepo.GetTagList(context.TODO(), &entity.Tag{ID: testTagList[0].ID})
require.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagListByIDs(t *testing.T) {
tagOnce.Do(addTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagCommonRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID})
require.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagListByName(t *testing.T) {
tagOnce.Do(addTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, false, false)
require.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagListByNames(t *testing.T) {
tagOnce.Do(addTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagCommonRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName})
require.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagPage(t *testing.T) {
tagOnce.Do(addTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, _, err := tagCommonRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "")
require.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_RemoveTag(t *testing.T) {
tagOnce.Do(addTagList)
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)
err := tagRepo.RemoveTag(context.TODO(), testTagList[1].ID)
require.NoError(t, err)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
_, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName)
require.NoError(t, err)
assert.False(t, exist)
}
func Test_tagRepo_UpdateTag(t *testing.T) {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)
testTagList[0].DisplayName = "golang"
err := tagRepo.UpdateTag(context.TODO(), testTagList[0])
require.NoError(t, err)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].DisplayName, gotTag.DisplayName)
}
func Test_tagRepo_UpdateTagQuestionCount(t *testing.T) {
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
testTagList[0].DisplayName = "golang"
err := tagCommonRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)
require.NoError(t, err)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, 100, gotTag.QuestionCount)
}
func Test_tagRepo_UpdateTagSynonym(t *testing.T) {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)
testTagList[0].DisplayName = "golang"
err := tagRepo.UpdateTag(context.TODO(), testTagList[0])
require.NoError(t, err)
err = tagRepo.UpdateTagSynonym(context.TODO(), []string{testTagList[2].SlugName},
converter.StringToInt64(testTagList[0].ID), testTagList[0].SlugName)
require.NoError(t, err)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID, true)
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].ID, fmt.Sprintf("%d", gotTag.MainTagID))
}
@@ -0,0 +1,74 @@
/*
* 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"
"time"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/repo/auth"
"github.com/apache/answer/internal/repo/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_userAdminRepo_GetUserInfo(t *testing.T) {
userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, exist, err := userAdminRepo.GetUserInfo(context.TODO(), "1")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, "1", got.ID)
}
func Test_userAdminRepo_GetUserPage(t *testing.T) {
userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, total, err := userAdminRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "", false)
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Equal(t, "1", got[0].ID)
}
func Test_userAdminRepo_UpdateUserStatus(t *testing.T) {
userAdminRepo := user.NewUserAdminRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, exist, err := userAdminRepo.GetUserInfo(context.TODO(), "1")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, entity.UserStatusAvailable, got.Status)
err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusSuspended, entity.EmailStatusAvailable,
"admin@admin.com", time.Now().Add(time.Minute*5))
require.NoError(t, err)
got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, entity.UserStatusSuspended, got.Status)
err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusAvailable, entity.EmailStatusAvailable,
"admin@admin.com", time.Time{})
require.NoError(t, err)
got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, entity.UserStatusAvailable, got.Status)
}
+140
View File
@@ -0,0 +1,140 @@
/*
* 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/answer/internal/entity"
"github.com/apache/answer/internal/repo/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_userRepo_AddUser(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
userInfo := &entity.User{
Username: "answer",
Pass: "answer",
EMail: "answer@example.com",
MailStatus: entity.EmailStatusAvailable,
Status: entity.UserStatusAvailable,
DisplayName: "answer",
IsAdmin: false,
}
err := userRepo.AddUser(context.TODO(), userInfo)
require.NoError(t, err)
}
func Test_userRepo_BatchGetByID(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
got, err := userRepo.BatchGetByID(context.TODO(), []string{"1"})
require.NoError(t, err)
assert.Len(t, got, 1)
assert.Equal(t, "admin", got[0].Username)
}
func Test_userRepo_GetByEmail(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
got, exist, err := userRepo.GetByEmail(context.TODO(), "admin@admin.com")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, "admin", got.Username)
}
func Test_userRepo_GetByUserID(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
got, exist, err := userRepo.GetByUserID(context.TODO(), "1")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, "admin", got.Username)
}
func Test_userRepo_GetByUsername(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
got, exist, err := userRepo.GetByUsername(context.TODO(), "admin")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, "admin", got.Username)
}
func Test_userRepo_IncreaseAnswerCount(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
err := userRepo.IncreaseAnswerCount(context.TODO(), "1", 1)
require.NoError(t, err)
got, exist, err := userRepo.GetByUserID(context.TODO(), "1")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, 1, got.AnswerCount)
}
func Test_userRepo_IncreaseQuestionCount(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
err := userRepo.IncreaseQuestionCount(context.TODO(), "1", 1)
require.NoError(t, err)
got, exist, err := userRepo.GetByUserID(context.TODO(), "1")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, 1, got.AnswerCount)
}
func Test_userRepo_UpdateEmail(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
err := userRepo.UpdateEmail(context.TODO(), "1", "admin@admin.com")
require.NoError(t, err)
}
func Test_userRepo_UpdateEmailStatus(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
err := userRepo.UpdateEmailStatus(context.TODO(), "1", entity.EmailStatusToBeVerified)
require.NoError(t, err)
}
func Test_userRepo_UpdateInfo(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
err := userRepo.UpdateInfo(context.TODO(), &entity.User{ID: "1", Bio: "test"})
require.NoError(t, err)
got, exist, err := userRepo.GetByUserID(context.TODO(), "1")
require.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, "test", got.Bio)
}
func Test_userRepo_UpdateLastLoginDate(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
err := userRepo.UpdateLastLoginDate(context.TODO(), "1")
require.NoError(t, err)
}
func Test_userRepo_UpdateNoticeStatus(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
err := userRepo.UpdateNoticeStatus(context.TODO(), "1", 1)
require.NoError(t, err)
}
func Test_userRepo_UpdatePass(t *testing.T) {
userRepo := user.NewUserRepo(testDataSource)
err := userRepo.UpdatePass(context.TODO(), "1", "admin")
require.NoError(t, err)
}
+102
View File
@@ -0,0 +1,102 @@
/*
* 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 report
import (
"context"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/report_common"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
)
// reportRepo report repository
type reportRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewReportRepo new repository
func NewReportRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) report_common.ReportRepo {
return &reportRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// AddReport add report
func (rr *reportRepo) AddReport(ctx context.Context, report *entity.Report) (err error) {
report.ID, err = rr.uniqueIDRepo.GenUniqueIDStr(ctx, report.TableName())
if err != nil {
return err
}
_, err = rr.data.DB.Context(ctx).Insert(report)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetReportListPage get report list page
func (rr *reportRepo) GetReportListPage(ctx context.Context, dto *schema.GetReportListPageDTO) (
reports []*entity.Report, total int64, err error) {
cond := &entity.Report{}
cond.Status = dto.Status
session := rr.data.DB.Context(ctx).Desc("updated_at")
total, err = pager.Help(dto.Page, dto.PageSize, &reports, cond, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetByID get report by ID
func (rr *reportRepo) GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error) {
report = &entity.Report{}
exist, err = rr.data.DB.Context(ctx).ID(id).Get(report)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateStatus update report status by ID
func (rr *reportRepo) UpdateStatus(ctx context.Context, id string, status int) (err error) {
_, err = rr.data.DB.Context(ctx).ID(id).Update(&entity.Report{Status: status})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (rr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) {
list := make([]*entity.Report, 0)
count, err = rr.data.DB.Context(ctx).Where("status =?", entity.ReportStatusPending).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+104
View File
@@ -0,0 +1,104 @@
/*
* 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 review
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/review"
"github.com/segmentfault/pacman/errors"
)
// reviewRepo review repository
type reviewRepo struct {
data *data.Data
}
// NewReviewRepo new repository
func NewReviewRepo(data *data.Data) review.ReviewRepo {
return &reviewRepo{
data: data,
}
}
// AddReview add review
func (cr *reviewRepo) AddReview(ctx context.Context, review *entity.Review) (err error) {
_, err = cr.data.DB.Context(ctx).Insert(review)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateReviewStatus update review status
func (cr *reviewRepo) UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error) {
_, err = cr.data.DB.Context(ctx).ID(reviewID).Update(&entity.Review{
ReviewerUserID: reviewerUserID, Status: status})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetReview get review one
func (cr *reviewRepo) GetReview(ctx context.Context, reviewID int) (
review *entity.Review, exist bool, err error) {
review = &entity.Review{}
exist, err = cr.data.DB.Context(ctx).ID(reviewID).Get(review)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetReviewByObject get review by object
func (cr *reviewRepo) GetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error) {
review = &entity.Review{}
exist, err = cr.data.DB.Context(ctx).Desc("id").Where("object_id = ?", objectID).Get(review)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetReviewCount get review count
func (cr *reviewRepo) GetReviewCount(ctx context.Context, status int) (count int64, err error) {
count, err = cr.data.DB.Context(ctx).Count(&entity.Review{Status: status})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetReviewPage get review page
func (cr *reviewRepo) GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) (
reviewList []*entity.Review, total int64, err error) {
session := cr.data.DB.Context(ctx).Asc("created_at")
reviewList = make([]*entity.Review, 0)
total, err = pager.Help(page, pageSize, &reviewList, cond, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+233
View File
@@ -0,0 +1,233 @@
/*
* 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 revision
import (
"context"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/revision"
"github.com/apache/answer/internal/service/unique"
"github.com/apache/answer/pkg/converter"
"github.com/apache/answer/pkg/obj"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
"xorm.io/xorm"
)
// revisionRepo revision repository
type revisionRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewRevisionRepo new repository
func NewRevisionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) revision.RevisionRepo {
return &revisionRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// AddRevision add revision
// autoUpdateRevisionID bool : if autoUpdateRevisionID is true , the object.revision_id will be updated,
// if not need auto update object.revision_id, it must be false.
// example: user can edit the object, but need audit, the revision_id will be updated when admin approved
func (rr *revisionRepo) AddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error) {
objectTypeNumber, err := obj.GetObjectTypeNumberByObjectID(revision.ObjectID)
if err != nil {
return errors.BadRequest(reason.ObjectNotFound)
}
revision.ObjectType = objectTypeNumber
if !rr.allowRecord(revision.ObjectType) {
return nil
}
_, err = rr.data.DB.Transaction(func(session *xorm.Session) (any, error) {
session = session.Context(ctx)
_, err = session.Insert(revision)
if err != nil {
_ = session.Rollback()
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if autoUpdateRevisionID {
err = rr.UpdateObjectRevisionId(ctx, revision, session)
if err != nil {
_ = session.Rollback()
return nil, err
}
}
return nil, nil
})
return err
}
// UpdateObjectRevisionId updates the object.revision_id field
func (rr *revisionRepo) UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error) {
tableName, err := obj.GetObjectTypeStrByObjectID(revision.ObjectID)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_, err = session.Table(tableName).Where("id = ?", revision.ObjectID).Cols("`revision_id`").Update(struct {
RevisionID string `xorm:"revision_id"`
}{
RevisionID: revision.ID,
})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// UpdateStatus update revision status
func (rr *revisionRepo) UpdateStatus(ctx context.Context, id string, status int, reviewUserID string) (err error) {
if id == "" {
return nil
}
var data entity.Revision
data.ID = id
data.Status = status
data.ReviewUserID = converter.StringToInt64(reviewUserID)
_, err = rr.data.DB.Context(ctx).Where("id =?", id).Cols("status", "review_user_id").Update(&data)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// GetRevision get revision one
func (rr *revisionRepo) GetRevision(ctx context.Context, id string) (
revision *entity.Revision, exist bool, err error,
) {
revision = &entity.Revision{}
exist, err = rr.data.DB.Context(ctx).ID(id).Get(revision)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetRevisionByID get object's last revision by object TagID
func (rr *revisionRepo) GetRevisionByID(ctx context.Context, revisionID string) (
revision *entity.Revision, exist bool, err error) {
revision = &entity.Revision{}
exist, err = rr.data.DB.Context(ctx).Where("id = ?", revisionID).Get(revision)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (rr *revisionRepo) ExistUnreviewedByObjectID(ctx context.Context, objectID string) (
revision *entity.Revision, exist bool, err error) {
revision = &entity.Revision{}
exist, err = rr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.RevisionUnreviewedStatus).Get(revision)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetLastRevisionByObjectID get object's last revision by object TagID
func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID string) (
revision *entity.Revision, exist bool, err error,
) {
revision = &entity.Revision{}
exist, err = rr.data.DB.Context(ctx).Where("object_id = ?", objectID).Desc("created_at").Get(revision)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetLastRevisionByFileURL get object's last revision by file url
func (rr *revisionRepo) GetLastRevisionByFileURL(ctx context.Context, fileURL string) (revision *entity.Revision, exist bool, err error) {
revision = &entity.Revision{}
exist, err = rr.data.DB.Context(ctx).Where("content LIKE ?", "%"+fileURL+"%").Desc("created_at").Get(revision)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetRevisionList get revision list all
func (rr *revisionRepo) GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error) {
revisionList = []entity.Revision{}
err = rr.data.DB.Context(ctx).Where(builder.Eq{
"object_id": revision.ObjectID,
}).OrderBy("created_at DESC").Find(&revisionList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// allowRecord check the object type can record revision or not
func (rr *revisionRepo) allowRecord(objectType int) (ok bool) {
switch objectType {
case constant.ObjectTypeStrMapping["question"]:
return true
case constant.ObjectTypeStrMapping["answer"]:
return true
case constant.ObjectTypeStrMapping["tag"]:
return true
default:
return false
}
}
// GetUnreviewedRevisionPage get unreviewed revision page
func (rr *revisionRepo) GetUnreviewedRevisionPage(ctx context.Context, page int, pageSize int,
objectTypeList []int) (revisionList []*entity.Revision, total int64, err error) {
revisionList = make([]*entity.Revision, 0)
if len(objectTypeList) == 0 {
return revisionList, 0, nil
}
session := rr.data.DB.Context(ctx)
session = session.And("status = ?", entity.RevisionUnreviewedStatus)
session = session.In("object_type", objectTypeList)
session = session.OrderBy("created_at asc")
total, err = pager.Help(page, pageSize, &revisionList, &entity.Revision{}, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// CountUnreviewedRevision get unreviewed revision count
func (rr *revisionRepo) CountUnreviewedRevision(ctx context.Context, objectTypeList []int) (count int64, err error) {
if len(objectTypeList) == 0 {
return 0, nil
}
session := rr.data.DB.Context(ctx)
session = session.And("status = ?", entity.RevisionUnreviewedStatus)
session = session.In("object_type", objectTypeList)
count, err = session.Count(&entity.Revision{})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+52
View File
@@ -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 role
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/role"
"github.com/segmentfault/pacman/errors"
)
// powerRepo power repository
type powerRepo struct {
data *data.Data
}
// NewPowerRepo new repository
func NewPowerRepo(data *data.Data) role.PowerRepo {
return &powerRepo{
data: data,
}
}
// GetPowerList get list all
func (pr *powerRepo) GetPowerList(ctx context.Context, power *entity.Power) (powerList []*entity.Power, err error) {
powerList = make([]*entity.Power, 0)
err = pr.data.DB.Context(ctx).Find(&powerList, power)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+53
View File
@@ -0,0 +1,53 @@
/*
* 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 role
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/service/role"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
)
// rolePowerRelRepo rolePowerRel repository
type rolePowerRelRepo struct {
data *data.Data
}
// NewRolePowerRelRepo new repository
func NewRolePowerRelRepo(data *data.Data) role.RolePowerRelRepo {
return &rolePowerRelRepo{
data: data,
}
}
// GetRolePowerTypeList get role power type list
func (rr *rolePowerRelRepo) GetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error) {
powers = make([]string, 0)
err = rr.data.DB.Context(ctx).Table("role_power_rel").
Cols("power_type").Where(builder.Eq{"role_id": roleID}).Find(&powers)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+65
View File
@@ -0,0 +1,65 @@
/*
* 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 role
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
service "github.com/apache/answer/internal/service/role"
"github.com/segmentfault/pacman/errors"
)
// roleRepo role repository
type roleRepo struct {
data *data.Data
}
// NewRoleRepo new repository
func NewRoleRepo(data *data.Data) service.RoleRepo {
return &roleRepo{
data: data,
}
}
// GetRoleAllList get role list all
func (rr *roleRepo) GetRoleAllList(ctx context.Context) (roleList []*entity.Role, err error) {
roleList = make([]*entity.Role, 0)
err = rr.data.DB.Context(ctx).Find(&roleList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetRoleAllMapping get role all mapping
func (rr *roleRepo) GetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) {
roleList, err := rr.GetRoleAllList(ctx)
if err != nil {
return nil, err
}
roleMapping = make(map[int]*entity.Role, 0)
for _, role := range roleList {
roleMapping[role.ID] = role
}
return roleMapping, nil
}
+103
View File
@@ -0,0 +1,103 @@
/*
* 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 role
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/role"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
"xorm.io/xorm"
)
// userRoleRelRepo userRoleRel repository
type userRoleRelRepo struct {
data *data.Data
}
// NewUserRoleRelRepo new repository
func NewUserRoleRelRepo(data *data.Data) role.UserRoleRelRepo {
return &userRoleRelRepo{
data: data,
}
}
// SaveUserRoleRel save user role rel
func (ur *userRoleRelRepo) SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error) {
_, err = ur.data.DB.Transaction(func(session *xorm.Session) (any, error) {
session = session.Context(ctx)
item := &entity.UserRoleRel{UserID: userID}
exist, err := session.Get(item)
if err != nil {
return nil, err
}
if exist {
item.RoleID = roleID
_, err = session.ID(item.ID).Update(item)
} else {
_, err = session.Insert(&entity.UserRoleRel{UserID: userID, RoleID: roleID})
}
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserRoleRelList get user role all
func (ur *userRoleRelRepo) GetUserRoleRelList(ctx context.Context, userIDs []string) (
userRoleRelList []*entity.UserRoleRel, err error) {
userRoleRelList = make([]*entity.UserRoleRel, 0)
err = ur.data.DB.Context(ctx).In("user_id", userIDs).Find(&userRoleRelList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserRoleRelListByRoleID get user role all by role id
func (ur *userRoleRelRepo) GetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) (
userRoleRelList []*entity.UserRoleRel, err error) {
userRoleRelList = make([]*entity.UserRoleRel, 0)
err = ur.data.DB.Context(ctx).In("role_id", roleIDs).Find(&userRoleRelList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserRoleRel get user role
func (ur *userRoleRelRepo) GetUserRoleRel(ctx context.Context, userID string) (
rolePowerRel *entity.UserRoleRel, exist bool, err error) {
rolePowerRel = &entity.UserRoleRel{}
exist, err = ur.data.DB.Context(ctx).Where(builder.Eq{"user_id": userID}).Get(rolePowerRel)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+618
View File
@@ -0,0 +1,618 @@
/*
* 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 search_common
import (
"context"
"fmt"
"strconv"
"strings"
"time"
tagcommon "github.com/apache/answer/internal/service/tag_common"
"github.com/apache/answer/plugin"
"github.com/apache/answer/pkg/htmltext"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/handler"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/search_common"
"github.com/apache/answer/internal/service/unique"
usercommon "github.com/apache/answer/internal/service/user_common"
"github.com/apache/answer/pkg/converter"
"github.com/apache/answer/pkg/obj"
"github.com/apache/answer/pkg/uid"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
)
var (
qFields = []string{
"`question`.`id`",
"`question`.`id` as `question_id`",
"`title`",
"`parsed_text`",
"`question`.`created_at` as `created_at`",
"`user_id`",
"`vote_count`",
"`answer_count`",
"CASE WHEN `accepted_answer_id` > 0 THEN 2 ELSE 0 END as `accepted`",
"`question`.`status` as `status`",
"`post_update_time`",
}
aFields = []string{
"`answer`.`id` as `id`",
"`question_id`",
"`question`.`title` as `title`",
"`answer`.`parsed_text` as `parsed_text`",
"`answer`.`created_at` as `created_at`",
"`answer`.`user_id` as `user_id`",
"`answer`.`vote_count` as `vote_count`",
"0 as `answer_count`",
"`adopted` as `accepted`",
"`answer`.`status` as `status`",
"`answer`.`created_at` as `post_update_time`",
}
)
// searchRepo tag repository
type searchRepo struct {
data *data.Data
userCommon *usercommon.UserCommon
uniqueIDRepo unique.UniqueIDRepo
tagCommon *tagcommon.TagCommonService
}
// NewSearchRepo new repository
func NewSearchRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
userCommon *usercommon.UserCommon,
tagCommon *tagcommon.TagCommonService,
) search_common.SearchRepo {
return &searchRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
userCommon: userCommon,
tagCommon: tagCommon,
}
}
// SearchContents search question and answer data
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) {
words = filterWords(words)
var (
b *builder.Builder
ub *builder.Builder
qfs = qFields
afs = aFields
argsQ = []any{}
argsA = []any{}
)
if order == "relevance" {
if len(words) > 0 {
qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs)
afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
} else {
order = "newest"
}
}
b = builder.MySQL().Select(qfs...).From("`question`")
ub = builder.MySQL().Select(afs...).From("`answer`").
LeftJoin("`question`", "`question`.id = `answer`.question_id")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Eq{"`question`.`show`": entity.QuestionShow})
ub.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).
And(builder.Eq{"`question`.`show`": entity.QuestionShow})
argsQ = append(argsQ, entity.QuestionStatusDeleted, entity.QuestionShow)
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
likeConQ := builder.NewCond()
likeConA := builder.NewCond()
for _, word := range words {
likeConQ = likeConQ.Or(builder.Like{"title", word}).
Or(builder.Like{"original_text", word})
argsQ = append(argsQ, "%"+word+"%")
argsQ = append(argsQ, "%"+word+"%")
likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word})
argsA = append(argsA, "%"+word+"%")
}
b.Where(likeConQ)
ub.Where(likeConA)
// check tag
for ti, tagID := range tagIDs {
ast := "tag_rel" + strconv.Itoa(ti)
b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id").
And(builder.Eq{
ast + ".status": entity.TagRelStatusAvailable,
}).
And(builder.In(ast+".tag_id", tagID))
ub.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id").
And(builder.Eq{
ast + ".status": entity.TagRelStatusAvailable,
}).
And(builder.In(ast+".tag_id", tagID))
argsQ = append(argsQ, entity.TagRelStatusAvailable)
argsA = append(argsA, entity.TagRelStatusAvailable)
for _, t := range tagID {
argsQ = append(argsQ, t)
argsA = append(argsA, t)
}
}
// check user
if userID != "" {
b.Where(builder.Eq{"question.user_id": userID})
ub.Where(builder.Eq{"answer.user_id": userID})
argsQ = append(argsQ, userID)
argsA = append(argsA, userID)
}
// check vote
if votes == 0 {
b.Where(builder.Eq{"question.vote_count": votes})
ub.Where(builder.Eq{"answer.vote_count": votes})
argsQ = append(argsQ, votes)
argsA = append(argsA, votes)
} else if votes > 0 {
b.Where(builder.Gte{"question.vote_count": votes})
ub.Where(builder.Gte{"answer.vote_count": votes})
argsQ = append(argsQ, votes)
argsA = append(argsA, votes)
}
// b = b.Union("all", ub)
ubSQL, _, err := ub.ToSQL()
if err != nil {
return
}
bSQL, _, err := b.ToSQL()
if err != nil {
return
}
sql := fmt.Sprintf("(%s UNION ALL %s)", bSQL, ubSQL)
countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL()
if err != nil {
return
}
startNum := (page - 1) * pageSize
querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL()
if err != nil {
return
}
queryArgs := []any{}
countArgs := []any{}
queryArgs = append(queryArgs, querySQL)
queryArgs = append(queryArgs, argsQ...)
queryArgs = append(queryArgs, argsA...)
countArgs = append(countArgs, countSQL)
countArgs = append(countArgs, argsQ...)
countArgs = append(countArgs, argsA...)
res, err := sr.data.DB.Context(ctx).Query(queryArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Context(ctx).Query(countArgs...)
if len(tr) != 0 {
total = converter.StringToInt64(string(tr[0]["total"]))
}
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
} else {
resp, err = sr.parseResult(ctx, res, words)
return
}
}
// SearchQuestions search question data
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) {
words = filterWords(words)
var (
qfs = qFields
args = []any{}
)
if order == "relevance" {
if len(words) > 0 {
qfs, args = addRelevanceField([]string{"title", "original_text"}, words, qfs)
} else {
order = "newest"
}
}
b := builder.MySQL().Select(qfs...).From("question")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
args = append(args, entity.QuestionStatusDeleted, entity.QuestionShow)
likeConQ := builder.NewCond()
for _, word := range words {
likeConQ = likeConQ.Or(builder.Like{"title", word}).
Or(builder.Like{"original_text", word})
args = append(args, "%"+word+"%")
args = append(args, "%"+word+"%")
}
b.Where(likeConQ)
// check tag
for ti, tagID := range tagIDs {
ast := "tag_rel" + strconv.Itoa(ti)
b.Join("INNER", "tag_rel as "+ast, "question.id = "+ast+".object_id").
And(builder.Eq{
ast + ".status": entity.TagRelStatusAvailable,
}).
And(builder.In(ast+".tag_id", tagID))
args = append(args, entity.TagRelStatusAvailable)
for _, t := range tagID {
args = append(args, t)
}
}
// check need filter has not accepted
if notAccepted {
b.And(builder.Eq{"accepted_answer_id": 0})
args = append(args, 0)
}
// check views
if views > -1 {
b.And(builder.Gte{"view_count": views})
args = append(args, views)
}
// check answers
if answers == 0 {
b.And(builder.Eq{"answer_count": answers})
args = append(args, answers)
} else if answers > 0 {
b.And(builder.Gte{"answer_count": answers})
args = append(args, answers)
}
if answers == 0 {
b.And(builder.Eq{"answer_count": 0})
args = append(args, 0)
} else if answers > 0 {
b.And(builder.Gte{"answer_count": answers})
args = append(args, answers)
}
queryArgs := []any{}
countArgs := []any{}
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
if err != nil {
return
}
startNum := (page - 1) * pageSize
querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL()
if err != nil {
return
}
queryArgs = append(queryArgs, querySQL)
queryArgs = append(queryArgs, args...)
countArgs = append(countArgs, countSQL)
countArgs = append(countArgs, args...)
res, err := sr.data.DB.Context(ctx).Query(queryArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Context(ctx).Query(countArgs...)
if err != nil {
return
}
if len(tr) != 0 {
total = converter.StringToInt64(string(tr[0]["total"]))
}
resp, err = sr.parseResult(ctx, res, words)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// SearchAnswers search answer data
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) {
words = filterWords(words)
var (
afs = aFields
args = []any{}
)
if order == "relevance" {
if len(words) > 0 {
afs, args = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
} else {
order = "newest"
}
}
b := builder.MySQL().Select(afs...).From("`answer`").
LeftJoin("`question`", "`question`.id = `answer`.question_id")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
likeConA := builder.NewCond()
for _, word := range words {
likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word})
args = append(args, "%"+word+"%")
}
b.Where(likeConA)
// check tag
for ti, tagID := range tagIDs {
ast := "tag_rel" + strconv.Itoa(ti)
b.Join("INNER", "tag_rel as "+ast, "question_id = "+ast+".object_id").
And(builder.Eq{
ast + ".status": entity.TagRelStatusAvailable,
}).
And(builder.In(ast+".tag_id", tagID))
args = append(args, entity.TagRelStatusAvailable)
for _, t := range tagID {
args = append(args, t)
}
}
// check limit accepted
if accepted {
b.Where(builder.Eq{"adopted": schema.AnswerAcceptedEnable})
args = append(args, schema.AnswerAcceptedEnable)
}
// check question id
if questionID != "" {
b.Where(builder.Eq{"question_id": questionID})
args = append(args, questionID)
}
queryArgs := []any{}
countArgs := []any{}
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
if err != nil {
return
}
startNum := (page - 1) * pageSize
querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL()
if err != nil {
return
}
queryArgs = append(queryArgs, querySQL)
queryArgs = append(queryArgs, args...)
countArgs = append(countArgs, countSQL)
countArgs = append(countArgs, args...)
res, err := sr.data.DB.Context(ctx).Query(queryArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Context(ctx).Query(countArgs...)
if err != nil {
return
}
total = converter.StringToInt64(string(tr[0]["total"]))
resp, err = sr.parseResult(ctx, res, words)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (sr *searchRepo) parseOrder(_ context.Context, order string) (res string) {
switch order {
case "newest":
res = "created_at desc"
case "active":
res = "post_update_time desc"
case "score":
res = "vote_count desc"
case "relevance":
res = "relevance desc"
default:
res = "created_at desc"
}
return
}
// ParseSearchPluginResult parse search plugin result
func (sr *searchRepo) ParseSearchPluginResult(ctx context.Context, sres []plugin.SearchResult, words []string) (resp []*schema.SearchResult, err error) {
var (
qres []map[string][]byte
res = make([]map[string][]byte, 0)
b *builder.Builder
)
for _, r := range sres {
switch r.Type {
case "question":
b = builder.MySQL().Select(qFields...).From("question").Where(builder.Eq{"id": r.ID}).
And(builder.Lt{"`status`": entity.QuestionStatusDeleted})
case "answer":
b = builder.MySQL().Select(aFields...).From("answer").LeftJoin("`question`", "`question`.`id` = `answer`.`question_id`").
Where(builder.Eq{"`answer`.`id`": r.ID}).
And(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
}
qres, err = sr.data.DB.Context(ctx).Query(b)
if err != nil || len(qres) == 0 {
continue
}
res = append(res, qres[0])
}
return sr.parseResult(ctx, res, words)
}
// parseResult parse search result, return the data structure
func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte, words []string) (resp []*schema.SearchResult, err error) {
questionIDs := make([]string, 0)
userIDs := make([]string, 0)
resultList := make([]*schema.SearchResult, 0)
for _, r := range res {
questionIDs = append(questionIDs, string(r["question_id"]))
userIDs = append(userIDs, string(r["user_id"]))
tp, _ := time.ParseInLocation("2006-01-02 15:04:05", string(r["created_at"]), time.Local)
var ID = string(r["id"])
var QuestionID = string(r["question_id"])
if handler.GetEnableShortID(ctx) {
ID = uid.EnShortID(ID)
QuestionID = uid.EnShortID(QuestionID)
}
object := &schema.SearchObject{
ID: ID,
QuestionID: QuestionID,
Title: string(r["title"]),
UrlTitle: htmltext.UrlTitle(string(r["title"])),
Excerpt: htmltext.FetchMatchedExcerpt(string(r["parsed_text"]), words, "...", 100),
CreatedAtParsed: tp.Unix(),
UserInfo: &schema.SearchObjectUser{
ID: string(r["user_id"]),
},
Tags: make([]*schema.TagResp, 0),
VoteCount: converter.StringToInt(string(r["vote_count"])),
Accepted: string(r["accepted"]) == "2",
AnswerCount: converter.StringToInt(string(r["answer_count"])),
}
objectKey, err := obj.GetObjectTypeStrByObjectID(string(r["id"]))
if err != nil {
continue
}
switch objectKey {
case "question":
for k, v := range entity.AdminQuestionSearchStatus {
if v == converter.StringToInt(string(r["status"])) {
object.StatusStr = k
break
}
}
case "answer":
for k, v := range entity.AdminAnswerSearchStatus {
if v == converter.StringToInt(string(r["status"])) {
object.StatusStr = k
break
}
}
}
resultList = append(resultList, &schema.SearchResult{
ObjectType: objectKey,
Object: object,
})
}
tagsMap, err := sr.tagCommon.BatchGetObjectTag(ctx, questionIDs)
if err != nil {
return nil, err
}
userInfoMap, err := sr.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
if err != nil {
return nil, err
}
for _, item := range resultList {
tags, ok := tagsMap[item.Object.QuestionID]
if ok {
item.Object.Tags = tags
}
if userInfo := userInfoMap[item.Object.UserInfo.ID]; userInfo != nil {
item.Object.UserInfo.Username = userInfo.Username
item.Object.UserInfo.DisplayName = userInfo.DisplayName
item.Object.UserInfo.Rank = userInfo.Rank
item.Object.UserInfo.Status = userInfo.Status
}
}
return resultList, nil
}
func addRelevanceField(searchFields, words, fields []string) (res []string, args []any) {
relevanceRes := []string{}
args = []any{}
for _, searchField := range searchFields {
var (
relevance = "(LENGTH(" + searchField + ") - LENGTH(%s))"
replacement = "REPLACE(%s, ?, '')"
replaceField = searchField
replaced string
argsField = []any{}
)
res = fields
for i, word := range words {
if i == 0 {
argsField = append(argsField, word)
replaced = fmt.Sprintf(replacement, replaceField)
} else {
argsField = append(argsField, word)
replaced = fmt.Sprintf(replacement, replaced)
}
}
args = append(args, argsField...)
relevance = fmt.Sprintf(relevance, replaced)
relevanceRes = append(relevanceRes, relevance)
}
res = append(res, "("+strings.Join(relevanceRes, " + ")+") as relevance")
return
}
func filterWords(words []string) (res []string) {
for _, word := range words {
if strings.TrimSpace(word) != "" {
res = append(res, word)
}
}
return
}
+141
View File
@@ -0,0 +1,141 @@
/*
* 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 search_sync
import (
"context"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/pkg/uid"
"github.com/apache/answer/plugin"
"github.com/segmentfault/pacman/log"
)
func NewPluginSyncer(data *data.Data) plugin.SearchSyncer {
return &PluginSyncer{data: data}
}
type PluginSyncer struct {
data *data.Data
}
func (p *PluginSyncer) GetAnswersPage(ctx context.Context, page, pageSize int) (
answerList []*plugin.SearchContent, err error) {
answers := make([]*entity.Answer, 0)
startNum := (page - 1) * pageSize
err = p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&answers)
if err != nil {
return nil, err
}
return p.convertAnswers(ctx, answers)
}
func (p *PluginSyncer) GetQuestionsPage(ctx context.Context, page, pageSize int) (
questionList []*plugin.SearchContent, err error) {
questions := make([]*entity.Question, 0)
startNum := (page - 1) * pageSize
err = p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&questions)
if err != nil {
return nil, err
}
return p.convertQuestions(ctx, questions)
}
func (p *PluginSyncer) convertAnswers(ctx context.Context, answers []*entity.Answer) (
answerList []*plugin.SearchContent, err error) {
for _, answer := range answers {
question := &entity.Question{}
exist, err := p.data.DB.Context(ctx).Where("id = ?", answer.QuestionID).Get(question)
if err != nil {
log.Errorf("get question failed %s", err)
continue
}
if !exist {
continue
}
tagListList := make([]*entity.TagRel, 0)
tags := make([]string, 0)
err = p.data.DB.Context(ctx).Where("object_id = ?", uid.DeShortID(question.ID)).
Where("status = ?", entity.TagRelStatusAvailable).Find(&tagListList)
if err != nil {
log.Errorf("get tag list failed %s", err)
}
for _, tag := range tagListList {
tags = append(tags, tag.TagID)
}
content := &plugin.SearchContent{
ObjectID: answer.ID,
Title: question.Title,
Type: constant.AnswerObjectType,
Content: answer.ParsedText,
Answers: 0,
Status: plugin.SearchContentStatus(answer.Status),
Tags: tags,
QuestionID: answer.QuestionID,
UserID: answer.UserID,
Views: int64(question.ViewCount),
Created: answer.CreatedAt.Unix(),
Active: answer.UpdatedAt.Unix(),
Score: int64(answer.VoteCount),
HasAccepted: answer.Accepted == schema.AnswerAcceptedEnable,
}
answerList = append(answerList, content)
}
return answerList, nil
}
func (p *PluginSyncer) convertQuestions(ctx context.Context, questions []*entity.Question) (
questionList []*plugin.SearchContent, err error) {
for _, question := range questions {
tagListList := make([]*entity.TagRel, 0)
tags := make([]string, 0)
err := p.data.DB.Context(ctx).Where("object_id = ?", question.ID).
Where("status = ?", entity.TagRelStatusAvailable).Find(&tagListList)
if err != nil {
log.Errorf("get tag list failed %s", err)
}
for _, tag := range tagListList {
tags = append(tags, tag.TagID)
}
content := &plugin.SearchContent{
ObjectID: question.ID,
Title: question.Title,
Type: constant.QuestionObjectType,
Content: question.ParsedText,
Answers: int64(question.AnswerCount),
Status: plugin.SearchContentStatus(question.Status),
Tags: tags,
QuestionID: question.ID,
UserID: question.UserID,
Views: int64(question.ViewCount),
Created: question.CreatedAt.Unix(),
Active: question.UpdatedAt.Unix(),
Score: int64(question.VoteCount),
HasAccepted: question.AcceptedAnswerID != "" && question.AcceptedAnswerID != "0",
}
questionList = append(questionList, content)
}
return questionList, nil
}
+120
View File
@@ -0,0 +1,120 @@
/*
* 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 site_info
import (
"context"
"encoding/json"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/siteinfo_common"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
)
type siteInfoRepo struct {
data *data.Data
}
func NewSiteInfo(data *data.Data) siteinfo_common.SiteInfoRepo {
return &siteInfoRepo{
data: data,
}
}
// SaveByType save site setting by type
func (sr *siteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) {
old := &entity.SiteInfo{}
exist, err := sr.data.DB.Context(ctx).Where(builder.Eq{"type": siteType}).Get(old)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
_, err = sr.data.DB.Context(ctx).ID(old.ID).Update(data)
} else {
_, err = sr.data.DB.Context(ctx).Insert(data)
}
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
sr.setCache(ctx, siteType, data)
return
}
// GetByType get site info by type
func (sr *siteInfoRepo) GetByType(ctx context.Context, siteType string, withoutCache ...bool) (siteInfo *entity.SiteInfo, exist bool, err error) {
if len(withoutCache) == 0 {
siteInfo = sr.getCache(ctx, siteType)
if siteInfo != nil {
return siteInfo, true, nil
}
}
siteInfo = &entity.SiteInfo{}
exist, err = sr.data.DB.Context(ctx).Where(builder.Eq{"type": siteType}).Get(siteInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return nil, false, err
}
if exist {
sr.setCache(ctx, siteType, siteInfo)
}
return
}
func (sr *siteInfoRepo) getCache(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo) {
siteInfoCache, exist, err := sr.data.Cache.GetString(ctx, constant.SiteInfoCacheKey+siteType)
if err != nil {
return nil
}
if !exist {
return nil
}
siteInfo = &entity.SiteInfo{}
_ = json.Unmarshal([]byte(siteInfoCache), siteInfo)
return siteInfo
}
func (sr *siteInfoRepo) setCache(ctx context.Context, siteType string, siteInfo *entity.SiteInfo) {
siteInfoCache, _ := json.Marshal(siteInfo)
err := sr.data.Cache.SetString(ctx,
constant.SiteInfoCacheKey+siteType, string(siteInfoCache), constant.SiteInfoCacheTime)
if err != nil {
log.Error(err)
}
}
func (sr *siteInfoRepo) IsBrandingFileUsed(ctx context.Context, filePath string) (bool, error) {
siteInfo := &entity.SiteInfo{}
count, err := sr.data.DB.Context(ctx).
Table("site_info").
Where(builder.Eq{"type": "branding"}).
And(builder.Like{"content", "%" + filePath + "%"}).
Count(&siteInfo)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count > 0, nil
}
+261
View File
@@ -0,0 +1,261 @@
/*
* 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 tag
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/handler"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
tagcommon "github.com/apache/answer/internal/service/tag_common"
"github.com/apache/answer/internal/service/unique"
"github.com/apache/answer/pkg/uid"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// tagRelRepo tag rel repository
type tagRelRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewTagRelRepo new repository
func NewTagRelRepo(data *data.Data,
uniqueIDRepo unique.UniqueIDRepo) tagcommon.TagRelRepo {
return &tagRelRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// AddTagRelList add tag list
func (tr *tagRelRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) {
for _, item := range tagList {
item.ObjectID = uid.DeShortID(item.ObjectID)
}
_, err = tr.data.DB.Context(ctx).Insert(tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if handler.GetEnableShortID(ctx) {
for _, item := range tagList {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
}
return
}
// RemoveTagRelListByObjectID delete tag list
func (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID)
_, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// RecoverTagRelListByObjectID recover tag list
func (tr *tagRelRepo) RecoverTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID)
_, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID)
_, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.TagRelStatusAvailable).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID)
_, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.TagRelStatusHide).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// RemoveTagRelListByIDs delete tag list
func (tr *tagRelRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) {
_, err = tr.data.DB.Context(ctx).In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetObjectTagRelWithoutStatus get object tag relation no matter status
func (tr *tagRelRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectID, tagID string) (
tagRel *entity.TagRel, exist bool, err error,
) {
objectID = uid.DeShortID(objectID)
tagRel = &entity.TagRel{}
session := tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("tag_id = ?", tagID)
exist, err = session.Get(tagRel)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
if handler.GetEnableShortID(ctx) {
tagRel.ObjectID = uid.EnShortID(tagRel.ObjectID)
}
return
}
// EnableTagRelByIDs update tag status to available
func (tr *tagRelRepo) EnableTagRelByIDs(ctx context.Context, ids []int64, hide bool) (err error) {
status := entity.TagRelStatusAvailable
if hide {
status = entity.TagRelStatusHide
}
_, err = tr.data.DB.Context(ctx).In("id", ids).Update(&entity.TagRel{Status: status})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetObjectTagRelList get object tag relation list all
func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string) (tagListList []*entity.TagRel, err error) {
objectID = uid.DeShortID(objectID)
tagListList = make([]*entity.TagRel, 0)
session := tr.data.DB.Context(ctx).Where("object_id = ?", objectID)
session.In("status", []int{entity.TagRelStatusAvailable, entity.TagRelStatusHide})
err = session.Find(&tagListList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
if handler.GetEnableShortID(ctx) {
for _, item := range tagListList {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
}
return
}
// BatchGetObjectTagRelList get object tag relation list all
func (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) {
for num, item := range objectIds {
objectIds[num] = uid.DeShortID(item)
}
tagListList = make([]*entity.TagRel, 0)
session := tr.data.DB.Context(ctx).In("object_id", objectIds)
session.Where("status = ?", entity.TagRelStatusAvailable)
err = session.Find(&tagListList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
if handler.GetEnableShortID(ctx) {
for _, item := range tagListList {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
}
return
}
// CountTagRelByTagID count tag relation
func (tr *tagRelRepo) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) {
count, err = tr.data.DB.Context(ctx).Count(&entity.TagRel{TagID: tagID, Status: entity.AnswerStatusAvailable})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagRelDefaultStatusByObjectID get tag rel default status
func (tr *tagRelRepo) GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error) {
question := entity.Question{}
exist, err := tr.data.DB.Context(ctx).ID(objectID).Cols("show", "status").Get(&question)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
if exist && (question.Show == entity.QuestionHide || question.Status == entity.QuestionStatusDeleted) {
return entity.TagRelStatusHide, nil
}
return entity.TagRelStatusAvailable, nil
}
// MigrateTagObjects migrate tag objects
func (tr *tagRelRepo) MigrateTagObjects(ctx context.Context, sourceTagId, targetTagId string) error {
_, err := tr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
// 1. Get all objects related to source tag
var sourceObjects []entity.TagRel
err = session.Where("tag_id = ?", sourceTagId).Find(&sourceObjects)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// 2. Get existing target tag relations
var existingTargets []entity.TagRel
err = session.Where("tag_id = ?", targetTagId).Find(&existingTargets)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// Create map of existing target objects for quick lookup
existingMap := make(map[string]bool)
for _, target := range existingTargets {
existingMap[target.ObjectID] = true
}
// 3. Create new relations for objects not already tagged with target
newRelations := make([]*entity.TagRel, 0)
for _, source := range sourceObjects {
if !existingMap[source.ObjectID] {
newRelations = append(newRelations, &entity.TagRel{
TagID: targetTagId,
ObjectID: source.ObjectID,
Status: source.Status,
})
}
}
if len(newRelations) > 0 {
_, err = session.Insert(newRelations)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
}
// 4. Remove old relations
_, err = session.Where("tag_id = ?", sourceTagId).Delete(&entity.TagRel{})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil, nil
})
return err
}
+140
View File
@@ -0,0 +1,140 @@
/*
* 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 tag
import (
"context"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/tag_common"
"github.com/apache/answer/internal/service/unique"
"github.com/apache/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
)
// tagRepo tag repository
type tagRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewTagRepo new repository
func NewTagRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
) tag_common.TagRepo {
return &tagRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// RemoveTag delete tag
func (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) {
session := tr.data.DB.Context(ctx).Where(builder.Eq{"id": tagID})
_, err = session.Update(&entity.Tag{Status: entity.TagStatusDeleted})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateTag update tag
func (tr *tagRepo) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) {
_, err = tr.data.DB.Context(ctx).Where(builder.Eq{"id": tag.ID}).Update(tag)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// RecoverTag recover deleted tag
func (tr *tagRepo) RecoverTag(ctx context.Context, tagID string) (err error) {
_, err = tr.data.DB.Context(ctx).ID(tagID).Update(&entity.Tag{Status: entity.TagStatusAvailable})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// MustGetTagByNameOrID get tag by name or id
func (tr *tagRepo) MustGetTagByNameOrID(ctx context.Context, tagID, slugName string) (
tag *entity.Tag, exist bool, err error) {
if len(tagID) == 0 && len(slugName) == 0 {
return nil, false, nil
}
tag = &entity.Tag{}
session := tr.data.DB.Context(ctx)
if len(tagID) > 0 {
session.ID(tagID)
}
if len(slugName) > 0 {
session.Where(builder.Eq{"slug_name": slugName})
}
exist, err = session.Get(tag)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateTagSynonym update synonym tag
func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64,
mainTagSlugName string,
) (err error) {
bean := &entity.Tag{MainTagID: mainTagID, MainTagSlugName: mainTagSlugName}
session := tr.data.DB.Context(ctx).In("slug_name", tagSlugNameList).MustCols("main_tag_id", "main_tag_slug_name")
_, err = session.Update(bean)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagRepo) GetTagSynonymCount(ctx context.Context, tagID string) (count int64, err error) {
count, err = tr.data.DB.Context(ctx).Count(&entity.Tag{MainTagID: converter.StringToInt64(tagID), Status: entity.TagStatusAvailable})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagRepo) GetIDsByMainTagId(ctx context.Context, mainTagID string) (tagIDs []string, err error) {
session := tr.data.DB.Context(ctx).Table(entity.Tag{}.TableName()).Where(builder.Eq{"status": entity.TagStatusAvailable, "main_tag_id": converter.StringToInt64(mainTagID)}).Cols("id")
err = session.Find(&tagIDs)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagList get tag list all
func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.Context(ctx).Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.Find(&tagList, tag)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+295
View File
@@ -0,0 +1,295 @@
/*
* 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 tag_common
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
tagcommon "github.com/apache/answer/internal/service/tag_common"
"github.com/apache/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
)
// tagCommonRepo tag repository
type tagCommonRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewTagCommonRepo new repository
func NewTagCommonRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
) tagcommon.TagCommonRepo {
return &tagCommonRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// GetTagListByIDs get tag list all
func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.Context(ctx).In("id", ids)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagBySlugName get tag by slug name
func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
tagInfo = &entity.Tag{}
session := tr.data.DB.Context(ctx).Where("LOWER(slug_name) = ?", slugName)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tagInfo)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByName get tag list all like name
func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, recommend, reserved bool) (tagList []*entity.Tag, err error) {
cond := &entity.Tag{}
session := tr.data.DB.Context(ctx)
if len(name) > 0 {
session.Where("slug_name LIKE ? OR display_name LIKE ?", strings.ToLower(name)+"%", name+"%")
}
var columns []string
if recommend {
columns = append(columns, "recommend")
cond.Recommend = true
}
if reserved {
columns = append(columns, "reserved")
cond.Reserved = true
}
if len(columns) > 0 {
session.UseBool(columns...)
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
tagList = make([]*entity.Tag, 0)
err = session.OrderBy("recommend DESC,reserved DESC,slug_name ASC").Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Context(ctx).Where("")
cond.Recommend = true
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Asc("slug_name")
session.UseBool("recommend")
err = session.Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Context(ctx).Where("")
cond.Reserved = true
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Asc("slug_name")
session.UseBool("reserved")
err = session.Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByNames get tag list all like name
func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.Context(ctx).In("slug_name", names).UseBool("recommend", "reserved")
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagByID get tag one
func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (
tag *entity.Tag, exist bool, err error,
) {
tag = &entity.Tag{}
session := tr.data.DB.Context(ctx).Where(builder.Eq{"id": tagID})
if !includeDeleted {
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
}
exist, err = session.Get(tag)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagPage get tag page
func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (
tagList []*entity.Tag, total int64, err error,
) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.Context(ctx)
if len(tag.SlugName) > 0 {
mainTagCond := builder.And(
builder.Or(
builder.Like{"slug_name", fmt.Sprintf("LOWER(%s)", tag.SlugName)},
builder.Like{"display_name", tag.SlugName},
),
builder.Eq{"main_tag_id": 0},
)
synonymCond := builder.And(
builder.Eq{"slug_name": tag.SlugName},
builder.Neq{"main_tag_id": 0},
)
session.Where(builder.Or(mainTagCond, synonymCond))
tag.SlugName = ""
} else {
session.Where(builder.Eq{"main_tag_id": 0})
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
switch queryCond {
case "popular":
session.Desc("question_count")
case "name":
session.Asc("slug_name")
case "newest":
session.Desc("created_at")
}
total, err = pager.Help(page, pageSize, &tagList, tag, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
for i := 0; i < len(tagList); i++ {
if tagList[i].MainTagID != 0 {
mainTag, exist, errSynonym := tr.GetTagByID(ctx, strconv.FormatInt(tagList[i].MainTagID, 10), false)
if errSynonym != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(errSynonym).WithStack()
return
}
if exist {
tagList[i] = mainTag
}
}
}
return
}
// AddTagList add tag
func (tr *tagCommonRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
addTags := make([]*entity.Tag, 0)
for _, item := range tagList {
exist, err := tr.updateDeletedTag(ctx, item)
if err != nil {
return err
}
if exist {
continue
}
addTags = append(addTags, item)
item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName())
if err != nil {
return err
}
item.RevisionID = "0"
}
if len(addTags) == 0 {
return nil
}
_, err = tr.data.DB.Context(ctx).Insert(addTags)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) updateDeletedTag(ctx context.Context, tag *entity.Tag) (exist bool, err error) {
old := &entity.Tag{SlugName: tag.SlugName}
exist, err = tr.data.DB.Context(ctx).Get(old)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist || old.Status != entity.TagStatusDeleted {
return false, nil
}
tag.ID = old.ID
tag.Status = entity.TagStatusAvailable
tag.RevisionID = "0"
if _, err = tr.data.DB.Context(ctx).ID(tag.ID).Update(tag); err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return true, nil
}
// UpdateTagQuestionCount update tag question count
func (tr *tagCommonRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) {
cond := &entity.Tag{QuestionCount: questionCount}
_, err = tr.data.DB.Context(ctx).Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) {
bean := &entity.Tag{}
switch attribute {
case "recommend":
bean.Recommend = value
case "reserved":
bean.Reserved = value
default:
return
}
session := tr.data.DB.Context(ctx).In("slug_name", tags).Cols(attribute).UseBool(attribute)
_, err = session.Update(bean)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+56
View File
@@ -0,0 +1,56 @@
/*
* 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 unique
import (
"context"
"fmt"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
)
// uniqueIDRepo Unique id repository
type uniqueIDRepo struct {
data *data.Data
}
// NewUniqueIDRepo new repository
func NewUniqueIDRepo(data *data.Data) unique.UniqueIDRepo {
return &uniqueIDRepo{
data: data,
}
}
// GenUniqueIDStr generate unique id string
// 1 + 00x(objectType) + 000000000000x(id)
func (ur *uniqueIDRepo) GenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error) {
objectType := constant.ObjectTypeStrMapping[key]
bean := &entity.Uniqid{UniqidType: objectType}
_, err = ur.data.DB.Context(ctx).Insert(bean)
if err != nil {
return "", errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return fmt.Sprintf("1%03d%013d", objectType, bean.ID), nil
}
+207
View File
@@ -0,0 +1,207 @@
/*
* 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 user
import (
"context"
"encoding/json"
"time"
"xorm.io/builder"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/pager"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/auth"
"github.com/apache/answer/internal/service/user_admin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// userAdminRepo user repository
type userAdminRepo struct {
data *data.Data
authRepo auth.AuthRepo
}
// NewUserAdminRepo new repository
func NewUserAdminRepo(data *data.Data, authRepo auth.AuthRepo) user_admin.UserAdminRepo {
return &userAdminRepo{
data: data,
authRepo: authRepo,
}
}
// UpdateUserStatus update user status
func (ur *userAdminRepo) UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int,
email string, suspendedUntil time.Time,
) (err error) {
cond := &entity.User{Status: userStatus, MailStatus: mailStatus, EMail: email}
switch userStatus {
case entity.UserStatusSuspended:
cond.SuspendedAt = time.Now()
cond.SuspendedUntil = suspendedUntil
case entity.UserStatusDeleted:
cond.DeletedAt = time.Now()
case entity.UserStatusAvailable:
// When restoring user status, clear suspended until time to zero
cond.SuspendedUntil = time.Time{}
}
_, err = ur.data.DB.Context(ctx).ID(userID).MustCols("status", "mail_status", "e_mail", "suspended_at", "suspended_until", "deleted_at").Update(cond)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
userCacheInfo := &entity.UserCacheInfo{
UserID: userID,
EmailStatus: mailStatus,
UserStatus: userStatus,
}
t, _ := json.Marshal(userCacheInfo)
log.Infof("user change status: %s", string(t))
err = ur.authRepo.SetUserStatus(ctx, userID, userCacheInfo)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// AddUser add user
func (ur *userAdminRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
_, err = ur.data.DB.Context(ctx).Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// AddUsers add users
func (ur *userAdminRepo) AddUsers(ctx context.Context, users []*entity.User) (err error) {
_, err = ur.data.DB.Context(ctx).Insert(users)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateUserPassword update user password
func (ur *userAdminRepo) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) {
_, err = ur.data.DB.Context(ctx).ID(userID).Update(&entity.User{Pass: password})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserInfo get user info
func (ur *userAdminRepo) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) {
user = &entity.User{}
exist, err = ur.data.DB.Context(ctx).ID(userID).Get(user)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
if err != nil {
return nil, false, err
}
return
}
// GetUserInfoByEmail get user info
func (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) {
userInfo := &entity.User{}
exist, err = ur.data.DB.Context(ctx).Where("e_mail = ?", email).
Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
if !exist {
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
if err != nil {
return nil, false, err
}
return
}
// GetUserPage get user page
func (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User,
usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) {
users = make([]*entity.User, 0)
session := ur.data.DB.Context(ctx)
switch user.Status {
case entity.UserStatusDeleted:
session.Desc("`user`.deleted_at")
case entity.UserStatusSuspended:
session.Desc("`user`.suspended_at")
default:
session.Desc("`user`.created_at")
}
if len(usernameOrDisplayName) > 0 {
session.And(builder.Or(
builder.Like{"`user`.username", usernameOrDisplayName},
builder.Like{"`user`.display_name", usernameOrDisplayName},
))
}
if isStaff {
session.Join("INNER", "user_role_rel", "`user`.id = `user_role_rel`.user_id AND `user_role_rel`.role_id > 1")
}
total, err = pager.Help(page, pageSize, &users, user, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, users)
return
}
// DeletePermanentlyUsers delete permanently users
func (ur *userAdminRepo) DeletePermanentlyUsers(ctx context.Context) (err error) {
_, err = ur.data.DB.Context(ctx).Where("status = ?", entity.UserStatusDeleted).Delete(&entity.User{})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetExpiredSuspendedUsers gets all suspended users whose suspension has expired
func (ur *userAdminRepo) GetExpiredSuspendedUsers(ctx context.Context) (users []*entity.User, err error) {
users = make([]*entity.User, 0)
now := time.Now()
err = ur.data.DB.Context(ctx).
Where("status = ?", entity.UserStatusSuspended).
Where("suspended_until < ?", now).
Find(&users)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return users, nil
}
+397
View File
@@ -0,0 +1,397 @@
/*
* 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 user
import (
"context"
"strings"
"time"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
usercommon "github.com/apache/answer/internal/service/user_common"
"github.com/apache/answer/pkg/converter"
"github.com/apache/answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
"xorm.io/xorm"
)
// userRepo user repository
type userRepo struct {
data *data.Data
}
// NewUserRepo new repository
func NewUserRepo(data *data.Data) usercommon.UserRepo {
return &userRepo{
data: data,
}
}
// AddUser add user
func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
_, err = ur.data.DB.Transaction(func(session *xorm.Session) (any, error) {
session = session.Context(ctx)
userInfo := &entity.User{}
exist, err := session.Where("username = ?", user.Username).Get(userInfo)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
return nil, errors.InternalServer(reason.UsernameDuplicate)
}
_, err = session.Insert(user)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil, nil
})
return
}
// IncreaseAnswerCount increase answer count
func (ur *userRepo) IncreaseAnswerCount(ctx context.Context, userID string, amount int) (err error) {
user := &entity.User{}
_, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Incr("answer_count", amount).Update(user)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// IncreaseQuestionCount increase question count
func (ur *userRepo) IncreaseQuestionCount(ctx context.Context, userID string, amount int) (err error) {
user := &entity.User{}
_, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Incr("question_count", amount).Update(user)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) {
user := &entity.User{}
user.QuestionCount = int(count)
_, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("question_count").Update(user)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *userRepo) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) {
user := &entity.User{}
user.AnswerCount = count
_, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("answer_count").Update(user)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// UpdateLastLoginDate update last login date
func (ur *userRepo) UpdateLastLoginDate(ctx context.Context, userID string) (err error) {
user := &entity.User{LastLoginDate: time.Now()}
_, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("last_login_date").Update(user)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// UpdateEmailStatus update email status
func (ur *userRepo) UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error {
cond := &entity.User{MailStatus: emailStatus}
_, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("mail_status").Update(cond)
if err != nil {
return err
}
return nil
}
// UpdateNoticeStatus update notice status
func (ur *userRepo) UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error {
cond := &entity.User{NoticeStatus: noticeStatus}
_, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("notice_status").Update(cond)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *userRepo) UpdatePass(ctx context.Context, userID, pass string) error {
_, err := ur.data.DB.Context(ctx).Where("id = ?", userID).Cols("pass").Update(&entity.User{Pass: pass})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err error) {
_, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Update(&entity.User{EMail: email})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (ur *userRepo) UpdateUserInterface(ctx context.Context, userID, language, colorSchema string) (err error) {
session := ur.data.DB.Context(ctx).Where("id = ?", userID)
_, err = session.Cols("language", "color_scheme").Update(&entity.User{Language: language, ColorScheme: colorSchema})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateInfo update user info
func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {
_, err = ur.data.DB.Context(ctx).Where("id = ?", userInfo.ID).
Cols("username", "display_name", "avatar", "bio", "bio_html", "website", "location").Update(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateUserProfile update user profile
func (ur *userRepo) UpdateUserProfile(ctx context.Context, userInfo *entity.User) (err error) {
_, err = ur.data.DB.Context(ctx).Where("id = ?", userInfo.ID).
Cols("username", "e_mail", "mail_status", "display_name").Update(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetByUserID get user info by user id
func (ur *userRepo) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) {
userInfo = &entity.User{}
exist, err = ur.data.DB.Context(ctx).Where("id = ?", userID).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)
if err != nil {
return nil, false, err
}
return
}
func (ur *userRepo) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) {
list := make([]*entity.User, 0)
err := ur.data.DB.Context(ctx).In("id", ids).Find(&list)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, list)
return list, nil
}
// GetByUsername get user by username
func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) {
userInfo = &entity.User{}
exist, err = ur.data.DB.Context(ctx).Where("username = ?", username).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)
if err != nil {
return nil, false, err
}
return
}
func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) {
list := make([]*entity.User, 0)
err := ur.data.DB.Context(ctx).Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return list, err
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, list)
return list, nil
}
// GetByEmail get user by email
func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) {
userInfo = &entity.User{}
exist, err = ur.data.DB.Context(ctx).Where("e_mail = ?", email).
Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
session := ur.data.DB.Context(ctx)
session.Where("status = ? OR status = ?", entity.UserStatusAvailable, entity.UserStatusSuspended)
count, err = session.Count(&entity.User{})
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count, nil
}
func (ur *userRepo) SearchUserListByName(ctx context.Context, name string, limit int,
onlyStaff bool) (userList []*entity.User, err error) {
userList = make([]*entity.User, 0)
session := ur.data.DB.Context(ctx)
if onlyStaff {
session.Join("INNER", "user_role_rel", "`user`.id = `user_role_rel`.user_id AND `user_role_rel`.role_id > 1")
}
session.Where("status = ?", entity.UserStatusAvailable)
session.Where("username LIKE ? OR display_name LIKE ?", strings.ToLower(name)+"%", name+"%")
session.OrderBy("username ASC, `user`.id DESC")
session.Limit(limit)
err = session.Find(&userList)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, userList)
return
}
func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) {
if original == nil {
return nil
}
uc, ok := plugin.GetUserCenter()
if !ok {
return nil
}
userInfo := &entity.UserExternalLogin{}
session := data.DB.Context(ctx).Where("user_id = ?", original.ID)
session.Where("provider = ?", uc.Info().SlugName)
exist, err := session.Get(userInfo)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return nil
}
userCenterBasicUserInfo, err := uc.UserInfo(userInfo.ExternalID)
if err != nil {
log.Error(err)
return errors.BadRequest(reason.UserNotFound).WithError(err).WithStack()
}
decorateByUserCenterUser(original, userCenterBasicUserInfo)
return nil
}
func tryToDecorateUserListFromUserCenter(ctx context.Context, data *data.Data, original []*entity.User) {
uc, ok := plugin.GetUserCenter()
if !ok {
return
}
ids := make([]string, 0)
originalUserIDMapping := make(map[string]*entity.User, 0)
for _, user := range original {
originalUserIDMapping[user.ID] = user
ids = append(ids, user.ID)
}
userExternalLoginList := make([]*entity.UserExternalLogin, 0)
session := data.DB.Context(ctx).Where("provider = ?", uc.Info().SlugName)
session.In("user_id", ids)
err := session.Find(&userExternalLoginList)
if err != nil {
log.Error(err)
return
}
userExternalIDs := make([]string, 0)
originalExternalIDMapping := make(map[string]*entity.User, 0)
for _, u := range userExternalLoginList {
originalExternalIDMapping[u.ExternalID] = originalUserIDMapping[u.UserID]
userExternalIDs = append(userExternalIDs, u.ExternalID)
}
if len(userExternalIDs) == 0 {
return
}
ucUsers, err := uc.UserList(userExternalIDs)
if err != nil {
log.Errorf("get user list from user center failed: %v, %v", err, userExternalIDs)
return
}
for _, ucUser := range ucUsers {
decorateByUserCenterUser(originalExternalIDMapping[ucUser.ExternalID], ucUser)
}
}
func decorateByUserCenterUser(original *entity.User, ucUser *plugin.UserCenterBasicUserInfo) {
if original == nil || ucUser == nil {
return
}
// In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies.
if original.Username != ucUser.Username {
log.Warnf("user %s username is inconsistent with user center", original.ID)
}
if len(ucUser.DisplayName) > 0 {
original.DisplayName = ucUser.DisplayName
}
if len(ucUser.Email) > 0 {
original.EMail = ucUser.Email
}
if len(ucUser.Avatar) > 0 {
original.Avatar = schema.CustomAvatar(ucUser.Avatar).ToJsonString()
}
if len(ucUser.Mobile) > 0 {
original.Mobile = ucUser.Mobile
}
if len(ucUser.Bio) > 0 {
original.BioHTML = converter.Markdown2HTML(ucUser.Bio) + original.BioHTML
}
// If plugin enable rank agent, use rank from user center.
if plugin.RankAgentEnabled() {
original.Rank = ucUser.Rank
}
if ucUser.Status != plugin.UserStatusAvailable {
original.Status = int(ucUser.Status)
}
}
func (ur *userRepo) IsAvatarFileUsed(ctx context.Context, filePath string) (bool, error) {
user := &entity.User{}
count, err := ur.data.DB.Context(ctx).
Table("user").
Where(builder.Like{"avatar", "%" + filePath + "%"}).
Count(&user)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return count > 0, nil
}
@@ -0,0 +1,164 @@
/*
* 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 user_external_login
import (
"context"
"encoding/json"
"time"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/schema"
"github.com/apache/answer/internal/service/user_external_login"
"github.com/segmentfault/pacman/errors"
)
type userExternalLoginRepo struct {
data *data.Data
}
// NewUserExternalLoginRepo new repository
func NewUserExternalLoginRepo(data *data.Data) user_external_login.UserExternalLoginRepo {
return &userExternalLoginRepo{
data: data,
}
}
// AddUserExternalLogin add external login information
func (ur *userExternalLoginRepo) AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error) {
_, err = ur.data.DB.Context(ctx).Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateInfo update user info
func (ur *userExternalLoginRepo) UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error) {
_, err = ur.data.DB.Context(ctx).ID(userInfo.ID).Update(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetByExternalID get by external ID
func (ur *userExternalLoginRepo) GetByExternalID(ctx context.Context, provider, externalID string) (
userInfo *entity.UserExternalLogin, exist bool, err error) {
userInfo = &entity.UserExternalLogin{}
exist, err = ur.data.DB.Context(ctx).Where("external_id = ?", externalID).Where("provider = ?", provider).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetByUserID get by user ID
func (ur *userExternalLoginRepo) GetByUserID(ctx context.Context, provider, userID string) (
userInfo *entity.UserExternalLogin, exist bool, err error) {
userInfo = &entity.UserExternalLogin{}
exist, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Where("provider = ?", provider).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserExternalLoginList get by external ID
func (ur *userExternalLoginRepo) GetUserExternalLoginList(ctx context.Context, userID string) (
resp []*entity.UserExternalLogin, err error) {
resp = make([]*entity.UserExternalLogin, 0)
err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).OrderBy("updated_at DESC").Find(&resp)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// DeleteUserExternalLogin delete external user login info
func (ur *userExternalLoginRepo) DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error) {
cond := &entity.UserExternalLogin{}
_, err = ur.data.DB.Context(ctx).Where("user_id = ? AND external_id = ?", userID, externalID).Delete(cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// DeleteUserExternalLoginByUserID delete external user login info by user ID
func (ur *userExternalLoginRepo) DeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error) {
cond := &entity.UserExternalLogin{}
_, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// SetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo(
ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) {
cacheData, _ := json.Marshal(info)
return ur.data.Cache.SetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key,
string(cacheData), constant.ConnectorUserExternalInfoCacheTime)
}
// GetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo(
ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) {
res, exist, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key)
if err != nil {
return info, err
}
if !exist {
return nil, nil
}
info = &schema.ExternalLoginUserInfoCache{}
_ = json.Unmarshal([]byte(res), &info)
return info, nil
}
func (ur *userExternalLoginRepo) SetCacheOAuthState(
ctx context.Context, state string, info *schema.ExternalLoginOAuthState, duration time.Duration) (err error) {
cacheData, _ := json.Marshal(info)
return ur.data.Cache.SetString(ctx, constant.ConnectorOAuthStateCacheKey+state,
string(cacheData), duration)
}
func (ur *userExternalLoginRepo) GetCacheOAuthState(
ctx context.Context, state string) (info *schema.ExternalLoginOAuthState, err error) {
res, exist, err := ur.data.Cache.GetString(ctx, constant.ConnectorOAuthStateCacheKey+state)
if err != nil {
return info, err
}
if !exist {
return nil, nil
}
info = &schema.ExternalLoginOAuthState{}
_ = json.Unmarshal([]byte(res), &info)
return info, nil
}
func (ur *userExternalLoginRepo) DeleteCacheOAuthState(ctx context.Context, state string) (err error) {
return ur.data.Cache.Del(ctx, constant.ConnectorOAuthStateCacheKey+state)
}
@@ -0,0 +1,128 @@
/*
* 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 user_notification_config
import (
"context"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/internal/service/user_notification_config"
"github.com/segmentfault/pacman/errors"
)
// userNotificationConfigRepo notification repository
type userNotificationConfigRepo struct {
data *data.Data
}
// NewUserNotificationConfigRepo new repository
func NewUserNotificationConfigRepo(data *data.Data) user_notification_config.UserNotificationConfigRepo {
return &userNotificationConfigRepo{
data: data,
}
}
// Add add notification config
func (ur *userNotificationConfigRepo) Add(ctx context.Context, userIDs []string, source, channels string) (err error) {
var configs []*entity.UserNotificationConfig
for _, userID := range userIDs {
configs = append(configs, &entity.UserNotificationConfig{
UserID: userID,
Source: source,
Channels: channels,
Enabled: true,
})
}
_, err = ur.data.DB.Context(ctx).Insert(configs)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// Save save notification config, if existed, update, if not exist, insert
func (ur *userNotificationConfigRepo) Save(ctx context.Context, uc *entity.UserNotificationConfig) (err error) {
old := &entity.UserNotificationConfig{UserID: uc.UserID, Source: uc.Source}
exist, err := ur.data.DB.Context(ctx).Get(old)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
old.Channels = uc.Channels
old.Enabled = uc.Enabled
_, err = ur.data.DB.Context(ctx).ID(old.ID).UseBool("enabled").Cols("channels", "enabled").Update(old)
} else {
_, err = ur.data.DB.Context(ctx).Insert(uc)
}
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// GetByUserID get notification config by user id
func (ur *userNotificationConfigRepo) GetByUserID(ctx context.Context, userID string) (
[]*entity.UserNotificationConfig, error) {
var configs []*entity.UserNotificationConfig
err := ur.data.DB.Context(ctx).Where("user_id = ?", userID).Find(&configs)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return configs, nil
}
// GetBySource get notification config by source
func (ur *userNotificationConfigRepo) GetBySource(ctx context.Context, source constant.NotificationSource) (
[]*entity.UserNotificationConfig, error) {
var configs []*entity.UserNotificationConfig
err := ur.data.DB.Context(ctx).UseBool("enabled").
Find(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return configs, nil
}
// GetByUserIDAndSource get notification config by user id and source
func (ur *userNotificationConfigRepo) GetByUserIDAndSource(ctx context.Context, userID string, source constant.NotificationSource) (
conf *entity.UserNotificationConfig, exist bool, err error) {
config := &entity.UserNotificationConfig{UserID: userID, Source: string(source)}
exist, err = ur.data.DB.Context(ctx).Get(config)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return config, exist, nil
}
// GetByUsersAndSource get notification config by user ids and source
func (ur *userNotificationConfigRepo) GetByUsersAndSource(
ctx context.Context, userIDs []string, source constant.NotificationSource) (
[]*entity.UserNotificationConfig, error) {
var configs []*entity.UserNotificationConfig
err := ur.data.DB.Context(ctx).UseBool("enabled").In("user_id", userIDs).
Find(&configs, &entity.UserNotificationConfig{Source: string(source), Enabled: true})
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return configs, nil
}
+240
View File
@@ -0,0 +1,240 @@
/*
* 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 vector_search_sync
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/pkg/uid"
"github.com/apache/answer/plugin"
"github.com/segmentfault/pacman/log"
)
// NewPluginSyncer creates a new VectorSearchSyncer that reads from the database.
func NewPluginSyncer(data *data.Data) plugin.VectorSearchSyncer {
return &PluginSyncer{data: data}
}
// PluginSyncer implements plugin.VectorSearchSyncer.
// It aggregates question/answer text with comments for vector embedding.
type PluginSyncer struct {
data *data.Data
}
// GetQuestionsPage returns a page of questions with aggregated text
// (question title + body + all answers + all comments).
func (p *PluginSyncer) GetQuestionsPage(ctx context.Context, page, pageSize int) (
[]*plugin.VectorSearchContent, error) {
questions := make([]*entity.Question, 0)
startNum := (page - 1) * pageSize
err := p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&questions)
if err != nil {
return nil, err
}
return p.buildQuestionContents(ctx, questions)
}
// GetAnswersPage returns a page of answers with aggregated text
// (parent question title + answer body + answer comments).
func (p *PluginSyncer) GetAnswersPage(ctx context.Context, page, pageSize int) (
[]*plugin.VectorSearchContent, error) {
answers := make([]*entity.Answer, 0)
startNum := (page - 1) * pageSize
err := p.data.DB.Context(ctx).Limit(pageSize, startNum).Find(&answers)
if err != nil {
return nil, err
}
return p.buildAnswerContents(ctx, answers)
}
// buildQuestionContents aggregates each question with its answers and comments.
func (p *PluginSyncer) buildQuestionContents(ctx context.Context, questions []*entity.Question) (
[]*plugin.VectorSearchContent, error) {
result := make([]*plugin.VectorSearchContent, 0, len(questions))
for _, q := range questions {
meta := plugin.VectorSearchMetadata{
QuestionID: uid.DeShortID(q.ID),
}
var parts []string
parts = append(parts, fmt.Sprintf("Question: %s\n%s", q.Title, q.OriginalText))
// Get answers for this question
answers := make([]*entity.Answer, 0)
err := p.data.DB.Context(ctx).Where("question_id = ?", q.ID).Find(&answers)
if err != nil {
log.Warnf("get answers for question %s failed: %v", q.ID, err)
} else {
for _, a := range answers {
parts = append(parts, fmt.Sprintf("Answer: %s", a.OriginalText))
answerMeta := plugin.VectorSearchMetadataAnswer{
AnswerID: uid.DeShortID(a.ID),
}
// Get comments on this answer
answerComments := make([]*entity.Comment, 0)
err := p.data.DB.Context(ctx).Where("object_id = ?", a.ID).
OrderBy("created_at ASC").Limit(50).Find(&answerComments)
if err != nil {
log.Warnf("get comments for answer %s failed: %v", a.ID, err)
} else {
for _, c := range answerComments {
parts = append(parts, fmt.Sprintf("Comment on answer: %s", c.OriginalText))
answerMeta.Comments = append(answerMeta.Comments, plugin.VectorSearchMetadataComment{
CommentID: uid.DeShortID(c.ID),
})
}
}
meta.Answers = append(meta.Answers, answerMeta)
}
}
// Get comments on the question
questionComments := make([]*entity.Comment, 0)
err = p.data.DB.Context(ctx).Where("object_id = ?", q.ID).
OrderBy("created_at ASC").Limit(50).Find(&questionComments)
if err != nil {
log.Warnf("get comments for question %s failed: %v", q.ID, err)
} else {
for _, c := range questionComments {
parts = append(parts, fmt.Sprintf("Comment: %s", c.OriginalText))
meta.Comments = append(meta.Comments, plugin.VectorSearchMetadataComment{
CommentID: uid.DeShortID(c.ID),
})
}
}
metaJSON, _ := json.Marshal(meta)
result = append(result, &plugin.VectorSearchContent{
ObjectID: uid.DeShortID(q.ID),
ObjectType: "question",
Title: q.Title,
Content: strings.Join(parts, "\n\n"),
Metadata: string(metaJSON),
})
}
return result, nil
}
// buildAnswerContents aggregates each answer with its parent question title and comments.
func (p *PluginSyncer) buildAnswerContents(ctx context.Context, answers []*entity.Answer) (
[]*plugin.VectorSearchContent, error) {
result := make([]*plugin.VectorSearchContent, 0, len(answers))
for _, a := range answers {
// Get parent question for title
question := &entity.Question{}
exist, err := p.data.DB.Context(ctx).Where("id = ?", a.QuestionID).Get(question)
if err != nil {
log.Errorf("get question %s failed: %v", a.QuestionID, err)
continue
}
if !exist {
continue
}
meta := plugin.VectorSearchMetadata{
QuestionID: uid.DeShortID(a.QuestionID),
AnswerID: uid.DeShortID(a.ID),
}
var parts []string
parts = append(parts, fmt.Sprintf("Question: %s", question.Title))
parts = append(parts, fmt.Sprintf("Answer: %s", a.OriginalText))
answerMeta := plugin.VectorSearchMetadataAnswer{
AnswerID: uid.DeShortID(a.ID),
}
// Get comments on this answer
answerComments := make([]*entity.Comment, 0)
err = p.data.DB.Context(ctx).Where("object_id = ?", a.ID).
OrderBy("created_at ASC").Limit(50).Find(&answerComments)
if err != nil {
log.Warnf("get comments for answer %s failed: %v", a.ID, err)
} else {
for _, c := range answerComments {
parts = append(parts, fmt.Sprintf("Comment: %s", c.OriginalText))
answerMeta.Comments = append(answerMeta.Comments, plugin.VectorSearchMetadataComment{
CommentID: uid.DeShortID(c.ID),
})
}
}
meta.Answers = append(meta.Answers, answerMeta)
metaJSON, _ := json.Marshal(meta)
result = append(result, &plugin.VectorSearchContent{
ObjectID: uid.DeShortID(a.ID),
ObjectType: "answer",
Title: question.Title,
Content: strings.Join(parts, "\n\n"),
Metadata: string(metaJSON),
})
}
return result, nil
}
// BuildQuestionContentByID builds vector content for one question using the same
// aggregation semantics as bulk vector sync.
func BuildQuestionContentByID(ctx context.Context, data *data.Data, questionID string) (*plugin.VectorSearchContent, error) {
question := &entity.Question{}
exist, err := data.DB.Context(ctx).Where("id = ?", uid.DeShortID(questionID)).Get(question)
if err != nil {
return nil, err
}
if !exist {
return nil, nil
}
syncer := &PluginSyncer{data: data}
contents, err := syncer.buildQuestionContents(ctx, []*entity.Question{question})
if err != nil {
return nil, err
}
if len(contents) == 0 {
return nil, nil
}
return contents[0], nil
}
// BuildAnswerContentByID builds vector content for one answer using the same
// aggregation semantics as bulk vector sync.
func BuildAnswerContentByID(ctx context.Context, data *data.Data, answerID string) (*plugin.VectorSearchContent, error) {
answer := &entity.Answer{}
exist, err := data.DB.Context(ctx).Where("id = ?", uid.DeShortID(answerID)).Get(answer)
if err != nil {
return nil, err
}
if !exist {
return nil, nil
}
syncer := &PluginSyncer{data: data}
contents, err := syncer.buildAnswerContents(ctx, []*entity.Answer{answer})
if err != nil {
return nil, err
}
if len(contents) == 0 {
return nil, nil
}
return contents[0], nil
}