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

View File

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

View File

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

View File

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

1
go.mod
View File

@ -48,7 +48,6 @@ require (
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.14.2 modernc.org/sqlite v1.14.2
xorm.io/builder v0.3.12 xorm.io/builder v0.3.12
xorm.io/core v0.7.3
xorm.io/xorm v1.3.2 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 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= 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.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 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 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= 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 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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-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.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 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 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.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.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.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.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/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.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM= xorm.io/builder v0.3.12 h1:ASZYX7fQmy+o8UJdhlLHSW57JDOkM8DNhcAF5d0LiJM=
xorm.io/builder v0.3.12/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= 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 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw= xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=

View File

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

View File

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

View File

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

View File

@ -7,3 +7,14 @@ const (
AvatarTypeGravatar = "gravatar" AvatarTypeGravatar = "gravatar"
AvatarTypeCustom = "custom" 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 // ScheduledTaskManager scheduled task manager
type ScheduledTaskManager struct { type ScheduledTaskManager struct {
siteInfoService *siteinfo_common.SiteInfoCommonService siteInfoService siteinfo_common.SiteInfoCommonService
questionService *service.QuestionService questionService *service.QuestionService
} }
// NewScheduledTaskManager new scheduled task manager // NewScheduledTaskManager new scheduled task manager
func NewScheduledTaskManager( func NewScheduledTaskManager(
siteInfoService *siteinfo_common.SiteInfoCommonService, siteInfoService siteinfo_common.SiteInfoCommonService,
questionService *service.QuestionService, questionService *service.QuestionService,
) *ScheduledTaskManager { ) *ScheduledTaskManager {
manager := &ScheduledTaskManager{ manager := &ScheduledTaskManager{

View File

@ -12,9 +12,9 @@ import (
"github.com/segmentfault/pacman/contrib/cache/memory" "github.com/segmentfault/pacman/contrib/cache/memory"
"github.com/segmentfault/pacman/log" "github.com/segmentfault/pacman/log"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
"xorm.io/core"
"xorm.io/xorm" "xorm.io/xorm"
ormlog "xorm.io/xorm/log" ormlog "xorm.io/xorm/log"
"xorm.io/xorm/names"
"xorm.io/xorm/schemas" "xorm.io/xorm/schemas"
) )
@ -71,7 +71,7 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) {
if dataConf.ConnMaxLifeTime > 0 { if dataConf.ConnMaxLifeTime > 0 {
engine.SetConnMaxLifetime(time.Duration(dataConf.ConnMaxLifeTime) * time.Second) engine.SetConnMaxLifetime(time.Duration(dataConf.ConnMaxLifeTime) * time.Second)
} }
engine.SetColumnMapper(core.GonicMapper{}) engine.SetColumnMapper(names.GonicMapper{})
return engine, nil 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/schema"
"github.com/answerdev/answer/internal/service/role" "github.com/answerdev/answer/internal/service/role"
"github.com/answerdev/answer/internal/service/siteinfo_common" "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/handler"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/pkg/converter"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
) )
@ -21,13 +21,13 @@ var ctxUUIDKey = "ctxUuidKey"
// AuthUserMiddleware auth user middleware // AuthUserMiddleware auth user middleware
type AuthUserMiddleware struct { type AuthUserMiddleware struct {
authService *auth.AuthService authService *auth.AuthService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService siteInfoCommonService siteinfo_common.SiteInfoCommonService
} }
// NewAuthUserMiddleware new auth user middleware // NewAuthUserMiddleware new auth user middleware
func NewAuthUserMiddleware( func NewAuthUserMiddleware(
authService *auth.AuthService, authService *auth.AuthService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware { siteInfoCommonService siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware {
return &AuthUserMiddleware{ return &AuthUserMiddleware{
authService: authService, authService: authService,
siteInfoCommonService: siteInfoCommonService, siteInfoCommonService: siteInfoCommonService,

View File

@ -32,31 +32,28 @@ func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig,
func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
return func(ctx *gin.Context) { return func(ctx *gin.Context) {
u := ctx.Request.RequestURI uri := ctx.Request.RequestURI
if strings.Contains(u, "/uploads/avatar/") { if strings.Contains(uri, "/uploads/avatar/") {
sizeStr := ctx.Query("s") size := converter.StringToInt(ctx.Query("s"))
size := converter.StringToInt(sizeStr) uriWithoutQuery, _ := url.Parse(uri)
uUrl, err := url.Parse(u) 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 { if err != nil {
ctx.Next() log.Error(err)
ctx.Abort()
return return
} }
_, urlfileName := filepath.Split(uUrl.Path) ctx.Header("content-type", fmt.Sprintf("image/%s", strings.TrimLeft(path.Ext(filePath), ".")))
uploadPath := am.serviceConfig.UploadPath _, err = ctx.Writer.Write(avatarFile)
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))
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
@ -64,7 +61,7 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
return return
} else { } else {
uUrl, err := url.Parse(u) uUrl, err := url.Parse(uri)
if err != nil { if err != nil {
ctx.Next() ctx.Next()
return return

View File

@ -8,4 +8,5 @@ import (
var ProviderSetMiddleware = wire.NewSet( var ProviderSetMiddleware = wire.NewSet(
NewAuthUserMiddleware, NewAuthUserMiddleware,
NewAvatarMiddleware, 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, viewRouter *router.UIRouter,
authUserMiddleware *middleware.AuthUserMiddleware, authUserMiddleware *middleware.AuthUserMiddleware,
avatarMiddleware *middleware.AvatarMiddleware, avatarMiddleware *middleware.AvatarMiddleware,
shortIDMiddleware *middleware.ShortIDMiddleware,
templateRouter *router.TemplateRouter, templateRouter *router.TemplateRouter,
pluginAPIRouter *router.PluginAPIRouter, pluginAPIRouter *router.PluginAPIRouter,
) *gin.Engine { ) *gin.Engine {
@ -30,7 +31,7 @@ func NewHTTPServer(debug bool,
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
} }
r := gin.New() 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") }) r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
html, _ := fs.Sub(ui.Template, "template") 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/base/middleware"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity" "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/internal/service/role"
"github.com/answerdev/answer/pkg/uid" "github.com/answerdev/answer/pkg/uid"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type ActivityController struct { type ActivityController struct {
activityCommonService *activity_common.ActivityCommon activityService *activity.ActivityService
activityService *activity.ActivityService
} }
// NewActivityController new activity controller. // NewActivityController new activity controller.
func NewActivityController( func NewActivityController(
activityCommonService *activity_common.ActivityCommon,
activityService *activity.ActivityService) *ActivityController { activityService *activity.ActivityService) *ActivityController {
return &ActivityController{activityCommonService: activityCommonService, activityService: activityService} return &ActivityController{activityService: activityService}
} }
// GetObjectTimeline get object timeline // GetObjectTimeline get object timeline

View File

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

View File

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

View File

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

View File

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

View File

@ -12,11 +12,11 @@ import (
type LangController struct { type LangController struct {
translator i18n.Translator translator i18n.Translator
siteInfoService *siteinfo_common.SiteInfoCommonService siteInfoService siteinfo_common.SiteInfoCommonService
} }
// NewLangController new language controller. // 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} return &LangController{translator: tr, siteInfoService: siteInfoService}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
package templaterender

View File

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

View File

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

View File

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

View File

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

View File

@ -1,188 +1,183 @@
package migrations package migrations
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/permission"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"xorm.io/xorm" "xorm.io/xorm"
) )
const ( type Mentor struct {
defaultSEORobotTxt = `User-agent: * ctx context.Context
Disallow: /admin engine *xorm.Engine
Disallow: /search userData *InitNeedUserInputData
Disallow: /install err error
Disallow: /review Done bool
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{},
} }
// InitDB init db func NewMentor(ctx context.Context, engine *xorm.Engine, data *InitNeedUserInputData) *Mentor {
func InitDB(dataConf *data.Database) (err error) { return &Mentor{ctx: ctx, engine: engine, userData: data}
engine, err := data.NewDB(false, dataConf) }
if err != nil {
fmt.Println("new database failed: ", err.Error())
return err
}
exist, err := engine.IsTableExist(&entity.Version{}) type InitNeedUserInputData struct {
if err != nil { Language string
return fmt.Errorf("check table exists failed: %s", err) 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") 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 { func (m *Mentor) syncTable() {
_, err := engine.InsertOne(&entity.User{ 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", ID: "1",
Username: "admin", Username: m.userData.AdminName,
Pass: "$2a$10$.gnUnpW.8ssRNaEvx.XwvOR2NuPsGzFLWWX2rqSIVAdIvLNZZYs5y", // admin Pass: string(generateFromPassword),
EMail: "admin@admin.com", EMail: m.userData.AdminEmail,
MailStatus: 1, MailStatus: 1,
NoticeStatus: 1, NoticeStatus: 1,
Status: 1, Status: 1,
Rank: 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{ interfaceData := map[string]string{
"language": language, "language": m.userData.Language,
"time_zone": "UTC", "time_zone": "UTC",
} }
interfaceDataBytes, _ := json.Marshal(interfaceData) interfaceDataBytes, _ := json.Marshal(interfaceData)
_, err := engine.InsertOne(&entity.SiteInfo{ _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "interface", Type: "interface",
Content: string(interfaceDataBytes), Content: string(interfaceDataBytes),
Status: 1, Status: 1,
}) })
if err != nil { }
return err
}
func (m *Mentor) initSiteInfoGeneralData() {
generalData := map[string]string{ generalData := map[string]string{
"name": siteName, "name": m.userData.SiteName,
"site_url": siteURL, "site_url": m.userData.SiteURL,
"contact_email": contactEmail, "contact_email": m.userData.ContactEmail,
} }
generalDataBytes, _ := json.Marshal(generalData) generalDataBytes, _ := json.Marshal(generalData)
_, err = engine.InsertOne(&entity.SiteInfo{ _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "general", Type: "general",
Content: string(generalDataBytes), Content: string(generalDataBytes),
Status: 1, Status: 1,
}) })
if err != nil { }
return err
}
func (m *Mentor) initSiteInfoLoginConfig() {
loginConfig := map[string]bool{ loginConfig := map[string]bool{
"allow_new_registrations": true, "allow_new_registrations": true,
"allow_email_registrations": true, "allow_email_registrations": true,
"login_required": false, "login_required": false,
} }
loginConfigDataBytes, _ := json.Marshal(loginConfig) loginConfigDataBytes, _ := json.Marshal(loginConfig)
_, err = engine.InsertOne(&entity.SiteInfo{ _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "login", Type: "login",
Content: string(loginConfigDataBytes), Content: string(loginConfigDataBytes),
Status: 1, Status: 1,
}) })
if err != nil { }
return err
}
func (m *Mentor) initSiteInfoThemeConfig() {
themeConfig := `{"theme":"default","theme_config":{"default":{"navbar_style":"colored","primary_color":"#0033ff"}}}` 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", Type: "theme",
Content: themeConfig, Content: themeConfig,
Status: 1, Status: 1,
}) })
if err != nil { }
return err
}
seoData := map[string]string{ func (m *Mentor) initSiteInfoSEOConfig() {
"robots": defaultSEORobotTxt + siteURL + "/sitemap.xml", seoData := map[string]interface{}{
"permalink": 1,
"robots": defaultSEORobotTxt + m.userData.SiteURL + "/sitemap.xml",
} }
seoDataBytes, _ := json.Marshal(seoData) seoDataBytes, _ := json.Marshal(seoData)
_, err = engine.InsertOne(&entity.SiteInfo{ _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "seo", Type: "seo",
Content: string(seoDataBytes), Content: string(seoDataBytes),
Status: 1, Status: 1,
}) })
if err != nil { }
return err
}
func (m *Mentor) initSiteInfoUsersConfig() {
usersData := map[string]any{ usersData := map[string]any{
"default_avatar": "gravatar", "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_display_name": true,
"allow_update_username": true, "allow_update_username": true,
"allow_update_avatar": true, "allow_update_avatar": true,
@ -191,344 +186,9 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
"allow_update_location": true, "allow_update_location": true,
} }
usersDataBytes, _ := json.Marshal(usersData) usersDataBytes, _ := json.Marshal(usersData)
_, err = engine.InsertOne(&entity.SiteInfo{ _, m.err = m.engine.Context(m.ctx).Insert(&entity.SiteInfo{
Type: "users", Type: "users",
Content: string(usersDataBytes), Content: string(usersDataBytes),
Status: 1, 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 ( import (
"context" "context"
"fmt"
"github.com/segmentfault/pacman/log"
"time" "time"
"xorm.io/builder"
"github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/data"
@ -15,343 +18,338 @@ import (
"github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/pkg/converter"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm" "xorm.io/xorm"
) )
// AnswerActivityRepo answer accepted // AnswerActivityRepo answer accepted
type AnswerActivityRepo struct { type AnswerActivityRepo struct {
data *data.Data data *data.Data
activityRepo activity_common.ActivityRepo activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo userRankRepo rank.UserRankRepo
notificationQueueService notice_queue.NotificationQueueService
} }
const (
acceptAction = "accept"
acceptedAction = "accepted"
)
var (
acceptActionList = []string{acceptAction, acceptedAction}
)
// NewAnswerActivityRepo new repository // NewAnswerActivityRepo new repository
func NewAnswerActivityRepo( func NewAnswerActivityRepo(
data *data.Data, data *data.Data,
activityRepo activity_common.ActivityRepo, activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo, userRankRepo rank.UserRankRepo,
notificationQueueService notice_queue.NotificationQueueService,
) activity.AnswerActivityRepo { ) activity.AnswerActivityRepo {
return &AnswerActivityRepo{ return &AnswerActivityRepo{
data: data, data: data,
activityRepo: activityRepo, activityRepo: activityRepo,
userRankRepo: userRankRepo, userRankRepo: userRankRepo,
notificationQueueService: notificationQueueService,
} }
} }
// NewQuestionActivityRepo new repository func (ar *AnswerActivityRepo) SaveAcceptAnswerActivity(ctx context.Context, op *schema.AcceptAnswerOperationInfo) (
func NewQuestionActivityRepo( err error) {
data *data.Data, // pre check
activityRepo activity_common.ActivityRepo, noNeedToDo, err := ar.activityPreCheck(ctx, op)
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)
if err != nil { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return err
} }
if !exist { if noNeedToDo {
return nil return nil
} }
// get all this object activity // save 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))
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx) 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"). userInfoMapping, err := ar.acquireUserInfo(session, op.GetUserIDs())
Update(&entity.Activity{Cancelled: entity.ActivityCancelled, CancelledAt: time.Now()}); e != nil { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() 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 return nil, nil
}) })
if err != nil { if err != nil {
return err return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
// get all answers // notification
answerList := make([]*entity.Answer, 0) ar.sendAcceptAnswerNotification(ctx, op)
err = ar.data.DB.Context(ctx).Find(&answerList, &entity.Answer{QuestionID: questionID}) 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 { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 { if err != nil {
log.Error(err) 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) changeUserRank(ctx context.Context, session *xorm.Session,
func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, op *schema.AcceptAnswerOperationInfo,
answerObjID, questionObjID, questionUserID, answerUserID string, isSelf bool, userInfoMapping map[string]*entity.User) (err error) {
) (err error) { for _, act := range op.Activities {
addActivityList := make([]*entity.Activity, 0) if act.Rank == 0 {
for _, action := range acceptActionList { continue
// 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{ user := userInfoMapping[act.ActivityUserID]
ObjectID: answerObjID, if user == nil {
OriginalObjectID: questionObjID, continue
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
} }
if action == acceptAction { if err = ar.userRankRepo.ChangeUserRank(ctx, session,
addActivity.UserID = questionUserID act.ActivityUserID, user.Rank, act.Rank); err != nil {
addActivity.TriggerUserID = converter.StringToInt64(answerUserID) log.Error(err)
addActivity.OriginalObjectID = questionObjID // if activity is 'accept' means this question is accept the answer. return err
} else {
addActivity.UserID = answerUserID
addActivity.TriggerUserID = converter.StringToInt64(answerUserID)
addActivity.OriginalObjectID = answerObjID // if activity is 'accepted' means this answer was accepted.
} }
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) { func (ar *AnswerActivityRepo) rollbackUserRank(ctx context.Context, session *xorm.Session,
session = session.Context(ctx) activities []*entity.Activity,
for _, addActivity := range addActivityList { userInfoMapping map[string]*entity.User) (err error) {
existsActivity, exists, e := ar.activityRepo.GetActivity( for _, act := range activities {
ctx, session, answerObjID, addActivity.UserID, addActivity.ActivityType) if act.Rank == 0 {
if e != nil { continue
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() }
} user := userInfoMapping[act.UserID]
if exists && existsActivity.Cancelled == entity.ActivityAvailable { if user == nil {
continue continue
} }
if err = ar.userRankRepo.ChangeUserRank(ctx, session,
// trigger user rank and send notification act.UserID, user.Rank, -act.Rank); err != nil {
if addActivity.Rank != 0 { log.Error(err)
reachStandard, e := ar.userRankRepo.TriggerUserRank( return err
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()
}
}
} }
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{ msg := &schema.NotificationMsg{
Type: schema.NotificationTypeAchievement, Type: schema.NotificationTypeAchievement,
ObjectID: act.ObjectID, ObjectID: op.AnswerObjectID,
ReceiverUserID: act.UserID, ReceiverUserID: act.ActivityUserID,
} }
if act.UserID == questionUserID { if act.ActivityUserID == op.QuestionUserID {
msg.TriggerUserID = answerUserID msg.TriggerUserID = op.AnswerUserID
msg.ObjectType = constant.AnswerObjectType msg.ObjectType = constant.AnswerObjectType
} else { } else {
msg.TriggerUserID = questionUserID msg.TriggerUserID = op.QuestionUserID
msg.ObjectType = constant.AnswerObjectType msg.ObjectType = constant.AnswerObjectType
} }
if msg.TriggerUserID != msg.ReceiverUserID { 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{ msg := &schema.NotificationMsg{
ReceiverUserID: act.UserID, ReceiverUserID: act.ActivityUserID,
Type: schema.NotificationTypeInbox, Type: schema.NotificationTypeInbox,
ObjectID: act.ObjectID, ObjectID: op.AnswerObjectID,
} }
if act.UserID != questionUserID { if act.ActivityUserID != op.QuestionUserID {
msg.TriggerUserID = questionUserID msg.TriggerUserID = op.QuestionUserID
msg.ObjectType = constant.AnswerObjectType msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.NotificationAcceptAnswer msg.NotificationAction = constant.NotificationAcceptAnswer
notice_queue.AddNotification(msg) ar.notificationQueueService.Send(ctx, msg)
} }
} }
return err
} }
// CancelAcceptAnswer accept other answer func (ar *AnswerActivityRepo) sendCancelAcceptAnswerNotification(
func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context, ctx context.Context, op *schema.AcceptAnswerOperationInfo) {
answerObjID, questionObjID, questionUserID, answerUserID string, for _, act := range op.Activities {
) (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 {
msg := &schema.NotificationMsg{ msg := &schema.NotificationMsg{
ReceiverUserID: act.UserID, ReceiverUserID: act.ActivityUserID,
Type: schema.NotificationTypeAchievement, Type: schema.NotificationTypeAchievement,
ObjectID: act.ObjectID, ObjectID: op.AnswerObjectID,
} }
if act.UserID == questionUserID { if act.ActivityUserID == op.QuestionObjectID {
msg.TriggerUserID = answerUserID msg.TriggerUserID = op.AnswerObjectID
msg.ObjectType = constant.QuestionObjectType msg.ObjectType = constant.QuestionObjectType
} else { } else {
msg.TriggerUserID = questionUserID msg.TriggerUserID = op.QuestionObjectID
msg.ObjectType = constant.AnswerObjectType msg.ObjectType = constant.AnswerObjectType
} }
if msg.TriggerUserID != msg.ReceiverUserID { 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 { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 { if err != nil {
return err return err
} }
@ -110,7 +110,7 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string)
if err != nil { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 { if err != nil {
return err return err
} }

View File

@ -2,6 +2,8 @@ package activity
import ( import (
"context" "context"
"fmt"
"xorm.io/builder"
"github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason" "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) { func (ar *UserActiveActivityRepo) UserActive(ctx context.Context, userID string) (err error) {
cfg, err := ar.configService.GetConfigByKey(ctx, UserActivated) cfg, err := ar.configService.GetConfigByKey(ctx, UserActivated)
if err != nil { if err != nil {
return err return err
} }
activityType := cfg.ID
deltaRank := cfg.GetIntValue()
addActivity := &entity.Activity{ addActivity := &entity.Activity{
UserID: userID, UserID: userID,
ObjectID: "0", ObjectID: "0",
OriginalObjectID: "0", OriginalObjectID: "0",
ActivityType: activityType, ActivityType: cfg.ID,
Rank: deltaRank, Rank: cfg.GetIntValue(),
HasRank: 1, HasRank: 1,
} }
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { _, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx) 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 { 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 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 { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, err
} }
_, err = session.Insert(addActivity) _, err = session.Insert(addActivity)
if err != nil { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, err
} }
return nil, nil 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 ( import (
"context" "context"
"strings" "fmt"
"github.com/segmentfault/pacman/log"
"time" "time"
"github.com/answerdev/answer/internal/base/constant" "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/pkg/converter"
"github.com/answerdev/answer/internal/base/pager" "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/internal/service/rank"
"github.com/answerdev/answer/pkg/obj" "github.com/answerdev/answer/pkg/obj"
"xorm.io/builder" "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/data"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service" "github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
"xorm.io/xorm" "xorm.io/xorm"
) )
// VoteRepo activity repository // VoteRepo activity repository
type VoteRepo struct { type VoteRepo struct {
data *data.Data data *data.Data
uniqueIDRepo unique.UniqueIDRepo activityRepo activity_common.ActivityRepo
configService *config.ConfigService userRankRepo rank.UserRankRepo
activityRepo activity_common.ActivityRepo notificationQueueService notice_queue.NotificationQueueService
userRankRepo rank.UserRankRepo
voteCommon activity_common.VoteRepo
} }
// NewVoteRepo new repository // NewVoteRepo new repository
func NewVoteRepo( func NewVoteRepo(
data *data.Data, data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
configService *config.ConfigService,
activityRepo activity_common.ActivityRepo, activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo, userRankRepo rank.UserRankRepo,
voteCommon activity_common.VoteRepo, notificationQueueService notice_queue.NotificationQueueService,
) service.VoteRepo { ) service.VoteRepo {
return &VoteRepo{ return &VoteRepo{
data: data, data: data,
uniqueIDRepo: uniqueIDRepo, activityRepo: activityRepo,
configService: configService, userRankRepo: userRankRepo,
activityRepo: activityRepo, notificationQueueService: notificationQueueService,
userRankRepo: userRankRepo,
voteCommon: voteCommon,
} }
} }
var LimitUpActions = map[string][]string{ func (vr *VoteRepo) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
"question": {"vote_up", "voted_up"}, noNeedToVote, err := vr.votePreCheck(ctx, op)
"answer": {"vote_up", "voted_up"}, if err != nil {
"comment": {"vote_up"}, 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 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) { _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx) 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) userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
if err != nil { if err != nil {
return return nil, err
}
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
}
}
} }
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 { if err != nil {
return return err
} }
resp, err = vr.GetVoteResultByObjectId(ctx, objectID) for _, activity := range op.Activities {
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID) if activity.Rank == 0 {
continue
for _, activityUserID := range achievementNotificationUserIDs { }
vr.sendNotification(ctx, activityUserID, objectUserID, objectID) vr.sendAchievementNotification(ctx, activity.ActivityUserID, op.ObjectCreatorUserID, op.ObjectID)
} }
if sendInboxNotification { 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) { func (vr *VoteRepo) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error) {
resp = &schema.VoteResp{} // Pre-Check
notificationUserIDs := make([]string, 0) // 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) { _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
session = session.Context(ctx) 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) userInfoMapping, err := vr.acquireUserInfo(session, userIDs)
if err != nil { if err != nil {
return return nil, err
}
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
}
}
} }
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 { if err != nil {
return return err
} }
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
for _, activityUserID := range notificationUserIDs { for _, activity := range activities {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID) 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 return
} }
func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { func (vr *VoteRepo) ListUserVotes(ctx context.Context, userID string,
resp = &schema.VoteResp{} page int, pageSize int, activityTypes []int) (voteList []*entity.Activity, total int64, err error) {
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) {
session := vr.data.DB.Context(ctx) session := vr.data.DB.Context(ctx)
cond := builder. cond := builder.
And( And(
@ -399,46 +178,243 @@ func (vr *VoteRepo) ListUserVotes(
builder.In("activity_type", activityTypes), 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 { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
return return
} }
// updateVotes func (vr *VoteRepo) votePreCheck(ctx context.Context, op *schema.VoteOperationInfo) (noNeedToVote bool, err error) {
// if votes < 0 Decr object vote_count,otherwise Incr object vote_count activities, err := vr.getExistActivity(ctx, op)
func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, objectID string, votes int) (err error) { if err != nil {
var ( return false, err
objectType string }
e error 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 { switch objectType {
case "question": case constant.QuestionObjectType:
_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Question{}) _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Question{VoteCount: voteCount})
case "answer": case constant.AnswerObjectType:
_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Answer{}) _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Answer{VoteCount: voteCount})
case "comment": case constant.CommentObjectType:
_, err = session.Where("id = ?", objectID).Incr("vote_count", votes).Update(&entity.Comment{}) _, err = session.ID(objectID).Cols("vote_count").Update(&entity.Comment{VoteCount: voteCount})
default:
e = errors.BadRequest(reason.DisallowVote)
} }
if err != nil {
if e != nil { log.Error(err)
err = e
} else if err != nil {
err = errors.BadRequest(reason.DatabaseError).WithError(err).WithStack()
} }
return return
} }
// sendNotification send rank triggered notification func (vr *VoteRepo) sendAchievementNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, objectUserID, objectID string) {
objectType, err := obj.GetObjectTypeStrByObjectID(objectID) objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil { if err != nil {
return return
@ -451,10 +427,10 @@ func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, object
ObjectID: objectID, ObjectID: objectID,
ObjectType: objectType, 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 { if triggerUserID == receiverUserID {
return return
} }
@ -487,6 +463,6 @@ func (vr *VoteRepo) sendVoteInboxNotification(triggerUserID, receiverUserID, obj
} }
} }
if len(msg.NotificationAction) > 0 { 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) ( func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) (
activityType, rank, hasRank int, err error) { activityType, rank, hasRank int, err error) {
objectKey, err := obj.GetObjectTypeStrByObjectID(objectID) objectType, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil { if err != nil {
return return
} }
confKey := fmt.Sprintf("%s.%s", objectKey, action) confKey := fmt.Sprintf("%s.%s", objectType, action)
cfg, err := ar.configService.GetConfigByKey(ctx, confKey) cfg, err := ar.configService.GetConfigByKey(ctx, confKey)
if err != nil { if err != nil {
return return
@ -59,11 +59,11 @@ func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID str
return return
} }
func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, action string) (activityType int, err error) { func (ar *ActivityRepo) GetActivityTypeByObjectType(ctx context.Context, objectType, action string) (activityType int, err error) {
configKey := fmt.Sprintf("%s.%s", objectKey, action) configKey := fmt.Sprintf("%s.%s", objectType, action)
cfg, err := ar.configService.GetConfigByKey(ctx, configKey) cfg, err := ar.configService.GetConfigByKey(ctx, configKey)
if err != nil { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
return cfg.ID, nil 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) { func (ar *ActivityRepo) GetActivityTypeByConfigKey(ctx context.Context, configKey string) (activityType int, err error) {
cfg, err := ar.configService.GetConfigByKey(ctx, configKey) cfg, err := ar.configService.GetConfigByKey(ctx, configKey)
if err != nil { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
return cfg.ID, nil return cfg.ID, nil
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/answerdev/answer/internal/service/unique" "github.com/answerdev/answer/internal/service/unique"
"github.com/answerdev/answer/pkg/obj" "github.com/answerdev/answer/pkg/obj"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
) )
// FollowRepo follow repository // 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) { func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (userIDs []string, err error) {
objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID) objectTypeStr, err := obj.GetObjectTypeStrByObjectID(objectID)
if err != nil { 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 { 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) userIDs = make([]string, 0)
@ -94,7 +96,7 @@ func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (us
// GetFollowIDs get all follow id list // GetFollowIDs get all follow id list
func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) { func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) {
followIDs = make([]string, 0) followIDs = make([]string, 0)
activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow") activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
if err != nil { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 return false, err
} }
activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow") activityType, err := ar.activityRepo.GetActivityTypeByObjectType(ctx, objectKey, "follow")
if err != nil { if err != nil {
return false, err return false, err
} }

View File

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

View File

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

View File

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

View File

@ -2,11 +2,14 @@ package question
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"github.com/segmentfault/pacman/log"
"strings" "strings"
"time" "time"
"unicode" "unicode"
"github.com/answerdev/answer/internal/base/handler"
"xorm.io/builder" "xorm.io/builder"
"github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/constant"
@ -50,7 +53,9 @@ func (qr *questionRepo) AddQuestion(ctx context.Context, question *entity.Questi
if err != nil { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 return
} }
@ -71,7 +76,9 @@ func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Que
if err != nil { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 return
} }
@ -164,7 +171,9 @@ func (qr *questionRepo) GetQuestion(ctx context.Context, id string) (
if err != nil { if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 return
} }
@ -175,8 +184,10 @@ func (qr *questionRepo) SearchByTitleLike(ctx context.Context, title string) (qu
if err != nil { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
for _, item := range questionList { if handler.GetEnableShortID(ctx) {
item.ID = uid.EnShortID(item.ID) for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
}
} }
return return
} }
@ -190,8 +201,10 @@ func (qr *questionRepo) FindByID(ctx context.Context, id []string) (questionList
if err != nil { if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
for _, item := range questionList { if handler.GetEnableShortID(ctx) {
item.ID = uid.EnShortID(item.ID) for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
}
} }
return 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) { func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) {
questionList := make([]*entity.Question, 0) session := qr.data.DB.Context(ctx)
session.Where("status = ? OR status = ?", entity.QuestionStatusAvailable, entity.QuestionStatusClosed)
count, err = qr.data.DB.Context(ctx).In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).FindAndCount(&questionList) count, err = session.Count(&entity.Question{Show: entity.QuestionShow})
if err != nil { 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) { 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 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) 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) rows := make([]*entity.Question, 0)
if page > 0 { session := qr.data.DB.Context(ctx)
page = page - 1 session.Select("id,title,created_at,post_update_time")
} else { session.Where("`show` = ?", entity.QuestionShow)
page = 0 session.Where("status = ? OR status = ?", entity.QuestionStatusAvailable, entity.QuestionStatusClosed)
} session.Limit(pageSize, page*pageSize)
if pageSize == 0 { session.Asc("created_at")
pageSize = constant.DefaultPageSize err = session.Find(&rows)
}
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)
if err != nil { if err != nil {
return questionIDList, err return questionIDList, err
} }
// warp data
for _, question := range rows { for _, question := range rows {
item := &schema.SiteMapQuestionInfo{} item := &schema.SiteMapQuestionInfo{ID: question.ID}
item.ID = uid.EnShortID(question.ID) if handler.GetEnableShortID(ctx) {
item.Title = htmltext.UrlTitle(question.Title) item.ID = uid.EnShortID(question.ID)
updateTime := fmt.Sprintf("%v", question.PostUpdateTime.Format(time.RFC3339)) }
if question.PostUpdateTime.Unix() < 1 { item.Title = htmltext.UrlTitle(question.Title)
updateTime = fmt.Sprintf("%v", question.CreatedAt.Format(time.RFC3339)) 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) 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 return questionIDList, nil
} }
@ -312,13 +339,15 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
if err != nil { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
for _, item := range questionList { if handler.GetEnableShortID(ctx) {
item.ID = uid.EnShortID(item.ID) for _, item := range questionList {
item.ID = uid.EnShortID(item.ID)
}
} }
return questionList, total, err 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 ( var (
count int64 count int64
err error err error
@ -379,8 +408,10 @@ func (qr *questionRepo) AdminSearchList(ctx context.Context, search *schema.Admi
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return rows, count, err return rows, count, err
} }
for _, item := range rows { if handler.GetEnableShortID(ctx) {
item.ID = uid.EnShortID(item.ID) for _, item := range rows {
item.ID = uid.EnShortID(item.ID)
}
} }
return rows, count, nil 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 // TriggerUserRank trigger user rank change
// session is need provider, it means this action must be success or failure // session is need provider, it means this action must be success or failure
// if outer action is failed then this action is need rollback // 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, session *xorm.Session, userID string, deltaRank int, activityType int,
) (isReachStandard bool, err error) { ) (isReachStandard bool, err error) {
// IMPORTANT: If user center enabled the rank agent, then we should not change user rank. // IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
if plugin.RankAgentEnabled() { if plugin.RankAgentEnabled() || deltaRank == 0 {
return false, nil
}
if deltaRank == 0 {
return false, nil return false, nil
} }
@ -114,7 +161,7 @@ func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context,
LessVal: start, LessVal: start,
MoreVal: end, MoreVal: end,
}) })
earned, err := session.Sum(&entity.Activity{}, "rank") earned, err := session.Sum(&entity.Activity{}, "`rank`")
if err != nil { if err != nil {
return false, err return false, err
} }
@ -137,7 +184,7 @@ func (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, p
) { ) {
rankPage = make([]*entity.Activity, 0) 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") session.Desc("created_at")
cond := &entity.Activity{UserID: userID} cond := &entity.Activity{UserID: userID}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/answerdev/answer/internal/base/data" "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/base/reason"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
tagcommon "github.com/answerdev/answer/internal/service/tag_common" 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 { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
} }
for _, item := range tagList { if handler.GetEnableShortID(ctx) {
item.ObjectID = uid.EnShortID(item.ObjectID) for _, item := range tagList {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
} }
return 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) { func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID) 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 { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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) { func (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID) 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 { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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) exist, err = session.Get(tagRel)
if err != nil { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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 return
} }
@ -112,9 +118,12 @@ func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string)
err = session.Find(&tagListList) err = session.Find(&tagListList)
if err != nil { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
} }
for _, item := range tagListList { if handler.GetEnableShortID(ctx) {
item.ObjectID = uid.EnShortID(item.ObjectID) for _, item := range tagListList {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
} }
return return
} }
@ -130,9 +139,12 @@ func (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []
err = session.Find(&tagListList) err = session.Find(&tagListList)
if err != nil { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
} }
for _, item := range tagListList { if handler.GetEnableShortID(ctx) {
item.ObjectID = uid.EnShortID(item.ObjectID) for _, item := range tagListList {
item.ObjectID = uid.EnShortID(item.ObjectID)
}
} }
return 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) { func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) {
user := &entity.User{} user := &entity.User{}
user.QuestionCount = int(count) 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 { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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) { func (ur *userRepo) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) {
user := &entity.User{} user := &entity.User{}
user.AnswerCount = count 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 { if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() 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) { func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) {
list := make([]*entity.User, 0) 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 { if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return list, err return list, err

View File

@ -26,7 +26,7 @@ type AnswerAPIRouter struct {
reasonController *controller.ReasonController reasonController *controller.ReasonController
themeController *controller_admin.ThemeController themeController *controller_admin.ThemeController
siteInfoController *controller_admin.SiteInfoController siteInfoController *controller_admin.SiteInfoController
siteinfoController *controller.SiteinfoController siteinfoController *controller.SiteInfoController
notificationController *controller.NotificationController notificationController *controller.NotificationController
dashboardController *controller.DashboardController dashboardController *controller.DashboardController
uploadController *controller.UploadController uploadController *controller.UploadController
@ -55,7 +55,7 @@ func NewAnswerAPIRouter(
reasonController *controller.ReasonController, reasonController *controller.ReasonController,
themeController *controller_admin.ThemeController, themeController *controller_admin.ThemeController,
siteInfoController *controller_admin.SiteInfoController, siteInfoController *controller_admin.SiteInfoController,
siteinfoController *controller.SiteinfoController, siteinfoController *controller.SiteInfoController,
notificationController *controller.NotificationController, notificationController *controller.NotificationController,
dashboardController *controller.DashboardController, dashboardController *controller.DashboardController,
uploadController *controller.UploadController, uploadController *controller.UploadController,
@ -244,9 +244,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
} }
func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(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.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) r.PUT("/answer/status", a.answerController.AdminSetAnswerStatus)
// report // report

View File

@ -17,7 +17,6 @@ func NewTemplateRouter(
templateController *controller.TemplateController, templateController *controller.TemplateController,
templateRenderController *templaterender.TemplateRenderController, templateRenderController *templaterender.TemplateRenderController,
siteInfoController *controller_admin.SiteInfoController, siteInfoController *controller_admin.SiteInfoController,
) *TemplateRouter { ) *TemplateRouter {
return &TemplateRouter{ return &TemplateRouter{
templateController: templateController, templateController: templateController,
@ -26,7 +25,7 @@ func NewTemplateRouter(
} }
} }
// TemplateRouter template router // RegisterTemplateRouter template router
func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) { func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) {
r.GET("/sitemap.xml", a.templateController.Sitemap) r.GET("/sitemap.xml", a.templateController.Sitemap)
r.GET("/sitemap/:page", a.templateController.SitemapPage) 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("/custom.css", a.siteInfoController.GetCss)
r.GET("/", a.templateController.Index) r.GET("/", a.templateController.Index)
r.GET("/index", a.templateController.Index)
r.GET("/questions", a.templateController.QuestionList) r.GET("/questions", a.templateController.QuestionList)
r.GET("/questions/:id", a.templateController.QuestionInfo) 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 // UIRouter is an interface that provides ui static file routers
type UIRouter struct { type UIRouter struct {
siteInfoController *controller.SiteinfoController siteInfoController *controller.SiteInfoController
siteInfoService *siteinfo_common.SiteInfoCommonService siteInfoService siteinfo_common.SiteInfoCommonService
} }
// NewUIRouter creates a new UIRouter instance with the embed resources // NewUIRouter creates a new UIRouter instance with the embed resources
func NewUIRouter( func NewUIRouter(
siteInfoController *controller.SiteinfoController, siteInfoController *controller.SiteInfoController,
siteInfoService *siteinfo_common.SiteInfoCommonService, siteInfoService siteinfo_common.SiteInfoCommonService,
) *UIRouter { ) *UIRouter {
return &UIRouter{ return &UIRouter{
siteInfoController: siteInfoController, siteInfoController: siteInfoController,

View File

@ -4,12 +4,13 @@ import "github.com/answerdev/answer/internal/base/constant"
// ActivityMsg activity message // ActivityMsg activity message
type ActivityMsg struct { type ActivityMsg struct {
UserID string `json:"user_id"` UserID string
TriggerUserID int64 `json:"trigger_user_id"` TriggerUserID int64
ObjectID string `json:"object_id"` ObjectID string
OriginalObjectID string `json:"original_object_id"` OriginalObjectID string
ActivityTypeKey constant.ActivityTypeKey `json:"activity_type_key"` ActivityTypeKey constant.ActivityTypeKey
RevisionID string `json:"revision_id"` RevisionID string
ExtraInfo map[string]string
} }
// GetObjectTimelineReq get object timeline request // 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 var AppStartTime time.Time
const ( const (
DashBoardCachekey = "answer@dashboard" DashboardCacheKey = "answer:dashboard"
DashBoardCacheTime = 60 * time.Minute DashboardCacheTime = 60 * time.Minute
) )
type DashboardInfo struct { type DashboardInfo struct {

View File

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

View File

@ -1,16 +1,16 @@
package schema package schema
import ( import (
"strings"
"time" "time"
"github.com/answerdev/answer/internal/base/validator" "github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/uid"
) )
const ( const (
SitemapMaxSize = 50000
SitemapCachekey = "answer@sitemap"
SitemapPageCachekey = "answer@sitemap@page%d"
QuestionOperationPin = "pin" QuestionOperationPin = "pin"
QuestionOperationUnPin = "unpin" QuestionOperationUnPin = "unpin"
QuestionOperationHide = "hide" QuestionOperationHide = "hide"
@ -361,12 +361,62 @@ type QuestionPageRespOperator struct {
DisplayName string `json:"display_name"` DisplayName string `json:"display_name"`
} }
type AdminQuestionSearch struct { type AdminQuestionPageReq struct {
Page int `json:"page" form:"page"` // Query number of pages Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `json:"page_size" form:"page_size"` // Search page size PageSize int `validate:"omitempty,min=1" form:"page_size"`
Status int `json:"-" form:"-"` StatusCond string `validate:"omitempty,oneof=normal closed deleted" form:"status"`
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 `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string 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 { type AdminSetQuestionStatusRequest struct {
@ -374,21 +424,6 @@ type AdminSetQuestionStatusRequest struct {
QuestionID string `json:"question_id" form:"question_id"` 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 { type PersonalQuestionPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"` Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"` PageSize int `validate:"omitempty,min=1" form:"page_size"`

View File

@ -14,11 +14,6 @@ import (
"github.com/segmentfault/pacman/errors" "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 // SiteGeneralReq site general request
type SiteGeneralReq struct { type SiteGeneralReq struct {
Name string `validate:"required,sanitizer,gt=1,lte=128" form:"name" json:"name"` 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"` 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() { func (r *SiteGeneralReq) FormatSiteUrl() {
parsedUrl, err := url.Parse(r.SiteUrl) parsedUrl, err := url.Parse(r.SiteUrl)
if err != nil { if err != nil {
@ -127,6 +117,16 @@ type SiteThemeReq struct {
ThemeConfig map[string]interface{} `validate:"omitempty" json:"theme_config"` 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 // SiteGeneralResp site general response
type SiteGeneralResp SiteGeneralReq type SiteGeneralResp SiteGeneralReq
@ -186,7 +186,7 @@ type SiteInfoResp struct {
Login *SiteLoginResp `json:"login"` Login *SiteLoginResp `json:"login"`
Theme *SiteThemeResp `json:"theme"` Theme *SiteThemeResp `json:"theme"`
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"` CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
SiteSeo *SiteSeoReq `json:"site_seo"` SiteSeo *SiteSeoResp `json:"site_seo"`
SiteUsers *SiteUsersResp `json:"site_users"` SiteUsers *SiteUsersResp `json:"site_users"`
Version string `json:"version"` Version string `json:"version"`
Revision string `json:"revision"` Revision string `json:"revision"`
@ -195,7 +195,7 @@ type TemplateSiteInfoResp struct {
General *SiteGeneralResp `json:"general"` General *SiteGeneralResp `json:"general"`
Interface *SiteInterfaceResp `json:"interface"` Interface *SiteInterfaceResp `json:"interface"`
Branding *SiteBrandingResp `json:"branding"` Branding *SiteBrandingResp `json:"branding"`
SiteSeo *SiteSeoReq `json:"site_seo"` SiteSeo *SiteSeoResp `json:"site_seo"`
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"` CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
Title string Title string
Year 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:"-"` UserID string `json:"-"`
} }
type VoteDTO struct { type VoteResp struct {
// object TagID UpVotes int64 `json:"up_votes"`
ObjectID string DownVotes int64 `json:"down_votes"`
// is cancel Votes int64 `json:"votes"`
IsCancel bool VoteStatus string `json:"vote_status"`
// user TagID
UserID string
} }
type VoteResp struct { // VoteOperationInfo vote operation info
UpVotes int `json:"up_votes"` type VoteOperationInfo struct {
DownVotes int `json:"down_votes"` // operation object id
Votes int `json:"votes"` ObjectID string
VoteStatus string `json:"vote_status"` // 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 { type GetVoteWithPageReq struct {
@ -28,23 +52,7 @@ type GetVoteWithPageReq struct {
// page size // page size
PageSize int `validate:"omitempty,min=1" form:"page_size"` PageSize int `validate:"omitempty,min=1" form:"page_size"`
// user id // user id
UserID string `validate:"required" form:"user_id"` UserID string `json:"-"`
}
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"`
} }
type GetVoteWithPageResp struct { type GetVoteWithPageResp struct {

View File

@ -7,9 +7,9 @@ import (
"strings" "strings"
"github.com/answerdev/answer/internal/base/constant" "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/entity"
"github.com/answerdev/answer/internal/schema" "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/comment_common"
"github.com/answerdev/answer/internal/service/config" "github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/meta" "github.com/answerdev/answer/internal/service/meta"
@ -30,22 +30,20 @@ type ActivityRepo interface {
// ActivityService activity service // ActivityService activity service
type ActivityService struct { type ActivityService struct {
activityRepo ActivityRepo activityRepo ActivityRepo
userCommon *usercommon.UserCommon userCommon *usercommon.UserCommon
activityCommonService *activity_common.ActivityCommon tagCommonService *tag_common.TagCommonService
tagCommonService *tag_common.TagCommonService objectInfoService *object_info.ObjService
objectInfoService *object_info.ObjService commentCommonService *comment_common.CommentCommonService
commentCommonService *comment_common.CommentCommonService revisionService *revision_common.RevisionService
revisionService *revision_common.RevisionService metaService *meta.MetaService
metaService *meta.MetaService configService *config.ConfigService
configService *config.ConfigService
} }
// NewActivityService new activity service // NewActivityService new activity service
func NewActivityService( func NewActivityService(
activityRepo ActivityRepo, activityRepo ActivityRepo,
userCommon *usercommon.UserCommon, userCommon *usercommon.UserCommon,
activityCommonService *activity_common.ActivityCommon,
tagCommonService *tag_common.TagCommonService, tagCommonService *tag_common.TagCommonService,
objectInfoService *object_info.ObjService, objectInfoService *object_info.ObjService,
commentCommonService *comment_common.CommentCommonService, commentCommonService *comment_common.CommentCommonService,
@ -54,15 +52,14 @@ func NewActivityService(
configService *config.ConfigService, configService *config.ConfigService,
) *ActivityService { ) *ActivityService {
return &ActivityService{ return &ActivityService{
objectInfoService: objectInfoService, objectInfoService: objectInfoService,
activityRepo: activityRepo, activityRepo: activityRepo,
userCommon: userCommon, userCommon: userCommon,
activityCommonService: activityCommonService, tagCommonService: tagCommonService,
tagCommonService: tagCommonService, commentCommonService: commentCommonService,
commentCommonService: commentCommonService, revisionService: revisionService,
revisionService: revisionService, metaService: metaService,
metaService: metaService, configService: configService,
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 { 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) 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" "time"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_queue" "github.com/answerdev/answer/internal/service/activity_queue"
"github.com/answerdev/answer/pkg/converter" "github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/uid" "github.com/answerdev/answer/pkg/uid"
@ -14,7 +15,7 @@ import (
type ActivityRepo interface { type ActivityRepo interface {
GetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank int, hasRank int, err error) 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) ( GetActivity(ctx context.Context, session *xorm.Session, objectID, userID string, activityType int) (
existsActivity *entity.Activity, exist bool, err error) existsActivity *entity.Activity, exist bool, err error)
GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error) GetUserIDObjectIDActivitySum(ctx context.Context, userID, objectID string) (int, error)
@ -27,51 +28,44 @@ type ActivityRepo interface {
} }
type ActivityCommon struct { type ActivityCommon struct {
activityRepo ActivityRepo activityRepo ActivityRepo
activityQueueService activity_queue.ActivityQueueService
} }
// NewActivityCommon new activity common // NewActivityCommon new activity common
func NewActivityCommon( func NewActivityCommon(
activityRepo ActivityRepo, activityRepo ActivityRepo,
activityQueueService activity_queue.ActivityQueueService,
) *ActivityCommon { ) *ActivityCommon {
activity := &ActivityCommon{ activity := &ActivityCommon{
activityRepo: activityRepo, activityRepo: activityRepo,
activityQueueService: activityQueueService,
} }
activity.HandleActivity() activity.activityQueueService.RegisterHandler(activity.HandleActivity)
return activity return activity
} }
// HandleActivity handle activity message // HandleActivity handle activity message
func (ac *ActivityCommon) HandleActivity() { func (ac *ActivityCommon) HandleActivity(ctx context.Context, msg *schema.ActivityMsg) error {
go func() { activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(ctx, string(msg.ActivityTypeKey))
defer func() { if err != nil {
if err := recover(); err != nil { log.Errorf("error getting activity type %s, activity type is %d", err, activityType)
log.Error(err) return err
} }
}()
for msg := range activity_queue.ActivityQueue { act := &entity.Activity{
log.Debugf("received activity %+v", msg) UserID: msg.UserID,
TriggerUserID: msg.TriggerUserID,
activityType, err := ac.activityRepo.GetActivityTypeByConfigKey(context.Background(), string(msg.ActivityTypeKey)) ObjectID: uid.DeShortID(msg.ObjectID),
if err != nil { OriginalObjectID: uid.DeShortID(msg.OriginalObjectID),
log.Errorf("error getting activity type %s, activity type is %d", err, activityType) ActivityType: activityType,
} Cancelled: entity.ActivityAvailable,
}
act := &entity.Activity{ if len(msg.RevisionID) > 0 {
UserID: msg.UserID, act.RevisionID = converter.StringToInt64(msg.RevisionID)
TriggerUserID: msg.TriggerUserID, }
ObjectID: uid.DeShortID(msg.ObjectID), if err := ac.activityRepo.AddActivity(ctx, act); err != nil {
OriginalObjectID: uid.DeShortID(msg.OriginalObjectID), return err
ActivityType: activityType, }
Cancelled: entity.ActivityAvailable, return nil
}
if len(msg.RevisionID) > 0 {
act.RevisionID = converter.StringToInt64(msg.RevisionID)
}
if err := ac.activityRepo.AddActivity(context.TODO(), act); err != nil {
log.Error(err)
}
}
}()
} }

View File

@ -1,14 +1,50 @@
package activity_queue package activity_queue
import ( import (
"context"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/log"
) )
var ( type ActivityQueueService interface {
ActivityQueue = make(chan *schema.ActivityMsg, 128) Send(ctx context.Context, msg *schema.ActivityMsg)
) RegisterHandler(handler func(ctx context.Context, msg *schema.ActivityMsg) error)
}
// AddActivity add new activity
func AddActivity(msg *schema.ActivityMsg) { type activityQueueService struct {
ActivityQueue <- msg 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 ( import (
"context" "context"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/uid"
) )
type AnswerRepo interface { type AnswerRepo interface {
@ -21,7 +23,7 @@ type AnswerRepo interface {
GetCountByUserID(ctx context.Context, userID string) (int64, error) GetCountByUserID(ctx context.Context, userID string) (int64, error)
GetByUserIDQuestionID(ctx context.Context, userID string, questionID string) (*entity.Answer, bool, error) GetByUserIDQuestionID(ctx context.Context, userID string, questionID string) (*entity.Answer, bool, error)
SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, 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) UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error)
GetAnswerCount(ctx context.Context) (count int64, 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 return has, nil
} }
func (as *AnswerCommon) AdminSearchList(ctx context.Context, search *entity.AdminAnswerSearch) ([]*entity.Answer, int64, error) { func (as *AnswerCommon) AdminSearchList(ctx context.Context, req *schema.AdminAnswerPageReq) (
if search.Status == 0 { resp []*entity.Answer, count int64, err error) {
search.Status = 1 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) { func (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) {

View File

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

View File

@ -58,13 +58,15 @@ func (c *CommentQuery) GetOrderBy() string {
// CommentService user service // CommentService user service
type CommentService struct { type CommentService struct {
commentRepo CommentRepo commentRepo CommentRepo
commentCommonRepo comment_common.CommentCommonRepo commentCommonRepo comment_common.CommentCommonRepo
userCommon *usercommon.UserCommon userCommon *usercommon.UserCommon
voteCommon activity_common.VoteRepo voteCommon activity_common.VoteRepo
objectInfoService *object_info.ObjService objectInfoService *object_info.ObjService
emailService *export.EmailService emailService *export.EmailService
userRepo usercommon.UserRepo userRepo usercommon.UserRepo
notificationQueueService notice_queue.NotificationQueueService
activityQueueService activity_queue.ActivityQueueService
} }
// NewCommentService new comment service // NewCommentService new comment service
@ -76,15 +78,19 @@ func NewCommentService(
voteCommon activity_common.VoteRepo, voteCommon activity_common.VoteRepo,
emailService *export.EmailService, emailService *export.EmailService,
userRepo usercommon.UserRepo, userRepo usercommon.UserRepo,
notificationQueueService notice_queue.NotificationQueueService,
activityQueueService activity_queue.ActivityQueueService,
) *CommentService { ) *CommentService {
return &CommentService{ return &CommentService{
commentRepo: commentRepo, commentRepo: commentRepo,
commentCommonRepo: commentCommonRepo, commentCommonRepo: commentCommonRepo,
userCommon: userCommon, userCommon: userCommon,
voteCommon: voteCommon, voteCommon: voteCommon,
objectInfoService: objectInfoService, objectInfoService: objectInfoService,
emailService: emailService, emailService: emailService,
userRepo: userRepo, userRepo: userRepo,
notificationQueueService: notificationQueueService,
activityQueueService: activityQueueService,
} }
} }
@ -161,7 +167,7 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
case constant.AnswerObjectType: case constant.AnswerObjectType:
activityMsg.ActivityTypeKey = constant.ActAnswerCommented activityMsg.ActivityTypeKey = constant.ActAnswerCommented
} }
activity_queue.AddActivity(activityMsg) cs.activityQueueService.Send(ctx, activityMsg)
return resp, nil return resp, nil
} }
@ -476,7 +482,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
} }
msg.ObjectType = constant.CommentObjectType msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationCommentQuestion msg.NotificationAction = constant.NotificationCommentQuestion
notice_queue.AddNotification(msg) cs.notificationQueueService.Send(ctx, msg)
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID) receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
if err != nil { if err != nil {
@ -535,7 +541,7 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
} }
msg.ObjectType = constant.CommentObjectType msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationCommentAnswer msg.NotificationAction = constant.NotificationCommentAnswer
notice_queue.AddNotification(msg) cs.notificationQueueService.Send(ctx, msg)
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID) receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
if err != nil { if err != nil {
@ -591,7 +597,7 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
} }
msg.ObjectType = constant.CommentObjectType msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationReplyToYou msg.NotificationAction = constant.NotificationReplyToYou
notice_queue.AddNotification(msg) cs.notificationQueueService.Send(ctx, msg)
} }
func (cs *CommentService) notificationMention( func (cs *CommentService) notificationMention(
@ -612,7 +618,7 @@ func (cs *CommentService) notificationMention(
} }
msg.ObjectType = constant.CommentObjectType msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.NotificationMentionYou msg.NotificationAction = constant.NotificationMentionYou
notice_queue.AddNotification(msg) cs.notificationQueueService.Send(ctx, msg)
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID) 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/constant"
"github.com/answerdev/answer/internal/base/data" "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/schema"
"github.com/answerdev/answer/internal/service/activity_common" "github.com/answerdev/answer/internal/service/activity_common"
answercommon "github.com/answerdev/answer/internal/service/answer_common" answercommon "github.com/answerdev/answer/internal/service/answer_common"
@ -24,11 +23,10 @@ import (
"github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common" usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/dir"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log" "github.com/segmentfault/pacman/log"
) )
type DashboardService struct { type dashboardService struct {
questionRepo questioncommon.QuestionRepo questionRepo questioncommon.QuestionRepo
answerRepo answercommon.AnswerRepo answerRepo answercommon.AnswerRepo
commentRepo comment_common.CommentCommonRepo commentRepo comment_common.CommentCommonRepo
@ -36,10 +34,9 @@ type DashboardService struct {
userRepo usercommon.UserRepo userRepo usercommon.UserRepo
reportRepo report_common.ReportRepo reportRepo report_common.ReportRepo
configService *config.ConfigService configService *config.ConfigService
siteInfoService *siteinfo_common.SiteInfoCommonService siteInfoService siteinfo_common.SiteInfoCommonService
serviceConfig *service_config.ServiceConfig serviceConfig *service_config.ServiceConfig
data *data.Data
data *data.Data
} }
func NewDashboardService( func NewDashboardService(
@ -50,12 +47,11 @@ func NewDashboardService(
userRepo usercommon.UserRepo, userRepo usercommon.UserRepo,
reportRepo report_common.ReportRepo, reportRepo report_common.ReportRepo,
configService *config.ConfigService, configService *config.ConfigService,
siteInfoService *siteinfo_common.SiteInfoCommonService, siteInfoService siteinfo_common.SiteInfoCommonService,
serviceConfig *service_config.ServiceConfig, serviceConfig *service_config.ServiceConfig,
data *data.Data, data *data.Data,
) *DashboardService { ) DashboardService {
return &DashboardService{ return &dashboardService{
questionRepo: questionRepo, questionRepo: questionRepo,
answerRepo: answerRepo, answerRepo: answerRepo,
commentRepo: commentRepo, commentRepo: commentRepo,
@ -65,63 +61,102 @@ func NewDashboardService(
configService: configService, configService: configService,
siteInfoService: siteInfoService, siteInfoService: siteInfoService,
serviceConfig: serviceConfig, serviceConfig: serviceConfig,
data: data,
data: data,
} }
} }
func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.DashboardInfo, error) { type DashboardService interface {
dashboardInfo := &schema.DashboardInfo{} Statistical(ctx context.Context) (resp *schema.DashboardInfo, err error)
infoStr, err := ds.data.Cache.GetString(ctx, schema.DashBoardCachekey) }
func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) {
dashboardInfo, err := ds.getFromCache(ctx)
if err != nil { if err != nil {
info, statisticalErr := ds.Statistical(ctx) dashboardInfo = &schema.DashboardInfo{}
if statisticalErr != nil { dashboardInfo.QuestionCount = ds.questionCount(ctx)
return nil, statisticalErr dashboardInfo.AnswerCount = ds.answerCount(ctx)
} dashboardInfo.CommentCount = ds.commentCount(ctx)
if setCacheErr := ds.SetCache(ctx, info); setCacheErr != nil { dashboardInfo.UserCount = ds.userCount(ctx)
log.Errorf("set dashboard statistical failed: %s", setCacheErr) dashboardInfo.ReportCount = ds.reportCount(ctx)
} dashboardInfo.VoteCount = ds.voteCount(ctx)
return info, nil 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) dashboardInfo.SMTP = ds.smtpStatus(ctx)
return nil, errors.InternalServer(reason.UnknownError) dashboardInfo.HTTPS = ds.httpsStatus(ctx)
} dashboardInfo.TimeZone = ds.getTimezone(ctx)
startTime := time.Now().Unix() - schema.AppStartTime.Unix() dashboardInfo.UploadingFiles = true
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) dashboardInfo.AppStartTime = fmt.Sprintf("%d", time.Now().Unix()-schema.AppStartTime.Unix())
dashboardInfo.VersionInfo.Version = constant.Version dashboardInfo.VersionInfo.Version = constant.Version
dashboardInfo.VersionInfo.Revision = constant.Revision dashboardInfo.VersionInfo.Revision = constant.Revision
ds.setCache(ctx, dashboardInfo)
return dashboardInfo, nil return dashboardInfo, nil
} }
func (ds *DashboardService) SetCache(ctx context.Context, info *schema.DashboardInfo) error { func (ds *dashboardService) getFromCache(ctx context.Context) (*schema.DashboardInfo, error) {
infoStr, err := json.Marshal(info) infoStr, err := ds.data.Cache.GetString(ctx, schema.DashboardCacheKey)
if err != nil { 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) dashboardInfo := &schema.DashboardInfo{}
if err != nil { if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() return nil, err
} }
return nil return dashboardInfo, nil
} }
// Statistical func (ds *dashboardService) setCache(ctx context.Context, info *schema.DashboardInfo) {
func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) { infoStr, _ := json.Marshal(info)
dashboardInfo := &schema.DashboardInfo{} 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) questionCount, err := ds.questionRepo.GetQuestionCount(ctx)
if err != nil { 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) answerCount, err := ds.answerRepo.GetAnswerCount(ctx)
if err != nil { 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) commentCount, err := ds.commentRepo.GetCommentCount(ctx)
if err != nil { 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{ typeKeys := []string{
"question.vote_up", "question.vote_up",
"question.vote_down", "question.vote_down",
@ -129,7 +164,6 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI
"answer.vote_down", "answer.vote_down",
} }
var activityTypes []int var activityTypes []int
for _, typeKey := range typeKeys { for _, typeKey := range typeKeys {
cfg, err := ds.configService.GetConfigByKey(ctx, typeKey) cfg, err := ds.configService.GetConfigByKey(ctx, typeKey)
if err != nil { if err != nil {
@ -137,69 +171,14 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI
} }
activityTypes = append(activityTypes, cfg.ID) activityTypes = append(activityTypes, cfg.ID)
} }
voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes) voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes)
if err != nil { if err != nil {
return dashboardInfo, err log.Errorf("get vote count failed: %s", err)
} }
userCount, err := ds.userRepo.GetUserCount(ctx) return voteCount
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
} }
func (ds *DashboardService) RemoteVersion(ctx context.Context) string { func (ds *dashboardService) remoteVersion(ctx context.Context) string {
url := "https://answer.dev/getlatest" url := "https://answer.dev/getlatest"
req, _ := http.NewRequest("GET", url, nil) req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", "Answer/"+constant.Version) req.Header.Set("User-Agent", "Answer/"+constant.Version)
@ -224,15 +203,48 @@ func (ds *DashboardService) RemoteVersion(ctx context.Context) string {
return remoteVersion.Release.Version 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") emailConf, err := ds.configService.GetStringValue(ctx, "email.config")
if err != nil { 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) err = json.Unmarshal([]byte(emailConf), ec)
if err != nil { 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) log.Errorf("get email config failed: %s", err)
return return
} }
if len(ec.SMTPHost) == 0 {
log.Warnf("smtp host is empty, skip send email")
return
}
m := gomail.NewMessage() m := gomail.NewMessage()
fromName := mime.QEncoding.Encode("utf-8", ec.FromName) fromName := mime.QEncoding.Encode("utf-8", ec.FromName)

View File

@ -1,13 +1,50 @@
package notice_queue package notice_queue
import ( import (
"context"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/log"
) )
var ( type NotificationQueueService interface {
NotificationQueue = make(chan *schema.NotificationMsg, 128) Send(ctx context.Context, msg *schema.NotificationMsg)
) RegisterHandler(handler func(ctx context.Context, msg *schema.NotificationMsg) error)
}
func AddNotification(msg *schema.NotificationMsg) {
NotificationQueue <- msg 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) ( func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) (
resp []*schema.NotificationContent, err error) { resp []*schema.NotificationContent, err error) {
lang := handler.GetLangByCtx(ctx) lang := handler.GetLangByCtx(ctx)
enableShortID := handler.GetEnableShortID(ctx)
for _, notificationInfo := range notifications { for _, notificationInfo := range notifications {
item := &schema.NotificationContent{} item := &schema.NotificationContent{}
if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil { 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.UpdateTime = notificationInfo.UpdatedAt.Unix()
item.IsRead = notificationInfo.IsRead == schema.NotificationRead item.IsRead = notificationInfo.IsRead == schema.NotificationRead
if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok { if enableShortID {
if item.ObjectInfo.ObjectID == answerID { if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok {
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"]) 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 {
if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok { item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
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) resp = append(resp, item)

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ package questioncommon
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"math" "math"
"time" "time"
@ -48,26 +47,27 @@ type QuestionRepo interface {
UpdateAccepted(ctx context.Context, question *entity.Question) (err error) UpdateAccepted(ctx context.Context, question *entity.Question) (err error)
UpdateLastAnswer(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) 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) GetQuestionCount(ctx context.Context) (count int64, err error)
GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error)
GetQuestionCountByIDs(ctx context.Context, ids []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 // QuestionCommon user service
type QuestionCommon struct { type QuestionCommon struct {
questionRepo QuestionRepo questionRepo QuestionRepo
answerRepo answercommon.AnswerRepo answerRepo answercommon.AnswerRepo
voteRepo activity_common.VoteRepo voteRepo activity_common.VoteRepo
followCommon activity_common.FollowRepo followCommon activity_common.FollowRepo
tagCommon *tagcommon.TagCommonService tagCommon *tagcommon.TagCommonService
userCommon *usercommon.UserCommon userCommon *usercommon.UserCommon
collectionCommon *collectioncommon.CollectionCommon collectionCommon *collectioncommon.CollectionCommon
AnswerCommon *answercommon.AnswerCommon AnswerCommon *answercommon.AnswerCommon
metaService *meta.MetaService metaService *meta.MetaService
configService *config.ConfigService configService *config.ConfigService
data *data.Data activityQueueService activity_queue.ActivityQueueService
data *data.Data
} }
func NewQuestionCommon(questionRepo QuestionRepo, func NewQuestionCommon(questionRepo QuestionRepo,
@ -80,21 +80,22 @@ func NewQuestionCommon(questionRepo QuestionRepo,
answerCommon *answercommon.AnswerCommon, answerCommon *answercommon.AnswerCommon,
metaService *meta.MetaService, metaService *meta.MetaService,
configService *config.ConfigService, configService *config.ConfigService,
activityQueueService activity_queue.ActivityQueueService,
data *data.Data, data *data.Data,
) *QuestionCommon { ) *QuestionCommon {
return &QuestionCommon{ return &QuestionCommon{
questionRepo: questionRepo, questionRepo: questionRepo,
answerRepo: answerRepo, answerRepo: answerRepo,
voteRepo: voteRepo, voteRepo: voteRepo,
followCommon: followCommon, followCommon: followCommon,
tagCommon: tagCommon, tagCommon: tagCommon,
userCommon: userCommon, userCommon: userCommon,
collectionCommon: collectionCommon, collectionCommon: collectionCommon,
AnswerCommon: answerCommon, AnswerCommon: answerCommon,
metaService: metaService, metaService: metaService,
configService: configService, configService: configService,
data: data, activityQueueService: activityQueueService,
data: data,
} }
} }
@ -513,7 +514,7 @@ func (qs *QuestionCommon) CloseQuestion(ctx context.Context, req *schema.CloseQu
return err return err
} }
activity_queue.AddActivity(&schema.ActivityMsg{ qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
UserID: questionInfo.UserID, UserID: questionInfo.UserID,
ObjectID: questionInfo.ID, ObjectID: questionInfo.ID,
OriginalObjectID: 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) { func (qs *QuestionCommon) SitemapCron(ctx context.Context) {
data := &schema.SiteMapList{}
questionNum, err := qs.questionRepo.GetQuestionCount(ctx) questionNum, err := qs.questionRepo.GetQuestionCount(ctx)
if err != nil { if err != nil {
log.Error("GetQuestionCount error", err) log.Error(err)
return return
} }
if questionNum <= schema.SitemapMaxSize { if questionNum <= constant.SitemapMaxSize {
questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, 0, int(questionNum)) _, err = qs.questionRepo.SitemapQuestions(ctx, 1, int(questionNum))
if err != nil { 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 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() 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 { if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
} }

View File

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

View File

@ -29,6 +29,10 @@ const (
) )
type UserRankRepo interface { 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) 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) UserRankPage(ctx context.Context, userId string, page, pageSize int) (rankPage []*entity.Activity, total int64, err error)
} }

View File

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

View File

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

View File

@ -16,7 +16,6 @@ import (
questioncommon "github.com/answerdev/answer/internal/service/question_common" questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/internal/service/siteinfo_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/pkg/uid"
"github.com/answerdev/answer/plugin" "github.com/answerdev/answer/plugin"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
@ -25,7 +24,7 @@ import (
type SiteInfoService struct { type SiteInfoService struct {
siteInfoRepo siteinfo_common.SiteInfoRepo siteInfoRepo siteinfo_common.SiteInfoRepo
siteInfoCommonService *siteinfo_common.SiteInfoCommonService siteInfoCommonService siteinfo_common.SiteInfoCommonService
emailService *export.EmailService emailService *export.EmailService
tagCommonService *tagcommon.TagCommonService tagCommonService *tagcommon.TagCommonService
configService *config.ConfigService configService *config.ConfigService
@ -34,7 +33,7 @@ type SiteInfoService struct {
func NewSiteInfoService( func NewSiteInfoService(
siteInfoRepo siteinfo_common.SiteInfoRepo, siteInfoRepo siteinfo_common.SiteInfoRepo,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService, siteInfoCommonService siteinfo_common.SiteInfoCommonService,
emailService *export.EmailService, emailService *export.EmailService,
tagCommonService *tagcommon.TagCommonService, tagCommonService *tagcommon.TagCommonService,
configService *config.ConfigService, 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) { func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (err error) {
var ( content, _ := json.Marshal(req)
siteType = constant.SiteTypeSeo
content []byte
)
content, _ = json.Marshal(req)
data := entity.SiteInfo{ data := entity.SiteInfo{
Type: siteType, Type: constant.SiteTypeSeo,
Content: string(content), Content: string(content),
} }
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeSeo, &data)
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
} }
func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema.GetPrivilegesConfigResp, err error) { 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/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/pkg/gravatar" "github.com/answerdev/answer/pkg/gravatar"
"github.com/answerdev/answer/pkg/uid"
"github.com/segmentfault/pacman/log" "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) GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error)
} }
// SiteInfoCommonService site info common service // siteInfoCommonService site info common service
type SiteInfoCommonService struct { type siteInfoCommonService struct {
siteInfoRepo SiteInfoRepo 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 // NewSiteInfoCommonService new site info common service
func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService { func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) SiteInfoCommonService {
siteInfo := &SiteInfoCommonService{ return &siteInfoCommonService{
siteInfoRepo: siteInfoRepo, 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 // 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{} resp = &schema.SiteGeneralResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil { if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
return nil, err return nil, err
@ -51,7 +55,7 @@ func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schem
} }
// GetSiteInterface get site info interface // 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{} resp = &schema.SiteInterfaceResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil { if err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil {
return nil, err return nil, err
@ -60,7 +64,7 @@ func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *sch
} }
// GetSiteBranding get site info branding // 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{} resp = &schema.SiteBrandingResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil { if err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {
return nil, err return nil, err
@ -69,7 +73,7 @@ func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *sche
} }
// GetSiteUsers get site info about users // 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{} resp = &schema.SiteUsersResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil { if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil {
return nil, err return nil, err
@ -78,13 +82,13 @@ func (s *SiteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.
} }
// FormatAvatar format avatar // 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) gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx)
return s.selectedAvatar(originalAvatarData, defaultAvatar, gravatarBaseURL, email) return s.selectedAvatar(originalAvatarData, defaultAvatar, gravatarBaseURL, email)
} }
// FormatListAvatar format avatar // 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) { avatarMapping map[string]*schema.AvatarInfo) {
gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx) gravatarBaseURL, defaultAvatar := s.getAvatarDefaultConfig(ctx)
avatarMapping = make(map[string]*schema.AvatarInfo) avatarMapping = make(map[string]*schema.AvatarInfo)
@ -94,19 +98,22 @@ func (s *SiteInfoCommonService) FormatListAvatar(ctx context.Context, userList [
return avatarMapping 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 gravatarBaseURL, defaultAvatar := constant.DefaultGravatarBaseURL, constant.DefaultAvatar
usersConfig, err := s.GetSiteUsers(ctx) usersConfig, err := s.GetSiteUsers(ctx)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} else { }
if len(usersConfig.GravatarBaseURL) > 0 {
gravatarBaseURL = usersConfig.GravatarBaseURL gravatarBaseURL = usersConfig.GravatarBaseURL
}
if len(usersConfig.DefaultAvatar) > 0 {
defaultAvatar = usersConfig.DefaultAvatar defaultAvatar = usersConfig.DefaultAvatar
} }
return gravatarBaseURL, defaultAvatar return gravatarBaseURL, defaultAvatar
} }
func (s *SiteInfoCommonService) selectedAvatar( func (s *siteInfoCommonService) selectedAvatar(
originalAvatarData string, defaultAvatar string, gravatarBaseURL string, email string) *schema.AvatarInfo { originalAvatarData string, defaultAvatar string, gravatarBaseURL string, email string) *schema.AvatarInfo {
avatarInfo := &schema.AvatarInfo{} avatarInfo := &schema.AvatarInfo{}
_ = json.Unmarshal([]byte(originalAvatarData), avatarInfo) _ = json.Unmarshal([]byte(originalAvatarData), avatarInfo)
@ -121,7 +128,7 @@ func (s *SiteInfoCommonService) selectedAvatar(
} }
// GetSiteWrite get site info write // 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{} resp = &schema.SiteWriteResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil { if err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {
return nil, err return nil, err
@ -130,7 +137,7 @@ func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.
} }
// GetSiteLegal get site info write // 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{} resp = &schema.SiteLegalResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil { if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil {
return nil, err return nil, err
@ -139,7 +146,7 @@ func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.
} }
// GetSiteLogin get site login config // 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{} resp = &schema.SiteLoginResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil { if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {
return nil, err return nil, err
@ -148,7 +155,7 @@ func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.
} }
// GetSiteCustomCssHTML get site custom css html config // 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{} resp = &schema.SiteCustomCssHTMLResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil { if err = s.GetSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {
return nil, err return nil, err
@ -157,7 +164,7 @@ func (s *SiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp
} }
// GetSiteTheme get site theme // 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{ resp = &schema.SiteThemeResp{
ThemeOptions: schema.GetThemeOptions, ThemeOptions: schema.GetThemeOptions,
} }
@ -169,15 +176,24 @@ func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.
} }
// GetSiteSeo get site seo // GetSiteSeo get site seo
func (s *SiteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) { func (s *siteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) {
resp = &schema.SiteSeoReq{} resp = &schema.SiteSeoResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil { if err = s.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
return nil, err return nil, err
} }
return resp, nil 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) siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, siteType)
if err != nil { if err != nil {
return err return err

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ type UserService struct {
activityRepo activity_common.ActivityRepo activityRepo activity_common.ActivityRepo
emailService *export.EmailService emailService *export.EmailService
authService *auth.AuthService authService *auth.AuthService
siteInfoService *siteinfo_common.SiteInfoCommonService siteInfoService siteinfo_common.SiteInfoCommonService
userRoleService *role.UserRoleRelService userRoleService *role.UserRoleRelService
userExternalLoginService *user_external_login.UserExternalLoginService userExternalLoginService *user_external_login.UserExternalLoginService
} }
@ -48,7 +48,7 @@ func NewUserService(userRepo usercommon.UserRepo,
activityRepo activity_common.ActivityRepo, activityRepo activity_common.ActivityRepo,
emailService *export.EmailService, emailService *export.EmailService,
authService *auth.AuthService, authService *auth.AuthService,
siteInfoService *siteinfo_common.SiteInfoCommonService, siteInfoService siteinfo_common.SiteInfoCommonService,
userRoleService *role.UserRoleRelService, userRoleService *role.UserRoleRelService,
userCommonService *usercommon.UserCommon, userCommonService *usercommon.UserCommon,
userExternalLoginService *user_external_login.UserExternalLoginService, 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) 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 return nil, nil
} }

View File

@ -2,6 +2,8 @@ package service
import ( import (
"context" "context"
"github.com/answerdev/answer/internal/service/activity_common"
"strings"
"github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler" "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/config"
"github.com/answerdev/answer/internal/service/object_info" "github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/obj"
"github.com/segmentfault/pacman/log" "github.com/segmentfault/pacman/log"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
answercommon "github.com/answerdev/answer/internal/service/answer_common" answercommon "github.com/answerdev/answer/internal/service/answer_common"
questioncommon "github.com/answerdev/answer/internal/service/question_common" questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
) )
// VoteRepo activity repository // VoteRepo activity repository
type VoteRepo interface { type VoteRepo interface {
VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) Vote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) CancelVote(ctx context.Context, op *schema.VoteOperationInfo) (err error)
VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) GetAndSaveVoteResult(ctx context.Context, objectID, objectType string) (up, down int64, err error)
VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) ListUserVotes(ctx context.Context, userID string, page int, pageSize int, activityTypes []int) (
GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) voteList []*entity.Activity, total int64, err error)
ListUserVotes(ctx context.Context, userID string, req schema.GetVoteWithPageReq, activityTypes []int) (voteList []entity.Activity, total int64, err error)
} }
// VoteService user service // VoteService user service
type VoteService struct { type VoteService struct {
voteRepo VoteRepo voteRepo VoteRepo
UniqueIDRepo unique.UniqueIDRepo
configService *config.ConfigService configService *config.ConfigService
questionRepo questioncommon.QuestionRepo questionRepo questioncommon.QuestionRepo
answerRepo answercommon.AnswerRepo answerRepo answercommon.AnswerRepo
commentCommonRepo comment_common.CommentCommonRepo commentCommonRepo comment_common.CommentCommonRepo
objectService *object_info.ObjService objectService *object_info.ObjService
activityRepo activity_common.ActivityRepo
} }
func NewVoteService( func NewVoteService(
VoteRepo VoteRepo, voteRepo VoteRepo,
uniqueIDRepo unique.UniqueIDRepo,
configService *config.ConfigService, configService *config.ConfigService,
questionRepo questioncommon.QuestionRepo, questionRepo questioncommon.QuestionRepo,
answerRepo answercommon.AnswerRepo, answerRepo answercommon.AnswerRepo,
@ -55,8 +53,7 @@ func NewVoteService(
objectService *object_info.ObjService, objectService *object_info.ObjService,
) *VoteService { ) *VoteService {
return &VoteService{ return &VoteService{
voteRepo: VoteRepo, voteRepo: voteRepo,
UniqueIDRepo: uniqueIDRepo,
configService: configService, configService: configService,
questionRepo: questionRepo, questionRepo: questionRepo,
answerRepo: answerRepo, answerRepo: answerRepo,
@ -66,94 +63,87 @@ func NewVoteService(
} }
// VoteUp vote up // VoteUp vote up
func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { func (vs *VoteService) VoteUp(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{} objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
var objectUserID string
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
if err != nil { if err != nil {
return return nil, err
} }
// make object id must be decoded
objectInfo.ObjectID = req.ObjectID
// check user is voting self or not // check user is voting self or not
if objectUserID == dto.UserID { if objectInfo.ObjectCreatorUserID == req.UserID {
err = errors.BadRequest(reason.DisallowVoteYourSelf) return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
return
} }
if dto.IsCancel { voteUpOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, true, objectInfo)
return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
// vote operation
if req.IsCancel {
err = vs.voteRepo.CancelVote(ctx, voteUpOperationInfo)
} else { } 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 // VoteDown vote down
func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) { func (vs *VoteService) VoteDown(ctx context.Context, req *schema.VoteReq) (resp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{} objectInfo, err := vs.objectService.GetInfo(ctx, req.ObjectID)
var objectUserID string
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
if err != nil { if err != nil {
return return nil, err
} }
// make object id must be decoded
objectInfo.ObjectID = req.ObjectID
// check user is voting self or not // check user is voting self or not
if objectUserID == dto.UserID { if objectInfo.ObjectCreatorUserID == req.UserID {
err = errors.BadRequest(reason.DisallowVoteYourSelf) return nil, errors.BadRequest(reason.DisallowVoteYourSelf)
return
} }
if dto.IsCancel { // vote operation
return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID) voteDownOperationInfo := vs.createVoteOperationInfo(ctx, req.UserID, false, objectInfo)
if req.IsCancel {
err = vs.voteRepo.CancelVote(ctx, voteDownOperationInfo)
} else { } 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 { if err != nil {
err = nil log.Error(err)
return
} }
resp.Votes = resp.UpVotes - resp.DownVotes
switch objectKey { if !req.IsCancel {
case "question": resp.VoteStatus = constant.ActVoteDown
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
} }
return resp, nil
return
} }
// ListUserVotes list user's votes // 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{ typeKeys := []string{
activity_type.QuestionVoteUp, activity_type.QuestionVoteUp,
activity_type.QuestionVoteDown, activity_type.QuestionVoteDown,
@ -172,14 +162,14 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
activityTypeMapping[cfg.ID] = typeKey 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 { if err != nil {
return return nil, err
} }
lang := handler.GetLangByCtx(ctx) lang := handler.GetLangByCtx(ctx)
resp := make([]*schema.GetVoteWithPageResp, 0) votes := make([]*schema.GetVoteWithPageResp, 0)
for _, voteInfo := range voteList { for _, voteInfo := range voteList {
objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID) objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID)
if err != nil { if err != nil {
@ -202,7 +192,65 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
if objInfo.QuestionStatus == entity.QuestionStatusDeleted { if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
item.Title = translator.Tr(lang, constant.DeletedQuestionTitleTrKey) 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) const salt = int64(100)
var ShortIDSwitch = false
// NumToString num to string // NumToString num to string
func NumToShortID(id int64) string { func NumToShortID(id int64) string {
sid := strconv.FormatInt(id, 10) sid := strconv.FormatInt(id, 10)
@ -45,14 +43,11 @@ func ShortIDToNum(code string) int64 {
} }
func EnShortID(id string) string { func EnShortID(id string) string {
if ShortIDSwitch { num, err := strconv.ParseInt(id, 10, 64)
num, err := strconv.ParseInt(id, 10, 64) if err != nil {
if err != nil { return id
return id
}
return NumToShortID(num)
} }
return id return NumToShortID(num)
} }
func DeShortID(sid string) string { func DeShortID(sid string) string {

View File

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

View File

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

View File

@ -1,6 +1,7 @@
plugin: plugin:
uc_login: uc_login:
ui: ui:
connect: Connect with {{ auth_name }}
login: Login login: Login
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in. 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. 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: plugin:
uc_login: uc_login:
ui: ui:
connect: 连接到 {{ auth_name }}
login: 登录 login: 登录
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录 qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录
login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。 login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。