Merge branch 'feat/1.1.0/sql' into feat/1.1.0/report

# Conflicts:
#	internal/controller/question_controller.go
#	internal/migrations/migrations.go
#	internal/migrations/v13.go
This commit is contained in:
LinkinStars 2023-05-23 16:43:15 +08:00
commit 01bd603570
33 changed files with 1171 additions and 44 deletions

View File

@ -15,7 +15,7 @@ builds:
- id: build
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X github.com/answerdev/answer/cmd.Version={{.Version}} -X github.com/answerdev/answer/cmd.Revision={{.ShortCommit}} -X github.com/answerdev/answer/cmd.Time={{.Date}} -X main.BuildUser=goreleaser
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
flags: -v
goos:
- linux
@ -26,7 +26,7 @@ builds:
- id: build-windows
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X github.com/answerdev/answer/cmd.Version={{.Version}} -X github.com/answerdev/answer/cmd.Revision={{.ShortCommit}} -X github.com/answerdev/answer/cmd.Time={{.Date}} -X main.BuildUser=goreleaser
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
flags: -v
goos:
- windows

View File

@ -1,5 +1,6 @@
{
"eslint.workingDirectories": [
"ui"
]
],
"commentTranslate.multiLineMerge": true
}

View File

@ -4,7 +4,7 @@
# Answer - 构建问答社区
一款问答形式的知识社区开源软件,用来快速构建产品你的产品技术社区、客户支持社区、用户社区等。
一款问答形式的知识社区开源软件,你可以使用它快速建立你的问答社区,用于产品技术支持、客户支持、用户交流等。
了解更多关于该项目的内容,请访问 [answer.dev](https://answer.dev).

View File

@ -3654,6 +3654,81 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/question/invite": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get question invite user info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "get question invite user info",
"parameters": [
{
"type": "string",
"default": "1",
"description": "Question ID",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update question invite user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "update question invite user",
"parameters": [
{
"description": "question",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.QuestionUpdateInviteUser"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question/operation": {
"put": {
"security": [
@ -5103,6 +5178,55 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/info/search": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "SearchUserListByName",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "SearchUserListByName",
"parameters": [
{
"type": "string",
"description": "username",
"name": "username",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetOtherUserInfoResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/interface": {
"put": {
"security": [
@ -7618,6 +7742,12 @@ const docTemplate = `{
"maxLength": 65535,
"minLength": 6
},
"mention_username_list": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"description": "tags",
"type": "array",
@ -7781,6 +7911,12 @@ const docTemplate = `{
"description": "question id",
"type": "string"
},
"invite_user": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"description": "tags",
"type": "array",
@ -7796,6 +7932,23 @@ const docTemplate = `{
}
}
},
"schema.QuestionUpdateInviteUser": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"invite_user": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.RemoveAnswerReq": {
"type": "object",
"required": [
@ -8925,6 +9078,14 @@ const docTemplate = `{
"pass"
],
"properties": {
"captcha_code": {
"type": "string",
"maxLength": 500
},
"captcha_id": {
"type": "string",
"maxLength": 500
},
"old_pass": {
"type": "string",
"maxLength": 32,

View File

@ -3642,6 +3642,81 @@
}
}
},
"/answer/api/v1/question/invite": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get question invite user info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "get question invite user info",
"parameters": [
{
"type": "string",
"default": "1",
"description": "Question ID",
"name": "id",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update question invite user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "update question invite user",
"parameters": [
{
"description": "question",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.QuestionUpdateInviteUser"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question/operation": {
"put": {
"security": [
@ -5091,6 +5166,55 @@
}
}
},
"/answer/api/v1/user/info/search": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "SearchUserListByName",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "SearchUserListByName",
"parameters": [
{
"type": "string",
"description": "username",
"name": "username",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetOtherUserInfoResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/interface": {
"put": {
"security": [
@ -7606,6 +7730,12 @@
"maxLength": 65535,
"minLength": 6
},
"mention_username_list": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"description": "tags",
"type": "array",
@ -7769,6 +7899,12 @@
"description": "question id",
"type": "string"
},
"invite_user": {
"type": "array",
"items": {
"type": "string"
}
},
"tags": {
"description": "tags",
"type": "array",
@ -7784,6 +7920,23 @@
}
}
},
"schema.QuestionUpdateInviteUser": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"invite_user": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.RemoveAnswerReq": {
"type": "object",
"required": [
@ -8913,6 +9066,14 @@
"pass"
],
"properties": {
"captcha_code": {
"type": "string",
"maxLength": 500
},
"captcha_id": {
"type": "string",
"maxLength": 500
},
"old_pass": {
"type": "string",
"maxLength": 32,

View File

@ -1218,6 +1218,10 @@ definitions:
maxLength: 65535
minLength: 6
type: string
mention_username_list:
items:
type: string
type: array
tags:
description: tags
items:
@ -1334,6 +1338,10 @@ definitions:
id:
description: question id
type: string
invite_user:
items:
type: string
type: array
tags:
description: tags
items:
@ -1350,6 +1358,17 @@ definitions:
- tags
- title
type: object
schema.QuestionUpdateInviteUser:
properties:
id:
type: string
invite_user:
items:
type: string
type: array
required:
- id
type: object
schema.RemoveAnswerReq:
properties:
id:
@ -2134,6 +2153,12 @@ definitions:
type: object
schema.UserModifyPasswordReq:
properties:
captcha_code:
maxLength: 500
type: string
captcha_id:
maxLength: 500
type: string
old_pass:
maxLength: 32
minLength: 8
@ -4467,6 +4492,53 @@ paths:
summary: get question details
tags:
- Question
/answer/api/v1/question/invite:
get:
consumes:
- application/json
description: get question invite user info
parameters:
- default: "1"
description: Question ID
in: query
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
summary: get question invite user info
tags:
- Question
put:
consumes:
- application/json
description: update question invite user
parameters:
- description: question
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.QuestionUpdateInviteUser'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update question invite user
tags:
- Question
/answer/api/v1/question/operation:
put:
consumes:
@ -5350,6 +5422,34 @@ paths:
summary: UserUpdateInfo update user info
tags:
- User
/answer/api/v1/user/info/search:
get:
consumes:
- application/json
description: SearchUserListByName
parameters:
- description: username
in: query
name: username
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetOtherUserInfoResp'
type: object
security:
- ApiKeyAuth: []
summary: SearchUserListByName
tags:
- User
/answer/api/v1/user/interface:
put:
consumes:

View File

@ -35,4 +35,6 @@ const (
NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
// NotificationYourCommentWasDeleted your comment was deleted
NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted"
// NotificationInvitedYouToAnswer invited you to answer
NotificationInvitedYouToAnswer = "notification.action.invited_you_to_answer"
)

View File

@ -221,6 +221,44 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, info)
}
// GetQuestionInviteUserInfo get question invite user info
// @Summary get question invite user info
// @Description get question invite user info
// @Tags Question
// @Security ApiKeyAuth
// @Accept json
// @Produce json
// @Param id query string true "Question ID" default(1)
// @Success 200 {string} string ""
// @Router /answer/api/v1/question/invite [get]
func (qc *QuestionController) GetQuestionInviteUserInfo(ctx *gin.Context) {
id := ctx.Query("id")
id = uid.DeShortID(id)
userID := middleware.GetLoginUserIDFromContext(ctx)
req := schema.QuestionPermission{}
canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{
permission.QuestionEdit,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, userID, id)
req.CanEdit = canList[0] || objectOwner
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
list, err := qc.questionService.InviteUserInfo(ctx, id)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
handler.HandleResponse(ctx, nil, list)
}
// SimilarQuestion godoc
// @Summary Search Similar Question
// @Description Search Similar Question
@ -500,6 +538,51 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview})
}
// UpdateQuestionInviteUser update question invite user
// @Summary update question invite user
// @Description update question invite user
// @Tags Question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.QuestionUpdateInviteUser true "question"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question/invite [put]
func (qc *QuestionController) UpdateQuestionInviteUser(ctx *gin.Context) {
req := &schema.QuestionUpdateInviteUser{}
errFields := handler.BindAndCheckReturnErr(ctx, req)
if ctx.IsAborted() {
return
}
req.ID = uid.DeShortID(req.ID)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.QuestionEdit,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
req.CanEdit = canList[0] || objectOwner
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
if len(errFields) > 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
return
}
err = qc.questionService.UpdateQuestionInviteUser(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
handler.HandleResponse(ctx, nil, nil)
}
// SearchByTitleLike add question title like
// @Summary add question title like
// @Description add question title like

View File

@ -470,6 +470,8 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
data["description"] = siteInfo.Description
data["language"] = handler.GetLang(ctx)
data["timezone"] = siteInfo.Interface.TimeZone
language := strings.Replace(siteInfo.Interface.Language, "_", "-", -1)
data["lang"] = language
data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead
data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter

View File

@ -350,6 +350,21 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.AccessToken = middleware.ExtractToken(ctx)
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeModifyPass, ctx.ClientIP(),
req.CaptchaID, req.CaptchaCode)
if !captchaPass {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
_, err := uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeModifyPass, ctx.ClientIP())
if err != nil {
log.Error(err)
}
oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
@ -363,6 +378,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)
return
}
if req.OldPass == req.Pass {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "pass",
@ -372,6 +388,9 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
return
}
err = uc.userService.UserModifyPassword(ctx, req)
if err == nil {
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
}
handler.HandleResponse(ctx, err, nil)
}
@ -588,3 +607,22 @@ func (uc *UserController) UserUnsubscribeEmailNotification(ctx *gin.Context) {
err := uc.userService.UserUnsubscribeEmailNotification(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// SearchUserListByName godoc
// @Summary SearchUserListByName
// @Description SearchUserListByName
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "username"
// @Success 200 {object} handler.RespBody{data=schema.GetOtherUserInfoResp}
// @Router /answer/api/v1/user/info/search [get]
func (uc *UserController) SearchUserListByName(ctx *gin.Context) {
req := &schema.GetOtherUserInfoByUsernameReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := uc.userService.SearchUserListByName(ctx, req.Username)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -32,6 +32,7 @@ type Question struct {
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
InviteUserID string `xorm:"TEXT invite_user_id"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`

View File

@ -4,6 +4,7 @@ import "time"
const (
TagRelStatusAvailable = 1
TagRelStatusHide = 2
TagRelStatusDeleted = 10
)

View File

@ -3,17 +3,24 @@ package migrations
import (
"encoding/json"
"fmt"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func updateCount(x *xorm.Engine) error {
addPrivilegeForInviteSomeoneToAnswer(x)
addGravatarBaseURL(x)
updateQuestionCount(x)
updateTagCount(x)
updateUserQuestionCount(x)
updateUserAnswerCount(x)
inviteAnswer(x)
return nil
}
@ -97,3 +104,236 @@ func addPrivilegeForInviteSomeoneToAnswer(x *xorm.Engine) error {
}
return nil
}
func updateQuestionCount(x *xorm.Engine) error {
//question answer count
answers := make([]entity.Answer, 0)
err := x.Find(&answers, &entity.Answer{Status: entity.AnswerStatusAvailable})
if err != nil {
return fmt.Errorf("get answers failed: %w", err)
}
questionAnswerCount := make(map[string]int)
for _, answer := range answers {
_, ok := questionAnswerCount[answer.QuestionID]
if !ok {
questionAnswerCount[answer.QuestionID] = 1
} else {
questionAnswerCount[answer.QuestionID]++
}
}
questionList := make([]entity.Question, 0)
err = x.Find(&questionList, &entity.Question{})
if err != nil {
return fmt.Errorf("get questions failed: %w", err)
}
for _, item := range questionList {
_, ok := questionAnswerCount[item.ID]
if ok {
item.AnswerCount = questionAnswerCount[item.ID]
if _, err = x.Update(item, &entity.Question{ID: item.ID}); err != nil {
log.Errorf("update %+v config failed: %s", item, err)
return fmt.Errorf("update question failed: %w", err)
}
}
}
return nil
}
// updateTagCount update tag count
func updateTagCount(x *xorm.Engine) error {
tagRelList := make([]entity.TagRel, 0)
err := x.Find(&tagRelList, &entity.TagRel{})
if err != nil {
return fmt.Errorf("get tag rel failed: %w", err)
}
questionIDs := make([]string, 0)
questionsAvailableMap := make(map[string]bool)
questionsHideMap := make(map[string]bool)
for _, item := range tagRelList {
questionIDs = append(questionIDs, item.ObjectID)
questionsAvailableMap[item.ObjectID] = false
questionsHideMap[item.ObjectID] = false
}
questionList := make([]entity.Question, 0)
err = x.In("id", questionIDs).In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &entity.Question{})
if err != nil {
return fmt.Errorf("get questions failed: %w", err)
}
for _, question := range questionList {
_, ok := questionsAvailableMap[question.ID]
if ok {
questionsAvailableMap[question.ID] = true
if question.Show == entity.QuestionHide {
questionsHideMap[question.ID] = true
}
}
}
for id, ok := range questionsHideMap {
if ok {
if _, err = x.Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide}, &entity.TagRel{ObjectID: id}); err != nil {
log.Errorf("update %+v config failed: %s", id, err)
}
}
}
for id, ok := range questionsAvailableMap {
if !ok {
if _, err = x.Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}, &entity.TagRel{ObjectID: id}); err != nil {
log.Errorf("update %+v config failed: %s", id, err)
}
}
}
//select tag count
newTagRelList := make([]entity.TagRel, 0)
err = x.Find(&newTagRelList, &entity.TagRel{Status: entity.TagRelStatusAvailable})
if err != nil {
return fmt.Errorf("get tag rel failed: %w", err)
}
tagCountMap := make(map[string]int)
for _, v := range newTagRelList {
_, ok := tagCountMap[v.TagID]
if !ok {
tagCountMap[v.TagID] = 1
} else {
tagCountMap[v.TagID]++
}
}
TagList := make([]entity.Tag, 0)
err = x.Find(&TagList, &entity.Tag{})
if err != nil {
return fmt.Errorf("get tag failed: %w", err)
}
for _, tag := range TagList {
_, ok := tagCountMap[tag.ID]
if ok {
tag.QuestionCount = tagCountMap[tag.ID]
if _, err = x.Update(tag, &entity.Tag{ID: tag.ID}); err != nil {
log.Errorf("update %+v tag failed: %s", tag.ID, err)
return fmt.Errorf("update tag failed: %w", err)
}
} else {
tag.QuestionCount = 0
if _, err = x.Update(tag, &entity.Tag{ID: tag.ID}); err != nil {
log.Errorf("update %+v tag failed: %s", tag.ID, err)
return fmt.Errorf("update tag failed: %w", err)
}
}
}
return nil
}
// updateUserQuestionCount update user question count
func updateUserQuestionCount(x *xorm.Engine) error {
questionList := make([]entity.Question, 0)
err := x.In("status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).Find(&questionList, &entity.Question{})
if err != nil {
return fmt.Errorf("get question failed: %w", err)
}
userQuestionCountMap := make(map[string]int)
for _, question := range questionList {
_, ok := userQuestionCountMap[question.UserID]
if !ok {
userQuestionCountMap[question.UserID] = 1
} else {
userQuestionCountMap[question.UserID]++
}
}
userList := make([]entity.User, 0)
err = x.Find(&userList, &entity.User{})
if err != nil {
return fmt.Errorf("get user failed: %w", err)
}
for _, user := range userList {
_, ok := userQuestionCountMap[user.ID]
if ok {
user.QuestionCount = userQuestionCountMap[user.ID]
if _, err = x.Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil {
log.Errorf("update %+v user failed: %s", user.ID, err)
return fmt.Errorf("update user failed: %w", err)
}
} else {
user.QuestionCount = 0
if _, err = x.Cols("question_count").Update(user, &entity.User{ID: user.ID}); err != nil {
log.Errorf("update %+v user failed: %s", user.ID, err)
return fmt.Errorf("update user failed: %w", err)
}
}
}
return nil
}
// updateUserAnswerCount update user answer count
func updateUserAnswerCount(x *xorm.Engine) error {
answers := make([]entity.Answer, 0)
err := x.Find(&answers, &entity.Answer{Status: entity.AnswerStatusAvailable})
if err != nil {
return fmt.Errorf("get answers failed: %w", err)
}
userAnswerCount := make(map[string]int)
for _, answer := range answers {
_, ok := userAnswerCount[answer.UserID]
if !ok {
userAnswerCount[answer.UserID] = 1
} else {
userAnswerCount[answer.UserID]++
}
}
userList := make([]entity.User, 0)
err = x.Find(&userList, &entity.User{})
if err != nil {
return fmt.Errorf("get user failed: %w", err)
}
for _, user := range userList {
_, ok := userAnswerCount[user.ID]
if ok {
user.AnswerCount = userAnswerCount[user.ID]
if _, err = x.Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil {
log.Errorf("update %+v user failed: %s", user.ID, err)
return fmt.Errorf("update user failed: %w", err)
}
} else {
user.AnswerCount = 0
if _, err = x.Cols("answer_count").Update(user, &entity.User{ID: user.ID}); err != nil {
log.Errorf("update %+v user failed: %s", user.ID, err)
return fmt.Errorf("update user failed: %w", err)
}
}
}
return nil
}
func inviteAnswer(x *xorm.Engine) error {
type Question struct {
ID string `xorm:"not null pk BIGINT(20) id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
InviteUserID string `xorm:"TEXT invite_user_id"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 INT(11) status"`
Pin int `xorm:"not null default 1 INT(11) pin"`
Show int `xorm:"not null default 1 INT(11) show"`
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
AnswerCount int `xorm:"not null default 0 INT(11) answer_count"`
CollectionCount int `xorm:"not null default 0 INT(11) collection_count"`
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"`
LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
err := x.Sync(new(Question))
if err != nil {
return err
}
return nil
}

View File

@ -219,6 +219,24 @@ func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err
return
}
func (qr *questionRepo) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) {
questionList := make([]*entity.Question, 0)
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).And("user_id = ?", userID).Count(&questionList)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (qr *questionRepo) GetQuestionCountByIDs(ctx context.Context, ids []string) (count int64, err error) {
questionList := make([]*entity.Question, 0)
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed}).In("id = ?", ids).Count(&questionList)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) {
questionIDList = make([]*schema.SiteMapQuestionInfo, 0)
rows := make([]*entity.Question, 0)

View File

@ -138,7 +138,7 @@ func (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, p
) {
rankPage = make([]*entity.Activity, 0)
session := ur.data.DB.Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0}))
session := ur.data.DB.Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0})).And(builder.Gt{"rank": 0})
session.Desc("created_at")
cond := &entity.Activity{UserID: userID}

View File

@ -9,6 +9,7 @@ import (
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/answerdev/answer/pkg/uid"
"github.com/davecgh/go-spew/spew"
"github.com/segmentfault/pacman/errors"
)
@ -52,6 +53,26 @@ func (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID s
return
}
func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
spew.Dump("====== HideTagRelListByObjectID")
objectID = uid.DeShortID(objectID)
_, err = tr.data.DB.Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
spew.Dump("====== ShowTagRelListByObjectID")
objectID = uid.DeShortID(objectID)
_, err = tr.data.DB.Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// RemoveTagRelListByIDs delete tag list
func (tr *tagRelRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) {
_, err = tr.data.DB.In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted})
@ -90,7 +111,7 @@ func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectID string)
objectID = uid.DeShortID(objectID)
tagListList = make([]*entity.TagRel, 0)
session := tr.data.DB.Where("object_id = ?", objectID)
session.Where("status = ?", entity.TagRelStatusAvailable)
session.In("status", []int{entity.TagRelStatusAvailable, entity.TagRelStatusHide})
err = session.Find(&tagListList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()

View File

@ -71,6 +71,26 @@ func (ur *userRepo) IncreaseQuestionCount(ctx context.Context, userID string, am
return nil
}
func (ur *userRepo) UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error) {
user := &entity.User{}
user.QuestionCount = int(count)
_, err = ur.data.DB.Where("id = ?", userID).Cols("question_count").Update(user)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *userRepo) UpdateAnswerCount(ctx context.Context, userID string, count int) (err error) {
user := &entity.User{}
user.AnswerCount = count
_, err = ur.data.DB.Where("id = ?", userID).Cols("answer_count").Update(user)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
// UpdateLastLoginDate update last login date
func (ur *userRepo) UpdateLastLoginDate(ctx context.Context, userID string) (err error) {
user := &entity.User{LastLoginDate: time.Now()}
@ -175,6 +195,17 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
return
}
func (ur *userRepo) GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error) {
list := make([]*entity.User, 0)
err := ur.data.DB.Where("status =?", entity.UserStatusAvailable).In("username", usernames).Find(&list)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return list, err
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, list)
return list, nil
}
// GetByEmail get user by email
func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) {
userInfo = &entity.User{}
@ -195,6 +226,23 @@ func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
return
}
func (ur *userRepo) SearchUserListByName(ctx context.Context, name string) (userList []*entity.User, err error) {
userList = make([]*entity.User, 0)
if name == "" {
return userList, nil
}
session := ur.data.DB.Where("")
session.Where("username LIKE LOWER(?) or display_name LIKE ?", name+"%", name+"%").And("status =?", entity.UserStatusAvailable)
session.Asc("username")
session = session.Limit(5, 0)
err = session.OrderBy("id desc").Find(&userList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, userList)
return
}
func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) {
if original == nil {
return nil

View File

@ -112,6 +112,7 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord)
routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord)
routerGroup.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
routerGroup.GET("/user/info/search", a.userController.SearchUserListByName)
}
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
@ -129,6 +130,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
//question
r.GET("/question/info", a.questionController.GetQuestion)
r.GET("/question/invite", a.questionController.GetQuestionInviteUserInfo)
r.GET("/question/page", a.questionController.QuestionPage)
r.GET("/question/similar/tag", a.questionController.SimilarQuestion)
r.GET("/personal/qa/top", a.questionController.UserTop)
@ -193,6 +195,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.POST("/question", a.questionController.AddQuestion)
r.POST("/question/answer", a.questionController.AddQuestionByAnswer)
r.PUT("/question", a.questionController.UpdateQuestion)
r.PUT("/question/invite", a.questionController.UpdateQuestionInviteUser)
r.DELETE("/question", a.questionController.RemoveQuestion)
r.PUT("/question/status", a.questionController.CloseQuestion)
r.PUT("/question/operation", a.questionController.OperationQuestion)

View File

@ -90,6 +90,9 @@ func (a *UIRouter) Register(r *gin.Engine) {
if branding.Favicon != "" {
c.String(http.StatusOK, htmltext.GetPicByUrl(branding.Favicon))
return
} else if branding.SquareIcon != "" {
c.String(http.StatusOK, htmltext.GetPicByUrl(branding.SquareIcon))
return
} else {
c.Header("content-type", "image/vnd.microsoft.icon")
filePath = UIRootFilePath + urlPath

View File

@ -87,7 +87,8 @@ type QuestionAddByAnswer struct {
// tags
Tags []*TagItem `validate:"required,dive" json:"tags"`
// user id
UserID string `json:"-"`
UserID string `json:"-"`
MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"`
QuestionPermission
}
@ -139,7 +140,8 @@ type QuestionUpdate struct {
// content
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
// html
HTML string `json:"-"`
HTML string `json:"-"`
InviteUser []string `validate:"omitempty" json:"invite_user"`
// tags
Tags []*TagItem `validate:"required,dive" json:"tags"`
// edit summary
@ -150,6 +152,13 @@ type QuestionUpdate struct {
QuestionPermission
}
type QuestionUpdateInviteUser struct {
ID string `validate:"required" json:"id"`
InviteUser []string `validate:"omitempty" json:"invite_user"`
UserID string `json:"-"`
QuestionPermission
}
func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) {
req.HTML = converter.Markdown2HTML(req.Content)
return nil, nil

View File

@ -5,6 +5,7 @@ type SimpleObjectInfo struct {
ObjectID string `json:"object_id"`
ObjectCreatorUserID string `json:"object_creator_user_id"`
QuestionID string `json:"question_id"`
QuestionStatus int `json:"status"`
AnswerID string `json:"answer_id"`
CommentID string `json:"comment_id"`
TagID string `json:"tag_id"`

View File

@ -222,9 +222,10 @@ const (
NoticeStatusOn = 1
NoticeStatusOff = 2
ActionRecordTypeLogin = "login"
ActionRecordTypeEmail = "e_mail"
ActionRecordTypeFindPass = "find_pass"
ActionRecordTypeLogin = "login"
ActionRecordTypeEmail = "e_mail"
ActionRecordTypeFindPass = "find_pass"
ActionRecordTypeModifyPass = "modify_pass"
)
var UserStatusShow = map[int]string{
@ -277,6 +278,8 @@ type UserModifyPasswordReq struct {
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
UserID string `json:"-"`
AccessToken string `json:"-"`
CaptchaID string `validate:"omitempty,gt=0,lte=500" json:"captcha_id"`
CaptchaCode string `validate:"omitempty,gt=0,lte=500" json:"captcha_code"`
}
func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {
@ -367,7 +370,7 @@ type UserNoticeSetResp struct {
type ActionRecordReq struct {
// action
Action string `validate:"required,oneof=login e_mail find_pass" form:"action"`
Action string `validate:"required,oneof=login e_mail find_pass modify_pass" form:"action"`
IP string `json:"-"`
}

View File

@ -1,14 +1,22 @@
package activity_type
import "github.com/answerdev/answer/internal/repo/config"
import (
"github.com/answerdev/answer/internal/repo/config"
)
const (
QuestionVoteUp = "question.vote_up"
QuestionVoteDown = "question.vote_down"
AnswerVoteUp = "answer.vote_up"
AnswerVoteDown = "answer.vote_down"
CommentVoteUp = "comment.vote_up"
CommentVoteDown = "comment.vote_down"
QuestionVoteUp = "question.vote_up"
QuestionVoteDown = "question.vote_down"
AnswerVoteUp = "answer.vote_up"
AnswerVoteDown = "answer.vote_down"
CommentVoteUp = "comment.vote_up"
CommentVoteDown = "comment.vote_down"
AnswerAccepted = "answer.accepted"
AnswerAccept = "answer.accept"
QuestionVotedUp = "question.voted_up"
QuestionVotedDown = "question.voted_down"
AnswerVotedUp = "answer.voted_up"
AnswerVotedDown = "answer.voted_down"
)
var (
@ -19,14 +27,26 @@ var (
AnswerVoteDown,
CommentVoteUp,
CommentVoteDown,
AnswerAccepted,
AnswerAccept,
QuestionVotedUp,
QuestionVotedDown,
AnswerVotedUp,
AnswerVotedDown,
}
activityTypeFlagMapping = map[string]string{
QuestionVoteUp: "upvote",
QuestionVoteDown: "downvote",
AnswerVoteUp: "upvote",
AnswerVoteDown: "downvote",
CommentVoteUp: "upvote",
CommentVoteDown: "downvote",
QuestionVoteUp: "upvote",
QuestionVoteDown: "downvote",
AnswerVoteUp: "upvote",
AnswerVoteDown: "downvote",
CommentVoteUp: "upvote",
CommentVoteDown: "downvote",
AnswerAccepted: "accepted",
AnswerAccept: "accept",
QuestionVotedUp: "upvoted",
QuestionVotedDown: "downvoted",
AnswerVotedUp: "upvoted",
AnswerVotedDown: "downvoted",
}
)

View File

@ -453,6 +453,9 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s
commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)
commentResp.QuestionID = objInfo.QuestionID
commentResp.AnswerID = objInfo.AnswerID
if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
commentResp.Title = "Deleted question"
}
}
}
resp = append(resp, commentResp)

View File

@ -135,6 +135,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
ObjectID: questionInfo.ID,
ObjectCreatorUserID: questionInfo.UserID,
QuestionID: questionInfo.ID,
QuestionStatus: questionInfo.Status,
ObjectType: objectType,
Title: questionInfo.Title,
Content: questionInfo.ParsedText, // todo trim
@ -158,6 +159,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
ObjectID: answerInfo.ID,
ObjectCreatorUserID: answerInfo.UserID,
QuestionID: answerInfo.QuestionID,
QuestionStatus: questionInfo.Status,
AnswerID: answerInfo.ID,
ObjectType: objectType,
Title: questionInfo.Title, // this should be question title
@ -185,6 +187,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
}
if exist {
objInfo.QuestionID = questionInfo.ID
objInfo.QuestionStatus = questionInfo.Status
objInfo.Title = questionInfo.Title
}
answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, commentInfo.ObjectID)

View File

@ -46,6 +46,8 @@ type QuestionRepo interface {
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
AdminSearchList(ctx context.Context, search *schema.AdminQuestionSearch) ([]*entity.Question, int64, error)
GetQuestionCount(ctx context.Context) (count int64, err error)
GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error)
GetQuestionCountByIDs(ctx context.Context, ids []string) (count int64, err error)
GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
}
@ -88,6 +90,10 @@ func NewQuestionCommon(questionRepo QuestionRepo,
}
}
func (qs *QuestionCommon) GetUserQuestionCount(ctx context.Context, userID string) (count int64, err error) {
return qs.questionRepo.GetUserQuestionCount(ctx, userID)
}
func (qs *QuestionCommon) UpdatePv(ctx context.Context, questionID string) error {
return qs.questionRepo.UpdatePvCount(ctx, questionID)
}
@ -144,6 +150,34 @@ func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string
return list, nil
}
func (qs *QuestionCommon) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) {
InviteUserInfo := make([]*schema.UserBasicInfo, 0)
dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)
if err != nil {
return InviteUserInfo, err
}
if !has {
return InviteUserInfo, errors.NotFound(reason.QuestionNotFound)
}
//InviteUser
if dbinfo.InviteUserID != "" {
InviteUserIDs := make([]string, 0)
err := json.Unmarshal([]byte(dbinfo.InviteUserID), &InviteUserIDs)
if err == nil {
inviteUserInfoMap, err := qs.userCommon.BatchUserBasicInfoByID(ctx, InviteUserIDs)
if err == nil {
for _, userid := range InviteUserIDs {
_, ok := inviteUserInfoMap[userid]
if ok {
InviteUserInfo = append(InviteUserInfo, inviteUserInfoMap[userid])
}
}
}
}
}
return InviteUserInfo, nil
}
func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (showinfo *schema.QuestionInfo, err error) {
dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID)
if err != nil {
@ -180,9 +214,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUser
operation.Level = schema.OperationLevelInfo
showinfo.Operation = operation
}
}
}
}
@ -431,14 +463,16 @@ func (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.Remove
return err
}
// user add question count
err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, -1)
userQuestionCount, err := qs.GetUserQuestionCount(ctx, questionInfo.UserID)
if err != nil {
log.Error("user UpdateQuestionCount error", err.Error())
log.Error("user GetUserQuestionCount error", err.Error())
} else {
err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)
if err != nil {
log.Error("user IncreaseQuestionCount error", err.Error())
}
}
// todo rank remove
return nil
}

View File

@ -289,9 +289,14 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
}
// user add question count
err = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, 1)
userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, question.UserID)
if err != nil {
log.Error("user IncreaseQuestionCount error", err.Error())
log.Error("user GetUserQuestionCount error", err.Error())
} else {
err = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, userQuestionCount)
if err != nil {
log.Error("user IncreaseQuestionCount error", err.Error())
}
}
activity_queue.AddActivity(&schema.ActivityMsg{
@ -327,8 +332,24 @@ func (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.Op
switch req.Operation {
case schema.QuestionOperationHide:
questionInfo.Show = entity.QuestionHide
err = qs.tagCommon.HideTagRelListByObjectID(ctx, req.ID)
if err != nil {
return err
}
err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID)
if err != nil {
return err
}
case schema.QuestionOperationShow:
questionInfo.Show = entity.QuestionShow
err = qs.tagCommon.ShowTagRelListByObjectID(ctx, req.ID)
if err != nil {
return err
}
err = qs.tagCommon.RefreshTagCountByQuestionID(ctx, req.ID)
if err != nil {
return err
}
case schema.QuestionOperationPin:
questionInfo.Pin = entity.QuestionPin
case schema.QuestionOperationUnPin:
@ -404,10 +425,33 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
return err
}
// user add question count
err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, -1)
userQuestionCount, err := qs.questioncommon.GetUserQuestionCount(ctx, questionInfo.UserID)
if err != nil {
log.Error("user IncreaseQuestionCount error", err.Error())
log.Error("user GetUserQuestionCount error", err.Error())
} else {
err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, userQuestionCount)
if err != nil {
log.Error("user IncreaseQuestionCount error", err.Error())
}
}
//tag count
tagIDs := make([]string, 0)
Tags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID)
if tagerr != nil {
log.Error("GetObjectEntityTag error", tagerr)
return nil
}
for _, v := range Tags {
tagIDs = append(tagIDs, v.ID)
}
err = qs.tagCommon.RemoveTagRelListByObjectID(ctx, req.ID)
if err != nil {
log.Error("RemoveTagRelListByObjectID error", err.Error())
}
err = qs.tagCommon.RefreshTagQuestionCount(ctx, tagIDs)
if err != nil {
log.Error("efreshTagQuestionCount error", err.Error())
}
// #2372 In order to simplify the process and complexity, as well as to consider if it is in-house,
@ -491,6 +535,54 @@ func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *sch
return nil, nil
}
func (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *schema.QuestionUpdateInviteUser) (err error) {
//verify invite user
inviteUserInfoList, err := qs.userCommon.BatchGetUserBasicInfoByUserNames(ctx, req.InviteUser)
if err != nil {
log.Error("BatchGetUserBasicInfoByUserNames error", err.Error())
}
inviteUser := make([]string, 0)
for _, item := range req.InviteUser {
_, ok := inviteUserInfoList[item]
if ok {
inviteUser = append(inviteUser, inviteUserInfoList[item].ID)
}
}
inviteUserStr := ""
inviteUserByte, err := json.Marshal(inviteUser)
if err != nil {
log.Error("json.Marshal error", err.Error())
inviteUserStr = "[]"
} else {
inviteUserStr = string(inviteUserByte)
}
question := &entity.Question{}
question.ID = uid.DeShortID(req.ID)
question.InviteUserID = inviteUserStr
saveerr := qs.questionRepo.UpdateQuestion(ctx, question, []string{"invite_user_id"})
if saveerr != nil {
return saveerr
}
qs.notificationInviteUser(ctx, inviteUser, req.ID, req.UserID)
return nil
}
func (qs *QuestionService) notificationInviteUser(
ctx context.Context, invitedUserIDs []string, questionID, questionUserID string) {
for _, userID := range invitedUserIDs {
msg := &schema.NotificationMsg{
ReceiverUserID: userID,
TriggerUserID: questionUserID,
Type: schema.NotificationTypeInbox,
ObjectID: questionID,
}
msg.ObjectType = constant.QuestionObjectType
msg.NotificationAction = constant.NotificationInvitedYouToAnswer
notice_queue.AddNotification(msg)
}
}
// UpdateQuestion update question
func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) {
var canUpdate bool
@ -712,6 +804,10 @@ func (qs *QuestionService) GetQuestionAndAddPV(ctx context.Context, questionID,
return qs.GetQuestion(ctx, questionID, loginUserID, per)
}
func (qs *QuestionService) InviteUserInfo(ctx context.Context, questionID string) (inviteList []*schema.UserBasicInfo, err error) {
return qs.questioncommon.InviteUserInfo(ctx, questionID)
}
func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) error {
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData)
}
@ -794,14 +890,18 @@ func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.P
_, ok := questionMaps[item.QuestionID]
if ok {
item.QuestionInfo = questionMaps[item.QuestionID]
} else {
continue
}
info := &schema.UserAnswerInfo{}
_ = copier.Copy(info, item)
info.AnswerID = item.ID
info.QuestionID = item.QuestionID
if item.QuestionInfo.Status != entity.QuestionStatusDeleted {
userAnswerlist = append(userAnswerlist, info)
if item.QuestionInfo.Status == entity.QuestionStatusDeleted {
info.QuestionInfo.Title = "Deleted question"
}
userAnswerlist = append(userAnswerlist, info)
}
return pager.NewPageModel(total, userAnswerlist), nil
@ -835,6 +935,9 @@ func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *sche
questionMaps[uid.EnShortID(id)].UpdateUserInfo = nil
questionMaps[uid.EnShortID(id)].Content = ""
questionMaps[uid.EnShortID(id)].HTML = ""
if questionMaps[uid.EnShortID(id)].Status == entity.QuestionStatusDeleted {
questionMaps[uid.EnShortID(id)].Title = "Deleted question"
}
list = append(list, questionMaps[uid.EnShortID(id)])
}
}

View File

@ -269,6 +269,9 @@ func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.
commentResp.Title = objInfo.Title
commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)
commentResp.Content = objInfo.Content
if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
commentResp.Title = "Deleted question"
}
commentResp.QuestionID = objInfo.QuestionID
commentResp.AnswerID = objInfo.AnswerID
}

View File

@ -44,6 +44,9 @@ type TagRepo interface {
type TagRelRepo interface {
AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error)
RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error)
ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error)
HideTagRelListByObjectID(ctx context.Context, objectID string) (err error)
RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error)
EnableTagRelByIDs(ctx context.Context, ids []int64) (err error)
GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) (tagRel *entity.TagRel, exist bool, err error)
@ -653,6 +656,35 @@ func (ts *TagCommonService) RefreshTagQuestionCount(ctx context.Context, tagIDs
return nil
}
func (ts *TagCommonService) RefreshTagCountByQuestionID(ctx context.Context, questionID string) (err error) {
tagListList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, questionID)
if err != nil {
return err
}
tagIDs := make([]string, 0)
for _, item := range tagListList {
tagIDs = append(tagIDs, item.TagID)
}
err = ts.RefreshTagQuestionCount(ctx, tagIDs)
if err != nil {
return err
}
return nil
}
// RemoveTagRelListByObjectID remove tag relation by object id
func (ts *TagCommonService) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
return ts.tagRelRepo.RemoveTagRelListByObjectID(ctx, objectID)
}
func (ts *TagCommonService) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
return ts.tagRelRepo.HideTagRelListByObjectID(ctx, objectID)
}
func (ts *TagCommonService) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
return ts.tagRelRepo.ShowTagRelListByObjectID(ctx, objectID)
}
// CreateOrUpdateTagRelList if tag relation is exists update status, if not create it
func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, objectId string, tagIDs []string) (err error) {
addTagIDMapping := make(map[string]bool)

View File

@ -20,6 +20,8 @@ type UserRepo interface {
AddUser(ctx context.Context, user *entity.User) (err error)
IncreaseAnswerCount(ctx context.Context, userID string, amount int) (err error)
IncreaseQuestionCount(ctx context.Context, userID string, amount int) (err error)
UpdateQuestionCount(ctx context.Context, userID string, count int64) (err error)
UpdateAnswerCount(ctx context.Context, userID string, count int) (err error)
UpdateLastLoginDate(ctx context.Context, userID string) (err error)
UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error
UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error
@ -30,8 +32,10 @@ type UserRepo interface {
GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)
BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error)
GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error)
GetByUsernames(ctx context.Context, usernames []string) ([]*entity.User, error)
GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error)
GetUserCount(ctx context.Context) (count int64, err error)
SearchUserListByName(ctx context.Context, name string) (userList []*entity.User, err error)
}
// UserCommon user service
@ -72,12 +76,25 @@ func (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username s
return info, exist, nil
}
func (us *UserCommon) UpdateAnswerCount(ctx context.Context, userID string, num int) error {
return us.userRepo.IncreaseAnswerCount(ctx, userID, num)
func (us *UserCommon) BatchGetUserBasicInfoByUserNames(ctx context.Context, usernames []string) (map[string]*schema.UserBasicInfo, error) {
infomap := make(map[string]*schema.UserBasicInfo)
list, err := us.userRepo.GetByUsernames(ctx, usernames)
if err != nil {
return infomap, err
}
for _, user := range list {
info := us.FormatUserBasicInfo(ctx, user)
infomap[user.Username] = info
}
return infomap, nil
}
func (us *UserCommon) UpdateQuestionCount(ctx context.Context, userID string, num int) error {
return us.userRepo.IncreaseQuestionCount(ctx, userID, num)
func (us *UserCommon) UpdateAnswerCount(ctx context.Context, userID string, num int) error {
return us.userRepo.UpdateAnswerCount(ctx, userID, num)
}
func (us *UserCommon) UpdateQuestionCount(ctx context.Context, userID string, num int64) error {
return us.userRepo.UpdateQuestionCount(ctx, userID, num)
}
func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, IDs []string) (map[string]*schema.UserBasicInfo, error) {

View File

@ -814,6 +814,19 @@ func (us *UserService) getUserInfoMapping(ctx context.Context, userIDs []string)
return userInfoMapping, nil
}
func (us *UserService) SearchUserListByName(ctx context.Context, name string) ([]*schema.UserBasicInfo, error) {
userinfolist := make([]*schema.UserBasicInfo, 0)
list, err := us.userRepo.SearchUserListByName(ctx, name)
if err != nil {
return userinfolist, err
}
for _, user := range list {
userinfo := us.userCommonService.FormatUserBasicInfo(ctx, user)
userinfolist = append(userinfolist, userinfo)
}
return userinfolist, nil
}
func (us *UserService) warpStatRankingResp(
userInfoMapping map[string]*entity.User,
rankStat []*entity.ActivityUserRankStat,

View File

@ -195,6 +195,9 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
Content: objInfo.Content,
VoteType: activity_type.Format(voteInfo.ActivityType),
}
if objInfo.QuestionStatus == entity.QuestionStatusDeleted {
item.Title = "Deleted question"
}
resp = append(resp, item)
}

View File

@ -1,6 +1,6 @@
{{define "header"}}
<!DOCTYPE html>
<html>
<html lang="{{.lang}}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />