Merge branch 'feat/1.1.1/state' into test

This commit is contained in:
LinkinStars 2023-07-03 14:43:46 +08:00
commit dabc5bfeb1
97 changed files with 2600 additions and 2304 deletions

View File

@ -45,7 +45,7 @@ import (
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/action"
activity2 "github.com/answerdev/answer/internal/service/activity"
activity_common2 "github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/internal/service/answer_common"
auth2 "github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/collection_common"
@ -56,6 +56,7 @@ import (
export2 "github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/follow"
meta2 "github.com/answerdev/answer/internal/service/meta"
"github.com/answerdev/answer/internal/service/notice_queue"
notification2 "github.com/answerdev/answer/internal/service/notification"
"github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/object_info"
@ -128,8 +129,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService)
captchaRepo := captcha.NewCaptchaRepo(dataData)
captchaService := action.NewCaptchaService(captchaRepo)
uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService, siteInfoCommonService)
userController := controller.NewUserController(authService, userService, captchaService, emailService, siteInfoCommonService)
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo)
answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
@ -139,10 +139,12 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo)
revisionService := revision_common.NewRevisionService(revisionRepo, userRepo)
tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService)
activityQueueService := activity_queue.NewActivityQueueService()
tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, tagRepo, revisionService, siteInfoCommonService, activityQueueService)
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService)
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo)
notificationQueueService := notice_queue.NewNotificationQueueService()
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, activityQueueService)
rolePowerRelRepo := role.NewRolePowerRelRepo(dataData)
rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService)
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService)
@ -150,11 +152,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reportRepo := report.NewReportRepo(dataData, uniqueIDRepo)
reportService := report2.NewReportService(reportRepo, objService)
reportController := controller.NewReportController(reportService, rankService)
serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configService, activityRepo, userRankRepo, voteRepo)
voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService)
serviceVoteRepo := activity.NewVoteRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
voteService := service.NewVoteService(serviceVoteRepo, configService, questionRepo, answerRepo, commentCommonRepo, objService)
voteController := controller.NewVoteController(voteService, rankService)
followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService)
tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService)
tagController := controller.NewTagController(tagService, tagCommonService, rankService)
followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo)
@ -165,25 +167,23 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
answerCommon := answercommon.NewAnswerCommon(answerRepo)
metaRepo := meta.NewMetaRepo(dataData)
metaService := meta2.NewMetaService(metaRepo)
questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, dataData)
questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, activityQueueService, dataData)
collectionService := service.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon)
collectionController := controller.NewCollectionController(collectionService)
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo)
questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo)
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, dataData, emailService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService)
questionController := controller.NewQuestionController(questionService, answerService, rankService)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, dataData)
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, activityQueueService, siteInfoCommonService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, activityQueueService)
questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService)
answerController := controller.NewAnswerController(answerService, rankService)
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
searchService := service.NewSearchService(searchParser, searchRepo)
searchController := controller.NewSearchController(searchService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService, objService, questionRepo, answerRepo, tagRepo, tagCommonService, notificationQueueService, activityQueueService)
revisionController := controller.NewRevisionController(serviceRevisionService, rankService)
rankController := controller.NewRankController(rankService)
reportHandle := report_handle_admin.NewReportHandle(questionCommon, commentRepo, configService)
reportHandle := report_handle_admin.NewReportHandle(questionCommon, commentRepo, configService, notificationQueueService)
reportAdminService := report_admin.NewReportAdminService(reportRepo, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, objService)
controller_adminReportController := controller_admin.NewReportController(reportAdminService)
userAdminRepo := user.NewUserAdminRepo(dataData, authRepo)
@ -195,36 +195,38 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
themeController := controller_admin.NewThemeController()
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configService, questionCommon)
siteInfoController := controller_admin.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData)
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService)
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService, notificationQueueService)
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon, revisionService)
notificationController := controller.NewNotificationController(notificationService, rankService)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configService, siteInfoCommonService, serviceConf, dataData)
dashboardController := controller.NewDashboardController(dashboardService)
uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
uploadController := controller.NewUploadController(uploaderService)
activityCommon := activity_common2.NewActivityCommon(activityRepo)
activityActivityRepo := activity.NewActivityRepo(dataData, configService)
commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo)
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService, configService)
activityController := controller.NewActivityController(activityCommon, activityService)
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, tagCommonService, objService, commentCommonService, revisionService, metaService, configService)
activityController := controller.NewActivityController(activityService)
roleController := controller_admin.NewRoleController(roleService)
pluginConfigRepo := plugin_config.NewPluginConfigRepo(dataData)
pluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, configService)
pluginController := controller_admin.NewPluginController(pluginCommonService)
permissionController := controller.NewPermissionController(rankService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter(siteinfoController, siteInfoCommonService)
uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService)
authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService)
avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService)
templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, dataData, siteInfoCommonService)
shortIDMiddleware := middleware.NewShortIDMiddleware(siteInfoCommonService)
templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, siteInfoCommonService, questionRepo)
templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService)
templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController)
connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService)
userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService)
userCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService)
pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter, pluginAPIRouter)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, shortIDMiddleware, templateRouter, pluginAPIRouter)
scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService)
application := newApplication(serverConf, ginEngine, scheduledTaskManager)
return application, func() {

View File

@ -49,7 +49,7 @@ const docTemplate = `{
"tags": [
"admin"
],
"summary": "AdminSearchAnswerList",
"summary": "AdminAnswerPage admin answer page",
"parameters": [
{
"type": "integer",
@ -379,7 +379,7 @@ const docTemplate = `{
"tags": [
"admin"
],
"summary": "AdminSearchList",
"summary": "AdminQuestionPage admin question page",
"parameters": [
{
"type": "integer",
@ -8186,7 +8186,7 @@ const docTemplate = `{
"type": "string"
},
"site_seo": {
"$ref": "#/definitions/schema.SiteSeoReq"
"$ref": "#/definitions/schema.SiteSeoResp"
},
"site_users": {
"$ref": "#/definitions/schema.SiteUsersResp"

View File

@ -37,7 +37,7 @@
"tags": [
"admin"
],
"summary": "AdminSearchAnswerList",
"summary": "AdminAnswerPage admin answer page",
"parameters": [
{
"type": "integer",
@ -367,7 +367,7 @@
"tags": [
"admin"
],
"summary": "AdminSearchList",
"summary": "AdminQuestionPage admin question page",
"parameters": [
{
"type": "integer",
@ -8174,7 +8174,7 @@
"type": "string"
},
"site_seo": {
"$ref": "#/definitions/schema.SiteSeoReq"
"$ref": "#/definitions/schema.SiteSeoResp"
},
"site_users": {
"$ref": "#/definitions/schema.SiteUsersResp"

View File

@ -1507,7 +1507,7 @@ definitions:
revision:
type: string
site_seo:
$ref: '#/definitions/schema.SiteSeoReq'
$ref: '#/definitions/schema.SiteSeoResp'
site_users:
$ref: '#/definitions/schema.SiteUsersResp'
theme:
@ -2335,7 +2335,7 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: AdminSearchAnswerList
summary: AdminAnswerPage admin answer page
tags:
- admin
/answer/admin/api/answer/status:
@ -2533,7 +2533,7 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: AdminSearchList
summary: AdminQuestionPage admin question page
tags:
- admin
/answer/admin/api/question/status:

1
go.mod
View File

@ -48,7 +48,6 @@ require (
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.14.2
xorm.io/builder v0.3.12
xorm.io/core v0.7.3
xorm.io/xorm v1.3.2
)

5
go.sum
View File

@ -221,7 +221,6 @@ github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXS
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -493,7 +492,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
@ -1084,7 +1082,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
@ -1329,7 +1326,5 @@ sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/core v0.7.3 h1:W8ws1PlrnkS1CZU1YWaYLMQcQilwAmQXU0BJDJon+H0=
xorm.io/core v0.7.3/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM=
xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=

View File

@ -1417,6 +1417,7 @@ ui:
change: Change
all: All
staff: Staff
more: More
inactive: Inactive
suspended: Suspended
deleted: Deleted

View File

@ -16,4 +16,7 @@ const (
ConfigKEY2ContentCacheKeyPrefix = "answer:config:key:"
ConnectorUserExternalInfoCacheKey = "answer:connector:"
ConnectorUserExternalInfoCacheTime = 10 * time.Minute
SiteMapQuestionCacheKeyPrefix = "answer:sitemap:question:%d"
SiteMapQuestionCacheTime = time.Hour
SitemapMaxSize = 50000
)

View File

@ -2,4 +2,5 @@ package constant
const (
AcceptLanguageFlag = "Accept-Language"
ShortIDFlag = "Short-ID-Enabled"
)

View File

@ -7,3 +7,14 @@ const (
AvatarTypeGravatar = "gravatar"
AvatarTypeCustom = "custom"
)
const (
// PermaLinkQuestionIDAndTitle /questions/10010000000000001/post-title
PermaLinkQuestionIDAndTitle = iota + 1
// PermaLinkQuestionID /questions/10010000000000001
PermaLinkQuestionID
// PermaLinkQuestionIDAndTitleByShortID /questions/11/post-title
PermaLinkQuestionIDAndTitleByShortID
// PermaLinkQuestionIDByShortID /questions/11
PermaLinkQuestionIDByShortID
)

View File

@ -12,13 +12,13 @@ import (
// ScheduledTaskManager scheduled task manager
type ScheduledTaskManager struct {
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoService siteinfo_common.SiteInfoCommonService
questionService *service.QuestionService
}
// NewScheduledTaskManager new scheduled task manager
func NewScheduledTaskManager(
siteInfoService *siteinfo_common.SiteInfoCommonService,
siteInfoService siteinfo_common.SiteInfoCommonService,
questionService *service.QuestionService,
) *ScheduledTaskManager {
manager := &ScheduledTaskManager{

View File

@ -12,9 +12,9 @@ import (
"github.com/segmentfault/pacman/contrib/cache/memory"
"github.com/segmentfault/pacman/log"
_ "modernc.org/sqlite"
"xorm.io/core"
"xorm.io/xorm"
ormlog "xorm.io/xorm/log"
"xorm.io/xorm/names"
"xorm.io/xorm/schemas"
)
@ -71,7 +71,7 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) {
if dataConf.ConnMaxLifeTime > 0 {
engine.SetConnMaxLifetime(time.Duration(dataConf.ConnMaxLifeTime) * time.Second)
}
engine.SetColumnMapper(core.GonicMapper{})
engine.SetColumnMapper(names.GonicMapper{})
return engine, nil
}

View File

@ -0,0 +1,16 @@
package handler
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
)
// GetEnableShortID get language from header
func GetEnableShortID(ctx context.Context) bool {
flag, ok := ctx.Value(constant.ShortIDFlag).(bool)
if ok {
return flag
}
return false
}

View File

@ -6,13 +6,13 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/role"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/gin-gonic/gin"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/pkg/converter"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
@ -21,13 +21,13 @@ var ctxUUIDKey = "ctxUuidKey"
// AuthUserMiddleware auth user middleware
type AuthUserMiddleware struct {
authService *auth.AuthService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
}
// NewAuthUserMiddleware new auth user middleware
func NewAuthUserMiddleware(
authService *auth.AuthService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware {
siteInfoCommonService siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware {
return &AuthUserMiddleware{
authService: authService,
siteInfoCommonService: siteInfoCommonService,

View File

@ -32,31 +32,28 @@ func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig,
func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
return func(ctx *gin.Context) {
u := ctx.Request.RequestURI
if strings.Contains(u, "/uploads/avatar/") {
sizeStr := ctx.Query("s")
size := converter.StringToInt(sizeStr)
uUrl, err := url.Parse(u)
uri := ctx.Request.RequestURI
if strings.Contains(uri, "/uploads/avatar/") {
size := converter.StringToInt(ctx.Query("s"))
uriWithoutQuery, _ := url.Parse(uri)
filename := filepath.Base(uriWithoutQuery.Path)
filePath := fmt.Sprintf("%s/avatar/%s", am.serviceConfig.UploadPath, filename)
var err error
if size != 0 {
filePath, err = am.uploaderService.AvatarThumbFile(ctx, filename, size)
if err != nil {
log.Error(err)
ctx.Abort()
}
}
avatarFile, err := os.ReadFile(filePath)
if err != nil {
ctx.Next()
log.Error(err)
ctx.Abort()
return
}
_, urlfileName := filepath.Split(uUrl.Path)
uploadPath := am.serviceConfig.UploadPath
filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, urlfileName)
var avatarfile []byte
if size == 0 {
avatarfile, err = os.ReadFile(filePath)
} else {
avatarfile, err = am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, size)
}
if err != nil {
ctx.Next()
return
}
ext := strings.ToLower(path.Ext(filePath)[1:])
ctx.Header("content-type", fmt.Sprintf("image/%s", ext))
_, err = ctx.Writer.WriteString(string(avatarfile))
ctx.Header("content-type", fmt.Sprintf("image/%s", strings.TrimLeft(path.Ext(filePath), ".")))
_, err = ctx.Writer.Write(avatarFile)
if err != nil {
log.Error(err)
}
@ -64,7 +61,7 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
return
} else {
uUrl, err := url.Parse(u)
uUrl, err := url.Parse(uri)
if err != nil {
ctx.Next()
return

View File

@ -8,4 +8,5 @@ import (
var ProviderSetMiddleware = wire.NewSet(
NewAuthUserMiddleware,
NewAvatarMiddleware,
NewShortIDMiddleware,
)

View File

@ -0,0 +1,29 @@
package middleware
import (
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
type ShortIDMiddleware struct {
siteInfoService siteinfo_common.SiteInfoCommonService
}
func NewShortIDMiddleware(siteInfoService siteinfo_common.SiteInfoCommonService) *ShortIDMiddleware {
return &ShortIDMiddleware{
siteInfoService: siteInfoService,
}
}
func (sm *ShortIDMiddleware) SetShortIDFlag() gin.HandlerFunc {
return func(ctx *gin.Context) {
siteSeo, err := sm.siteInfoService.GetSiteSeo(ctx)
if err != nil {
log.Error(err)
return
}
ctx.Set(constant.ShortIDFlag, siteSeo.IsShortLink())
}
}

View File

@ -20,6 +20,7 @@ func NewHTTPServer(debug bool,
viewRouter *router.UIRouter,
authUserMiddleware *middleware.AuthUserMiddleware,
avatarMiddleware *middleware.AvatarMiddleware,
shortIDMiddleware *middleware.ShortIDMiddleware,
templateRouter *router.TemplateRouter,
pluginAPIRouter *router.PluginAPIRouter,
) *gin.Engine {
@ -30,7 +31,7 @@ func NewHTTPServer(debug bool,
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(brotli.Brotli(brotli.DefaultCompression), middleware.ExtractAndSetAcceptLanguage)
r.Use(brotli.Brotli(brotli.DefaultCompression), middleware.ExtractAndSetAcceptLanguage, shortIDMiddleware.SetShortIDFlag())
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
html, _ := fs.Sub(ui.Template, "template")

View File

@ -5,22 +5,19 @@ import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/role"
"github.com/answerdev/answer/pkg/uid"
"github.com/gin-gonic/gin"
)
type ActivityController struct {
activityCommonService *activity_common.ActivityCommon
activityService *activity.ActivityService
activityService *activity.ActivityService
}
// NewActivityController new activity controller.
func NewActivityController(
activityCommonService *activity_common.ActivityCommon,
activityService *activity.ActivityService) *ActivityController {
return &ActivityController{activityCommonService: activityCommonService, activityService: activityService}
return &ActivityController{activityService: activityService}
}
// GetObjectTimeline get object timeline

View File

@ -8,7 +8,6 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/dashboard"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/uid"
@ -18,20 +17,17 @@ import (
// AnswerController answer controller
type AnswerController struct {
answerService *service.AnswerService
rankService *rank.RankService
dashboardService *dashboard.DashboardService
answerService *service.AnswerService
rankService *rank.RankService
}
// NewAnswerController new controller
func NewAnswerController(answerService *service.AnswerService,
rankService *rank.RankService,
dashboardService *dashboard.DashboardService,
) *AnswerController {
return &AnswerController{
answerService: answerService,
rankService: rankService,
dashboardService: dashboardService,
answerService: answerService,
rankService: rankService,
}
}

View File

@ -23,14 +23,14 @@ const (
// ConnectorController comment controller
type ConnectorController struct {
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoService siteinfo_common.SiteInfoCommonService
userExternalService *user_external_login.UserExternalLoginService
emailService *export.EmailService
}
// NewConnectorController new controller
func NewConnectorController(
siteInfoService *siteinfo_common.SiteInfoCommonService,
siteInfoService siteinfo_common.SiteInfoCommonService,
emailService *export.EmailService,
userExternalService *user_external_login.UserExternalLoginService,
) *ConnectorController {

View File

@ -19,7 +19,7 @@ var ProviderSetController = wire.NewSet(
NewRankController,
NewReasonController,
NewNotificationController,
NewSiteinfoController,
NewSiteInfoController,
NewDashboardController,
NewUploadController,
NewActivityController,

View File

@ -7,12 +7,12 @@ import (
)
type DashboardController struct {
dashboardService *dashboard.DashboardService
dashboardService dashboard.DashboardService
}
// NewDashboardController new controller
func NewDashboardController(
dashboardService *dashboard.DashboardService,
dashboardService dashboard.DashboardService,
) *DashboardController {
return &DashboardController{
dashboardService: dashboardService,
@ -29,7 +29,7 @@ func NewDashboardController(
// @Router /answer/admin/api/dashboard [get]
// @Success 200 {object} handler.RespBody
func (ac *DashboardController) DashboardInfo(ctx *gin.Context) {
info, err := ac.dashboardService.StatisticalByCache(ctx)
info, err := ac.dashboardService.Statistical(ctx)
handler.HandleResponse(ctx, err, gin.H{
"info": info,
})

View File

@ -12,11 +12,11 @@ import (
type LangController struct {
translator i18n.Translator
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoService siteinfo_common.SiteInfoCommonService
}
// NewLangController new language controller.
func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController {
func NewLangController(tr i18n.Translator, siteInfoService siteinfo_common.SiteInfoCommonService) *LangController {
return &LangController{translator: tr, siteInfoService: siteInfoService}
}

View File

@ -22,13 +22,13 @@ const (
// UserCenterController comment controller
type UserCenterController struct {
userCenterLoginService *user_external_login.UserCenterLoginService
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoService siteinfo_common.SiteInfoCommonService
}
// NewUserCenterController new controller
func NewUserCenterController(
userCenterLoginService *user_external_login.UserCenterLoginService,
siteInfoService *siteinfo_common.SiteInfoCommonService,
siteInfoService siteinfo_common.SiteInfoCommonService,
) *UserCenterController {
return &UserCenterController{
userCenterLoginService: userCenterLoginService,

View File

@ -7,11 +7,11 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/pkg/uid"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
@ -23,6 +23,7 @@ type QuestionController struct {
questionService *service.QuestionService
answerService *service.AnswerService
rankService *rank.RankService
siteInfoService siteinfo_common.SiteInfoCommonService
}
// NewQuestionController new controller
@ -30,11 +31,13 @@ func NewQuestionController(
questionService *service.QuestionService,
answerService *service.AnswerService,
rankService *rank.RankService,
siteInfoService siteinfo_common.SiteInfoCommonService,
) *QuestionController {
return &QuestionController{
questionService: questionService,
answerService: answerService,
rankService: rankService,
siteInfoService: siteInfoService,
}
}
@ -220,7 +223,9 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
return
}
info.ID = uid.EnShortID(info.ID)
if handler.GetEnableShortID(ctx) {
info.ID = uid.EnShortID(info.ID)
}
handler.HandleResponse(ctx, nil, info)
}
@ -703,8 +708,8 @@ func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// AdminSearchList godoc
// @Summary AdminSearchList
// AdminQuestionPage admin question page
// @Summary AdminQuestionPage admin question page
// @Description Status:[available,closed,deleted]
// @Tags admin
// @Accept json
@ -716,21 +721,19 @@ func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) {
// @Param query query string false "question id or title"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/question/page [get]
func (qc *QuestionController) AdminSearchList(ctx *gin.Context) {
req := &schema.AdminQuestionSearch{}
func (qc *QuestionController) AdminQuestionPage(ctx *gin.Context) {
req := &schema.AdminQuestionPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.AdminSearchList(ctx, req, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := qc.questionService.AdminQuestionPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// AdminSearchAnswerList godoc
// @Summary AdminSearchAnswerList
// AdminAnswerPage admin answer page
// @Summary AdminAnswerPage admin answer page
// @Description Status:[available,deleted]
// @Tags admin
// @Accept json
@ -743,21 +746,15 @@ func (qc *QuestionController) AdminSearchList(ctx *gin.Context) {
// @Param question_id query string false "question id"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/answer/page [get]
func (qc *QuestionController) AdminSearchAnswerList(ctx *gin.Context) {
req := &entity.AdminAnswerSearch{}
func (qc *QuestionController) AdminAnswerPage(ctx *gin.Context) {
req := &schema.AdminAnswerPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.QuestionID = uid.DeShortID(req.QuestionID)
if req.QuestionID == "0" {
req.QuestionID = ""
}
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.AdminSearchAnswerList(ctx, req, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := qc.questionService.AdminAnswerPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// AdminSetQuestionStatus godoc

View File

@ -11,13 +11,13 @@ import (
"github.com/segmentfault/pacman/log"
)
type SiteinfoController struct {
siteInfoService *siteinfo_common.SiteInfoCommonService
type SiteInfoController struct {
siteInfoService siteinfo_common.SiteInfoCommonService
}
// NewSiteinfoController new siteinfo controller.
func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController {
return &SiteinfoController{
// NewSiteInfoController new site info controller.
func NewSiteInfoController(siteInfoService siteinfo_common.SiteInfoCommonService) *SiteInfoController {
return &SiteInfoController{
siteInfoService: siteInfoService,
}
}
@ -29,7 +29,7 @@ func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonServic
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteInfoResp}
// @Router /answer/api/v1/siteinfo [get]
func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
func (sc *SiteInfoController) GetSiteInfo(ctx *gin.Context) {
var err error
resp := &schema.SiteInfoResp{Version: constant.Version, Revision: constant.Revision}
resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)
@ -80,7 +80,7 @@ func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.GetSiteLegalInfoResp}
// @Router /answer/api/v1/siteinfo/legal [get]
func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) {
func (sc *SiteInfoController) GetSiteLegalInfo(ctx *gin.Context) {
req := &schema.GetSiteLegalInfoReq{}
if handler.BindAndCheck(ctx, req) {
return
@ -102,7 +102,7 @@ func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) {
}
// GetManifestJson get manifest.json
func (sc *SiteinfoController) GetManifestJson(ctx *gin.Context) {
func (sc *SiteInfoController) GetManifestJson(ctx *gin.Context) {
favicon := "favicon.ico"
resp := &schema.GetManifestJsonResp{
ManifestVersion: 3,

View File

@ -30,13 +30,13 @@ type TemplateController struct {
scriptPath string
cssPath string
templateRenderController *templaterender.TemplateRenderController
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoService siteinfo_common.SiteInfoCommonService
}
// NewTemplateController new controller
func NewTemplateController(
templateRenderController *templaterender.TemplateRenderController,
siteInfoService *siteinfo_common.SiteInfoCommonService,
siteInfoService siteinfo_common.SiteInfoCommonService,
) *TemplateController {
script, css := GetStyle()
return &TemplateController{
@ -116,7 +116,7 @@ func (tc *TemplateController) Index(ctx *gin.Context) {
siteInfo.Canonical = siteInfo.General.SiteUrl
UrlUseTitle := false
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionIDAndTitle {
UrlUseTitle = true
}
siteInfo.Title = ""
@ -149,7 +149,7 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) {
}
UrlUseTitle := false
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionIDAndTitle {
UrlUseTitle = true
}
siteInfo.Title = fmt.Sprintf("Questions - %s", siteInfo.General.Name)
@ -164,16 +164,21 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s
id := ctx.Param("id")
title := ctx.Param("title")
titleIsAnswerID := false
NeedChangeShortID := false
needChangeShortID := false
siteSeo, err := tc.siteInfoService.GetSiteSeo(ctx)
if err != nil {
return false, ""
}
isShortID := uid.IsShortID(id)
if uid.ShortIDSwitch {
if siteSeo.IsShortLink() {
if !isShortID {
id = uid.EnShortID(id)
NeedChangeShortID = true
needChangeShortID = true
}
} else {
if isShortID {
NeedChangeShortID = true
needChangeShortID = true
id = uid.DeShortID(id)
}
}
@ -186,11 +191,11 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s
}
siteInfo = tc.SiteInfo(ctx)
url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionID || siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDByShortID {
if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionID || siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionIDByShortID {
if len(ctx.Request.URL.Query()) > 0 {
url = fmt.Sprintf("%s?%s", url, ctx.Request.URL.RawQuery)
}
if NeedChangeShortID {
if needChangeShortID {
return true, url
}
//not have title
@ -216,7 +221,7 @@ func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *s
}
//have title
if len(title) > 0 && !titleIsAnswerID && correctTitle {
if NeedChangeShortID {
if needChangeShortID {
return true, url
}
return false, ""
@ -277,9 +282,11 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
}
// comments
objectIDs := []string{id}
objectIDs := []string{uid.DeShortID(id)}
for _, answer := range answers {
objectIDs = append(objectIDs, answer.ID)
answerID := uid.DeShortID(answer.ID)
objectIDs = append(objectIDs, answerID)
}
comments, err := tc.templateRenderController.CommentList(ctx, objectIDs)
if err != nil {
@ -287,7 +294,7 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
return
}
siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle)
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionID {
if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionID {
siteInfo.Canonical = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
}
jsonLD := &schema.QAPageJsonLD{}
@ -402,7 +409,7 @@ func (tc *TemplateController) TagInfo(ctx *gin.Context) {
siteInfo.Keywords = taginifo.DisplayName
UrlUseTitle := false
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
if siteInfo.SiteSeo.PermaLink == constant.PermaLinkQuestionIDAndTitle {
UrlUseTitle = true
}
siteInfo.Title = fmt.Sprintf("'%s' Questions - %s", taginifo.DisplayName, siteInfo.General.Name)

View File

@ -1,16 +1,16 @@
package templaterender
import (
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"math"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/service/comment"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/google/wire"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/tag"
"github.com/google/wire"
)
// ProviderSetTemplateRenderController is template render controller providers.
@ -24,8 +24,8 @@ type TemplateRenderController struct {
tagService *tag.TagService
answerService *service.AnswerService
commentService *comment.CommentService
data *data.Data
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoService siteinfo_common.SiteInfoCommonService
questionRepo questioncommon.QuestionRepo
}
func NewTemplateRenderController(
@ -34,9 +34,8 @@ func NewTemplateRenderController(
tagService *tag.TagService,
answerService *service.AnswerService,
commentService *comment.CommentService,
data *data.Data,
siteInfoService *siteinfo_common.SiteInfoCommonService,
siteInfoService siteinfo_common.SiteInfoCommonService,
questionRepo questioncommon.QuestionRepo,
) *TemplateRenderController {
return &TemplateRenderController{
questionService: questionService,
@ -44,7 +43,7 @@ func NewTemplateRenderController(
tagService: tagService,
answerService: answerService,
commentService: commentService,
data: data,
questionRepo: questionRepo,
siteInfoService: siteInfoService,
}
}

View File

@ -1 +0,0 @@
package templaterender

View File

@ -1,11 +1,11 @@
package templaterender
import (
"encoding/json"
"fmt"
"html/template"
"math"
"net/http"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
@ -31,48 +31,46 @@ func (t *TemplateRenderController) Sitemap(ctx *gin.Context) {
return
}
sitemapInfo := &schema.SiteMapList{}
infoStr, err := t.data.Cache.GetString(ctx, schema.SitemapCachekey)
questions, err := t.questionRepo.SitemapQuestions(ctx, 1, constant.SitemapMaxSize)
if err != nil {
log.Errorf("get Cache failed: %s", err)
return
}
hasTitle := false
if siteInfo.PermaLink == schema.PermaLinkQuestionIDAndTitle || siteInfo.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID {
hasTitle = true
}
if err = json.Unmarshal([]byte(infoStr), sitemapInfo); err != nil {
log.Errorf("get sitemap info failed: %s", err)
log.Errorf("get sitemap questions failed: %s", err)
return
}
if len(sitemapInfo.QuestionIDs) > 0 {
//question url list
ctx.Header("Content-Type", "application/xml")
ctx.Header("Content-Type", "application/xml")
if len(questions) < constant.SitemapMaxSize {
ctx.HTML(
http.StatusOK, "sitemap.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"list": sitemapInfo.QuestionIDs,
"general": general,
"hastitle": hasTitle,
},
)
} else {
//question list page
ctx.Header("Content-Type", "application/xml")
ctx.HTML(
http.StatusOK, "sitemap-list.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"page": sitemapInfo.MaxPageNum,
"list": questions,
"general": general,
"hastitle": siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitle ||
siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitleByShortID,
},
)
return
}
questionNum, err := t.questionRepo.GetQuestionCount(ctx)
if err != nil {
log.Error("GetQuestionCount error", err)
return
}
var pageList []int
totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize)))
for i := 1; i <= totalPages; i++ {
pageList = append(pageList, i)
}
ctx.HTML(
http.StatusOK, "sitemap-list.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"page": pageList,
"general": general,
},
)
}
func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error {
sitemapInfo := &schema.SiteMapPageList{}
general, err := t.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Error("get site general failed:", err)
@ -83,28 +81,20 @@ func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error
log.Error("get site GetSiteSeo failed:", err)
return err
}
hasTitle := false
if siteInfo.PermaLink == schema.PermaLinkQuestionIDAndTitle || siteInfo.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID {
hasTitle = true
}
cachekey := fmt.Sprintf(schema.SitemapPageCachekey, page)
infoStr, err := t.data.Cache.GetString(ctx, cachekey)
questions, err := t.questionRepo.SitemapQuestions(ctx, page, constant.SitemapMaxSize)
if err != nil {
//If there is no cache, return directly.
return nil
}
if err = json.Unmarshal([]byte(infoStr), sitemapInfo); err != nil {
log.Errorf("get sitemap info failed: %s", err)
log.Errorf("get sitemap questions failed: %s", err)
return err
}
ctx.Header("Content-Type", "application/xml")
ctx.HTML(
http.StatusOK, "sitemap.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"list": sitemapInfo.PageData,
"list": questions,
"general": general,
"hastitle": hasTitle,
"hastitle": siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitle ||
siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitleByShortID,
},
)
return nil

View File

@ -12,7 +12,6 @@ import (
"github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/pkg/checker"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
@ -24,9 +23,8 @@ type UserController struct {
userService *service.UserService
authService *auth.AuthService
actionService *action.CaptchaService
uploaderService uploader.UploaderService
emailService *export.EmailService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
}
// NewUserController new controller
@ -35,14 +33,12 @@ func NewUserController(
userService *service.UserService,
actionService *action.CaptchaService,
emailService *export.EmailService,
uploaderService uploader.UploaderService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
) *UserController {
return &UserController{
authService: authService,
userService: userService,
actionService: actionService,
uploaderService: uploaderService,
emailService: emailService,
siteInfoCommonService: siteInfoCommonService,
}

View File

@ -10,7 +10,6 @@ import (
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/uid"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
)
@ -54,9 +53,7 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) {
return
}
dto := &schema.VoteDTO{}
_ = copier.Copy(dto, req)
resp, err := vc.VoteService.VoteUp(ctx, dto)
resp, err := vc.VoteService.VoteUp(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeToast)
} else {
@ -93,9 +90,7 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) {
return
}
dto := &schema.VoteDTO{}
_ = copier.Copy(dto, req)
resp, err := vc.VoteService.VoteDown(ctx, dto)
resp, err := vc.VoteService.VoteDown(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeToast)
} else {
@ -103,9 +98,9 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) {
}
}
// UserVotes godoc
// @Summary user's votes
// @Description user's vote
// UserVotes user votes
// @Summary get user personal votes
// @Description get user personal votes
// @Tags Activity
// @Accept json
// @Produce json
@ -116,21 +111,12 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) {
// @Router /answer/api/v1/personal/vote/page [get]
func (vc *VoteController) UserVotes(ctx *gin.Context) {
req := schema.GetVoteWithPageReq{}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if handler.BindAndCheck(ctx, &req) {
return
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 30
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := vc.VoteService.ListUserVotes(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeModal)
} else {
handler.HandleResponse(ctx, err, resp)
}
handler.HandleResponse(ctx, err, resp)
}

View File

@ -42,15 +42,6 @@ type AnswerSearch struct {
PageSize int `json:"page_size" form:"page_size"` // Search page size
}
type AdminAnswerSearch struct {
Page int `json:"page" form:"page"` // Query number of pages
PageSize int `json:"page_size" form:"page_size"` // Search page size
Status int `json:"-" form:"-"`
StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 Deleted
Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string
QuestionID string `validate:"omitempty,gt=0,lte=24" json:"question_id" form:"question_id" ` //Query string
}
// TableName answer table name
func (Answer) TableName() string {
return "answer"

View File

@ -16,6 +16,7 @@ import (
"github.com/answerdev/answer/internal/migrations"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
@ -184,17 +185,17 @@ func InitBaseInfo(ctx *gin.Context) {
return
}
if err := migrations.InitDB(c.Data.Database); err != nil {
log.Error("init database error: ", err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), schema.ErrTypeAlert)
return
engine, err := data.NewDB(false, c.Data.Database)
if err != nil {
log.Errorf("init database failed %s", err)
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), nil)
}
err = migrations.UpdateInstallInfo(c.Data.Database, req.Language, req.SiteName, req.SiteURL, req.ContactEmail,
req.AdminName, req.AdminPassword, req.AdminEmail)
if err != nil {
log.Error(err)
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), nil)
inputData := &migrations.InitNeedUserInputData{}
_ = copier.Copy(inputData, req)
if err := migrations.NewMentor(ctx, engine, inputData).InitDB(); err != nil {
log.Error("init database error: ", err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), schema.ErrTypeAlert)
return
}

View File

@ -1,188 +1,183 @@
package migrations
import (
"context"
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/permission"
"golang.org/x/crypto/bcrypt"
"xorm.io/xorm"
)
const (
defaultSEORobotTxt = `User-agent: *
Disallow: /admin
Disallow: /search
Disallow: /install
Disallow: /review
Disallow: /users/login
Disallow: /users/register
Disallow: /users/account-recovery
Disallow: /users/oauth/*
Disallow: /users/*/*
Disallow: /answer/api
Disallow: /*?code*
Sitemap: `
)
var tables = []interface{}{
&entity.Activity{},
&entity.Answer{},
&entity.Collection{},
&entity.CollectionGroup{},
&entity.Comment{},
&entity.Config{},
&entity.Meta{},
&entity.Notification{},
&entity.Question{},
&entity.Report{},
&entity.Revision{},
&entity.SiteInfo{},
&entity.Tag{},
&entity.TagRel{},
&entity.Uniqid{},
&entity.User{},
&entity.Version{},
&entity.Role{},
&entity.RolePowerRel{},
&entity.Power{},
&entity.UserRoleRel{},
&entity.PluginConfig{},
&entity.UserExternalLogin{},
type Mentor struct {
ctx context.Context
engine *xorm.Engine
userData *InitNeedUserInputData
err error
Done bool
}
// InitDB init db
func InitDB(dataConf *data.Database) (err error) {
engine, err := data.NewDB(false, dataConf)
if err != nil {
fmt.Println("new database failed: ", err.Error())
return err
}
func NewMentor(ctx context.Context, engine *xorm.Engine, data *InitNeedUserInputData) *Mentor {
return &Mentor{ctx: ctx, engine: engine, userData: data}
}
exist, err := engine.IsTableExist(&entity.Version{})
if err != nil {
return fmt.Errorf("check table exists failed: %s", err)
type InitNeedUserInputData struct {
Language string
SiteName string
SiteURL string
ContactEmail string
AdminName string
AdminPassword string
AdminEmail string
}
func (m *Mentor) InitDB() error {
m.do("check table exist", m.checkTableExist)
m.do("sync table", m.syncTable)
m.do("init version table", m.initVersionTable)
m.do("init admin user", m.initAdminUser)
m.do("init config", m.initConfig)
m.do("init role", m.initRole)
m.do("init power", m.initPower)
m.do("init role power rel", m.initRolePowerRel)
m.do("init admin user role rel", m.initAdminUserRoleRel)
m.do("init site info interface", m.initSiteInfoInterface)
m.do("init site info general config", m.initSiteInfoGeneralData)
m.do("init site info login config", m.initSiteInfoLoginConfig)
m.do("init site info theme config", m.initSiteInfoThemeConfig)
m.do("init site info seo config", m.initSiteInfoSEOConfig)
m.do("init site info user config", m.initSiteInfoUsersConfig)
return m.err
}
func (m *Mentor) do(taskName string, fn func()) {
if m.err != nil || m.Done {
return
}
if exist {
fn()
if m.err != nil {
m.err = fmt.Errorf("%s failed: %s", taskName, m.err)
}
}
func (m *Mentor) checkTableExist() {
m.Done, m.err = m.engine.Context(m.ctx).IsTableExist(&entity.Version{})
if m.Done {
fmt.Println("[database] already exists")
return nil
}
err = engine.Sync(tables...)
if err != nil {
return fmt.Errorf("sync table failed: %s", err)
}
_, err = engine.InsertOne(&entity.Version{ID: 1, VersionNumber: ExpectedVersion()})
if err != nil {
return fmt.Errorf("init version table failed: %s", err)
}
err = initAdminUser(engine)
if err != nil {
return fmt.Errorf("init admin user failed: %s", err)
}
err = initConfigTable(engine)
if err != nil {
return fmt.Errorf("init config table: %s", err)
}
err = initRolePower(engine)
if err != nil {
return fmt.Errorf("init role and power failed: %s", err)
}
return nil
}
func initAdminUser(engine *xorm.Engine) error {
_, err := engine.InsertOne(&entity.User{
func (m *Mentor) syncTable() {
m.err = m.engine.Context(m.ctx).Sync(tables...)
}
func (m *Mentor) initVersionTable() {
_, m.err = m.engine.Context(m.ctx).Insert(&entity.Version{ID: 1, VersionNumber: ExpectedVersion()})
}
func (m *Mentor) initAdminUser() {
generateFromPassword, _ := bcrypt.GenerateFromPassword([]byte(m.userData.AdminPassword), bcrypt.DefaultCost)
_, m.err = m.engine.Context(m.ctx).Insert(&entity.User{
ID: "1",
Username: "admin",
Pass: "$2a$10$.gnUnpW.8ssRNaEvx.XwvOR2NuPsGzFLWWX2rqSIVAdIvLNZZYs5y", // admin
EMail: "admin@admin.com",
Username: m.userData.AdminName,
Pass: string(generateFromPassword),
EMail: m.userData.AdminEmail,
MailStatus: 1,
NoticeStatus: 1,
Status: 1,
Rank: 1,
DisplayName: "admin",
DisplayName: m.userData.AdminName,
})
return err
}
func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail string) error {
func (m *Mentor) initConfig() {
_, m.err = m.engine.Context(m.ctx).Insert(defaultConfigTable)
}
func (m *Mentor) initRole() {
_, m.err = m.engine.Context(m.ctx).Insert(roles)
}
func (m *Mentor) initPower() {
_, m.err = m.engine.Context(m.ctx).Insert(powers)
}
func (m *Mentor) initRolePowerRel() {
_, m.err = m.engine.Context(m.ctx).Insert(rolePowerRels)
}
func (m *Mentor) initAdminUserRoleRel() {
_, m.err = m.engine.Context(m.ctx).Insert(adminUserRoleRel)
}
func (m *Mentor) initSiteInfoInterface() {
interfaceData := map[string]string{
"language": language,
"language": m.userData.Language,
"time_zone": "UTC",
}
interfaceDataBytes, _ := json.Marshal(interfaceData)
_, err := engine.InsertOne(&entity.SiteInfo{
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "interface",
Content: string(interfaceDataBytes),
Status: 1,
})
if err != nil {
return err
}
}
func (m *Mentor) initSiteInfoGeneralData() {
generalData := map[string]string{
"name": siteName,
"site_url": siteURL,
"contact_email": contactEmail,
"name": m.userData.SiteName,
"site_url": m.userData.SiteURL,
"contact_email": m.userData.ContactEmail,
}
generalDataBytes, _ := json.Marshal(generalData)
_, err = engine.InsertOne(&entity.SiteInfo{
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "general",
Content: string(generalDataBytes),
Status: 1,
})
if err != nil {
return err
}
}
func (m *Mentor) initSiteInfoLoginConfig() {
loginConfig := map[string]bool{
"allow_new_registrations": true,
"allow_email_registrations": true,
"login_required": false,
}
loginConfigDataBytes, _ := json.Marshal(loginConfig)
_, err = engine.InsertOne(&entity.SiteInfo{
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "login",
Content: string(loginConfigDataBytes),
Status: 1,
})
if err != nil {
return err
}
}
func (m *Mentor) initSiteInfoThemeConfig() {
themeConfig := `{"theme":"default","theme_config":{"default":{"navbar_style":"colored","primary_color":"#0033ff"}}}`
_, err = engine.InsertOne(&entity.SiteInfo{
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "theme",
Content: themeConfig,
Status: 1,
})
if err != nil {
return err
}
}
seoData := map[string]string{
"robots": defaultSEORobotTxt + siteURL + "/sitemap.xml",
func (m *Mentor) initSiteInfoSEOConfig() {
seoData := map[string]interface{}{
"permalink": 1,
"robots": defaultSEORobotTxt + m.userData.SiteURL + "/sitemap.xml",
}
seoDataBytes, _ := json.Marshal(seoData)
_, err = engine.InsertOne(&entity.SiteInfo{
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "seo",
Content: string(seoDataBytes),
Status: 1,
})
if err != nil {
return err
}
}
func (m *Mentor) initSiteInfoUsersConfig() {
usersData := map[string]any{
"default_avatar": "gravatar",
"default_gravatar_base_url": "https://www.gravatar.com/avatar/",
"gravatar_base_url": "https://www.gravatar.com/avatar/",
"allow_update_display_name": true,
"allow_update_username": true,
"allow_update_avatar": true,
@ -191,344 +186,9 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
"allow_update_location": true,
}
usersDataBytes, _ := json.Marshal(usersData)
_, err = engine.InsertOne(&entity.SiteInfo{
_, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "users",
Content: string(usersDataBytes),
Status: 1,
})
if err != nil {
return err
}
return err
}
func updateAdminInfo(engine *xorm.Engine, adminName, adminPassword, adminEmail string) error {
generateFromPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
adminPassword = string(generateFromPassword)
// update admin info
_, err = engine.ID("1").Update(&entity.User{
Username: adminName,
Pass: adminPassword,
EMail: adminEmail,
DisplayName: adminName,
})
if err != nil {
return fmt.Errorf("update admin user info failed: %s", err)
}
return nil
}
// UpdateInstallInfo update some init data about the admin interface and admin password
func UpdateInstallInfo(dataConf *data.Database, language string,
siteName string,
siteURL string,
contactEmail string,
adminName string,
adminPassword string,
adminEmail string) error {
engine, err := data.NewDB(false, dataConf)
if err != nil {
return fmt.Errorf("database connection error: %s", err)
}
err = updateAdminInfo(engine, adminName, adminPassword, adminEmail)
if err != nil {
return fmt.Errorf("update admin info failed: %s", err)
}
err = initSiteInfo(engine, language, siteName, siteURL, contactEmail)
if err != nil {
return fmt.Errorf("init site info failed: %s", err)
}
return err
}
func initConfigTable(engine *xorm.Engine) error {
defaultConfigTable := []*entity.Config{
{ID: 1, Key: "answer.accepted", Value: `15`},
{ID: 2, Key: "answer.voted_up", Value: `10`},
{ID: 3, Key: "question.voted_up", Value: `10`},
{ID: 4, Key: "tag.edit_accepted", Value: `2`},
{ID: 5, Key: "answer.accept", Value: `2`},
{ID: 6, Key: "answer.voted_down_cancel", Value: `2`},
{ID: 7, Key: "question.voted_down_cancel", Value: `2`},
{ID: 8, Key: "answer.vote_down_cancel", Value: `1`},
{ID: 9, Key: "question.vote_down_cancel", Value: `1`},
{ID: 10, Key: "user.activated", Value: `1`},
{ID: 11, Key: "edit.accepted", Value: `2`},
{ID: 12, Key: "answer.vote_down", Value: `-1`},
{ID: 13, Key: "question.voted_down", Value: `-2`},
{ID: 14, Key: "answer.voted_down", Value: `-2`},
{ID: 15, Key: "answer.accept_cancel", Value: `-2`},
{ID: 16, Key: "answer.deleted", Value: `-5`},
{ID: 17, Key: "question.voted_up_cancel", Value: `-10`},
{ID: 18, Key: "answer.voted_up_cancel", Value: `-10`},
{ID: 19, Key: "answer.accepted_cancel", Value: `-15`},
{ID: 20, Key: "object.reported", Value: `-100`},
{ID: 21, Key: "edit.rejected", Value: `-2`},
{ID: 22, Key: "daily_rank_limit", Value: `200`},
{ID: 23, Key: "daily_rank_limit.exclude", Value: `["answer.accepted"]`},
{ID: 24, Key: "user.follow", Value: `0`},
{ID: 25, Key: "comment.vote_up", Value: `0`},
{ID: 26, Key: "comment.vote_up_cancel", Value: `0`},
{ID: 27, Key: "question.vote_down", Value: `0`},
{ID: 28, Key: "question.vote_up", Value: `0`},
{ID: 29, Key: "question.vote_up_cancel", Value: `0`},
{ID: 30, Key: "answer.vote_up", Value: `0`},
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
{ID: 32, Key: "question.follow", Value: `0`},
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"}`},
{ID: 35, Key: "tag.follow", Value: `0`},
{ID: 36, Key: "rank.question.add", Value: `1`},
{ID: 37, Key: "rank.question.edit", Value: `200`},
{ID: 38, Key: "rank.question.delete", Value: `-1`},
{ID: 39, Key: "rank.question.vote_up", Value: `15`},
{ID: 40, Key: "rank.question.vote_down", Value: `125`},
{ID: 41, Key: "rank.answer.add", Value: `1`},
{ID: 42, Key: "rank.answer.edit", Value: `200`},
{ID: 43, Key: "rank.answer.delete", Value: `-1`},
{ID: 44, Key: "rank.answer.accept", Value: `-1`},
{ID: 45, Key: "rank.answer.vote_up", Value: `15`},
{ID: 46, Key: "rank.answer.vote_down", Value: `125`},
{ID: 47, Key: "rank.comment.add", Value: `1`},
{ID: 48, Key: "rank.comment.edit", Value: `-1`},
{ID: 49, Key: "rank.comment.delete", Value: `-1`},
{ID: 50, Key: "rank.report.add", Value: `1`},
{ID: 51, Key: "rank.tag.add", Value: `1`},
{ID: 52, Key: "rank.tag.edit", Value: `100`},
{ID: 53, Key: "rank.tag.delete", Value: `-1`},
{ID: 54, Key: "rank.tag.synonym", Value: `20000`},
{ID: 55, Key: "rank.link.url_limit", Value: `10`},
{ID: 56, Key: "rank.vote.detail", Value: `0`},
{ID: 57, Key: "reason.spam", Value: `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`},
{ID: 58, Key: "reason.rude_or_abusive", Value: `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`},
{ID: 59, Key: "reason.something", Value: `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`},
{ID: 60, Key: "reason.a_duplicate", Value: `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`},
{ID: 61, Key: "reason.not_a_answer", Value: `{"name":"not a answer","description":"This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.","content_type":""}`},
{ID: 62, Key: "reason.no_longer_needed", Value: `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`},
{ID: 63, Key: "reason.community_specific", Value: `{"name":"a community-specific reason","description":"This question doesnt meet a community guideline."}`},
{ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only.","content_type":"text"}`},
{ID: 65, Key: "reason.normal", Value: `{"name":"normal","description":"A normal post available to everyone."}`},
{ID: 66, Key: "reason.normal.user", Value: `{"name":"normal","description":"A normal user can ask and answer questions."}`},
{ID: 67, Key: "reason.closed", Value: `{"name":"closed","description":"A closed question cant answer, but still can edit, vote and comment."}`},
{ID: 68, Key: "reason.deleted", Value: `{"name":"deleted","description":"All reputation gained and lost will be restored."}`},
{ID: 69, Key: "reason.deleted.user", Value: `{"name":"deleted","description":"Delete profile, authentication associations."}`},
{ID: 70, Key: "reason.suspended", Value: `{"name":"suspended","description":"A suspended user cant log in."}`},
{ID: 71, Key: "reason.inactive", Value: `{"name":"inactive","description":"An inactive user must re-validate their email."}`},
{ID: 72, Key: "reason.looks_ok", Value: `{"name":"looks ok","description":"This post is good as-is and not low quality."}`},
{ID: 73, Key: "reason.needs_edit", Value: `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`},
{ID: 74, Key: "reason.needs_close", Value: `{"name":"needs close","description":"A closed question cant answer, but still can edit, vote and comment."}`},
{ID: 75, Key: "reason.needs_delete", Value: `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`},
{ID: 76, Key: "question.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`},
{ID: 77, Key: "answer.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`},
{ID: 78, Key: "comment.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`},
{ID: 79, Key: "question.close.reasons", Value: `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`},
{ID: 80, Key: "question.status.reasons", Value: `["reason.normal","reason.closed","reason.deleted"]`},
{ID: 81, Key: "answer.status.reasons", Value: `["reason.normal","reason.deleted"]`},
{ID: 82, Key: "comment.status.reasons", Value: `["reason.normal","reason.deleted"]`},
{ID: 83, Key: "user.status.reasons", Value: `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`},
{ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`},
{ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 87, Key: "question.asked", Value: `0`},
{ID: 88, Key: "question.closed", Value: `0`},
{ID: 89, Key: "question.reopened", Value: `0`},
{ID: 90, Key: "question.answered", Value: `0`},
{ID: 91, Key: "question.commented", Value: `0`},
{ID: 92, Key: "question.accept", Value: `0`},
{ID: 93, Key: "question.edited", Value: `0`},
{ID: 94, Key: "question.rollback", Value: `0`},
{ID: 95, Key: "question.deleted", Value: `0`},
{ID: 96, Key: "question.undeleted", Value: `0`},
{ID: 97, Key: "answer.answered", Value: `0`},
{ID: 98, Key: "answer.commented", Value: `0`},
{ID: 99, Key: "answer.edited", Value: `0`},
{ID: 100, Key: "answer.rollback", Value: `0`},
{ID: 101, Key: "answer.undeleted", Value: `0`},
{ID: 102, Key: "tag.created", Value: `0`},
{ID: 103, Key: "tag.edited", Value: `0`},
{ID: 104, Key: "tag.rollback", Value: `0`},
{ID: 105, Key: "tag.deleted", Value: `0`},
{ID: 106, Key: "tag.undeleted", Value: `0`},
{ID: 107, Key: "rank.comment.vote_up", Value: `1`},
{ID: 108, Key: "rank.comment.vote_down", Value: `1`},
{ID: 109, Key: "rank.question.edit_without_review", Value: `2000`},
{ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`},
{ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`},
{ID: 112, Key: "rank.answer.audit", Value: `2000`},
{ID: 113, Key: "rank.question.audit", Value: `2000`},
{ID: 114, Key: "rank.tag.audit", Value: `20000`},
{ID: 115, Key: "rank.question.close", Value: `-1`},
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
{ID: 118, Key: "plugin.status", Value: `{}`},
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
{ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`},
}
_, err := engine.Insert(defaultConfigTable)
return err
}
func initRolePower(engine *xorm.Engine) (err error) {
roles := []*entity.Role{
{ID: 1, Name: "User", Description: "Default with no special access."},
{ID: 2, Name: "Admin", Description: "Have the full power to access the site."},
{ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."},
}
_, err = engine.Insert(roles)
if err != nil {
return err
}
powers := []*entity.Power{
{ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"},
{ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"},
{ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"},
{ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"},
{ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"},
{ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"},
{ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"},
{ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"},
{ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"},
{ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"},
{ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"},
{ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"},
{ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"},
{ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"},
{ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"},
{ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"},
{ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"},
{ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"},
{ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"},
{ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"},
{ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"},
{ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"},
{ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"},
{ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"},
{ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"},
{ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"},
{ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"},
{ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"},
{ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"},
{ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"},
{ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
{ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
{ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
{ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"},
{ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"},
{ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"},
{ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"},
{ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"},
}
_, err = engine.Insert(powers)
if err != nil {
return err
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.AdminAccess},
{RoleID: 2, PowerType: permission.QuestionAdd},
{RoleID: 2, PowerType: permission.QuestionEdit},
{RoleID: 2, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 2, PowerType: permission.QuestionDelete},
{RoleID: 2, PowerType: permission.QuestionClose},
{RoleID: 2, PowerType: permission.QuestionReopen},
{RoleID: 2, PowerType: permission.QuestionVoteUp},
{RoleID: 2, PowerType: permission.QuestionVoteDown},
{RoleID: 2, PowerType: permission.AnswerAdd},
{RoleID: 2, PowerType: permission.AnswerEdit},
{RoleID: 2, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 2, PowerType: permission.AnswerDelete},
{RoleID: 2, PowerType: permission.AnswerAccept},
{RoleID: 2, PowerType: permission.AnswerVoteUp},
{RoleID: 2, PowerType: permission.AnswerVoteDown},
{RoleID: 2, PowerType: permission.CommentAdd},
{RoleID: 2, PowerType: permission.CommentEdit},
{RoleID: 2, PowerType: permission.CommentDelete},
{RoleID: 2, PowerType: permission.CommentVoteUp},
{RoleID: 2, PowerType: permission.CommentVoteDown},
{RoleID: 2, PowerType: permission.ReportAdd},
{RoleID: 2, PowerType: permission.TagAdd},
{RoleID: 2, PowerType: permission.TagEdit},
{RoleID: 2, PowerType: permission.TagEditSlugName},
{RoleID: 2, PowerType: permission.TagEditWithoutReview},
{RoleID: 2, PowerType: permission.TagDelete},
{RoleID: 2, PowerType: permission.TagSynonym},
{RoleID: 2, PowerType: permission.LinkUrlLimit},
{RoleID: 2, PowerType: permission.VoteDetail},
{RoleID: 2, PowerType: permission.AnswerAudit},
{RoleID: 2, PowerType: permission.QuestionAudit},
{RoleID: 2, PowerType: permission.TagAudit},
{RoleID: 2, PowerType: permission.TagUseReservedTag},
{RoleID: 2, PowerType: permission.QuestionPin},
{RoleID: 2, PowerType: permission.QuestionHide},
{RoleID: 2, PowerType: permission.QuestionUnPin},
{RoleID: 2, PowerType: permission.QuestionShow},
{RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer},
{RoleID: 3, PowerType: permission.QuestionAdd},
{RoleID: 3, PowerType: permission.QuestionEdit},
{RoleID: 3, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 3, PowerType: permission.QuestionDelete},
{RoleID: 3, PowerType: permission.QuestionClose},
{RoleID: 3, PowerType: permission.QuestionReopen},
{RoleID: 3, PowerType: permission.QuestionVoteUp},
{RoleID: 3, PowerType: permission.QuestionVoteDown},
{RoleID: 3, PowerType: permission.AnswerAdd},
{RoleID: 3, PowerType: permission.AnswerEdit},
{RoleID: 3, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 3, PowerType: permission.AnswerDelete},
{RoleID: 3, PowerType: permission.AnswerAccept},
{RoleID: 3, PowerType: permission.AnswerVoteUp},
{RoleID: 3, PowerType: permission.AnswerVoteDown},
{RoleID: 3, PowerType: permission.CommentAdd},
{RoleID: 3, PowerType: permission.CommentEdit},
{RoleID: 3, PowerType: permission.CommentDelete},
{RoleID: 3, PowerType: permission.CommentVoteUp},
{RoleID: 3, PowerType: permission.CommentVoteDown},
{RoleID: 3, PowerType: permission.ReportAdd},
{RoleID: 3, PowerType: permission.TagAdd},
{RoleID: 3, PowerType: permission.TagEdit},
{RoleID: 3, PowerType: permission.TagEditSlugName},
{RoleID: 3, PowerType: permission.TagEditWithoutReview},
{RoleID: 3, PowerType: permission.TagDelete},
{RoleID: 3, PowerType: permission.TagSynonym},
{RoleID: 3, PowerType: permission.LinkUrlLimit},
{RoleID: 3, PowerType: permission.VoteDetail},
{RoleID: 3, PowerType: permission.AnswerAudit},
{RoleID: 3, PowerType: permission.QuestionAudit},
{RoleID: 3, PowerType: permission.TagAudit},
{RoleID: 3, PowerType: permission.TagUseReservedTag},
{RoleID: 3, PowerType: permission.QuestionPin},
{RoleID: 3, PowerType: permission.QuestionHide},
{RoleID: 3, PowerType: permission.QuestionUnPin},
{RoleID: 3, PowerType: permission.QuestionShow},
{RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer},
}
_, err = engine.Insert(rolePowerRels)
if err != nil {
return err
}
adminUserRoleRel := &entity.UserRoleRel{
UserID: "1",
RoleID: 2,
}
_, err = engine.Insert(adminUserRoleRel)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,313 @@
package migrations
import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/permission"
)
const (
defaultSEORobotTxt = `User-agent: *
Disallow: /admin
Disallow: /search
Disallow: /install
Disallow: /review
Disallow: /users/login
Disallow: /users/register
Disallow: /users/account-recovery
Disallow: /users/oauth/*
Disallow: /users/*/*
Disallow: /answer/api
Disallow: /*?code*
Sitemap: `
)
var (
tables = []interface{}{
&entity.Activity{},
&entity.Answer{},
&entity.Collection{},
&entity.CollectionGroup{},
&entity.Comment{},
&entity.Config{},
&entity.Meta{},
&entity.Notification{},
&entity.Question{},
&entity.Report{},
&entity.Revision{},
&entity.SiteInfo{},
&entity.Tag{},
&entity.TagRel{},
&entity.Uniqid{},
&entity.User{},
&entity.Version{},
&entity.Role{},
&entity.RolePowerRel{},
&entity.Power{},
&entity.UserRoleRel{},
&entity.PluginConfig{},
&entity.UserExternalLogin{},
}
roles = []*entity.Role{
{ID: 1, Name: "User", Description: "Default with no special access."},
{ID: 2, Name: "Admin", Description: "Have the full power to access the site."},
{ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."},
}
powers = []*entity.Power{
{ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"},
{ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"},
{ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"},
{ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"},
{ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"},
{ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"},
{ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"},
{ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"},
{ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"},
{ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"},
{ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"},
{ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"},
{ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"},
{ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"},
{ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"},
{ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"},
{ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"},
{ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"},
{ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"},
{ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"},
{ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"},
{ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"},
{ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"},
{ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"},
{ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"},
{ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"},
{ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"},
{ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"},
{ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"},
{ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"},
{ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
{ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
{ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
{ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"},
{ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"},
{ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"},
{ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"},
{ID: 38, Name: "invite someone to answer", PowerType: permission.AnswerInviteSomeoneToAnswer, Description: "invite someone to answer"},
}
rolePowerRels = []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.AdminAccess},
{RoleID: 2, PowerType: permission.QuestionAdd},
{RoleID: 2, PowerType: permission.QuestionEdit},
{RoleID: 2, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 2, PowerType: permission.QuestionDelete},
{RoleID: 2, PowerType: permission.QuestionClose},
{RoleID: 2, PowerType: permission.QuestionReopen},
{RoleID: 2, PowerType: permission.QuestionVoteUp},
{RoleID: 2, PowerType: permission.QuestionVoteDown},
{RoleID: 2, PowerType: permission.AnswerAdd},
{RoleID: 2, PowerType: permission.AnswerEdit},
{RoleID: 2, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 2, PowerType: permission.AnswerDelete},
{RoleID: 2, PowerType: permission.AnswerAccept},
{RoleID: 2, PowerType: permission.AnswerVoteUp},
{RoleID: 2, PowerType: permission.AnswerVoteDown},
{RoleID: 2, PowerType: permission.CommentAdd},
{RoleID: 2, PowerType: permission.CommentEdit},
{RoleID: 2, PowerType: permission.CommentDelete},
{RoleID: 2, PowerType: permission.CommentVoteUp},
{RoleID: 2, PowerType: permission.CommentVoteDown},
{RoleID: 2, PowerType: permission.ReportAdd},
{RoleID: 2, PowerType: permission.TagAdd},
{RoleID: 2, PowerType: permission.TagEdit},
{RoleID: 2, PowerType: permission.TagEditSlugName},
{RoleID: 2, PowerType: permission.TagEditWithoutReview},
{RoleID: 2, PowerType: permission.TagDelete},
{RoleID: 2, PowerType: permission.TagSynonym},
{RoleID: 2, PowerType: permission.LinkUrlLimit},
{RoleID: 2, PowerType: permission.VoteDetail},
{RoleID: 2, PowerType: permission.AnswerAudit},
{RoleID: 2, PowerType: permission.QuestionAudit},
{RoleID: 2, PowerType: permission.TagAudit},
{RoleID: 2, PowerType: permission.TagUseReservedTag},
{RoleID: 2, PowerType: permission.QuestionPin},
{RoleID: 2, PowerType: permission.QuestionHide},
{RoleID: 2, PowerType: permission.QuestionUnPin},
{RoleID: 2, PowerType: permission.QuestionShow},
{RoleID: 2, PowerType: permission.AnswerInviteSomeoneToAnswer},
{RoleID: 3, PowerType: permission.QuestionAdd},
{RoleID: 3, PowerType: permission.QuestionEdit},
{RoleID: 3, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 3, PowerType: permission.QuestionDelete},
{RoleID: 3, PowerType: permission.QuestionClose},
{RoleID: 3, PowerType: permission.QuestionReopen},
{RoleID: 3, PowerType: permission.QuestionVoteUp},
{RoleID: 3, PowerType: permission.QuestionVoteDown},
{RoleID: 3, PowerType: permission.AnswerAdd},
{RoleID: 3, PowerType: permission.AnswerEdit},
{RoleID: 3, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 3, PowerType: permission.AnswerDelete},
{RoleID: 3, PowerType: permission.AnswerAccept},
{RoleID: 3, PowerType: permission.AnswerVoteUp},
{RoleID: 3, PowerType: permission.AnswerVoteDown},
{RoleID: 3, PowerType: permission.CommentAdd},
{RoleID: 3, PowerType: permission.CommentEdit},
{RoleID: 3, PowerType: permission.CommentDelete},
{RoleID: 3, PowerType: permission.CommentVoteUp},
{RoleID: 3, PowerType: permission.CommentVoteDown},
{RoleID: 3, PowerType: permission.ReportAdd},
{RoleID: 3, PowerType: permission.TagAdd},
{RoleID: 3, PowerType: permission.TagEdit},
{RoleID: 3, PowerType: permission.TagEditSlugName},
{RoleID: 3, PowerType: permission.TagEditWithoutReview},
{RoleID: 3, PowerType: permission.TagDelete},
{RoleID: 3, PowerType: permission.TagSynonym},
{RoleID: 3, PowerType: permission.LinkUrlLimit},
{RoleID: 3, PowerType: permission.VoteDetail},
{RoleID: 3, PowerType: permission.AnswerAudit},
{RoleID: 3, PowerType: permission.QuestionAudit},
{RoleID: 3, PowerType: permission.TagAudit},
{RoleID: 3, PowerType: permission.TagUseReservedTag},
{RoleID: 3, PowerType: permission.QuestionPin},
{RoleID: 3, PowerType: permission.QuestionHide},
{RoleID: 3, PowerType: permission.QuestionUnPin},
{RoleID: 3, PowerType: permission.QuestionShow},
{RoleID: 3, PowerType: permission.AnswerInviteSomeoneToAnswer},
}
adminUserRoleRel = &entity.UserRoleRel{
UserID: "1",
RoleID: 2,
}
defaultConfigTable = []*entity.Config{
{ID: 1, Key: "answer.accepted", Value: `15`},
{ID: 2, Key: "answer.voted_up", Value: `10`},
{ID: 3, Key: "question.voted_up", Value: `10`},
{ID: 4, Key: "tag.edit_accepted", Value: `2`},
{ID: 5, Key: "answer.accept", Value: `2`},
{ID: 6, Key: "answer.voted_down_cancel", Value: `2`},
{ID: 7, Key: "question.voted_down_cancel", Value: `2`},
{ID: 8, Key: "answer.vote_down_cancel", Value: `1`},
{ID: 9, Key: "question.vote_down_cancel", Value: `1`},
{ID: 10, Key: "user.activated", Value: `1`},
{ID: 11, Key: "edit.accepted", Value: `2`},
{ID: 12, Key: "answer.vote_down", Value: `-1`},
{ID: 13, Key: "question.voted_down", Value: `-2`},
{ID: 14, Key: "answer.voted_down", Value: `-2`},
{ID: 15, Key: "answer.accept_cancel", Value: `-2`},
{ID: 16, Key: "answer.deleted", Value: `-5`},
{ID: 17, Key: "question.voted_up_cancel", Value: `-10`},
{ID: 18, Key: "answer.voted_up_cancel", Value: `-10`},
{ID: 19, Key: "answer.accepted_cancel", Value: `-15`},
{ID: 20, Key: "object.reported", Value: `-100`},
{ID: 21, Key: "edit.rejected", Value: `-2`},
{ID: 22, Key: "daily_rank_limit", Value: `200`},
{ID: 23, Key: "daily_rank_limit.exclude", Value: `["answer.accepted"]`},
{ID: 24, Key: "user.follow", Value: `0`},
{ID: 25, Key: "comment.vote_up", Value: `0`},
{ID: 26, Key: "comment.vote_up_cancel", Value: `0`},
{ID: 27, Key: "question.vote_down", Value: `0`},
{ID: 28, Key: "question.vote_up", Value: `0`},
{ID: 29, Key: "question.vote_up_cancel", Value: `0`},
{ID: 30, Key: "answer.vote_up", Value: `0`},
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
{ID: 32, Key: "question.follow", Value: `0`},
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email.","new_answer_title":"[{{.SiteName}}] {{.DisplayName}} answered your question","new_answer_body":"<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>","new_comment_title":"[{{.SiteName}}] {{.DisplayName}} commented on your post","new_comment_body":"<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"}`},
{ID: 35, Key: "tag.follow", Value: `0`},
{ID: 36, Key: "rank.question.add", Value: `1`},
{ID: 37, Key: "rank.question.edit", Value: `200`},
{ID: 38, Key: "rank.question.delete", Value: `-1`},
{ID: 39, Key: "rank.question.vote_up", Value: `15`},
{ID: 40, Key: "rank.question.vote_down", Value: `125`},
{ID: 41, Key: "rank.answer.add", Value: `1`},
{ID: 42, Key: "rank.answer.edit", Value: `200`},
{ID: 43, Key: "rank.answer.delete", Value: `-1`},
{ID: 44, Key: "rank.answer.accept", Value: `-1`},
{ID: 45, Key: "rank.answer.vote_up", Value: `15`},
{ID: 46, Key: "rank.answer.vote_down", Value: `125`},
{ID: 47, Key: "rank.comment.add", Value: `1`},
{ID: 48, Key: "rank.comment.edit", Value: `-1`},
{ID: 49, Key: "rank.comment.delete", Value: `-1`},
{ID: 50, Key: "rank.report.add", Value: `1`},
{ID: 51, Key: "rank.tag.add", Value: `1`},
{ID: 52, Key: "rank.tag.edit", Value: `100`},
{ID: 53, Key: "rank.tag.delete", Value: `-1`},
{ID: 54, Key: "rank.tag.synonym", Value: `20000`},
{ID: 55, Key: "rank.link.url_limit", Value: `10`},
{ID: 56, Key: "rank.vote.detail", Value: `0`},
{ID: 57, Key: "reason.spam", Value: `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`},
{ID: 58, Key: "reason.rude_or_abusive", Value: `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`},
{ID: 59, Key: "reason.something", Value: `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`},
{ID: 60, Key: "reason.a_duplicate", Value: `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`},
{ID: 61, Key: "reason.not_a_answer", Value: `{"name":"not a answer","description":"This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.","content_type":""}`},
{ID: 62, Key: "reason.no_longer_needed", Value: `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`},
{ID: 63, Key: "reason.community_specific", Value: `{"name":"a community-specific reason","description":"This question doesnt meet a community guideline."}`},
{ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only.","content_type":"text"}`},
{ID: 65, Key: "reason.normal", Value: `{"name":"normal","description":"A normal post available to everyone."}`},
{ID: 66, Key: "reason.normal.user", Value: `{"name":"normal","description":"A normal user can ask and answer questions."}`},
{ID: 67, Key: "reason.closed", Value: `{"name":"closed","description":"A closed question cant answer, but still can edit, vote and comment."}`},
{ID: 68, Key: "reason.deleted", Value: `{"name":"deleted","description":"All reputation gained and lost will be restored."}`},
{ID: 69, Key: "reason.deleted.user", Value: `{"name":"deleted","description":"Delete profile, authentication associations."}`},
{ID: 70, Key: "reason.suspended", Value: `{"name":"suspended","description":"A suspended user cant log in."}`},
{ID: 71, Key: "reason.inactive", Value: `{"name":"inactive","description":"An inactive user must re-validate their email."}`},
{ID: 72, Key: "reason.looks_ok", Value: `{"name":"looks ok","description":"This post is good as-is and not low quality."}`},
{ID: 73, Key: "reason.needs_edit", Value: `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`},
{ID: 74, Key: "reason.needs_close", Value: `{"name":"needs close","description":"A closed question cant answer, but still can edit, vote and comment."}`},
{ID: 75, Key: "reason.needs_delete", Value: `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`},
{ID: 76, Key: "question.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`},
{ID: 77, Key: "answer.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`},
{ID: 78, Key: "comment.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`},
{ID: 79, Key: "question.close.reasons", Value: `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`},
{ID: 80, Key: "question.status.reasons", Value: `["reason.normal","reason.closed","reason.deleted"]`},
{ID: 81, Key: "answer.status.reasons", Value: `["reason.normal","reason.deleted"]`},
{ID: 82, Key: "comment.status.reasons", Value: `["reason.normal","reason.deleted"]`},
{ID: 83, Key: "user.status.reasons", Value: `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`},
{ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`},
{ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`},
{ID: 87, Key: "question.asked", Value: `0`},
{ID: 88, Key: "question.closed", Value: `0`},
{ID: 89, Key: "question.reopened", Value: `0`},
{ID: 90, Key: "question.answered", Value: `0`},
{ID: 91, Key: "question.commented", Value: `0`},
{ID: 92, Key: "question.accept", Value: `0`},
{ID: 93, Key: "question.edited", Value: `0`},
{ID: 94, Key: "question.rollback", Value: `0`},
{ID: 95, Key: "question.deleted", Value: `0`},
{ID: 96, Key: "question.undeleted", Value: `0`},
{ID: 97, Key: "answer.answered", Value: `0`},
{ID: 98, Key: "answer.commented", Value: `0`},
{ID: 99, Key: "answer.edited", Value: `0`},
{ID: 100, Key: "answer.rollback", Value: `0`},
{ID: 101, Key: "answer.undeleted", Value: `0`},
{ID: 102, Key: "tag.created", Value: `0`},
{ID: 103, Key: "tag.edited", Value: `0`},
{ID: 104, Key: "tag.rollback", Value: `0`},
{ID: 105, Key: "tag.deleted", Value: `0`},
{ID: 106, Key: "tag.undeleted", Value: `0`},
{ID: 107, Key: "rank.comment.vote_up", Value: `1`},
{ID: 108, Key: "rank.comment.vote_down", Value: `1`},
{ID: 109, Key: "rank.question.edit_without_review", Value: `2000`},
{ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`},
{ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`},
{ID: 112, Key: "rank.answer.audit", Value: `2000`},
{ID: 113, Key: "rank.question.audit", Value: `2000`},
{ID: 114, Key: "rank.tag.audit", Value: `20000`},
{ID: 115, Key: "rank.question.close", Value: `-1`},
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
{ID: 118, Key: "plugin.status", Value: `{}`},
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
{ID: 127, Key: "rank.answer.invite_someone_to_answer", Value: `1000`},
}
)

View File

@ -2,7 +2,10 @@ package activity
import (
"context"
"fmt"
"github.com/segmentfault/pacman/log"
"time"
"xorm.io/builder"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
@ -15,343 +18,338 @@ import (
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
// AnswerActivityRepo answer accepted
type AnswerActivityRepo struct {
data *data.Data
activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo
data *data.Data
activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo
notificationQueueService notice_queue.NotificationQueueService
}
const (
acceptAction = "accept"
acceptedAction = "accepted"
)
var (
acceptActionList = []string{acceptAction, acceptedAction}
)
// NewAnswerActivityRepo new repository
func NewAnswerActivityRepo(
data *data.Data,
activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo,
notificationQueueService notice_queue.NotificationQueueService,
) activity.AnswerActivityRepo {
return &AnswerActivityRepo{
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
notificationQueueService: notificationQueueService,
}
}
// NewQuestionActivityRepo new repository
func NewQuestionActivityRepo(
data *data.Data,
activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo,
) activity.QuestionActivityRepo {
return &AnswerActivityRepo{
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
}
}
func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID string) (err error) {
questionInfo := &entity.Question{}
exist, err := ar.data.DB.Context(ctx).Where("id = ?", questionID).Get(questionInfo)
func (ar *AnswerActivityRepo) SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (
err error) {
// pre check
noNeedToDo, err := ar.activityPreCheck(ctx, op)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return err
}
if !exist {
if noNeedToDo {
return nil
}
// get all this object activity
activityList := make([]*entity.Activity, 0)
session := ar.data.DB.Context(ctx).Where("has_rank = 1")
session.Where("cancelled = ?", entity.ActivityAvailable)
err = session.Find(&activityList, &entity.Activity{ObjectID: questionID})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if len(activityList) == 0 {
return nil
}
log.Infof("questionInfo %s deleted will rollback activity %d", questionID, len(activityList))
// save activity
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
for _, act := range activityList {
log.Infof("user %s rollback rank %d", act.UserID, -act.Rank)
_, e := ar.userRankRepo.TriggerUserRank(
ctx, session, act.UserID, -act.Rank, act.ActivityType)
if e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
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 err
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
// get all answers
answerList := make([]*entity.Answer, 0)
err = ar.data.DB.Context(ctx).Find(&answerList, &entity.Answer{QuestionID: questionID})
// 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()
}
for _, answerInfo := range answerList {
err = ar.DeleteAnswer(ctx, answerInfo.ID)
// notification
ar.sendCancelAcceptAnswerNotification(ctx, op)
return nil
}
func (ar *AnswerActivityRepo) activityPreCheck(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (
noNeedToDo bool, err error) {
activities, err := ar.getExistActivity(ctx, op)
if err != nil {
return false, err
}
done := 0
for _, act := range activities {
if act.Cancelled == entity.ActivityAvailable {
done++
}
}
return done == len(op.Activities), 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 {
if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); 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
return nil
}
// AcceptAnswer accept other answer
func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool,
) (err error) {
addActivityList := make([]*entity.Activity, 0)
for _, action := range acceptActionList {
// get accept answer need add rank amount
activityType, deltaRank, hasRank, e := ar.activityRepo.GetActivityTypeByObjID(ctx, answerObjID, action)
if e != nil {
return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
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
}
addActivity := &entity.Activity{
ObjectID: answerObjID,
OriginalObjectID: questionObjID,
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
user := userInfoMapping[act.ActivityUserID]
if user == nil {
continue
}
if action == acceptAction {
addActivity.UserID = questionUserID
addActivity.TriggerUserID = converter.StringToInt64(answerUserID)
addActivity.OriginalObjectID = questionObjID // if activity is 'accept' means this question is accept the answer.
} else {
addActivity.UserID = answerUserID
addActivity.TriggerUserID = converter.StringToInt64(answerUserID)
addActivity.OriginalObjectID = answerObjID // if activity is 'accepted' means this answer was accepted.
if err = ar.userRankRepo.ChangeUserRank(ctx, session,
act.ActivityUserID, user.Rank, act.Rank); err != nil {
log.Error(err)
return err
}
if isSelf {
addActivity.Rank = 0
addActivity.HasRank = 0
}
addActivityList = append(addActivityList, addActivity)
}
return nil
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
for _, addActivity := range addActivityList {
existsActivity, exists, e := ar.activityRepo.GetActivity(
ctx, session, answerObjID, addActivity.UserID, addActivity.ActivityType)
if e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if exists && existsActivity.Cancelled == entity.ActivityAvailable {
continue
}
// trigger user rank and send notification
if addActivity.Rank != 0 {
reachStandard, e := ar.userRankRepo.TriggerUserRank(
ctx, session, addActivity.UserID, addActivity.Rank, addActivity.ActivityType)
if e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if reachStandard {
addActivity.Rank = 0
}
}
if exists {
if _, e = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
} else {
if _, e = session.Insert(addActivity); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
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, nil
})
if err != nil {
return err
}
for _, act := range addActivityList {
return nil
}
func (ar *AnswerActivityRepo) getExistActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) ([]*entity.Activity, error) {
var activities []*entity.Activity
for _, action := range op.Activities {
t := &entity.Activity{}
exist, err := ar.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.AnswerObjectID}).
Get(t)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
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: act.ObjectID,
ReceiverUserID: act.UserID,
ObjectID: op.AnswerObjectID,
ReceiverUserID: act.ActivityUserID,
}
if act.UserID == questionUserID {
msg.TriggerUserID = answerUserID
if act.ActivityUserID == op.QuestionUserID {
msg.TriggerUserID = op.AnswerUserID
msg.ObjectType = constant.AnswerObjectType
} else {
msg.TriggerUserID = questionUserID
msg.TriggerUserID = op.QuestionUserID
msg.ObjectType = constant.AnswerObjectType
}
if msg.TriggerUserID != msg.ReceiverUserID {
notice_queue.AddNotification(msg)
ar.notificationQueueService.Send(ctx, msg)
}
}
for _, act := range addActivityList {
for _, act := range op.Activities {
msg := &schema.NotificationMsg{
ReceiverUserID: act.UserID,
ReceiverUserID: act.ActivityUserID,
Type: schema.NotificationTypeInbox,
ObjectID: act.ObjectID,
ObjectID: op.AnswerObjectID,
}
if act.UserID != questionUserID {
msg.TriggerUserID = questionUserID
if act.ActivityUserID != op.QuestionUserID {
msg.TriggerUserID = op.QuestionUserID
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.NotificationAcceptAnswer
notice_queue.AddNotification(msg)
ar.notificationQueueService.Send(ctx, msg)
}
}
return err
}
// CancelAcceptAnswer accept other answer
func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string,
) (err error) {
addActivityList := make([]*entity.Activity, 0)
for _, action := range acceptActionList {
// get accept answer need add rank amount
activityType, deltaRank, hasRank, e := ar.activityRepo.GetActivityTypeByObjID(ctx, answerObjID, action)
if e != nil {
return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
addActivity := &entity.Activity{
ObjectID: answerObjID,
ActivityType: activityType,
Rank: -deltaRank,
HasRank: hasRank,
}
if action == acceptAction {
addActivity.UserID = questionUserID
addActivity.OriginalObjectID = questionObjID
} else {
addActivity.UserID = answerUserID
addActivity.OriginalObjectID = answerObjID
}
addActivityList = append(addActivityList, addActivity)
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
for _, addActivity := range addActivityList {
existsActivity, exists, e := ar.activityRepo.GetActivity(
ctx, session, answerObjID, addActivity.UserID, addActivity.ActivityType)
if e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if exists && existsActivity.Cancelled == entity.ActivityCancelled {
continue
}
if !exists {
continue
}
if existsActivity.Rank != 0 {
_, e = ar.userRankRepo.TriggerUserRank(
ctx, session, addActivity.UserID, addActivity.Rank, addActivity.ActivityType)
if e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
if _, e := session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
return nil, nil
})
if err != nil {
return err
}
for _, act := range addActivityList {
func (ar *AnswerActivityRepo) sendCancelAcceptAnswerNotification(
ctx context.Context, op *schema.AcceptAnswerOperationInfo) {
for _, act := range op.Activities {
msg := &schema.NotificationMsg{
ReceiverUserID: act.UserID,
ReceiverUserID: act.ActivityUserID,
Type: schema.NotificationTypeAchievement,
ObjectID: act.ObjectID,
ObjectID: op.AnswerObjectID,
}
if act.UserID == questionUserID {
msg.TriggerUserID = answerUserID
if act.ActivityUserID == op.QuestionObjectID {
msg.TriggerUserID = op.AnswerObjectID
msg.ObjectType = constant.QuestionObjectType
} else {
msg.TriggerUserID = questionUserID
msg.TriggerUserID = op.QuestionObjectID
msg.ObjectType = constant.AnswerObjectType
}
if msg.TriggerUserID != msg.ReceiverUserID {
notice_queue.AddNotification(msg)
ar.notificationQueueService.Send(ctx, msg)
}
}
return err
}
func (ar *AnswerActivityRepo) DeleteAnswer(ctx context.Context, answerID string) (err error) {
answerInfo := &entity.Answer{}
exist, err := ar.data.DB.Context(ctx).Where("id = ?", answerID).Get(answerInfo)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return nil
}
// get all this object activity
activityList := make([]*entity.Activity, 0)
session := ar.data.DB.Context(ctx).Where("has_rank = 1")
session.Where("cancelled = ?", entity.ActivityAvailable)
err = session.Find(&activityList, &entity.Activity{ObjectID: answerID})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if len(activityList) == 0 {
return nil
}
log.Infof("answerInfo %s deleted will rollback activity %d", answerID, len(activityList))
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx)
for _, act := range activityList {
log.Infof("user %s rollback rank %d", act.UserID, -act.Rank)
_, e := ar.userRankRepo.TriggerUserRank(
ctx, session, act.UserID, -act.Rank, act.ActivityType)
if e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if _, e := session.Where("id = ?", act.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
return nil, nil
})
if err != nil {
return err
}
return
}

View File

@ -43,7 +43,7 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow")
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
if err != nil {
return err
}
@ -110,7 +110,7 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow")
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
if err != nil {
return err
}

View File

@ -2,6 +2,8 @@ package activity
import (
"context"
"fmt"
"xorm.io/builder"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
@ -41,43 +43,58 @@ func NewUserActiveActivityRepo(
}
}
// UserActive accept other answer
// 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
}
activityType := cfg.ID
deltaRank := cfg.GetIntValue()
addActivity := &entity.Activity{
UserID: userID,
ObjectID: "0",
OriginalObjectID: "0",
ActivityType: activityType,
Rank: deltaRank,
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)
_, exists, err := ar.activityRepo.GetActivity(ctx, session, "0", addActivity.UserID, activityType)
user := &entity.User{}
exist, err := session.ID(userID).ForUpdate().Get(user)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return nil, err
}
if exists {
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.TriggerUserRank(ctx, session, addActivity.UserID, addActivity.Rank, activityType)
err = ar.userRankRepo.ChangeUserRank(ctx, session, addActivity.UserID, user.Rank, addActivity.Rank)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return nil, err
}
_, err = session.Insert(addActivity)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return nil, err
}
return nil, nil
})
return err
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}

View File

@ -2,395 +2,174 @@ package activity
import (
"context"
"strings"
"fmt"
"github.com/segmentfault/pacman/log"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/service/notice_queue"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/notice_queue"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/obj"
"xorm.io/builder"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// VoteRepo activity repository
type VoteRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
configService *config.ConfigService
activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo
voteCommon activity_common.VoteRepo
data *data.Data
activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo
notificationQueueService notice_queue.NotificationQueueService
}
// NewVoteRepo new repository
func NewVoteRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
configService *config.ConfigService,
activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo,
voteCommon activity_common.VoteRepo,
notificationQueueService notice_queue.NotificationQueueService,
) service.VoteRepo {
return &VoteRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
configService: configService,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
voteCommon: voteCommon,
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
notificationQueueService: notificationQueueService,
}
}
var LimitUpActions = map[string][]string{
"question": {"vote_up", "voted_up"},
"answer": {"vote_up", "voted_up"},
"comment": {"vote_up"},
}
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
}
var LimitDownActions = map[string][]string{
"question": {"vote_down", "voted_down"},
"answer": {"vote_down", "voted_down"},
"comment": {"vote_down"},
}
func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
achievementNotificationUserIDs := make([]string, 0)
sendInboxNotification := false
upVote := 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)
result = nil
for _, action := range actions {
var (
existsActivity entity.Activity
insertActivity entity.Activity
has bool
triggerUserID,
activityUserID string
activityType, deltaRank, hasRank int
)
activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action)
if err != nil {
return
}
triggerUserID = userID
if userID == activityUserID {
triggerUserID = "0"
}
// check is voted up
has, _ = session.
Where(builder.Eq{"object_id": objectID}).
And(builder.Eq{"user_id": activityUserID}).
And(builder.Eq{"trigger_user_id": triggerUserID}).
And(builder.Eq{"activity_type": activityType}).
Get(&existsActivity)
// is is voted,return
if has && existsActivity.Cancelled == entity.ActivityAvailable {
return
}
insertActivity = entity.Activity{
ObjectID: objectID,
OriginalObjectID: objectID,
UserID: activityUserID,
TriggerUserID: converter.StringToInt64(triggerUserID),
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
Cancelled: entity.ActivityAvailable,
}
// trigger user rank and send notification
if hasRank != 0 {
var isReachStandard bool
isReachStandard, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, deltaRank, activityType)
if err != nil {
return nil, err
}
if isReachStandard {
insertActivity.Rank = 0
}
achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID)
}
if has {
if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{
Cancelled: entity.ActivityAvailable,
}); err != nil {
return
}
} else {
_, err = session.Insert(&insertActivity)
if err != nil {
return nil, err
}
sendInboxNotification = true
}
// update votes
if action == constant.ActVoteDown || action == constant.ActVoteUp {
votes := 1
if action == constant.ActVoteDown {
upVote = false
votes = -1
} else {
upVote = true
}
err = vr.updateVotes(ctx, session, objectID, votes)
if err != nil {
return
}
}
userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
if err != nil {
return nil, err
}
return
err = vr.setActivityRankToZeroIfUserReachLimit(ctx, session, op, 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
return err
}
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
for _, activityUserID := range achievementNotificationUserIDs {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
for _, activity := range op.Activities {
if activity.Rank == 0 {
continue
}
vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID)
}
if sendInboxNotification {
vr.sendVoteInboxNotification(userID, objectUserID, objectID, upVote)
vr.sendVoteInboxNotification(ctx, op.OperatingUserID, op.ObjectCreatorUserID, op.ObjectID, op.VoteUp)
}
return
return nil
}
func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
notificationUserIDs := make([]string, 0)
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)
for _, action := range actions {
var (
existsActivity entity.Activity
has bool
triggerUserID,
activityUserID string
activityType,
deltaRank, hasRank int
)
result = nil
activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action)
if err != nil {
return
}
triggerUserID = userID
if userID == activityUserID {
triggerUserID = "0"
}
has, err = session.
Where(builder.Eq{"user_id": activityUserID}).
And(builder.Eq{"trigger_user_id": triggerUserID}).
And(builder.Eq{"activity_type": activityType}).
And(builder.Eq{"object_id": objectID}).
Get(&existsActivity)
if !has {
return
}
if existsActivity.Cancelled == entity.ActivityCancelled {
return
}
if _, err = session.Where("id = ?", existsActivity.ID).Cols("cancelled", "cancelled_at").
Update(&entity.Activity{
Cancelled: entity.ActivityCancelled,
CancelledAt: time.Now(),
}); err != nil {
return
}
// trigger user rank and send notification
if hasRank != 0 && existsActivity.Rank != 0 {
_, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType)
if err != nil {
return
}
notificationUserIDs = append(notificationUserIDs, activityUserID)
}
// update votes
if action == "vote_down" || action == "vote_up" {
votes := -1
if action == "vote_down" {
votes = 1
}
err = vr.updateVotes(ctx, session, objectID, votes)
if err != nil {
return
}
}
userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
if err != nil {
return nil, err
}
return
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
return err
}
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
for _, activityUserID := range notificationUserIDs {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
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) VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
err = errors.BadRequest(reason.ObjectNotFound)
return
}
actions, ok := LimitUpActions[objectType]
if !ok {
err = errors.BadRequest(reason.DisallowVote)
return
}
_, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserID)
return vr.vote(ctx, objectID, userID, objectUserID, actions)
}
func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
err = errors.BadRequest(reason.ObjectNotFound)
return
}
actions, ok := LimitDownActions[objectType]
if !ok {
err = errors.BadRequest(reason.DisallowVote)
return
}
_, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserID)
return vr.vote(ctx, objectID, userID, objectUserID, actions)
}
func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
var objectType string
resp = &schema.VoteResp{}
objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
err = errors.BadRequest(reason.ObjectNotFound)
return
}
actions, ok := LimitUpActions[objectType]
if !ok {
err = errors.BadRequest(reason.DisallowVote)
return
}
return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
}
func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) {
var objectType string
resp = &schema.VoteResp{}
objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
err = errors.BadRequest(reason.ObjectNotFound)
return
}
actions, ok := LimitDownActions[objectType]
if !ok {
err = errors.BadRequest(reason.DisallowVote)
return
}
return vr.voteCancel(ctx, objectID, userID, objectUserID, actions)
}
func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserID, userID string, action string) (activityUserID string, activityType, rank, hasRank int, err error) {
activityType, rank, hasRank, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)
if err != nil {
return
}
activityUserID = userID
if strings.Contains(action, "voted") {
activityUserID = objectUserID
}
return activityUserID, activityType, rank, hasRank, nil
}
func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
for _, action := range []string{"vote_up", "vote_down"} {
var (
activity entity.Activity
votes int64
activityType int
)
activityType, _, _, _ = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action)
votes, 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 {
return
}
if action == "vote_up" {
resp.UpVotes = int(votes)
} else {
resp.DownVotes = int(votes)
}
}
resp.Votes = resp.UpVotes - resp.DownVotes
return resp, nil
}
func (vr *VoteRepo) ListUserVotes(
ctx context.Context,
userID string,
req schema.GetVoteWithPageReq,
activityTypes []int,
) (voteList []entity.Activity, total int64, err error) {
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(
@ -399,46 +178,243 @@ func (vr *VoteRepo) ListUserVotes(
builder.In("activity_type", activityTypes),
)
session.Where(cond).OrderBy("updated_at desc")
session.Where(cond).Desc("updated_at")
total, err = pager.Help(req.Page, req.PageSize, &voteList, &entity.Activity{}, session)
total, err = pager.Help(page, pageSize, &voteList, &entity.Activity{}, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// updateVotes
// if votes < 0 Decr object vote_count,otherwise Incr object vote_count
func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) {
var (
objectType string
e error
)
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
}
objectType, err = obj.GetObjectTypeStrByObjectID(objectID)
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, maxDailyRank int) (err error) {
// check if user reach daily rank limit
for _, activity := range op.Activities {
reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank)
if err != nil {
log.Error(err)
return err
}
if reach {
activity.Rank = 0
}
}
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 {
if _, err = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); 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 "question":
_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Question{})
case "answer":
_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Answer{})
case "comment":
_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Comment{})
default:
e = errors.BadRequest(reason.DisallowVote)
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 e != nil {
err = e
} else if err != nil {
err = errors.BadRequest(reason.DatabaseError).WithError(err).WithStack()
if err != nil {
log.Error(err)
}
return
}
// sendNotification send rank triggered notification
func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return
@ -451,10 +427,10 @@ func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, object
ObjectID: objectID,
ObjectType: objectType,
}
notice_queue.AddNotification(msg)
vr.notificationQueueService.Send(ctx, msg)
}
func (vr *VoteRepo) sendVoteInboxNotification(triggerUserID, receiverUserID, objectID string, upvote bool) {
func (vr *VoteRepo) sendVoteInboxNotification(ctx context.Context, triggerUserID, receiverUserID, objectID string, upvote bool) {
if triggerUserID == receiverUserID {
return
}
@ -487,6 +463,6 @@ func (vr *VoteRepo) sendVoteInboxNotification(triggerUserID, receiverUserID, obj
}
}
if len(msg.NotificationAction) > 0 {
notice_queue.AddNotification(msg)
vr.notificationQueueService.Send(ctx, msg)
}
}

View File

@ -41,12 +41,12 @@ func NewActivityRepo(
func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) (
activityType, rank, hasRank int, err error) {
objectKey, err := obj.GetObjectTypeStrByObjectID(objectID)
objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return
}
confKey := fmt.Sprintf("%s.%s", objectKey, action)
confKey := fmt.Sprintf("%s.%s", objectType, action)
cfg, err := ar.configService.GetConfigByKey(ctx, confKey)
if err != nil {
return
@ -59,11 +59,11 @@ func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID str
return
}
func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error) {
configKey := fmt.Sprintf("%s.%s", objectKey, action)
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 {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return cfg.ID, nil
}
@ -71,7 +71,7 @@ func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey,
func (ar *ActivityRepo) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) {
cfg, err := ar.configService.GetConfigByKey(ctx, configKey)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return cfg.ID, nil
}

View File

@ -10,6 +10,7 @@ import (
"github.com/answerdev/answer/internal/service/unique"
"github.com/answerdev/answer/pkg/obj"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// FollowRepo follow repository
@ -71,11 +72,12 @@ func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectID string) (fol
func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) {
objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return nil, err
}
activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectTypeStr, "follow")
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectTypeStr, "follow")
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
log.Errorf("can't get activity type by object key: %s", objectTypeStr)
return nil, err
}
userIDs = make([]string, 0)
@ -94,7 +96,7 @@ func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (us
// 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.GetActivityTypeByObjKey(ctx, objectKey, "follow")
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@ -116,7 +118,7 @@ func (ar *FollowRepo) IsFollowed(ctx context.Context, userID, objectID string) (
return false, err
}
activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow")
activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
if err != nil {
return false, err
}

View File

@ -2,14 +2,11 @@ package answer
import (
"context"
"strings"
"time"
"unicode"
"xorm.io/builder"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
@ -58,8 +55,10 @@ func (ar *answerRepo) AddAnswer(ctx context.Context, answer *entity.Answer) (err
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
answer.ID = uid.EnShortID(answer.ID)
answer.QuestionID = uid.EnShortID(answer.QuestionID)
if handler.GetEnableShortID(ctx) {
answer.ID = uid.EnShortID(answer.ID)
answer.QuestionID = uid.EnShortID(answer.QuestionID)
}
return nil
}
@ -109,9 +108,10 @@ func (ar *answerRepo) GetAnswer(ctx context.Context, id string) (
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
answer.ID = uid.EnShortID(answer.ID)
answer.QuestionID = uid.EnShortID(answer.QuestionID)
if handler.GetEnableShortID(ctx) {
answer.ID = uid.EnShortID(answer.ID)
answer.QuestionID = uid.EnShortID(answer.QuestionID)
}
return
}
@ -134,9 +134,11 @@ func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, item := range answerList {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
if handler.GetEnableShortID(ctx) {
for _, item := range answerList {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
}
return
}
@ -150,9 +152,11 @@ func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, ans
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, item := range answerList {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
if handler.GetEnableShortID(ctx) {
for _, item := range answerList {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
}
return
}
@ -191,8 +195,10 @@ func (ar *answerRepo) GetByID(ctx context.Context, id string) (*entity.Answer, b
if err != nil {
return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
resp.ID = uid.EnShortID(resp.ID)
resp.QuestionID = uid.EnShortID(resp.QuestionID)
if handler.GetEnableShortID(ctx) {
resp.ID = uid.EnShortID(resp.ID)
resp.QuestionID = uid.EnShortID(resp.QuestionID)
}
return &resp, has, nil
}
@ -222,8 +228,10 @@ func (ar *answerRepo) GetByUserIDQuestionID(ctx context.Context, userID string,
if err != nil {
return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
resp.ID = uid.EnShortID(resp.ID)
resp.QuestionID = uid.EnShortID(resp.QuestionID)
if handler.GetEnableShortID(ctx) {
resp.ID = uid.EnShortID(resp.ID)
resp.QuestionID = uid.EnShortID(resp.QuestionID)
}
return &resp, has, nil
}
@ -274,87 +282,40 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
if err != nil {
return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, item := range rows {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
if handler.GetEnableShortID(ctx) {
for _, item := range rows {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
}
return rows, count, nil
}
func (ar *answerRepo) AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error) {
var (
count int64
err error
session = ar.data.DB.Context(ctx).Table([]string{entity.Answer{}.TableName(), "a"}).Select("a.*")
)
if search.QuestionID != "" {
search.QuestionID = uid.DeShortID(search.QuestionID)
}
session.Where(builder.Eq{
"a.status": search.Status,
})
rows := make([]*entity.Answer, 0)
if search.Page > 0 {
search.Page = search.Page - 1
} else {
search.Page = 0
}
if search.PageSize == 0 {
search.PageSize = constant.DefaultPageSize
}
// search by question title like or answer id
if len(search.Query) > 0 {
// check id search
var (
idSearch = false
id = ""
)
if strings.Contains(search.Query, "answer:") {
idSearch = true
id = strings.TrimSpace(strings.TrimPrefix(search.Query, "answer:"))
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.Join("LEFT", []string{entity.Question{}.TableName(), "q"}, "q.id = a.question_id")
session.And(builder.Like{
"q.title", search.Query,
})
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+"%")
}
}
// check search by question id
if len(search.QuestionID) > 0 {
session.And(builder.Eq{
"question_id": search.QuestionID,
})
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")
offset := search.Page * search.PageSize
session.
OrderBy("a.created_at desc").
Limit(search.PageSize, offset)
count, err = session.FindAndCount(&rows)
resp = make([]*entity.Answer, 0)
total, err = pager.Help(req.Page, req.PageSize, &resp, cond, session)
if err != nil {
return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return nil, 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, item := range rows {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
return rows, count, nil
return resp, total, nil
}

View File

@ -37,7 +37,7 @@ func (cr configRepo) GetConfigByID(ctx context.Context, id int) (c *entity.Confi
}
c = &entity.Config{}
exist, err := cr.data.DB.ID(id).Get(c)
exist, err := cr.data.DB.Context(ctx).ID(id).Get(c)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@ -80,7 +80,7 @@ func (cr configRepo) GetConfigByKey(ctx context.Context, key string) (c *entity.
func (cr configRepo) UpdateConfig(ctx context.Context, key string, value string) (err error) {
// check if key exists
oldConfig := &entity.Config{}
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()

View File

@ -52,7 +52,6 @@ var ProviderSetRepo = wire.NewSet(
activity.NewVoteRepo,
activity.NewFollowRepo,
activity.NewAnswerActivityRepo,
activity.NewQuestionActivityRepo,
activity.NewUserActiveActivityRepo,
activity.NewActivityRepo,
tag.NewTagRepo,

View File

@ -2,11 +2,14 @@ package question
import (
"context"
"encoding/json"
"fmt"
"github.com/segmentfault/pacman/log"
"strings"
"time"
"unicode"
"github.com/answerdev/answer/internal/base/handler"
"xorm.io/builder"
"github.com/answerdev/answer/internal/base/constant"
@ -50,7 +53,9 @@ func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Questi
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
question.ID = uid.EnShortID(question.ID)
if handler.GetEnableShortID(ctx) {
question.ID = uid.EnShortID(question.ID)
}
return
}
@ -71,7 +76,9 @@ func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Que
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
question.ID = uid.EnShortID(question.ID)
if handler.GetEnableShortID(ctx) {
question.ID = uid.EnShortID(question.ID)
}
return
}
@ -164,7 +171,9 @@ func (qr *questionRepo) GetQuestion(ctx context.Context, id string) (
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
question.ID = uid.EnShortID(question.ID)
if handler.GetEnableShortID(ctx) {
question.ID = uid.EnShortID(question.ID)
}
return
}
@ -175,8 +184,10 @@ func (qr *questionRepo) SearchByTitleLike(ctx context.Context, title string) (qu
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
if handler.GetEnableShortID(ctx) {
for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
}
}
return
}
@ -190,8 +201,10 @@ func (qr *questionRepo) FindByID(ctx context.Context, id []string) (questionList
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
if handler.GetEnableShortID(ctx) {
for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
}
}
return
}
@ -211,13 +224,13 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu
}
func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) {
questionList := make([]*entity.Question, 0)
count, err = qr.data.DB.Context(ctx).In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).FindAndCount(&questionList)
session := qr.data.DB.Context(ctx)
session.Where("status = ? OR status = ?", entity.QuestionStatusAvailable, entity.QuestionStatusClosed)
count, err = session.Count(&entity.Question{Show: entity.QuestionShow})
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
return count, nil
}
func (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) {
@ -238,38 +251,52 @@ func (qr *questionRepo) GetQuestionCountByIDs(ctx context.Context, ids []string)
return
}
func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) {
func (qr *questionRepo) SitemapQuestions(ctx context.Context, page, pageSize int) (
questionIDList []*schema.SiteMapQuestionInfo, err error) {
page = page - 1
questionIDList = make([]*schema.SiteMapQuestionInfo, 0)
// try to get sitemap data from cache
cacheKey := fmt.Sprintf(constant.SiteMapQuestionCacheKeyPrefix, page)
cacheData, err := qr.data.Cache.GetString(ctx, cacheKey)
if err == nil && len(cacheKey) > 0 {
_ = json.Unmarshal([]byte(cacheData), &questionIDList)
return questionIDList, nil
}
// get sitemap data from db
rows := make([]*entity.Question, 0)
if page > 0 {
page = page - 1
} else {
page = 0
}
if pageSize == 0 {
pageSize = constant.DefaultPageSize
}
offset := page * pageSize
session := qr.data.DB.Context(ctx).Table("question")
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
session.And("question.show = ?", entity.QuestionShow)
session = session.Limit(pageSize, offset)
session = session.OrderBy("question.created_at asc")
err = session.Select("id,title,created_at,post_update_time").Find(&rows)
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{}
item.ID = uid.EnShortID(question.ID)
item.Title = htmltext.UrlTitle(question.Title)
updateTime := fmt.Sprintf("%v", question.PostUpdateTime.Format(time.RFC3339))
if question.PostUpdateTime.Unix() < 1 {
updateTime = fmt.Sprintf("%v", question.CreatedAt.Format(time.RFC3339))
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)
}
item.UpdateTime = updateTime
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
}
@ -312,13 +339,15 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
if handler.GetEnableShortID(ctx) {
for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
}
}
return questionList, total, err
}
func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error) {
func (qr *questionRepo) AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error) {
var (
count int64
err error
@ -379,8 +408,10 @@ func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.Admi
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return rows, count, err
}
for _, item := range rows {
item.ID = uid.EnShortID(item.ID)
if handler.GetEnableShortID(ctx) {
for _, item := range rows {
item.ID = uid.EnShortID(item.ID)
}
}
return rows, count, nil
}

View File

@ -31,6 +31,56 @@ func NewUserRankRepo(data *data.Data, configService *config.ConfigService) rank.
}
}
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.Sum(&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
@ -38,10 +88,7 @@ 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() {
return false, nil
}
if deltaRank == 0 {
if plugin.RankAgentEnabled() || deltaRank == 0 {
return false, nil
}
@ -114,7 +161,7 @@ func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context,
LessVal: start,
MoreVal: end,
})
earned, err := session.Sum(&entity.Activity{}, "rank")
earned, err := session.Sum(&entity.Activity{}, "`rank`")
if err != nil {
return false, err
}
@ -137,7 +184,7 @@ func (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, p
) {
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 := 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}

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
@ -36,8 +37,10 @@ func (tr *tagRelRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRe
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, item := range tagList {
item.ObjectID = uid.EnShortID(item.ObjectID)
if handler.GetEnableShortID(ctx) {
for _, item := range tagList {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
}
return
}
@ -54,7 +57,7 @@ func (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID s
func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID)
_, err = tr.data.DB.Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide})
_, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@ -63,7 +66,7 @@ func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID str
func (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID)
_, err = tr.data.DB.Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})
_, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@ -89,8 +92,11 @@ func (tr *tagRelRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectID
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)
}
tagRel.ObjectID = uid.EnShortID(tagRel.ObjectID)
return
}
@ -112,9 +118,12 @@ func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string)
err = session.Find(&tagListList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
for _, item := range tagListList {
item.ObjectID = uid.EnShortID(item.ObjectID)
if handler.GetEnableShortID(ctx) {
for _, item := range tagListList {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
}
return
}
@ -130,9 +139,12 @@ func (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []
err = session.Find(&tagListList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
for _, item := range tagListList {
item.ObjectID = uid.EnShortID(item.ObjectID)
if handler.GetEnableShortID(ctx) {
for _, item := range tagListList {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
}
return
}

View File

@ -72,7 +72,7 @@ func (ur *userRepo) IncreaseQuestionCount(ctx context.Context, userID string, am
func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) {
user := &entity.User{}
user.QuestionCount = int(count)
_, err = ur.data.DB.Where("id = ?", userID).Cols("question_count").Update(user)
_, 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()
}
@ -82,7 +82,7 @@ func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, coun
func (ur *userRepo) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) {
user := &entity.User{}
user.AnswerCount = count
_, err = ur.data.DB.Where("id = ?", userID).Cols("answer_count").Update(user)
_, 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()
}
@ -195,7 +195,7 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) {
list := make([]*entity.User, 0)
err := ur.data.DB.Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list)
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

View File

@ -26,7 +26,7 @@ type AnswerAPIRouter struct {
reasonController *controller.ReasonController
themeController *controller_admin.ThemeController
siteInfoController *controller_admin.SiteInfoController
siteinfoController *controller.SiteinfoController
siteinfoController *controller.SiteInfoController
notificationController *controller.NotificationController
dashboardController *controller.DashboardController
uploadController *controller.UploadController
@ -55,7 +55,7 @@ func NewAnswerAPIRouter(
reasonController *controller.ReasonController,
themeController *controller_admin.ThemeController,
siteInfoController *controller_admin.SiteInfoController,
siteinfoController *controller.SiteinfoController,
siteinfoController *controller.SiteInfoController,
notificationController *controller.NotificationController,
dashboardController *controller.DashboardController,
uploadController *controller.UploadController,
@ -244,9 +244,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
}
func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
r.GET("/question/page", a.questionController.AdminSearchList)
r.GET("/question/page", a.questionController.AdminQuestionPage)
r.PUT("/question/status", a.questionController.AdminSetQuestionStatus)
r.GET("/answer/page", a.questionController.AdminSearchAnswerList)
r.GET("/answer/page", a.questionController.AdminAnswerPage)
r.PUT("/answer/status", a.answerController.AdminSetAnswerStatus)
// report

View File

@ -17,7 +17,6 @@ func NewTemplateRouter(
templateController *controller.TemplateController,
templateRenderController *templaterender.TemplateRenderController,
siteInfoController *controller_admin.SiteInfoController,
) *TemplateRouter {
return &TemplateRouter{
templateController: templateController,
@ -26,7 +25,7 @@ func NewTemplateRouter(
}
}
// TemplateRouter template router
// RegisterTemplateRouter template router
func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) {
r.GET("/sitemap.xml", a.templateController.Sitemap)
r.GET("/sitemap/:page", a.templateController.SitemapPage)
@ -35,7 +34,6 @@ func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) {
r.GET("/custom.css", a.siteInfoController.GetCss)
r.GET("/", a.templateController.Index)
r.GET("/index", a.templateController.Index)
r.GET("/questions", a.templateController.QuestionList)
r.GET("/questions/:id", a.templateController.QuestionInfo)

View File

@ -21,14 +21,14 @@ const UIStaticPath = "build/static"
// UIRouter is an interface that provides ui static file routers
type UIRouter struct {
siteInfoController *controller.SiteinfoController
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoController *controller.SiteInfoController
siteInfoService siteinfo_common.SiteInfoCommonService
}
// NewUIRouter creates a new UIRouter instance with the embed resources
func NewUIRouter(
siteInfoController *controller.SiteinfoController,
siteInfoService *siteinfo_common.SiteInfoCommonService,
siteInfoController *controller.SiteInfoController,
siteInfoService siteinfo_common.SiteInfoCommonService,
) *UIRouter {
return &UIRouter{
siteInfoController: siteInfoController,

View File

@ -4,12 +4,13 @@ import "github.com/answerdev/answer/internal/base/constant"
// ActivityMsg activity message
type ActivityMsg struct {
UserID string `json:"user_id"`
TriggerUserID int64 `json:"trigger_user_id"`
ObjectID string `json:"object_id"`
OriginalObjectID string `json:"original_object_id"`
ActivityTypeKey constant.ActivityTypeKey `json:"activity_type_key"`
RevisionID string `json:"revision_id"`
UserID string
TriggerUserID int64
ObjectID string
OriginalObjectID string
ActivityTypeKey constant.ActivityTypeKey
RevisionID string
ExtraInfo map[string]string
}
// GetObjectTimelineReq get object timeline request

View File

@ -0,0 +1,35 @@
package schema
// AcceptAnswerOperationInfo accept answer operation info
type AcceptAnswerOperationInfo struct {
QuestionObjectID string
QuestionUserID string
AnswerObjectID string
AnswerUserID string
// vote activity info
Activities []*AcceptAnswerActivity
}
// AcceptAnswerActivity accept answer activity
type AcceptAnswerActivity struct {
ActivityType int
ActivityUserID string
TriggerUserID string
OriginalObjectID string
Rank int
}
func (v *AcceptAnswerActivity) HasRank() int {
if v.Rank != 0 {
return 1
}
return 0
}
func (a *AcceptAnswerOperationInfo) GetUserIDs() (userIDs []string) {
for _, act := range a.Activities {
userIDs = append(userIDs, act.ActivityUserID)
}
return userIDs
}

View File

@ -5,8 +5,8 @@ import "time"
var AppStartTime time.Time
const (
DashBoardCachekey = "answer@dashboard"
DashBoardCacheTime = 60 * time.Minute
DashboardCacheKey = "answer:dashboard"
DashboardCacheTime = 60 * time.Minute
)
type DashboardInfo struct {

View File

@ -63,6 +63,8 @@ type NotificationMsg struct {
NotificationAction string
// if true no need to send notification to all followers
NoNeedPushAllFollow bool
// extra info
ExtraInfo map[string]string
}
type ObjectInfo struct {

View File

@ -1,16 +1,16 @@
package schema
import (
"strings"
"time"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/uid"
)
const (
SitemapMaxSize = 50000
SitemapCachekey = "answer@sitemap"
SitemapPageCachekey = "answer@sitemap@page%d"
QuestionOperationPin = "pin"
QuestionOperationUnPin = "unpin"
QuestionOperationHide = "hide"
@ -361,12 +361,62 @@ type QuestionPageRespOperator struct {
DisplayName string `json:"display_name"`
}
type AdminQuestionSearch struct {
Page int `json:"page" form:"page"` // Query number of pages
PageSize int `json:"page_size" form:"page_size"` // Search page size
Status int `json:"-" form:"-"`
StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 UserDeleted
Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string
type AdminQuestionPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
StatusCond string `validate:"omitempty,oneof=normal closed deleted" form:"status"`
Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" `
Status int `json:"-"`
LoginUserID string `json:"-"`
}
func (req *AdminQuestionPageReq) Check() (errField []*validator.FormErrorField, err error) {
status, ok := entity.AdminQuestionSearchStatus[req.StatusCond]
if ok {
req.Status = status
}
if req.Status == 0 {
req.Status = 1
}
return nil, nil
}
// AdminAnswerPageReq admin answer page req
type AdminAnswerPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
StatusCond string `validate:"omitempty,oneof=normal deleted" form:"status"`
Query string `validate:"omitempty,gt=0,lte=100" form:"query"`
QuestionID string `validate:"omitempty,gt=0,lte=24" form:"question_id"`
QuestionTitle string `json:"-"`
AnswerID string `json:"-"`
Status int `json:"-"`
LoginUserID string `json:"-"`
}
func (req *AdminAnswerPageReq) Check() (errField []*validator.FormErrorField, err error) {
req.QuestionID = uid.DeShortID(req.QuestionID)
if req.QuestionID == "0" {
req.QuestionID = ""
}
if status, ok := entity.AdminAnswerSearchStatus[req.StatusCond]; ok {
req.Status = status
}
if req.Status == 0 {
req.Status = 1
}
// parse query condition
if len(req.Query) > 0 {
prefix := "answer:"
if strings.Contains(req.Query, prefix) {
req.AnswerID = uid.DeShortID(strings.TrimSpace(strings.TrimPrefix(req.Query, prefix)))
} else {
req.QuestionTitle = strings.TrimSpace(req.Query)
}
}
return nil, nil
}
type AdminSetQuestionStatusRequest struct {
@ -374,21 +424,6 @@ type AdminSetQuestionStatusRequest struct {
QuestionID string `json:"question_id" form:"question_id"`
}
type SiteMapList struct {
QuestionIDs []*SiteMapQuestionInfo `json:"question_ids"`
MaxPageNum []int `json:"max_page_num"`
}
type SiteMapPageList struct {
PageData []*SiteMapQuestionInfo `json:"page_data"`
}
type SiteMapQuestionInfo struct {
ID string `json:"id"`
Title string `json:"title"`
UpdateTime string `json:"time"`
}
type PersonalQuestionPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`

View File

@ -14,11 +14,6 @@ import (
"github.com/segmentfault/pacman/errors"
)
const PermaLinkQuestionIDAndTitle = 1 // /questions/10010000000000001/post-title
const PermaLinkQuestionID = 2 // /questions/10010000000000001
const PermaLinkQuestionIDAndTitleByShortID = 3 // /questions/11/post-title
const PermaLinkQuestionIDByShortID = 4 // /questions/11
// SiteGeneralReq site general request
type SiteGeneralReq struct {
Name string `validate:"required,sanitizer,gt=1,lte=128" form:"name" json:"name"`
@ -28,11 +23,6 @@ type SiteGeneralReq struct {
ContactEmail string `validate:"required,sanitizer,gt=1,lte=512,email" form:"contact_email" json:"contact_email"`
}
type SiteSeoReq struct {
PermaLink int `validate:"required,lte=4,gte=0" form:"permalink" json:"permalink"`
Robots string `validate:"required" form:"robots" json:"robots"`
}
func (r *SiteGeneralReq) FormatSiteUrl() {
parsedUrl, err := url.Parse(r.SiteUrl)
if err != nil {
@ -127,6 +117,16 @@ type SiteThemeReq struct {
ThemeConfig map[string]interface{} `validate:"omitempty" json:"theme_config"`
}
type SiteSeoReq struct {
PermaLink int `validate:"required,lte=4,gte=0" form:"permalink" json:"permalink"`
Robots string `validate:"required" form:"robots" json:"robots"`
}
func (s *SiteSeoResp) IsShortLink() bool {
return s.PermaLink == constant.PermaLinkQuestionIDAndTitleByShortID ||
s.PermaLink == constant.PermaLinkQuestionIDByShortID
}
// SiteGeneralResp site general response
type SiteGeneralResp SiteGeneralReq
@ -186,7 +186,7 @@ type SiteInfoResp struct {
Login *SiteLoginResp `json:"login"`
Theme *SiteThemeResp `json:"theme"`
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
SiteSeo *SiteSeoReq `json:"site_seo"`
SiteSeo *SiteSeoResp `json:"site_seo"`
SiteUsers *SiteUsersResp `json:"site_users"`
Version string `json:"version"`
Revision string `json:"revision"`
@ -195,7 +195,7 @@ type TemplateSiteInfoResp struct {
General *SiteGeneralResp `json:"general"`
Interface *SiteInterfaceResp `json:"interface"`
Branding *SiteBrandingResp `json:"branding"`
SiteSeo *SiteSeoReq `json:"site_seo"`
SiteSeo *SiteSeoResp `json:"site_seo"`
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
Title string
Year string

View File

@ -0,0 +1,16 @@
package schema
type SiteMapList struct {
QuestionIDs []*SiteMapQuestionInfo `json:"question_ids"`
MaxPageNum []int `json:"max_page_num"`
}
type SiteMapPageList struct {
PageData []*SiteMapQuestionInfo `json:"page_data"`
}
type SiteMapQuestionInfo struct {
ID string `json:"id"`
Title string `json:"title"`
UpdateTime string `json:"time"`
}

View File

@ -6,20 +6,44 @@ type VoteReq struct {
UserID string `json:"-"`
}
type VoteDTO struct {
// object TagID
ObjectID string
// is cancel
IsCancel bool
// user TagID
UserID string
type VoteResp struct {
UpVotes int64 `json:"up_votes"`
DownVotes int64 `json:"down_votes"`
Votes int64 `json:"votes"`
VoteStatus string `json:"vote_status"`
}
type VoteResp struct {
UpVotes int `json:"up_votes"`
DownVotes int `json:"down_votes"`
Votes int `json:"votes"`
VoteStatus string `json:"vote_status"`
// VoteOperationInfo vote operation info
type VoteOperationInfo struct {
// operation object id
ObjectID string
// question answer comment
ObjectType string
// object owner user id
ObjectCreatorUserID string
// operation user id
OperatingUserID string
// vote up
VoteUp bool
// vote down
VoteDown bool
// vote activity info
Activities []*VoteActivity
}
// VoteActivity vote activity
type VoteActivity struct {
ActivityType int
ActivityUserID string
TriggerUserID string
Rank int
}
func (v *VoteActivity) HasRank() int {
if v.Rank != 0 {
return 1
}
return 0
}
type GetVoteWithPageReq struct {
@ -28,23 +52,7 @@ type GetVoteWithPageReq struct {
// page size
PageSize int `validate:"omitempty,min=1" form:"page_size"`
// user id
UserID string `validate:"required" form:"user_id"`
}
type VoteQuestion struct {
// object ID
ID string `json:"id"`
// title
Title string `json:"title"`
}
type VoteAnswer struct {
// object ID
ID string `json:"id"`
// question ID
QuestionID string `json:"question_id"`
// title
Title string `json:"title"`
UserID string `json:"-"`
}
type GetVoteWithPageResp struct {

View File

@ -7,9 +7,9 @@ import (
"strings"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/comment_common"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/meta"
@ -30,22 +30,20 @@ type ActivityRepo interface {
// ActivityService activity service
type ActivityService struct {
activityRepo ActivityRepo
userCommon *usercommon.UserCommon
activityCommonService *activity_common.ActivityCommon
tagCommonService *tag_common.TagCommonService
objectInfoService *object_info.ObjService
commentCommonService *comment_common.CommentCommonService
revisionService *revision_common.RevisionService
metaService *meta.MetaService
configService *config.ConfigService
activityRepo ActivityRepo
userCommon *usercommon.UserCommon
tagCommonService *tag_common.TagCommonService
objectInfoService *object_info.ObjService
commentCommonService *comment_common.CommentCommonService
revisionService *revision_common.RevisionService
metaService *meta.MetaService
configService *config.ConfigService
}
// NewActivityService new activity service
func NewActivityService(
activityRepo ActivityRepo,
userCommon *usercommon.UserCommon,
activityCommonService *activity_common.ActivityCommon,
tagCommonService *tag_common.TagCommonService,
objectInfoService *object_info.ObjService,
commentCommonService *comment_common.CommentCommonService,
@ -54,15 +52,14 @@ func NewActivityService(
configService *config.ConfigService,
) *ActivityService {
return &ActivityService{
objectInfoService: objectInfoService,
activityRepo: activityRepo,
userCommon: userCommon,
activityCommonService: activityCommonService,
tagCommonService: tagCommonService,
commentCommonService: commentCommonService,
revisionService: revisionService,
metaService: metaService,
configService: configService,
objectInfoService: objectInfoService,
activityRepo: activityRepo,
userCommon: userCommon,
tagCommonService: tagCommonService,
commentCommonService: commentCommonService,
revisionService: revisionService,
metaService: metaService,
configService: configService,
}
}
@ -97,7 +94,9 @@ func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.Ge
}
if item.ObjectType == constant.QuestionObjectType || item.ObjectType == constant.AnswerObjectType {
item.ObjectID = uid.EnShortID(act.ObjectID)
if handler.GetEnableShortID(ctx) {
item.ObjectID = uid.EnShortID(act.ObjectID)
}
}
cfg, err := as.configService.GetConfigByID(ctx, act.ActivityType)

View File

@ -1,77 +0,0 @@
package activity
import (
"context"
"time"
"github.com/segmentfault/pacman/log"
)
// AnswerActivityRepo answer activity
type AnswerActivityRepo interface {
AcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error)
CancelAcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string) (err error)
DeleteAnswer(ctx context.Context, answerID string) (err error)
}
// QuestionActivityRepo answer activity
type QuestionActivityRepo interface {
DeleteQuestion(ctx context.Context, questionID string) (err error)
}
// AnswerActivityService user service
type AnswerActivityService struct {
answerActivityRepo AnswerActivityRepo
questionActivityRepo QuestionActivityRepo
}
// NewAnswerActivityService new comment service
func NewAnswerActivityService(
answerActivityRepo AnswerActivityRepo, questionActivityRepo QuestionActivityRepo) *AnswerActivityService {
return &AnswerActivityService{
answerActivityRepo: answerActivityRepo,
questionActivityRepo: questionActivityRepo,
}
}
// AcceptAnswer accept answer change activity
func (as *AnswerActivityService) AcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) {
return as.answerActivityRepo.AcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID, isSelf)
}
// CancelAcceptAnswer cancel accept answer change activity
func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string) (err error) {
return as.answerActivityRepo.CancelAcceptAnswer(ctx, answerObjID, questionObjID, questionUserID, answerUserID)
}
// DeleteAnswer delete answer change activity
func (as *AnswerActivityService) DeleteAnswer(ctx context.Context, answerID string, createdAt time.Time,
voteCount int) (err error) {
if voteCount >= 3 {
log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", answerID, voteCount)
return nil
}
if createdAt.Before(time.Now().AddDate(0, 0, -60)) {
log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", answerID, createdAt.String())
return nil
}
return as.answerActivityRepo.DeleteAnswer(ctx, answerID)
}
// DeleteQuestion delete question change activity
func (as *AnswerActivityService) DeleteQuestion(ctx context.Context, questionID string, createdAt time.Time,
voteCount int) (err error) {
if voteCount >= 3 {
log.Infof("There is no need to roll back the reputation by answering likes above the target value. %s %d", questionID, voteCount)
return nil
}
if createdAt.Before(time.Now().AddDate(0, 0, -60)) {
log.Infof("There is no need to roll back the reputation by answer's existence time meets the target. %s %s", questionID, createdAt.String())
return nil
}
return as.questionActivityRepo.DeleteQuestion(ctx, questionID)
}

View File

@ -0,0 +1,92 @@
package activity
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_type"
"github.com/answerdev/answer/internal/service/config"
"github.com/segmentfault/pacman/log"
)
// AnswerActivityRepo answer activity
type AnswerActivityRepo interface {
SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error)
SaveCancelAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (err error)
}
// AnswerActivityService answer activity service
type AnswerActivityService struct {
answerActivityRepo AnswerActivityRepo
configService *config.ConfigService
}
// NewAnswerActivityService new comment service
func NewAnswerActivityService(
answerActivityRepo AnswerActivityRepo,
configService *config.ConfigService,
) *AnswerActivityService {
return &AnswerActivityService{
answerActivityRepo: answerActivityRepo,
configService: configService,
}
}
// AcceptAnswer accept answer change activity
func (as *AnswerActivityService) AcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) (err error) {
operationInfo := as.createAcceptAnswerOperationInfo(ctx,
answerObjID, questionObjID, questionUserID, answerUserID, isSelf)
return as.answerActivityRepo.SaveAcceptAnswerActivity(ctx, operationInfo)
}
// CancelAcceptAnswer cancel accept answer change activity
func (as *AnswerActivityService) CancelAcceptAnswer(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string) (err error) {
operationInfo := as.createAcceptAnswerOperationInfo(ctx,
answerObjID, questionObjID, questionUserID, answerUserID, false)
return as.answerActivityRepo.SaveCancelAcceptAnswerActivity(ctx, operationInfo)
}
func (as *AnswerActivityService) createAcceptAnswerOperationInfo(ctx context.Context,
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool) *schema.AcceptAnswerOperationInfo {
operationInfo := &schema.AcceptAnswerOperationInfo{
QuestionObjectID: questionObjID,
QuestionUserID: questionUserID,
AnswerObjectID: answerObjID,
AnswerUserID: answerUserID,
}
operationInfo.Activities = as.getActivities(ctx, operationInfo)
if isSelf {
for _, activity := range operationInfo.Activities {
activity.Rank = 0
}
}
return operationInfo
}
func (as *AnswerActivityService) getActivities(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (
activities []*schema.AcceptAnswerActivity) {
activities = make([]*schema.AcceptAnswerActivity, 0)
for _, action := range []string{activity_type.AnswerAccept, activity_type.AnswerAccepted} {
t := &schema.AcceptAnswerActivity{}
cfg, err := as.configService.GetConfigByKey(ctx, action)
if err != nil {
log.Warnf("get config by key error: %v", err)
continue
}
t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue()
if action == activity_type.AnswerAccept {
t.ActivityUserID = op.QuestionUserID
t.TriggerUserID = op.AnswerUserID
t.OriginalObjectID = op.QuestionObjectID // if activity is 'accept' means this question is accept the answer.
} else {
t.ActivityUserID = op.AnswerUserID
t.TriggerUserID = op.AnswerUserID
t.OriginalObjectID = op.AnswerObjectID // if activity is 'accepted' means this answer was accepted.
}
activities = append(activities, t)
}
return activities
}

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/uid"
@ -14,7 +15,7 @@ import (
type ActivityRepo interface {
GetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank int, hasRank int, err error)
GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error)
GetActivityTypeByObjectType(ctx context.Context, objectKey, action string) (activityType int, err error)
GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) (
existsActivity *entity.Activity, exist bool, err error)
GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error)
@ -27,51 +28,44 @@ type ActivityRepo interface {
}
type ActivityCommon struct {
activityRepo ActivityRepo
activityRepo ActivityRepo
activityQueueService activity_queue.ActivityQueueService
}
// NewActivityCommon new activity common
func NewActivityCommon(
activityRepo ActivityRepo,
activityQueueService activity_queue.ActivityQueueService,
) *ActivityCommon {
activity := &ActivityCommon{
activityRepo: activityRepo,
activityRepo: activityRepo,
activityQueueService: activityQueueService,
}
activity.HandleActivity()
activity.activityQueueService.RegisterHandler(activity.HandleActivity)
return activity
}
// HandleActivity handle activity message
func (ac *ActivityCommon) HandleActivity() {
go func() {
defer func() {
if err := recover(); err != nil {
log.Error(err)
}
}()
func (ac *ActivityCommon) HandleActivity(ctx context.Context, msg *schema.ActivityMsg) error {
activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(ctx, string(msg.ActivityTypeKey))
if err != nil {
log.Errorf("error getting activity type %s, activity type is %d", err, activityType)
return err
}
for msg := range activity_queue.ActivityQueue {
log.Debugf("received activity %+v", msg)
activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(context.Background(), string(msg.ActivityTypeKey))
if err != nil {
log.Errorf("error getting activity type %s, activity type is %d", err, activityType)
}
act := &entity.Activity{
UserID: msg.UserID,
TriggerUserID: msg.TriggerUserID,
ObjectID: uid.DeShortID(msg.ObjectID),
OriginalObjectID: uid.DeShortID(msg.OriginalObjectID),
ActivityType: activityType,
Cancelled: entity.ActivityAvailable,
}
if len(msg.RevisionID) > 0 {
act.RevisionID = converter.StringToInt64(msg.RevisionID)
}
if err := ac.activityRepo.AddActivity(context.TODO(), act); err != nil {
log.Error(err)
}
}
}()
act := &entity.Activity{
UserID: msg.UserID,
TriggerUserID: msg.TriggerUserID,
ObjectID: uid.DeShortID(msg.ObjectID),
OriginalObjectID: uid.DeShortID(msg.OriginalObjectID),
ActivityType: activityType,
Cancelled: entity.ActivityAvailable,
}
if len(msg.RevisionID) > 0 {
act.RevisionID = converter.StringToInt64(msg.RevisionID)
}
if err := ac.activityRepo.AddActivity(ctx, act); err != nil {
return err
}
return nil
}

View File

@ -1,14 +1,50 @@
package activity_queue
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/log"
)
var (
ActivityQueue = make(chan *schema.ActivityMsg, 128)
)
// AddActivity add new activity
func AddActivity(msg *schema.ActivityMsg) {
ActivityQueue <- msg
type ActivityQueueService interface {
Send(ctx context.Context, msg *schema.ActivityMsg)
RegisterHandler(handler func(ctx context.Context, msg *schema.ActivityMsg) error)
}
type activityQueueService struct {
Queue chan *schema.ActivityMsg
Handler func(ctx context.Context, msg *schema.ActivityMsg) error
}
func (ns *activityQueueService) Send(ctx context.Context, msg *schema.ActivityMsg) {
ns.Queue <- msg
}
func (ns *activityQueueService) RegisterHandler(
handler func(ctx context.Context, msg *schema.ActivityMsg) error) {
ns.Handler = handler
}
func (ns *activityQueueService) working() {
go func() {
for msg := range ns.Queue {
log.Debugf("received activity %+v", msg)
if ns.Handler == nil {
log.Warnf("no handler for activity")
continue
}
if err := ns.Handler(context.Background(), msg); err != nil {
log.Error(err)
}
}
}()
}
// NewActivityQueueService create a new activity queue service
func NewActivityQueueService() ActivityQueueService {
ns := &activityQueueService{}
ns.Queue = make(chan *schema.ActivityMsg, 128)
ns.working()
return ns
}

View File

@ -3,9 +3,11 @@ package answercommon
import (
"context"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/uid"
)
type AnswerRepo interface {
@ -21,7 +23,7 @@ type AnswerRepo interface {
GetCountByUserID(ctx context.Context, userID string) (int64, error)
GetByUserIDQuestionID(ctx context.Context, userID string, questionID string) (*entity.Answer, bool, error)
SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error)
AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error)
AdminSearchList(ctx context.Context, search *schema.AdminAnswerPageReq) ([]*entity.Answer, int64, error)
UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error)
GetAnswerCount(ctx context.Context) (count int64, err error)
}
@ -45,11 +47,16 @@ func (as *AnswerCommon) SearchAnswered(ctx context.Context, userID, questionID s
return has, nil
}
func (as *AnswerCommon) AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error) {
if search.Status == 0 {
search.Status = 1
func (as *AnswerCommon) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) (
resp []*entity.Answer, count int64, err error) {
resp, count, err = as.answerRepo.AdminSearchList(ctx, req)
if handler.GetEnableShortID(ctx) {
for _, item := range resp {
item.ID = uid.EnShortID(item.ID)
item.QuestionID = uid.EnShortID(item.QuestionID)
}
}
return as.answerRepo.AdminSearchList(ctx, search)
return resp, count, err
}
func (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) {

View File

@ -31,18 +31,20 @@ import (
// AnswerService user service
type AnswerService struct {
answerRepo answercommon.AnswerRepo
questionRepo questioncommon.QuestionRepo
questionCommon *questioncommon.QuestionCommon
answerActivityService *activity.AnswerActivityService
userCommon *usercommon.UserCommon
collectionCommon *collectioncommon.CollectionCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
AnswerCommon *answercommon.AnswerCommon
voteRepo activity_common.VoteRepo
emailService *export.EmailService
roleService *role.UserRoleRelService
answerRepo answercommon.AnswerRepo
questionRepo questioncommon.QuestionRepo
questionCommon *questioncommon.QuestionCommon
answerActivityService *activity.AnswerActivityService
userCommon *usercommon.UserCommon
collectionCommon *collectioncommon.CollectionCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
AnswerCommon *answercommon.AnswerCommon
voteRepo activity_common.VoteRepo
emailService *export.EmailService
roleService *role.UserRoleRelService
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
}
func NewAnswerService(
@ -58,20 +60,24 @@ func NewAnswerService(
voteRepo activity_common.VoteRepo,
emailService *export.EmailService,
roleService *role.UserRoleRelService,
notificationQueueService notice_queue.NotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
) *AnswerService {
return &AnswerService{
answerRepo: answerRepo,
questionRepo: questionRepo,
userCommon: userCommon,
collectionCommon: collectionCommon,
questionCommon: questionCommon,
userRepo: userRepo,
revisionService: revisionService,
answerActivityService: answerAcceptActivityRepo,
AnswerCommon: answerCommon,
voteRepo: voteRepo,
emailService: emailService,
roleService: roleService,
answerRepo: answerRepo,
questionRepo: questionRepo,
userCommon: userCommon,
collectionCommon: collectionCommon,
questionCommon: questionCommon,
userRepo: userRepo,
revisionService: revisionService,
answerActivityService: answerAcceptActivityRepo,
AnswerCommon: answerCommon,
voteRepo: voteRepo,
emailService: emailService,
roleService: roleService,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
}
}
@ -136,7 +142,7 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
//if err != nil {
// log.Errorf("delete answer activity change failed: %s", err.Error())
//}
activity_queue.AddActivity(&schema.ActivityMsg{
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: answerInfo.ID,
OriginalObjectID: answerInfo.ID,
@ -205,14 +211,14 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (
as.notificationAnswerTheQuestion(ctx, questionInfo.UserID, questionInfo.ID, insertData.ID, req.UserID, questionInfo.Title,
insertData.OriginalText)
activity_queue.AddActivity(&schema.ActivityMsg{
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: insertData.UserID,
ObjectID: insertData.ID,
OriginalObjectID: insertData.ID,
ActivityTypeKey: constant.ActAnswerAnswered,
RevisionID: revisionID,
})
activity_queue.AddActivity(&schema.ActivityMsg{
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: insertData.UserID,
ObjectID: insertData.ID,
OriginalObjectID: questionInfo.ID,
@ -305,7 +311,7 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq
return insertData.ID, err
}
if canUpdate {
activity_queue.AddActivity(&schema.ActivityMsg{
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: insertData.UserID,
ObjectID: insertData.ID,
OriginalObjectID: insertData.ID,
@ -472,7 +478,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A
//if err != nil {
// log.Errorf("admin delete question then rank rollback error %s", err.Error())
//}
activity_queue.AddActivity(&schema.ActivityMsg{
as.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: answerInfo.ID,
OriginalObjectID: answerInfo.ID,
@ -487,7 +493,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A
msg.TriggerUserID = answerInfo.UserID
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.NotificationYourAnswerWasDeleted
notice_queue.AddNotification(msg)
as.notificationQueueService.Send(ctx, msg)
return nil
}
@ -579,7 +585,7 @@ func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionU
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.NotificationUpdateAnswer
notice_queue.AddNotification(msg)
as.notificationQueueService.Send(ctx, msg)
}
func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
@ -596,7 +602,7 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.NotificationAnswerTheQuestion
notice_queue.AddNotification(msg)
as.notificationQueueService.Send(ctx, msg)
userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
if err != nil {

View File

@ -58,13 +58,15 @@ func (c *CommentQuery) GetOrderBy() string {
// CommentService user service
type CommentService struct {
commentRepo CommentRepo
commentCommonRepo comment_common.CommentCommonRepo
userCommon *usercommon.UserCommon
voteCommon activity_common.VoteRepo
objectInfoService *object_info.ObjService
emailService *export.EmailService
userRepo usercommon.UserRepo
commentRepo CommentRepo
commentCommonRepo comment_common.CommentCommonRepo
userCommon *usercommon.UserCommon
voteCommon activity_common.VoteRepo
objectInfoService *object_info.ObjService
emailService *export.EmailService
userRepo usercommon.UserRepo
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
}
// NewCommentService new comment service
@ -76,15 +78,19 @@ func NewCommentService(
voteCommon activity_common.VoteRepo,
emailService *export.EmailService,
userRepo usercommon.UserRepo,
notificationQueueService notice_queue.NotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
) *CommentService {
return &CommentService{
commentRepo: commentRepo,
commentCommonRepo: commentCommonRepo,
userCommon: userCommon,
voteCommon: voteCommon,
objectInfoService: objectInfoService,
emailService: emailService,
userRepo: userRepo,
commentRepo: commentRepo,
commentCommonRepo: commentCommonRepo,
userCommon: userCommon,
voteCommon: voteCommon,
objectInfoService: objectInfoService,
emailService: emailService,
userRepo: userRepo,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
}
}
@ -161,7 +167,7 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
case constant.AnswerObjectType:
activityMsg.ActivityTypeKey = constant.ActAnswerCommented
}
activity_queue.AddActivity(activityMsg)
cs.activityQueueService.Send(ctx, activityMsg)
return resp, nil
}
@ -476,7 +482,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationCommentQuestion
notice_queue.AddNotification(msg)
cs.notificationQueueService.Send(ctx, msg)
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
if err != nil {
@ -535,7 +541,7 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationCommentAnswer
notice_queue.AddNotification(msg)
cs.notificationQueueService.Send(ctx, msg)
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
if err != nil {
@ -591,7 +597,7 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationReplyToYou
notice_queue.AddNotification(msg)
cs.notificationQueueService.Send(ctx, msg)
}
func (cs *CommentService) notificationMention(
@ -612,7 +618,7 @@ func (cs *CommentService) notificationMention(
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationMentionYou
notice_queue.AddNotification(msg)
cs.notificationQueueService.Send(ctx, msg)
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
}
}

View File

@ -11,7 +11,6 @@ import (
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
answercommon "github.com/answerdev/answer/internal/service/answer_common"
@ -24,11 +23,10 @@ import (
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/dir"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type DashboardService struct {
type dashboardService struct {
questionRepo questioncommon.QuestionRepo
answerRepo answercommon.AnswerRepo
commentRepo comment_common.CommentCommonRepo
@ -36,10 +34,9 @@ type DashboardService struct {
userRepo usercommon.UserRepo
reportRepo report_common.ReportRepo
configService *config.ConfigService
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoService siteinfo_common.SiteInfoCommonService
serviceConfig *service_config.ServiceConfig
data *data.Data
data *data.Data
}
func NewDashboardService(
@ -50,12 +47,11 @@ func NewDashboardService(
userRepo usercommon.UserRepo,
reportRepo report_common.ReportRepo,
configService *config.ConfigService,
siteInfoService *siteinfo_common.SiteInfoCommonService,
siteInfoService siteinfo_common.SiteInfoCommonService,
serviceConfig *service_config.ServiceConfig,
data *data.Data,
) *DashboardService {
return &DashboardService{
) DashboardService {
return &dashboardService{
questionRepo: questionRepo,
answerRepo: answerRepo,
commentRepo: commentRepo,
@ -65,63 +61,102 @@ func NewDashboardService(
configService: configService,
siteInfoService: siteInfoService,
serviceConfig: serviceConfig,
data: data,
data: data,
}
}
func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.DashboardInfo, error) {
dashboardInfo := &schema.DashboardInfo{}
infoStr, err := ds.data.Cache.GetString(ctx, schema.DashBoardCachekey)
type DashboardService interface {
Statistical(ctx context.Context) (resp *schema.DashboardInfo, err error)
}
func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) {
dashboardInfo, err := ds.getFromCache(ctx)
if err != nil {
info, statisticalErr := ds.Statistical(ctx)
if statisticalErr != nil {
return nil, statisticalErr
}
if setCacheErr := ds.SetCache(ctx, info); setCacheErr != nil {
log.Errorf("set dashboard statistical failed: %s", setCacheErr)
}
return info, nil
dashboardInfo = &schema.DashboardInfo{}
dashboardInfo.QuestionCount = ds.questionCount(ctx)
dashboardInfo.AnswerCount = ds.answerCount(ctx)
dashboardInfo.CommentCount = ds.commentCount(ctx)
dashboardInfo.UserCount = ds.userCount(ctx)
dashboardInfo.ReportCount = ds.reportCount(ctx)
dashboardInfo.VoteCount = ds.voteCount(ctx)
dashboardInfo.OccupyingStorageSpace = ds.calculateStorage()
dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx)
}
if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil {
log.Errorf("parsing dashboard information failed: %s", err)
return nil, errors.InternalServer(reason.UnknownError)
}
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
dashboardInfo.SMTP = ds.smtpStatus(ctx)
dashboardInfo.HTTPS = ds.httpsStatus(ctx)
dashboardInfo.TimeZone = ds.getTimezone(ctx)
dashboardInfo.UploadingFiles = true
dashboardInfo.AppStartTime = fmt.Sprintf("%d", time.Now().Unix()-schema.AppStartTime.Unix())
dashboardInfo.VersionInfo.Version = constant.Version
dashboardInfo.VersionInfo.Revision = constant.Revision
ds.setCache(ctx, dashboardInfo)
return dashboardInfo, nil
}
func (ds *DashboardService) SetCache(ctx context.Context, info *schema.DashboardInfo) error {
infoStr, err := json.Marshal(info)
func (ds *dashboardService) getFromCache(ctx context.Context) (*schema.DashboardInfo, error) {
infoStr, err := ds.data.Cache.GetString(ctx, schema.DashboardCacheKey)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
return nil, err
}
err = ds.data.Cache.SetString(ctx, schema.DashBoardCachekey, string(infoStr), schema.DashBoardCacheTime)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
dashboardInfo := &schema.DashboardInfo{}
if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil {
return nil, err
}
return nil
return dashboardInfo, nil
}
// Statistical
func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) {
dashboardInfo := &schema.DashboardInfo{}
func (ds *dashboardService) setCache(ctx context.Context, info *schema.DashboardInfo) {
infoStr, _ := json.Marshal(info)
err := ds.data.Cache.SetString(ctx, schema.DashboardCacheKey, string(infoStr), schema.DashboardCacheTime)
if err != nil {
log.Errorf("set dashboard statistical failed: %s", err)
}
}
func (ds *dashboardService) questionCount(ctx context.Context) int64 {
questionCount, err := ds.questionRepo.GetQuestionCount(ctx)
if err != nil {
return dashboardInfo, err
log.Errorf("get question count failed: %s", err)
}
return questionCount
}
func (ds *dashboardService) answerCount(ctx context.Context) int64 {
answerCount, err := ds.answerRepo.GetAnswerCount(ctx)
if err != nil {
return dashboardInfo, err
log.Errorf("get answer count failed: %s", err)
}
return answerCount
}
func (ds *dashboardService) commentCount(ctx context.Context) int64 {
commentCount, err := ds.commentRepo.GetCommentCount(ctx)
if err != nil {
return dashboardInfo, err
log.Errorf("get comment count failed: %s", err)
}
return commentCount
}
func (ds *dashboardService) userCount(ctx context.Context) int64 {
userCount, err := ds.userRepo.GetUserCount(ctx)
if err != nil {
log.Errorf("get user count failed: %s", err)
}
return userCount
}
func (ds *dashboardService) reportCount(ctx context.Context) int64 {
reportCount, err := ds.reportRepo.GetReportCount(ctx)
if err != nil {
log.Errorf("get report count failed: %s", err)
}
return reportCount
}
// count vote
func (ds *dashboardService) voteCount(ctx context.Context) int64 {
typeKeys := []string{
"question.vote_up",
"question.vote_down",
@ -129,7 +164,6 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI
"answer.vote_down",
}
var activityTypes []int
for _, typeKey := range typeKeys {
cfg, err := ds.configService.GetConfigByKey(ctx, typeKey)
if err != nil {
@ -137,69 +171,14 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI
}
activityTypes = append(activityTypes, cfg.ID)
}
voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes)
if err != nil {
return dashboardInfo, err
log.Errorf("get vote count failed: %s", err)
}
userCount, err := ds.userRepo.GetUserCount(ctx)
if err != nil {
return dashboardInfo, err
}
reportCount, err := ds.reportRepo.GetReportCount(ctx)
if err != nil {
return dashboardInfo, err
}
siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx)
if err != nil {
return dashboardInfo, err
}
dashboardInfo.QuestionCount = questionCount
dashboardInfo.AnswerCount = answerCount
dashboardInfo.CommentCount = commentCount
dashboardInfo.VoteCount = voteCount
dashboardInfo.UserCount = userCount
dashboardInfo.ReportCount = reportCount
dashboardInfo.UploadingFiles = true
emailconfig, err := ds.GetEmailConfig(ctx)
if err != nil {
return dashboardInfo, err
}
if emailconfig.SMTPHost != "" {
dashboardInfo.SMTP = true
}
siteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
return dashboardInfo, err
}
siteUrl, err := url.Parse(siteGeneral.SiteUrl)
if err != nil {
return dashboardInfo, err
}
if siteUrl.Scheme == "https" {
dashboardInfo.HTTPS = true
}
dirSize, err := dir.DirSize(ds.serviceConfig.UploadPath)
if err != nil {
return dashboardInfo, err
}
size := dir.FormatFileSize(dirSize)
dashboardInfo.OccupyingStorageSpace = size
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
dashboardInfo.TimeZone = siteInfoInterface.TimeZone
dashboardInfo.VersionInfo.Version = constant.Version
dashboardInfo.VersionInfo.Revision = constant.Revision
dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx)
return dashboardInfo, nil
return voteCount
}
func (ds *DashboardService) RemoteVersion(ctx context.Context) string {
func (ds *dashboardService) remoteVersion(ctx context.Context) string {
url := "https://answer.dev/getlatest"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", "Answer/"+constant.Version)
@ -224,15 +203,48 @@ func (ds *DashboardService) RemoteVersion(ctx context.Context) string {
return remoteVersion.Release.Version
}
func (ds *DashboardService) GetEmailConfig(ctx context.Context) (ec *export.EmailConfig, err error) {
func (ds *dashboardService) smtpStatus(ctx context.Context) (enabled bool) {
emailConf, err := ds.configService.GetStringValue(ctx, "email.config")
if err != nil {
return nil, err
log.Errorf("get email config failed: %s", err)
return false
}
ec = &export.EmailConfig{}
ec := &export.EmailConfig{}
err = json.Unmarshal([]byte(emailConf), ec)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
log.Errorf("parsing email config failed: %s", err)
return false
}
return ec, nil
return ec.SMTPHost != ""
}
func (ds *dashboardService) httpsStatus(ctx context.Context) (enabled bool) {
siteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site general failed: %s", err)
return false
}
siteUrl, err := url.Parse(siteGeneral.SiteUrl)
if err != nil {
log.Errorf("parse site url failed: %s", err)
return false
}
return siteUrl.Scheme == "https"
}
func (ds *dashboardService) getTimezone(ctx context.Context) string {
siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx)
if err != nil {
return ""
}
return siteInfoInterface.TimeZone
}
func (ds *dashboardService) calculateStorage() string {
dirSize, err := dir.DirSize(ds.serviceConfig.UploadPath)
if err != nil {
log.Errorf("get upload dir size failed: %s", err)
return ""
}
return dir.FormatFileSize(dirSize)
}

View File

@ -106,6 +106,10 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body str
log.Errorf("get email config failed: %s", err)
return
}
if len(ec.SMTPHost) == 0 {
log.Warnf("smtp host is empty, skip send email")
return
}
m := gomail.NewMessage()
fromName := mime.QEncoding.Encode("utf-8", ec.FromName)

View File

@ -1,13 +1,50 @@
package notice_queue
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/log"
)
var (
NotificationQueue = make(chan *schema.NotificationMsg, 128)
)
func AddNotification(msg *schema.NotificationMsg) {
NotificationQueue <- msg
type NotificationQueueService interface {
Send(ctx context.Context, msg *schema.NotificationMsg)
RegisterHandler(handler func(ctx context.Context, msg *schema.NotificationMsg) error)
}
type notificationQueueService struct {
Queue chan *schema.NotificationMsg
Handler func(ctx context.Context, msg *schema.NotificationMsg) error
}
func (ns *notificationQueueService) Send(ctx context.Context, msg *schema.NotificationMsg) {
ns.Queue <- msg
}
func (ns *notificationQueueService) RegisterHandler(
handler func(ctx context.Context, msg *schema.NotificationMsg) error) {
ns.Handler = handler
}
func (ns *notificationQueueService) working() {
go func() {
for msg := range ns.Queue {
log.Debugf("received notification %+v", msg)
if ns.Handler == nil {
log.Warnf("no handler for notification")
continue
}
if err := ns.Handler(context.Background(), msg); err != nil {
log.Error(err)
}
}
}()
}
// NewNotificationQueueService create a new notification queue service
func NewNotificationQueueService() NotificationQueueService {
ns := &notificationQueueService{}
ns.Queue = make(chan *schema.NotificationMsg, 128)
ns.working()
return ns
}

View File

@ -146,6 +146,7 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) (
resp []*schema.NotificationContent, err error) {
lang := handler.GetLangByCtx(ctx)
enableShortID := handler.GetEnableShortID(ctx)
for _, notificationInfo := range notifications {
item := &schema.NotificationContent{}
if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil {
@ -163,17 +164,19 @@ func (ns *NotificationService) formatNotificationPage(ctx context.Context, notif
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
item.IsRead = notificationInfo.IsRead == schema.NotificationRead
if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok {
if item.ObjectInfo.ObjectID == answerID {
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
if enableShortID {
if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok {
if item.ObjectInfo.ObjectID == answerID {
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
}
item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
}
item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
}
if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok {
if item.ObjectInfo.ObjectID == questionID {
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok {
if item.ObjectInfo.ObjectID == questionID {
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
}
item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
}
item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
}
resp = append(resp, item)

View File

@ -33,12 +33,13 @@ type NotificationRepo interface {
}
type NotificationCommon struct {
data *data.Data
notificationRepo NotificationRepo
activityRepo activity_common.ActivityRepo
followRepo activity_common.FollowRepo
userCommon *usercommon.UserCommon
objectInfoService *object_info.ObjService
data *data.Data
notificationRepo NotificationRepo
activityRepo activity_common.ActivityRepo
followRepo activity_common.FollowRepo
userCommon *usercommon.UserCommon
objectInfoService *object_info.ObjService
notificationQueueService notice_queue.NotificationQueueService
}
func NewNotificationCommon(
@ -48,31 +49,21 @@ func NewNotificationCommon(
activityRepo activity_common.ActivityRepo,
followRepo activity_common.FollowRepo,
objectInfoService *object_info.ObjService,
notificationQueueService notice_queue.NotificationQueueService,
) *NotificationCommon {
notification := &NotificationCommon{
data: data,
notificationRepo: notificationRepo,
activityRepo: activityRepo,
followRepo: followRepo,
userCommon: userCommon,
objectInfoService: objectInfoService,
data: data,
notificationRepo: notificationRepo,
activityRepo: activityRepo,
followRepo: followRepo,
userCommon: userCommon,
objectInfoService: objectInfoService,
notificationQueueService: notificationQueueService,
}
notification.HandleNotification()
notificationQueueService.RegisterHandler(notification.AddNotification)
return notification
}
func (ns *NotificationCommon) HandleNotification() {
go func() {
for msg := range notice_queue.NotificationQueue {
log.Debugf("received notification %+v", msg)
err := ns.AddNotification(context.TODO(), msg)
if err != nil {
log.Error(err)
}
}
}()
}
// AddNotification
// need set
// LoginUserID
@ -172,7 +163,7 @@ func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.N
log.Error("addRedDot Error", err.Error())
}
go ns.SendNotificationToAllFollower(context.Background(), msg, questionID)
go ns.SendNotificationToAllFollower(ctx, msg, questionID)
return nil
}
@ -213,6 +204,6 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context,
t.ReceiverUserID = userID
t.TriggerUserID = msg.TriggerUserID
t.NoNeedPushAllFollow = true
notice_queue.AddNotification(t)
ns.notificationQueueService.Send(ctx, t)
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
answercommon "github.com/answerdev/answer/internal/service/answer_common"
@ -51,7 +52,9 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st
if err != nil {
return nil, err
}
questionInfo.ID = uid.EnShortID(questionInfo.ID)
if handler.GetEnableShortID(ctx) {
questionInfo.ID = uid.EnShortID(questionInfo.ID)
}
if !exist {
break
}
@ -87,7 +90,9 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st
if !exist {
break
}
questionInfo.ID = uid.EnShortID(questionInfo.ID)
if handler.GetEnableShortID(ctx) {
questionInfo.ID = uid.EnShortID(questionInfo.ID)
}
objInfo = &schema.UnreviewedRevisionInfoInfo{
ObjectID: answerInfo.ID,
Title: questionInfo.Title,

View File

@ -4,6 +4,7 @@ import (
"github.com/answerdev/answer/internal/service/action"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/activity_queue"
answercommon "github.com/answerdev/answer/internal/service/answer_common"
"github.com/answerdev/answer/internal/service/auth"
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
@ -14,6 +15,7 @@ import (
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/follow"
"github.com/answerdev/answer/internal/service/meta"
"github.com/answerdev/answer/internal/service/notice_queue"
"github.com/answerdev/answer/internal/service/notification"
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/object_info"
@ -86,4 +88,6 @@ var ProviderSetService = wire.NewSet(
user_external_login.NewUserCenterLoginService,
plugin_common.NewPluginCommonService,
config.NewConfigService,
notice_queue.NewNotificationQueueService,
activity_queue.NewActivityQueueService,
)

View File

@ -3,7 +3,6 @@ package questioncommon
import (
"context"
"encoding/json"
"fmt"
"math"
"time"
@ -48,26 +47,27 @@ type QuestionRepo interface {
UpdateAccepted(ctx context.Context, question *entity.Question) (err error)
UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error)
AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error)
GetQuestionCount(ctx context.Context) (count int64, err error)
GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error)
GetQuestionCountByIDs(ctx context.Context, ids []string) (count int64, err error)
GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
SitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
}
// QuestionCommon user service
type QuestionCommon struct {
questionRepo QuestionRepo
answerRepo answercommon.AnswerRepo
voteRepo activity_common.VoteRepo
followCommon activity_common.FollowRepo
tagCommon *tagcommon.TagCommonService
userCommon *usercommon.UserCommon
collectionCommon *collectioncommon.CollectionCommon
AnswerCommon *answercommon.AnswerCommon
metaService *meta.MetaService
configService *config.ConfigService
data *data.Data
questionRepo QuestionRepo
answerRepo answercommon.AnswerRepo
voteRepo activity_common.VoteRepo
followCommon activity_common.FollowRepo
tagCommon *tagcommon.TagCommonService
userCommon *usercommon.UserCommon
collectionCommon *collectioncommon.CollectionCommon
AnswerCommon *answercommon.AnswerCommon
metaService *meta.MetaService
configService *config.ConfigService
activityQueueService activity_queue.ActivityQueueService
data *data.Data
}
func NewQuestionCommon(questionRepo QuestionRepo,
@ -80,21 +80,22 @@ func NewQuestionCommon(questionRepo QuestionRepo,
answerCommon *answercommon.AnswerCommon,
metaService *meta.MetaService,
configService *config.ConfigService,
activityQueueService activity_queue.ActivityQueueService,
data *data.Data,
) *QuestionCommon {
return &QuestionCommon{
questionRepo: questionRepo,
answerRepo: answerRepo,
voteRepo: voteRepo,
followCommon: followCommon,
tagCommon: tagCommon,
userCommon: userCommon,
collectionCommon: collectionCommon,
AnswerCommon: answerCommon,
metaService: metaService,
configService: configService,
data: data,
questionRepo: questionRepo,
answerRepo: answerRepo,
voteRepo: voteRepo,
followCommon: followCommon,
tagCommon: tagCommon,
userCommon: userCommon,
collectionCommon: collectionCommon,
AnswerCommon: answerCommon,
metaService: metaService,
configService: configService,
activityQueueService: activityQueueService,
data: data,
}
}
@ -513,7 +514,7 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
@ -551,40 +552,26 @@ func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err erro
}
func (qs *QuestionCommon) SitemapCron(ctx context.Context) {
data := &schema.SiteMapList{}
questionNum, err := qs.questionRepo.GetQuestionCount(ctx)
if err != nil {
log.Error("GetQuestionCount error", err)
log.Error(err)
return
}
if questionNum <= schema.SitemapMaxSize {
questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, 0, int(questionNum))
if questionNum <= constant.SitemapMaxSize {
_, err = qs.questionRepo.SitemapQuestions(ctx, 1, int(questionNum))
if err != nil {
log.Error("GetQuestionIDsPage error", err)
log.Errorf("get site map question error: %v", err)
}
return
}
totalPages := int(math.Ceil(float64(questionNum) / float64(constant.SitemapMaxSize)))
for i := 1; i <= totalPages; i++ {
_, err = qs.questionRepo.SitemapQuestions(ctx, i, constant.SitemapMaxSize)
if err != nil {
log.Errorf("get site map question error: %v", err)
return
}
data.QuestionIDs = questionIDList
} else {
nums := make([]int, 0)
totalpages := int(math.Ceil(float64(questionNum) / float64(schema.SitemapMaxSize)))
for i := 1; i <= totalpages; i++ {
siteMapPagedata := &schema.SiteMapPageList{}
nums = append(nums, i)
questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, i, int(schema.SitemapMaxSize))
if err != nil {
log.Error("GetQuestionIDsPage error", err)
return
}
siteMapPagedata.PageData = questionIDList
if setCacheErr := qs.SetCache(ctx, fmt.Sprintf(schema.SitemapPageCachekey, i), siteMapPagedata); setCacheErr != nil {
log.Errorf("set sitemap cron SetCache failed: %s", setCacheErr)
}
}
data.MaxPageNum = nums
}
if setCacheErr := qs.SetCache(ctx, schema.SitemapCachekey, data); setCacheErr != nil {
log.Errorf("set sitemap cron SetCache failed: %s", setCacheErr)
}
}
@ -594,7 +581,7 @@ func (qs *QuestionCommon) SetCache(ctx context.Context, cachekey string, info in
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashBoardCacheTime)
err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashboardCacheTime)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}

View File

@ -3,11 +3,11 @@ package service
import (
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"strings"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
@ -41,17 +41,19 @@ import (
// QuestionService user service
type QuestionService struct {
questionRepo questioncommon.QuestionRepo
tagCommon *tagcommon.TagCommonService
questioncommon *questioncommon.QuestionCommon
userCommon *usercommon.UserCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
metaService *meta.MetaService
collectionCommon *collectioncommon.CollectionCommon
answerActivityService *activity.AnswerActivityService
data *data.Data
emailService *export.EmailService
questionRepo questioncommon.QuestionRepo
tagCommon *tagcommon.TagCommonService
questioncommon *questioncommon.QuestionCommon
userCommon *usercommon.UserCommon
userRepo usercommon.UserRepo
revisionService *revision_common.RevisionService
metaService *meta.MetaService
collectionCommon *collectioncommon.CollectionCommon
answerActivityService *activity.AnswerActivityService
emailService *export.EmailService
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
siteInfoService siteinfo_common.SiteInfoCommonService
}
func NewQuestionService(
@ -64,21 +66,25 @@ func NewQuestionService(
metaService *meta.MetaService,
collectionCommon *collectioncommon.CollectionCommon,
answerActivityService *activity.AnswerActivityService,
data *data.Data,
emailService *export.EmailService,
notificationQueueService notice_queue.NotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
siteInfoService siteinfo_common.SiteInfoCommonService,
) *QuestionService {
return &QuestionService{
questionRepo: questionRepo,
tagCommon: tagCommon,
questioncommon: questioncommon,
userCommon: userCommon,
userRepo: userRepo,
revisionService: revisionService,
metaService: metaService,
collectionCommon: collectionCommon,
answerActivityService: answerActivityService,
data: data,
emailService: emailService,
questionRepo: questionRepo,
tagCommon: tagCommon,
questioncommon: questioncommon,
userCommon: userCommon,
userRepo: userRepo,
revisionService: revisionService,
metaService: metaService,
collectionCommon: collectionCommon,
answerActivityService: answerActivityService,
emailService: emailService,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
siteInfoService: siteInfoService,
}
}
@ -106,7 +112,7 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
@ -130,7 +136,7 @@ func (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.Reope
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
@ -312,7 +318,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
}
}
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: question.UserID,
ObjectID: question.ID,
OriginalObjectID: question.ID,
@ -381,7 +387,7 @@ func (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.Op
actMap[schema.QuestionOperationShow] = constant.ActQuestionShow
_, ok := actMap[req.Operation]
if ok {
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
@ -473,7 +479,7 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
// if err != nil {
// log.Errorf("user DeleteQuestion rank rollback error %s", err.Error())
// }
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
@ -632,7 +638,7 @@ func (qs *QuestionService) notificationInviteUser(
}
msg.ObjectType = constant.QuestionObjectType
msg.NotificationAction = constant.NotificationInvitedYouToAnswer
notice_queue.AddNotification(msg)
qs.notificationQueueService.Send(ctx, msg)
userInfo, ok := invitee[userID]
if !ok {
@ -822,7 +828,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
return
}
if canUpdate {
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: question.ID,
ActivityTypeKey: constant.ActQuestionEdited,
@ -1017,16 +1023,19 @@ func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *sche
return nil, err
}
for _, id := range questionIDs {
_, ok := questionMaps[uid.EnShortID(id)]
if handler.GetEnableShortID(ctx) {
id = uid.EnShortID(id)
}
_, ok := questionMaps[id]
if ok {
questionMaps[uid.EnShortID(id)].LastAnsweredUserInfo = nil
questionMaps[uid.EnShortID(id)].UpdateUserInfo = nil
questionMaps[uid.EnShortID(id)].Content = ""
questionMaps[uid.EnShortID(id)].HTML = ""
if questionMaps[uid.EnShortID(id)].Status == entity.QuestionStatusDeleted {
questionMaps[uid.EnShortID(id)].Title = "Deleted question"
questionMaps[id].LastAnsweredUserInfo = nil
questionMaps[id].UpdateUserInfo = nil
questionMaps[id].Content = ""
questionMaps[id].HTML = ""
if questionMaps[id].Status == entity.QuestionStatusDeleted {
questionMaps[id].Title = "Deleted question"
}
list = append(list, questionMaps[uid.EnShortID(id)])
list = append(list, questionMaps[id])
}
}
@ -1213,7 +1222,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
//if err != nil {
// log.Errorf("admin delete question then rank rollback error %s", err.Error())
//}
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
@ -1221,7 +1230,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
})
}
if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed {
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
@ -1229,7 +1238,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
})
}
if setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed {
activity_queue.AddActivity(&schema.ActivityMsg{
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: questionInfo.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
@ -1243,93 +1252,77 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
msg.TriggerUserID = questionInfo.UserID
msg.ObjectType = constant.QuestionObjectType
msg.NotificationAction = constant.NotificationYourQuestionWasDeleted
notice_queue.AddNotification(msg)
qs.notificationQueueService.Send(ctx, msg)
return nil
}
func (qs *QuestionService) AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch, loginUserID string) ([]*schema.AdminQuestionInfo, int64, error) {
func (qs *QuestionService) AdminQuestionPage(
ctx context.Context, req *schema.AdminQuestionPageReq) (
resp *pager.PageModel, err error) {
list := make([]*schema.AdminQuestionInfo, 0)
status, ok := entity.AdminQuestionSearchStatus[search.StatusStr]
if ok {
search.Status = status
}
if search.Status == 0 {
search.Status = 1
}
dblist, count, err := qs.questionRepo.AdminSearchList(ctx, search)
questionList, count, err := qs.questionRepo.AdminQuestionPage(ctx, req)
if err != nil {
return list, count, err
return nil, err
}
userIds := make([]string, 0)
for _, dbitem := range dblist {
for _, info := range questionList {
item := &schema.AdminQuestionInfo{}
_ = copier.Copy(item, dbitem)
item.CreateTime = dbitem.CreatedAt.Unix()
item.UpdateTime = dbitem.PostUpdateTime.Unix()
item.EditTime = dbitem.UpdatedAt.Unix()
_ = copier.Copy(item, info)
item.CreateTime = info.CreatedAt.Unix()
item.UpdateTime = info.PostUpdateTime.Unix()
item.EditTime = info.UpdatedAt.Unix()
list = append(list, item)
userIds = append(userIds, dbitem.UserID)
userIds = append(userIds, info.UserID)
}
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
if err != nil {
return list, count, err
return nil, err
}
for _, item := range list {
_, ok = userInfoMap[item.UserID]
if ok {
item.UserInfo = userInfoMap[item.UserID]
if u, ok := userInfoMap[item.UserID]; ok {
item.UserInfo = u
}
}
return list, count, nil
return pager.NewPageModel(count, list), nil
}
// AdminSearchList
func (qs *QuestionService) AdminSearchAnswerList(ctx context.Context, search *entity.AdminAnswerSearch, loginUserID string) ([]*schema.AdminAnswerInfo, int64, error) {
answerlist := make([]*schema.AdminAnswerInfo, 0)
status, ok := entity.AdminAnswerSearchStatus[search.StatusStr]
if ok {
search.Status = status
}
if search.Status == 0 {
search.Status = 1
}
dblist, count, err := qs.questioncommon.AnswerCommon.AdminSearchList(ctx, search)
// AdminAnswerPage search answer list
func (qs *QuestionService) AdminAnswerPage(ctx context.Context, req *schema.AdminAnswerPageReq) (
resp *pager.PageModel, err error) {
answerList, count, err := qs.questioncommon.AnswerCommon.AdminSearchList(ctx, req)
if err != nil {
return answerlist, count, err
return nil, err
}
questionIDs := make([]string, 0)
userIds := make([]string, 0)
for _, item := range dblist {
answerinfo := qs.questioncommon.AnswerCommon.AdminShowFormat(ctx, item)
answerlist = append(answerlist, answerinfo)
answerResp := make([]*schema.AdminAnswerInfo, 0)
for _, item := range answerList {
answerInfo := qs.questioncommon.AnswerCommon.AdminShowFormat(ctx, item)
answerResp = append(answerResp, answerInfo)
questionIDs = append(questionIDs, item.QuestionID)
userIds = append(userIds, item.UserID)
}
userInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, userIds)
if err != nil {
return answerlist, count, err
return nil, err
}
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID)
if err != nil {
return nil, err
}
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
if err != nil {
return answerlist, count, err
}
for _, item := range answerlist {
_, ok := questionMaps[item.QuestionID]
if ok {
item.QuestionInfo.Title = questionMaps[item.QuestionID].Title
for _, item := range answerResp {
if q, ok := questionMaps[item.QuestionID]; ok {
item.QuestionInfo.Title = q.Title
}
_, ok = userInfoMap[item.UserID]
if ok {
item.UserInfo = userInfoMap[item.UserID]
if u, ok := userInfoMap[item.UserID]; ok {
item.UserInfo = u
}
}
return answerlist, count, nil
return pager.NewPageModel(count, answerResp), nil
}
func (qs *QuestionService) changeQuestionToRevision(ctx context.Context, questionInfo *entity.Question, tags []*entity.Tag) (
@ -1346,5 +1339,11 @@ func (qs *QuestionService) changeQuestionToRevision(ctx context.Context, questio
}
func (qs *QuestionService) SitemapCron(ctx context.Context) {
siteSeo, err := qs.siteInfoService.GetSiteSeo(ctx)
if err != nil {
log.Error(err)
return
}
ctx = context.WithValue(ctx, constant.ShortIDFlag, siteSeo.IsShortLink())
qs.questioncommon.SitemapCron(ctx)
}

View File

@ -29,6 +29,10 @@ const (
)
type UserRankRepo interface {
GetMaxDailyRank(ctx context.Context) (maxDailyRank int, err error)
CheckReachLimit(ctx context.Context, session *xorm.Session, userID string, maxDailyRank int) (reach bool, err error)
ChangeUserRank(ctx context.Context, session *xorm.Session,
userID string, userCurrentScore, deltaRank int) (err error)
TriggerUserRank(ctx context.Context, session *xorm.Session, userId string, rank int, activityType int) (isReachStandard bool, err error)
UserRankPage(ctx context.Context, userId string, page, pageSize int) (rankPage []*entity.Activity, total int64, err error)
}

View File

@ -4,30 +4,34 @@ import (
"context"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/notice_queue"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/comment"
"github.com/answerdev/answer/internal/service/notice_queue"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/pkg/obj"
)
type ReportHandle struct {
questionCommon *questioncommon.QuestionCommon
commentRepo comment.CommentRepo
configService *config.ConfigService
questionCommon *questioncommon.QuestionCommon
commentRepo comment.CommentRepo
configService *config.ConfigService
notificationQueueService notice_queue.NotificationQueueService
}
func NewReportHandle(
questionCommon *questioncommon.QuestionCommon,
commentRepo comment.CommentRepo,
configService *config.ConfigService) *ReportHandle {
configService *config.ConfigService,
notificationQueueService notice_queue.NotificationQueueService,
) *ReportHandle {
return &ReportHandle{
questionCommon: questionCommon,
commentRepo: commentRepo,
configService: configService,
questionCommon: questionCommon,
commentRepo: commentRepo,
configService: configService,
notificationQueueService: notificationQueueService,
}
}
@ -88,5 +92,5 @@ func (rh *ReportHandle) sendNotification(ctx context.Context, reportedUserID, ob
ObjectType: constant.ReportObjectType,
NotificationAction: notificationAction,
}
notice_queue.AddNotification(msg)
rh.notificationQueueService.Send(ctx, msg)
}

View File

@ -28,15 +28,17 @@ import (
// RevisionService user service
type RevisionService struct {
revisionRepo revision.RevisionRepo
userCommon *usercommon.UserCommon
questionCommon *questioncommon.QuestionCommon
answerService *AnswerService
objectInfoService *object_info.ObjService
questionRepo questioncommon.QuestionRepo
answerRepo answercommon.AnswerRepo
tagRepo tag_common.TagRepo
tagCommon *tagcommon.TagCommonService
revisionRepo revision.RevisionRepo
userCommon *usercommon.UserCommon
questionCommon *questioncommon.QuestionCommon
answerService *AnswerService
objectInfoService *object_info.ObjService
questionRepo questioncommon.QuestionRepo
answerRepo answercommon.AnswerRepo
tagRepo tag_common.TagRepo
tagCommon *tagcommon.TagCommonService
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
}
func NewRevisionService(
@ -49,17 +51,21 @@ func NewRevisionService(
answerRepo answercommon.AnswerRepo,
tagRepo tag_common.TagRepo,
tagCommon *tagcommon.TagCommonService,
notificationQueueService notice_queue.NotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
) *RevisionService {
return &RevisionService{
revisionRepo: revisionRepo,
userCommon: userCommon,
questionCommon: questionCommon,
answerService: answerService,
objectInfoService: objectInfoService,
questionRepo: questionRepo,
answerRepo: answerRepo,
tagRepo: tagRepo,
tagCommon: tagCommon,
revisionRepo: revisionRepo,
userCommon: userCommon,
questionCommon: questionCommon,
answerService: answerService,
objectInfoService: objectInfoService,
questionRepo: questionRepo,
answerRepo: answerRepo,
tagRepo: tagRepo,
tagCommon: tagCommon,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
}
}
@ -155,7 +161,7 @@ func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionit
if saveerr != nil {
return saveerr
}
activity_queue.AddActivity(&schema.ActivityMsg{
rs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: revisionitem.UserID,
ObjectID: revisionitem.ObjectID,
ActivityTypeKey: constant.ActQuestionEdited,
@ -210,9 +216,9 @@ func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.NotificationUpdateAnswer
notice_queue.AddNotification(msg)
rs.notificationQueueService.Send(ctx, msg)
activity_queue.AddActivity(&schema.ActivityMsg{
rs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: revisionitem.UserID,
ObjectID: insertData.ID,
OriginalObjectID: insertData.ID,
@ -258,7 +264,7 @@ func (rs *RevisionService) revisionAuditTag(ctx context.Context, revisionitem *s
}
}
activity_queue.AddActivity(&schema.ActivityMsg{
rs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: revisionitem.UserID,
ObjectID: taginfo.TagID,
OriginalObjectID: taginfo.TagID,

View File

@ -16,7 +16,6 @@ import (
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/siteinfo_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/pkg/uid"
"github.com/answerdev/answer/plugin"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
@ -25,7 +24,7 @@ import (
type SiteInfoService struct {
siteInfoRepo siteinfo_common.SiteInfoRepo
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
emailService *export.EmailService
tagCommonService *tagcommon.TagCommonService
configService *config.ConfigService
@ -34,7 +33,7 @@ type SiteInfoService struct {
func NewSiteInfoService(
siteInfoRepo siteinfo_common.SiteInfoRepo,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
emailService *export.EmailService,
tagCommonService *tagcommon.TagCommonService,
configService *config.ConfigService,
@ -280,28 +279,12 @@ func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoReq,
}
func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (err error) {
var (
siteType = constant.SiteTypeSeo
content []byte
)
content, _ = json.Marshal(req)
content, _ := json.Marshal(req)
data := entity.SiteInfo{
Type: siteType,
Type: constant.SiteTypeSeo,
Content: string(content),
}
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
if err != nil {
return
}
if req.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID || req.PermaLink == schema.PermaLinkQuestionIDByShortID {
uid.ShortIDSwitch = true
} else {
uid.ShortIDSwitch = false
}
s.questioncommon.SitemapCron(ctx)
return
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeSeo, &data)
}
func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema.GetPrivilegesConfigResp, err error) {

View File

@ -8,7 +8,6 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/pkg/gravatar"
"github.com/answerdev/answer/pkg/uid"
"github.com/segmentfault/pacman/log"
)
@ -18,31 +17,36 @@ type SiteInfoRepo interface {
GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error)
}
// SiteInfoCommonService site info common service
type SiteInfoCommonService struct {
// siteInfoCommonService site info common service
type siteInfoCommonService struct {
siteInfoRepo SiteInfoRepo
}
type SiteInfoCommonService interface {
GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error)
GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error)
GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error)
GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error)
FormatAvatar(ctx context.Context, originalAvatarData, email string) *schema.AvatarInfo
FormatListAvatar(ctx context.Context, userList []*entity.User) (userID2AvatarMapping map[string]*schema.AvatarInfo)
GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error)
GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error)
GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error)
GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error)
GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error)
GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error)
GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error)
}
// NewSiteInfoCommonService new site info common service
func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService {
siteInfo := &SiteInfoCommonService{
func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) SiteInfoCommonService {
return &siteInfoCommonService{
siteInfoRepo: siteInfoRepo,
}
seoinfo, err := siteInfo.GetSiteSeo(context.Background())
if err != nil {
log.Error("seoinfo error", err)
}
if seoinfo.PermaLink == schema.PermaLinkQuestionIDAndTitleByShortID || seoinfo.PermaLink == schema.PermaLinkQuestionIDByShortID {
uid.ShortIDSwitch = true
} else {
uid.ShortIDSwitch = false
}
return siteInfo
}
// GetSiteGeneral get site info general
func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
func (s *siteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
resp = &schema.SiteGeneralResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
return nil, err
@ -51,7 +55,7 @@ func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schem
}
// GetSiteInterface get site info interface
func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
func (s *siteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
resp = &schema.SiteInterfaceResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil {
return nil, err
@ -60,7 +64,7 @@ func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *sch
}
// GetSiteBranding get site info branding
func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {
func (s *siteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {
resp = &schema.SiteBrandingResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {
return nil, err
@ -69,7 +73,7 @@ func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *sche
}
// GetSiteUsers get site info about users
func (s *SiteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {
func (s *siteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {
resp = &schema.SiteUsersResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil {
return nil, err
@ -78,13 +82,13 @@ func (s *SiteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.
}
// FormatAvatar format avatar
func (s *SiteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string) *schema.AvatarInfo {
func (s *siteInfoCommonService) FormatAvatar(ctx context.Context, originalAvatarData, email string) *schema.AvatarInfo {
gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx)
return s.selectedAvatar(originalAvatarData, defaultAvatar, gravatarBaseURL, email)
}
// FormatListAvatar format avatar
func (s *SiteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) (
func (s *siteInfoCommonService) FormatListAvatar(ctx context.Context, userList []*entity.User) (
avatarMapping map[string]*schema.AvatarInfo) {
gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx)
avatarMapping = make(map[string]*schema.AvatarInfo)
@ -94,19 +98,22 @@ func (s *SiteInfoCommonService) FormatListAvatar(ctx context.Context, userList [
return avatarMapping
}
func (s *SiteInfoCommonService) getAvatarDefaultConfig(ctx context.Context) (string, string) {
func (s *siteInfoCommonService) getAvatarDefaultConfig(ctx context.Context) (string, string) {
gravatarBaseURL, defaultAvatar := constant.DefaultGravatarBaseURL, constant.DefaultAvatar
usersConfig, err := s.GetSiteUsers(ctx)
if err != nil {
log.Error(err)
} else {
}
if len(usersConfig.GravatarBaseURL) > 0 {
gravatarBaseURL = usersConfig.GravatarBaseURL
}
if len(usersConfig.DefaultAvatar) > 0 {
defaultAvatar = usersConfig.DefaultAvatar
}
return gravatarBaseURL, defaultAvatar
}
func (s *SiteInfoCommonService) selectedAvatar(
func (s *siteInfoCommonService) selectedAvatar(
originalAvatarData string, defaultAvatar string, gravatarBaseURL string, email string) *schema.AvatarInfo {
avatarInfo := &schema.AvatarInfo{}
_ = json.Unmarshal([]byte(originalAvatarData), avatarInfo)
@ -121,7 +128,7 @@ func (s *SiteInfoCommonService) selectedAvatar(
}
// GetSiteWrite get site info write
func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
func (s *siteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {
return nil, err
@ -130,7 +137,7 @@ func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.
}
// GetSiteLegal get site info write
func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
func (s *siteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
resp = &schema.SiteLegalResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil {
return nil, err
@ -139,7 +146,7 @@ func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.
}
// GetSiteLogin get site login config
func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) {
func (s *siteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) {
resp = &schema.SiteLoginResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {
return nil, err
@ -148,7 +155,7 @@ func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.
}
// GetSiteCustomCssHTML get site custom css html config
func (s *SiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) {
func (s *siteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) {
resp = &schema.SiteCustomCssHTMLResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {
return nil, err
@ -157,7 +164,7 @@ func (s *SiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp
}
// GetSiteTheme get site theme
func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) {
func (s *siteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.SiteThemeResp, err error) {
resp = &schema.SiteThemeResp{
ThemeOptions: schema.GetThemeOptions,
}
@ -169,15 +176,24 @@ func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.
}
// GetSiteSeo get site seo
func (s *SiteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {
resp = &schema.SiteSeoReq{}
func (s *siteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) {
resp = &schema.SiteSeoResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
return nil, err
}
return resp, nil
}
func (s *SiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) {
func (s *siteInfoCommonService) EnableShortID(ctx context.Context) (enabled bool) {
siteSeo, err := s.GetSiteSeo(ctx)
if err != nil {
log.Error(err)
return false
}
return siteSeo.IsShortLink()
}
func (s *siteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) {
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, siteType)
if err != nil {
return err

View File

@ -10,6 +10,7 @@ import (
"github.com/answerdev/answer/internal/service/siteinfo_common"
tagcommonser "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/jinzhu/copier"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
@ -18,18 +19,18 @@ import (
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/pkg/converter"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// TagService user service
type TagService struct {
tagRepo tagcommonser.TagRepo
tagCommonService *tagcommonser.TagCommonService
revisionService *revision_common.RevisionService
followCommon activity_common.FollowRepo
siteInfoService *siteinfo_common.SiteInfoCommonService
tagRepo tagcommonser.TagRepo
tagCommonService *tagcommonser.TagCommonService
revisionService *revision_common.RevisionService
followCommon activity_common.FollowRepo
siteInfoService siteinfo_common.SiteInfoCommonService
activityQueueService activity_queue.ActivityQueueService
}
// NewTagService new tag service
@ -38,13 +39,16 @@ func NewTagService(
tagCommonService *tagcommonser.TagCommonService,
revisionService *revision_common.RevisionService,
followCommon activity_common.FollowRepo,
siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService {
siteInfoService siteinfo_common.SiteInfoCommonService,
activityQueueService activity_queue.ActivityQueueService,
) *TagService {
return &TagService{
tagRepo: tagRepo,
tagCommonService: tagCommonService,
revisionService: revisionService,
followCommon: followCommon,
siteInfoService: siteInfoService,
tagRepo: tagRepo,
tagCommonService: tagCommonService,
revisionService: revisionService,
followCommon: followCommon,
siteInfoService: siteInfoService,
activityQueueService: activityQueueService,
}
}
@ -73,7 +77,7 @@ func (ts *TagService) RemoveTag(ctx context.Context, req *schema.RemoveTagReq) (
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
ts.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: req.TagID,
OriginalObjectID: req.TagID,
@ -298,7 +302,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
ts.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: tag.ID,
OriginalObjectID: tag.ID,

View File

@ -57,11 +57,12 @@ type TagRelRepo interface {
// TagCommonService user service
type TagCommonService struct {
revisionService *revision_common.RevisionService
tagCommonRepo TagCommonRepo
tagRelRepo TagRelRepo
tagRepo TagRepo
siteInfoService *siteinfo_common.SiteInfoCommonService
revisionService *revision_common.RevisionService
tagCommonRepo TagCommonRepo
tagRelRepo TagRelRepo
tagRepo TagRepo
siteInfoService siteinfo_common.SiteInfoCommonService
activityQueueService activity_queue.ActivityQueueService
}
// NewTagCommonService new tag service
@ -70,14 +71,16 @@ func NewTagCommonService(
tagRelRepo TagRelRepo,
tagRepo TagRepo,
revisionService *revision_common.RevisionService,
siteInfoService *siteinfo_common.SiteInfoCommonService,
siteInfoService siteinfo_common.SiteInfoCommonService,
activityQueueService activity_queue.ActivityQueueService,
) *TagCommonService {
return &TagCommonService{
tagCommonRepo: tagCommonRepo,
tagRelRepo: tagRelRepo,
tagRepo: tagRepo,
revisionService: revisionService,
siteInfoService: siteInfoService,
tagCommonRepo: tagCommonRepo,
tagRelRepo: tagRelRepo,
tagRepo: tagRepo,
revisionService: revisionService,
siteInfoService: siteInfoService,
activityQueueService: activityQueueService,
}
}
@ -645,7 +648,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
if err != nil {
return err
}
activity_queue.AddActivity(&schema.ActivityMsg{
ts.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: objectTagData.UserID,
ObjectID: tag.ID,
OriginalObjectID: tag.ID,
@ -845,7 +848,7 @@ func (ts *TagCommonService) UpdateTag(ctx context.Context, req *schema.UpdateTag
return err
}
if canUpdate {
activity_queue.AddActivity(&schema.ActivityMsg{
ts.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: req.UserID,
ObjectID: tagInfo.ID,
OriginalObjectID: tagInfo.ID,

View File

@ -53,20 +53,20 @@ var (
type UploaderService interface {
UploadAvatarFile(ctx *gin.Context) (url string, err error)
AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (avatarFile []byte, err error)
UploadPostFile(ctx *gin.Context) (url string, err error)
UploadBrandingFile(ctx *gin.Context) (url string, err error)
AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error)
}
// uploaderService uploader service
type uploaderService struct {
serviceConfig *service_config.ServiceConfig
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoService siteinfo_common.SiteInfoCommonService
}
// NewUploaderService new upload service
func NewUploaderService(serviceConfig *service_config.ServiceConfig,
siteInfoService *siteinfo_common.SiteInfoCommonService) UploaderService {
siteInfoService siteinfo_common.SiteInfoCommonService) UploaderService {
for _, subPath := range subPathList {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath))
if err != nil {
@ -105,26 +105,26 @@ func (us *uploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err e
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (
avatarfile []byte, err error) {
func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) {
if size > 1024 {
size = 1024
}
thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName)
thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName)
avatarfile, err = os.ReadFile(thumbfilePath)
thumbFilePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, avatarThumbSubPath, thumbFileName)
avatarfile, err := os.ReadFile(thumbFilePath)
if err == nil {
return avatarfile, nil
return thumbFilePath, nil
}
filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName)
filePath := fmt.Sprintf("%s/avatar/%s", us.serviceConfig.UploadPath, fileName)
avatarfile, err = os.ReadFile(filePath)
if err != nil {
return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
reader := bytes.NewReader(avatarfile)
img, err := imaging.Decode(reader)
if err != nil {
return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
new_image := imaging.Fill(img, size, size, imaging.Center, imaging.Linear)
var buf bytes.Buffer
@ -133,29 +133,29 @@ func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
_, ok := FormatExts[fileSuffix]
if !ok {
return avatarfile, fmt.Errorf("img extension not exist")
return "", fmt.Errorf("img extension not exist")
}
err = imaging.Encode(&buf, new_image, FormatExts[fileSuffix])
if err != nil {
return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
thumbReader := bytes.NewReader(buf.Bytes())
err = dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath))
if err != nil {
return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
avatarFilePath := path.Join(avatarThumbSubPath, thumbFileName)
savefilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath)
out, err := os.Create(savefilePath)
if err != nil {
return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
defer out.Close()
_, err = io.Copy(out, thumbReader)
if err != nil {
return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
return buf.Bytes(), nil
return savefilePath, nil
}
func (us *uploaderService) UploadPostFile(ctx *gin.Context) (

View File

@ -41,7 +41,7 @@ type UserAdminService struct {
authService *auth.AuthService
userCommonService *usercommon.UserCommon
userActivity activity.UserActiveActivityRepo
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
}
// NewUserAdminService new user admin service
@ -51,7 +51,7 @@ func NewUserAdminService(
authService *auth.AuthService,
userCommonService *usercommon.UserCommon,
userActivity activity.UserActiveActivityRepo,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
) *UserAdminService {
return &UserAdminService{
userRepo: userRepo,

View File

@ -44,14 +44,14 @@ type UserCommon struct {
userRepo UserRepo
userRoleService *role.UserRoleRelService
authService *auth.AuthService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
}
func NewUserCommon(
userRepo UserRepo,
userRoleService *role.UserRoleRelService,
authService *auth.AuthService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
) *UserCommon {
return &UserCommon{
userRepo: userRepo,

View File

@ -27,7 +27,7 @@ type UserCenterLoginService struct {
userExternalLoginRepo UserExternalLoginRepo
userCommonService *usercommon.UserCommon
userActivity activity.UserActiveActivityRepo
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
}
// NewUserCenterLoginService new user external login service
@ -36,7 +36,7 @@ func NewUserCenterLoginService(
userCommonService *usercommon.UserCommon,
userExternalLoginRepo UserExternalLoginRepo,
userActivity activity.UserActiveActivityRepo,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
) *UserCenterLoginService {
return &UserCenterLoginService{
userRepo: userRepo,
@ -228,10 +228,10 @@ func (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Conte
desc := userCenter.Description()
// If user status agent is enabled, admin can not update user status in answer.
resp.AllowUpdateUserStatus = !desc.UserStatusAgentEnabled
resp.AllowUpdateUserRole = !desc.UserRoleAgentEnabled
// If original user system is enabled, admin can update user password and role in answer.
resp.AllowUpdateUserPassword = desc.EnabledOriginalUserSystem
resp.AllowUpdateUserRole = desc.EnabledOriginalUserSystem
resp.AllowCreateUser = desc.EnabledOriginalUserSystem
return resp, nil
}

View File

@ -41,7 +41,7 @@ type UserExternalLoginService struct {
userExternalLoginRepo UserExternalLoginRepo
userCommonService *usercommon.UserCommon
emailService *export.EmailService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
siteInfoCommonService siteinfo_common.SiteInfoCommonService
userActivity activity.UserActiveActivityRepo
}
@ -51,7 +51,7 @@ func NewUserExternalLoginService(
userCommonService *usercommon.UserCommon,
userExternalLoginRepo UserExternalLoginRepo,
emailService *export.EmailService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
siteInfoCommonService siteinfo_common.SiteInfoCommonService,
userActivity activity.UserActiveActivityRepo,
) *UserExternalLoginService {
return &UserExternalLoginService{

View File

@ -38,7 +38,7 @@ type UserService struct {
activityRepo activity_common.ActivityRepo
emailService *export.EmailService
authService *auth.AuthService
siteInfoService *siteinfo_common.SiteInfoCommonService
siteInfoService siteinfo_common.SiteInfoCommonService
userRoleService *role.UserRoleRelService
userExternalLoginService *user_external_login.UserExternalLoginService
}
@ -48,7 +48,7 @@ func NewUserService(userRepo usercommon.UserRepo,
activityRepo activity_common.ActivityRepo,
emailService *export.EmailService,
authService *auth.AuthService,
siteInfoService *siteinfo_common.SiteInfoCommonService,
siteInfoService siteinfo_common.SiteInfoCommonService,
userRoleService *role.UserRoleRelService,
userCommonService *usercommon.UserCommon,
userExternalLoginService *user_external_login.UserExternalLoginService,
@ -606,7 +606,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
}
log.Infof("send email confirmation %s", verifyEmailURL)
go us.emailService.SendAndSaveCode(context.Background(), req.Email, title, body, code, data.ToJSONString())
go us.emailService.SendAndSaveCode(ctx, req.Email, title, body, code, data.ToJSONString())
return nil, nil
}

View File

@ -2,6 +2,8 @@ package service
import (
"context"
"github.com/answerdev/answer/internal/service/activity_common"
"strings"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
@ -13,41 +15,37 @@ import (
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/obj"
"github.com/segmentfault/pacman/log"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
answercommon "github.com/answerdev/answer/internal/service/answer_common"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
)
// VoteRepo activity repository
type VoteRepo interface {
VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error)
GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error)
ListUserVotes(ctx context.Context, userID string, req schema.GetVoteWithPageReq, activityTypes []int) (voteList []entity.Activity, total int64, err error)
Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error)
ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) (
voteList []*entity.Activity, total int64, err error)
}
// VoteService user service
type VoteService struct {
voteRepo VoteRepo
UniqueIDRepo unique.UniqueIDRepo
configService *config.ConfigService
questionRepo questioncommon.QuestionRepo
answerRepo answercommon.AnswerRepo
commentCommonRepo comment_common.CommentCommonRepo
objectService *object_info.ObjService
activityRepo activity_common.ActivityRepo
}
func NewVoteService(
VoteRepo VoteRepo,
uniqueIDRepo unique.UniqueIDRepo,
voteRepo VoteRepo,
configService *config.ConfigService,
questionRepo questioncommon.QuestionRepo,
answerRepo answercommon.AnswerRepo,
@ -55,8 +53,7 @@ func NewVoteService(
objectService *object_info.ObjService,
) *VoteService {
return &VoteService{
voteRepo: VoteRepo,
UniqueIDRepo: uniqueIDRepo,
voteRepo: voteRepo,
configService: configService,
questionRepo: questionRepo,
answerRepo: answerRepo,
@ -66,94 +63,87 @@ func NewVoteService(
}
// VoteUp vote up
func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{}
var objectUserID string
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
if err != nil {
return
return nil, err
}
// make object id must be decoded
objectInfo.ObjectID = req.ObjectID
// check user is voting self or not
if objectUserID == dto.UserID {
err = errors.BadRequest(reason.DisallowVoteYourSelf)
return
if objectInfo.ObjectCreatorUserID == req.UserID {
return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
}
if dto.IsCancel {
return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)
// vote operation
if req.IsCancel {
err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo)
} else {
return vs.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
// cancel vote down if exist
voteOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
err = vs.voteRepo.CancelVote(ctx, voteOperationInfo)
if err != nil {
return nil, err
}
err = vs.voteRepo.Vote(ctx, voteUpOperationInfo)
}
resp = &schema.VoteResp{}
resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)
if err != nil {
log.Error(err)
}
resp.Votes = resp.UpVotes - resp.DownVotes
if !req.IsCancel {
resp.VoteStatus = constant.ActVoteUp
}
return resp, nil
}
// VoteDown vote down
func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{}
var objectUserID string
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
if err != nil {
return
return nil, err
}
// make object id must be decoded
objectInfo.ObjectID = req.ObjectID
// check user is voting self or not
if objectUserID == dto.UserID {
err = errors.BadRequest(reason.DisallowVoteYourSelf)
return
if objectInfo.ObjectCreatorUserID == req.UserID {
return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
}
if dto.IsCancel {
return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
// vote operation
voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
if req.IsCancel {
err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo)
} else {
return vs.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
// cancel vote up if exist
err = vs.voteRepo.CancelVote(ctx, vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo))
if err != nil {
return nil, err
}
err = vs.voteRepo.Vote(ctx, voteDownOperationInfo)
}
}
func (vs *VoteService) GetObjectUserID(ctx context.Context, objectID string) (userID string, err error) {
var objectKey string
objectKey, err = obj.GetObjectTypeStrByObjectID(objectID)
resp = &schema.VoteResp{}
resp.UpVotes, resp.DownVotes, err = vs.voteRepo.GetAndSaveVoteResult(ctx, req.ObjectID, objectInfo.ObjectType)
if err != nil {
err = nil
return
log.Error(err)
}
switch objectKey {
case "question":
object, has, e := vs.questionRepo.GetQuestion(ctx, objectID)
if e != nil || !has {
err = errors.BadRequest(reason.QuestionNotFound).WithError(e).WithStack()
return
}
userID = object.UserID
case "answer":
object, has, e := vs.answerRepo.GetAnswer(ctx, objectID)
if e != nil || !has {
err = errors.BadRequest(reason.AnswerNotFound).WithError(e).WithStack()
return
}
userID = object.UserID
case "comment":
object, has, e := vs.commentCommonRepo.GetComment(ctx, objectID)
if e != nil || !has {
err = errors.BadRequest(reason.CommentNotFound).WithError(e).WithStack()
return
}
userID = object.UserID
default:
err = errors.BadRequest(reason.DisallowVote).WithError(err).WithStack()
return
resp.Votes = resp.UpVotes - resp.DownVotes
if !req.IsCancel {
resp.VoteStatus = constant.ActVoteDown
}
return
return resp, nil
}
// ListUserVotes list user's votes
func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (model *pager.PageModel, err error) {
func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWithPageReq) (resp *pager.PageModel, err error) {
typeKeys := []string{
activity_type.QuestionVoteUp,
activity_type.QuestionVoteDown,
@ -172,14 +162,14 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
activityTypeMapping[cfg.ID] = typeKey
}
voteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req, activityTypes)
voteList, total, err := vs.voteRepo.ListUserVotes(ctx, req.UserID, req.Page, req.PageSize, activityTypes)
if err != nil {
return
return nil, err
}
lang := handler.GetLangByCtx(ctx)
resp := make([]*schema.GetVoteWithPageResp, 0)
votes := make([]*schema.GetVoteWithPageResp, 0)
for _, voteInfo := range voteList {
objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID)
if err != nil {
@ -202,7 +192,65 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
item.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey)
}
resp = append(resp, item)
votes = append(votes, item)
}
return pager.NewPageModel(total, resp), err
return pager.NewPageModel(total, votes), err
}
func (vs *VoteService) createVoteOperationInfo(ctx context.Context,
userID string, voteUp bool, objectInfo *schema.SimpleObjectInfo) *schema.VoteOperationInfo {
// warp vote operation
voteOperationInfo := &schema.VoteOperationInfo{
ObjectID: objectInfo.ObjectID,
ObjectType: objectInfo.ObjectType,
ObjectCreatorUserID: objectInfo.ObjectCreatorUserID,
OperatingUserID: userID,
VoteUp: voteUp,
VoteDown: !voteUp,
}
voteOperationInfo.Activities = vs.getActivities(ctx, voteOperationInfo)
return voteOperationInfo
}
func (vs *VoteService) getActivities(ctx context.Context, op *schema.VoteOperationInfo) (
activities []*schema.VoteActivity) {
activities = make([]*schema.VoteActivity, 0)
var actions []string
switch op.ObjectType {
case constant.QuestionObjectType:
if op.VoteUp {
actions = []string{activity_type.QuestionVoteUp, activity_type.QuestionVotedUp}
} else {
actions = []string{activity_type.QuestionVoteDown, activity_type.QuestionVotedDown}
}
case constant.AnswerObjectType:
if op.VoteUp {
actions = []string{activity_type.AnswerVoteUp, activity_type.AnswerVotedUp}
} else {
actions = []string{activity_type.AnswerVoteDown, activity_type.AnswerVotedDown}
}
case constant.CommentObjectType:
actions = []string{activity_type.CommentVoteUp}
}
for _, action := range actions {
t := &schema.VoteActivity{}
cfg, err := vs.configService.GetConfigByKey(ctx, action)
if err != nil {
log.Warnf("get config by key error: %v", err)
continue
}
t.ActivityType, t.Rank = cfg.ID, cfg.GetIntValue()
if strings.Contains(action, "voted") {
t.ActivityUserID = op.ObjectCreatorUserID
t.TriggerUserID = op.OperatingUserID
} else {
t.ActivityUserID = op.OperatingUserID
t.TriggerUserID = "0"
}
activities = append(activities, t)
}
return activities
}

View File

@ -8,8 +8,6 @@ import (
const salt = int64(100)
var ShortIDSwitch = false
// NumToString num to string
func NumToShortID(id int64) string {
sid := strconv.FormatInt(id, 10)
@ -45,14 +43,11 @@ func ShortIDToNum(code string) int64 {
}
func EnShortID(id string) string {
if ShortIDSwitch {
num, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return id
}
return NumToShortID(num)
num, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return id
}
return id
return NumToShortID(num)
}
func DeShortID(sid string) string {

View File

@ -31,7 +31,6 @@ func Test_ShortID(t *testing.T) {
func Test_EnDeShortID(t *testing.T) {
nums := []string{"0", "1", "10", "100", "1000", "10000", "100000", "1234567", "10000000000000000", "10010000000001316", "19930000000001316"}
ShortIDSwitch = true
for _, num := range nums {
code := EnShortID(num)
denum := DeShortID(code)

View File

@ -33,6 +33,7 @@ type UserCenterDesc struct {
SignUpRedirectURL string `json:"sign_up_redirect_url"`
RankAgentEnabled bool `json:"rank_agent_enabled"`
UserStatusAgentEnabled bool `json:"user_status_agent_enabled"`
UserRoleAgentEnabled bool `json:"user_role_agent_enabled"`
MustAuthEmailEnabled bool `json:"must_auth_email_enabled"`
EnabledOriginalUserSystem bool `json:"enabled_original_user_system"`
}

View File

@ -1,6 +1,7 @@
plugin:
uc_login:
ui:
connect: Connect with {{ auth_name }}
login: Login
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.

View File

@ -1,6 +1,7 @@
plugin:
uc_login:
ui:
connect: 连接到 {{ auth_name }}
login: 登录
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录
login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。