chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 ¬ificationRepo{
|
||||
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, ¬ificationList, 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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user