initial commit

This commit is contained in:
mingcheng 2022-09-27 17:59:05 +08:00
commit 8e6c49a531
461 changed files with 64888 additions and 0 deletions

20
.editorconfig Normal file
View File

@ -0,0 +1,20 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.go]
indent_style = tab
indent_size = 2
[{Makefile, Dockerfile}]
indent_style = tab
indent_size = 4
[{*.yml, *.json}]
indent_style = space
indent_size = 2

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
*.exe
*.orig
*.rej
*.so
*~
.DS_Store
._*
/.idea
/.vscode/*.log
/cmd/answer/*.sh
/cmd/logs
/configs/*
/go.work*
/logs
/ui/build
/ui/node_modules
/vendor
Thumbs*.db
tmp
vendor/

66
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,66 @@
include:
- project: "segmentfault/devops/templates"
file: ".docker-build-push.yml"
- project: "segmentfault/devops/templates"
file: ".deploy-helm.yml"
stages:
- compile-html
- compile-golang
- push
- deploy-dev
"compile the html and other static files":
image: node:16
stage: compile-html
tags:
- runner-nanjing
before_script:
- npm config set registry https://repo.huaweicloud.com/repository/npm/
- make install-ui-packages
script:
- make ui
artifacts:
paths:
- ./build
"compile the golang project":
image: golang:1.18
stage: compile-golang
before_script:
- export GOPROXY="https://goproxy.cn"
- export GOPRIVATE=git.backyard.segmentfault.com
- sh ./script/prebuild.sh
script:
- make build
artifacts:
paths:
- ./answer
"build docker images and push":
stage: push
extends: .docker-build-push
only:
- dev
- master
- main
variables:
DockerNamespace: sf_app
DockerImage: answer
DockerTag: "$CI_COMMIT_SHORT_SHA latest"
DockerfilePath: .
PushPolicy: qingcloud
"deploy-to-local-develop-environment":
stage: deploy-dev
extends: .deploy-helm
only:
- main
variables:
LoadBalancerIP: 10.0.10.98
KubernetesCluster: dev
KubernetesNamespace: "sf-web"
InstallArgs: --set service.loadBalancerIP=${LoadBalancerIP} --set image.tag=$CI_COMMIT_SHORT_SHA --set replicaCount=1 --set serivce.targetPort=80
ChartName: answer
InstallPolicy: replace

53
Dockerfile Normal file
View File

@ -0,0 +1,53 @@
FROM node:16 AS node-builder
LABEL maintainer="mingcheng<mc@sf.com>"
COPY . /answer
WORKDIR /answer
RUN make install-ui-packages ui
RUN mv ui/build /tmp
CMD ls -al /tmp
RUN du -sh /tmp/build
FROM golang:1.18 AS golang-builder
LABEL maintainer="aichy"
ENV GOPATH /go
ENV GOROOT /usr/local/go
ENV PACKAGE github.com/segmentfault/answer
ENV GOPROXY https://goproxy.cn,direct
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
ENV GOPRIVATE git.backyard.segmentfault.com
# Build
COPY . ${BUILD_DIR}
WORKDIR ${BUILD_DIR}
COPY --from=node-builder /tmp/build ${BUILD_DIR}/web/html
CMD ls -al ${BUILD_DIR}/web/html
RUN make clean build && \
cp answer /usr/bin/answer && \
cp configs/config.yaml /etc/config.yaml && \
mkdir -p /tmp/cache && chmod 777 /tmp/cache && \
mkdir -p /data/upfiles && chmod 777 /data/upfiles && cp -r i18n /data
FROM debian:bullseye
ENV TZ "Asia/Shanghai"
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list \
&& echo "Asia/Shanghai" > /etc/timezone \
&& apt -y update \
&& apt -y upgrade \
&& apt -y install ca-certificates openssl tzdata curl netcat dumb-init \
&& apt -y autoremove
COPY --from=golang-builder /data /data
VOLUME /data
COPY --from=golang-builder /etc/config.yaml /etc/answer.yaml
COPY --from=golang-builder /usr/bin/answer /usr/bin/answer
EXPOSE 80
ENTRYPOINT ["dumb-init", "/usr/bin/answer", "-c", "/etc/answer.yaml"]

1
INSTALL.md Normal file
View File

@ -0,0 +1 @@
# How to build and install

9
INSTALL_CN.md Normal file
View File

@ -0,0 +1,9 @@
# Answer 安装指引
## 前端安装
## 后端安装
## 编译镜像
## 常见问题

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) since 2022 The Segmentfault Development Team.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

31
Makefile Normal file
View File

@ -0,0 +1,31 @@
.PHONY: build clean ui
VERSION=0.0.1
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
GO_ENV=CGO_ENABLED=0
GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X 'main.Time=`date`' -extldflags -static"
GO=$(GO_ENV) $(shell which go)
build:
@$(GO_ENV) $(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
test:
@$(GO) test ./...
# clean all build result
clean:
@$(GO) clean ./...
@rm -f $(BIN)
install-ui-packages:
@corepack enable
@corepack prepare pnpm@v7.12.2 --activate
ui:
@npm config set registry https://repo.huaweicloud.com/repository/npm/
@cd ui && pnpm install && pnpm build && cd -
all: clean build

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# answer
问答社区主项目代码
# Dependence
github.com/segmentfault/pacman
* config-file `viper` https://github.com/spf13/viper
* web `gin` https://gin-gonic.com/zh-cn/
* log `zap` https://github.com/uber-go/zap
* orm `xorm` https://xorm.io/zh/
* redis `go-redis` https://github.com/go-redis/redis
# module
- email github.com/jordan-wright/email
- session github.com/gin-contrib/sessions
- Captcha github.com/mojocn/base64Captcha
# Run
```
cd cmd
export GOPRIVATE=git.backyard.segmentfault.com
go mod tidy
./dev.sh
```
# pprof
```
# Installation dependency
go get -u github.com/google/pprof
brew install graphviz
```
```
pprof -http :8082 http://XXX/debug/pprof/profile\?seconds\=10
```

9
README_CN.md Normal file
View File

@ -0,0 +1,9 @@
# Answer 问答社区
## 功能说明
## 安装
## 配置
## 常见问题

2
build/README.md Normal file
View File

@ -0,0 +1,2 @@
# /build
Packaging and Continuous Integration.

8
charts/.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
charts/.idea/charts.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
charts/.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/charts.iml" filepath="$PROJECT_DIR$/.idea/charts.iml" />
</modules>
</component>
</project>

6
charts/.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

6
charts/Chart.yaml Normal file
View File

@ -0,0 +1,6 @@
apiVersion: v2
name: answer
description: a simple answer deployments for kubernetes
type: application
version: 0.1.0
appVersion: "0.1.0"

2
charts/README.md Normal file
View File

@ -0,0 +1,2 @@
# Helm Charts for Answer project

View File

@ -0,0 +1,51 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "answer.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "answer.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "answer.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "answer.labels" -}}
helm.sh/chart: {{ .Release.Name }}
{{ include "answer.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "answer.selectorLabels" -}}
app.kubernetes.io/name: answer
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View File

@ -0,0 +1,9 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: answer-config
namespace: {{ .Values.namespace | default "default" | quote }}
data:
default.yaml: |-
#

View File

@ -0,0 +1,17 @@
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "answer.fullname" . }}
labels:
{{- include "answer.labels" . | nindent 4 }}
namespace: {{ .Values.namespace | default "default" | quote }}
spec:
type: ClusterIP
ports:
- name: answer
port: 80
targetPort: 80
protocol: TCP
selector:
{{- include "answer.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,31 @@
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: answer
namespace: {{ .Values.namespace | default "default" | quote }}
spec:
selector:
matchLabels:
{{- include "answer.labels" . | nindent 6 }}
serviceName: answer
replicas: 1
template:
metadata:
labels:
{{- include "answer.labels" . | nindent 8 }}
spec:
containers:
- name: answer
image: nginx:stable
ports:
- containerPort: 80
name: answer-ui
volumeMounts:
- name: config
mountPath: "/etc/answer.yaml"
subPath: default.yaml
volumes:
- name: config
configMap:
name: answer-config

1
charts/values.yaml Normal file
View File

@ -0,0 +1 @@
namespace: default

70
cmd/answer/main.go Normal file
View File

@ -0,0 +1,70 @@
package main
import (
"flag"
"os"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/conf"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/contrib/conf/viper"
"github.com/segmentfault/pacman/contrib/log/zap"
"github.com/segmentfault/pacman/contrib/server/http"
"github.com/segmentfault/pacman/log"
)
// go build -ldflags "-X main.Version=x.y.z"
var (
// Name is the name of the project
Name = "answer"
// Version is the version of the project
Version string
// confFlag is the config flag.
confFlag string
// log level
logLevel = os.Getenv("LOG_LEVEL")
// log path
logPath = os.Getenv("LOG_PATH")
)
func init() {
flag.StringVar(&confFlag, "c", "../../configs/config.yaml", "config path, eg: -c config.yaml")
}
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
func main() {
flag.Parse()
log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName(Name), zap.WithPath(logPath), zap.WithCallerFullPath()))
// init config
c := &conf.AllConfig{}
config, err := viper.NewWithPath(confFlag)
if err != nil {
panic(err)
}
if err = config.Parse(&c); err != nil {
panic(err)
}
app, cleanup, err := initApplication(
c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger())
if err != nil {
panic(err)
}
defer cleanup()
if err := app.Run(); err != nil {
panic(err)
}
}
func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application {
return pacman.NewApp(
pacman.WithName(Name),
pacman.WithVersion(Version),
pacman.WithServer(http.NewServer(server, serverConf.HTTP.Addr)),
)
}

46
cmd/answer/wire.go Normal file
View File

@ -0,0 +1,46 @@
//go:build wireinject
// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"github.com/google/wire"
"github.com/segmentfault/answer/internal/base/conf"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/server"
"github.com/segmentfault/answer/internal/base/translator"
"github.com/segmentfault/answer/internal/controller"
"github.com/segmentfault/answer/internal/controller_backyard"
"github.com/segmentfault/answer/internal/repo"
"github.com/segmentfault/answer/internal/router"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/answer/internal/service/service_config"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/log"
)
// initApplication init application.
func initApplication(
debug bool,
serverConf *conf.Server,
dbConf *data.Database,
cacheConf *data.CacheConf,
i18nConf *translator.I18n,
swaggerConf *router.SwaggerConfig,
serviceConf *service_config.ServiceConfig,
logConf log.Logger) (*pacman.Application, func(), error) {
panic(wire.Build(
server.ProviderSetServer,
router.ProviderSetRouter,
controller.ProviderSetController,
controller_backyard.ProviderSetController,
service.ProviderSetService,
repo.ProviderSetRepo,
translator.ProviderSet,
middleware.ProviderSetMiddleware,
newApplication,
))
}

180
cmd/answer/wire_gen.go Normal file
View File

@ -0,0 +1,180 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"github.com/segmentfault/answer/internal/base/conf"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/server"
"github.com/segmentfault/answer/internal/base/translator"
"github.com/segmentfault/answer/internal/controller"
"github.com/segmentfault/answer/internal/controller_backyard"
"github.com/segmentfault/answer/internal/repo"
"github.com/segmentfault/answer/internal/repo/activity"
"github.com/segmentfault/answer/internal/repo/activity_common"
"github.com/segmentfault/answer/internal/repo/auth"
"github.com/segmentfault/answer/internal/repo/captcha"
"github.com/segmentfault/answer/internal/repo/collection"
"github.com/segmentfault/answer/internal/repo/comment"
"github.com/segmentfault/answer/internal/repo/common"
"github.com/segmentfault/answer/internal/repo/config"
"github.com/segmentfault/answer/internal/repo/export"
"github.com/segmentfault/answer/internal/repo/meta"
"github.com/segmentfault/answer/internal/repo/notification"
"github.com/segmentfault/answer/internal/repo/rank"
"github.com/segmentfault/answer/internal/repo/reason"
"github.com/segmentfault/answer/internal/repo/report"
"github.com/segmentfault/answer/internal/repo/revision"
"github.com/segmentfault/answer/internal/repo/tag"
"github.com/segmentfault/answer/internal/repo/unique"
"github.com/segmentfault/answer/internal/repo/user"
"github.com/segmentfault/answer/internal/router"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/answer/internal/service/action"
activity2 "github.com/segmentfault/answer/internal/service/activity"
"github.com/segmentfault/answer/internal/service/answer_common"
auth2 "github.com/segmentfault/answer/internal/service/auth"
"github.com/segmentfault/answer/internal/service/collection_common"
comment2 "github.com/segmentfault/answer/internal/service/comment"
export2 "github.com/segmentfault/answer/internal/service/export"
"github.com/segmentfault/answer/internal/service/follow"
meta2 "github.com/segmentfault/answer/internal/service/meta"
notification2 "github.com/segmentfault/answer/internal/service/notification"
"github.com/segmentfault/answer/internal/service/notification_common"
"github.com/segmentfault/answer/internal/service/object_info"
"github.com/segmentfault/answer/internal/service/question_common"
rank2 "github.com/segmentfault/answer/internal/service/rank"
reason2 "github.com/segmentfault/answer/internal/service/reason"
report2 "github.com/segmentfault/answer/internal/service/report"
"github.com/segmentfault/answer/internal/service/report_backyard"
"github.com/segmentfault/answer/internal/service/report_handle_backyard"
"github.com/segmentfault/answer/internal/service/revision_common"
"github.com/segmentfault/answer/internal/service/service_config"
tag2 "github.com/segmentfault/answer/internal/service/tag"
"github.com/segmentfault/answer/internal/service/tag_common"
"github.com/segmentfault/answer/internal/service/uploader"
"github.com/segmentfault/answer/internal/service/user_backyard"
"github.com/segmentfault/answer/internal/service/user_common"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/log"
)
// Injectors from wire.go:
// initApplication init application.
func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, logConf log.Logger) (*pacman.Application, func(), error) {
staticRouter := router.NewStaticRouter(serviceConf)
i18nTranslator, err := translator.NewTranslator(i18nConf)
if err != nil {
return nil, nil, err
}
langController := controller.NewLangController(i18nTranslator)
engine := data.NewDB(debug, dbConf)
cache, cleanup, err := data.NewCache(cacheConf)
if err != nil {
return nil, nil, err
}
dataData, cleanup2, err := data.NewData(engine, cache)
if err != nil {
cleanup()
return nil, nil, err
}
authRepo := auth.NewAuthRepo(dataData)
authService := auth2.NewAuthService(authRepo)
configRepo := config.NewConfigRepo(dataData)
userRepo := user.NewUserRepo(dataData, configRepo)
uniqueIDRepo := unique.NewUniqueIDRepo(dataData)
activityRepo := repo.NewActivityRepo(dataData, uniqueIDRepo, configRepo)
userRankRepo := rank.NewUserRankRepo(dataData, configRepo)
userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo)
emailRepo := export.NewEmailRepo(dataData)
emailService := export2.NewEmailService(configRepo, emailRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf)
captchaRepo := captcha.NewCaptchaRepo(dataData)
captchaService := action.NewCaptchaService(captchaRepo)
uploaderService := uploader.NewUploaderService(serviceConf)
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService)
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo)
answerRepo := repo.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
questionRepo := repo.NewQuestionRepo(dataData, uniqueIDRepo)
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagRepo)
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userRepo, objService, voteRepo)
rankService := rank2.NewRankService(userRepo, userRankRepo, objService, configRepo)
commentController := controller.NewCommentController(commentService, rankService)
reportRepo := report.NewReportRepo(dataData, uniqueIDRepo)
reportService := report2.NewReportService(reportRepo, objService)
reportController := controller.NewReportController(reportService, rankService)
serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configRepo, activityRepo, userRankRepo, voteRepo)
voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configRepo, questionRepo, answerRepo, commentCommonRepo, objService)
voteController := controller.NewVoteController(voteService)
revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo)
revisionService := revision_common.NewRevisionService(revisionRepo, userRepo)
followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
tagService := tag2.NewTagService(tagRepo, revisionService, followRepo)
tagController := controller.NewTagController(tagService, rankService)
followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
followService := follow.NewFollowService(followFollowRepo, followRepo, tagRepo)
followController := controller.NewFollowController(followService)
collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo)
collectionGroupRepo := collection.NewCollectionGroupRepo(dataData)
tagRelRepo := tag.NewTagListRepo(dataData)
tagCommonService := tagcommon.NewTagCommonService(tagRepo, tagRelRepo, revisionService)
userCommon := usercommon.NewUserCommon(userRepo)
collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo)
answerCommon := answercommon.NewAnswerCommon(answerRepo)
metaRepo := meta.NewMetaRepo(dataData)
metaService := meta2.NewMetaService(metaRepo)
questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configRepo)
collectionService := service.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon)
collectionController := controller.NewCollectionController(collectionService)
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo)
questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo)
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService)
questionController := controller.NewQuestionController(questionService, rankService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
answerController := controller.NewAnswerController(answerService, rankService)
searchRepo := repo.NewSearchRepo(dataData, uniqueIDRepo, userRepo)
searchService := service.NewSearchService(searchRepo, tagRepo, userRepo, followRepo)
searchController := controller.NewSearchController(searchService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userRepo, questionCommon, answerService)
revisionController := controller.NewRevisionController(serviceRevisionService)
rankController := controller.NewRankController(rankService)
commonRepo := common.NewCommonRepo(dataData, uniqueIDRepo)
reportHandle := report_handle_backyard.NewReportHandle(questionCommon, commentRepo, configRepo)
reportBackyardService := report_backyard.NewReportBackyardService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo)
controller_backyardReportController := controller_backyard.NewReportController(reportBackyardService)
userBackyardRepo := user.NewUserBackyardRepo(dataData)
userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo)
userBackyardController := controller_backyard.NewUserBackyardController(userBackyardService)
reasonRepo := reason.NewReasonRepo(configRepo)
reasonService := reason2.NewReasonService(reasonRepo)
reasonController := controller.NewReasonController(reasonService)
themeController := controller_backyard.NewThemeController()
siteInfoRepo := repo.NewSiteInfo(dataData)
siteInfoService := service.NewSiteInfoService(siteInfoRepo)
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoService)
notificationRepo := notification.NewNotificationRepo(dataData)
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService)
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon)
notificationController := controller.NewNotificationController(notificationService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
viewRouter := router.NewViewRouter()
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, viewRouter, authUserMiddleware)
application := newApplication(serverConf, ginEngine)
return application, func() {
cleanup2()
cleanup()
}, nil
}

3
deployments/README.md Normal file
View File

@ -0,0 +1,3 @@
# /deployments
IaaS, PaaS, system and container orchestration deployment configurations and templates (docker-compose, kubernetes/helm, mesos, terraform, bosh).

7
docker-compose.yaml Normal file
View File

@ -0,0 +1,7 @@
version: "3"
services:
answer:
build:
context: .
image: github.com/segmentfault/answer
restart: on-failure

5676
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

5648
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

3639
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

87
go.mod Normal file
View File

@ -0,0 +1,87 @@
module github.com/segmentfault/answer
go 1.18
require (
github.com/Chain-Zhang/pinyin v0.1.3
github.com/bwmarrin/snowflake v0.3.0
github.com/davecgh/go-spew v1.1.1
github.com/gin-gonic/gin v1.8.1
github.com/go-playground/locales v0.14.0
github.com/go-playground/universal-translator v0.18.0
github.com/go-playground/validator/v10 v10.11.1
github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-json v0.9.11
github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0
github.com/jinzhu/copier v0.3.5
github.com/jinzhu/now v1.1.5
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/mojocn/base64Captcha v1.3.5
github.com/segmentfault/pacman v1.0.1
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220926035018-18f894415e5b
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220926035018-18f894415e5b
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220926035018-18f894415e5b
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220926035018-18f894415e5b
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220926035018-18f894415e5b
github.com/stretchr/testify v1.8.0
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
github.com/swaggo/gin-swagger v1.5.3
github.com/swaggo/swag v1.8.6
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
golang.org/x/net v0.0.0-20220926192436-02166a98028e
xorm.io/builder v0.3.12
xorm.io/core v0.7.3
xorm.io/xorm v1.3.2
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nicksnyder/go-i18n/v2 v2.2.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.13.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect
golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

1167
go.sum Normal file

File diff suppressed because it is too large Load Diff

160
i18n/en_US.yaml Normal file
View File

@ -0,0 +1,160 @@
base:
success:
other: "success"
unknown:
other: "unknown error"
request_format_error:
other: "request format is not valid"
unauthorized_error:
other: "unauthorized"
database_error:
other: "data server error"
email:
other: "email"
password:
other: "password"
email_or_password_wrong_error: &email_or_password_wrong
other: "email or password wrong"
error:
admin:
email_or_password_wrong: *email_or_password_wrong
answer:
not_found:
other: "answer not found"
comment:
edit_without_permission:
other: "comment not allowed to edit"
not_found:
other: "comment not found"
email:
duplicate:
other: "email already exists"
need_to_be_verified:
other: "email should be verified"
verify_url_expired:
other: "email verified url is expired, please resend the email"
lang:
not_found:
other: "language not found"
object:
captcha_verification_failed:
other: "captcha wrong"
disallow_follow:
other: "You are not allowed to follow"
disallow_vote:
other: "You are not allowed to vote"
disallow_vote_your_self:
other: "You can't vote for your own post!"
not_found:
other: "object not found"
question:
not_found:
other: "question not found"
rank:
fail_to_meet_the_condition:
other: "rank fail to meet the condition"
report:
handle_failed:
other: "report handle failed"
not_found:
other: "report not found"
tag:
not_found:
other: "tag not found"
theme:
not_found:
other: "theme not found"
user:
email_or_password_wrong:
other: *email_or_password_wrong
not_found:
other: "user not found"
suspended:
other: "user is suspended"
report:
spam:
name:
other: "spam"
description:
other: "This post is an advertisement,or vandalism.It is not useful or relevant to the current topic."
rude:
name:
other: "rude or abusive"
description:
other: "A reasonable person would find this content inappropriate for respectful discourse."
duplicate:
name:
other: "a duplicate"
description:
other: "This question has been asked before and already has an answer."
not_answer:
name:
other: "not an answer"
description:
other: "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."
not_need:
name:
other: "no longer needed"
description:
other: "This comment is outdated,conversational or not relevant to this post."
other:
name:
other: "something else"
description:
other: "This post requires staff attention for another reason not listed above."
question:
close:
duplicate:
name:
other: "spam"
description:
other: "This question has been asked before and already has an answer."
guideline:
name:
other: "a community-specific reason"
description:
other: "This question doesn't meet a community guideline."
multiple:
name:
other: "needs details or clarity"
description:
other: "This question currently includes multiple questions in one. It should focus on one problem only."
other:
name:
other: "something else"
description:
other: "This post requires another reason not listed above."
notification:
action:
update_question:
other: "update question"
answer_the_question:
other: "answer the question"
update_answer:
other: "update answer"
adopt_answer:
other: "adopt answer"
comment_question:
other: "comment question"
comment_answer:
other: "comment answer"
reply_to_you:
other: "reply to you"
mention_you:
other: "mention you"
your_question_is_closed:
other: "your question is closed"
your_question_was_deleted:
other: "your question was deleted"
your_answer_was_deleted:
other: "your answer was deleted"
your_comment_was_deleted:
other: "your comment was deleted"

25
i18n/zh_CN.yaml Normal file
View File

@ -0,0 +1,25 @@
base:
success:
other: "成功"
unknown:
other: "未知错误"
request_format_error:
other: "请求格式错误"
unauthorized_error:
other: "未登录"
database_error:
other: "数据服务异常"
email:
other: "邮箱"
password:
other: "密码"
username_or_password_wrong_error: &username_or_password_wrong
other: "用户名或密码错误"
error:
user:
username_or_password_wrong: *username_or_password_wrong
admin:
username_or_password_wrong: *username_or_password_wrong

View File

@ -0,0 +1,71 @@
package conf
import (
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/base/server"
"github.com/segmentfault/answer/internal/base/translator"
"github.com/segmentfault/answer/internal/router"
"github.com/segmentfault/answer/internal/service/service_config"
)
// AllConfig all config
type AllConfig struct {
Debug bool `json:"debug" mapstructure:"debug"`
Data *Data `json:"data" mapstructure:"data"`
Server *Server `json:"server" mapstructure:"server"`
I18n *translator.I18n `json:"i18n" mapstructure:"i18n"`
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui"`
ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config"`
}
// Server server config
type Server struct {
HTTP *server.HTTP `json:"http" mapstructure:"http"`
}
// Data data config
type Data struct {
Database *data.Database `json:"database" mapstructure:"database"`
Cache *data.CacheConf `json:"cache" mapstructure:"cache"`
}
// ------------------ remove
// log .
type log struct {
Dir string `json:"dir"`
Name string `json:"name"`
Access bool `json:"access"`
Level string `json:"level"`
MaxSize int `json:"max_size"`
MaxBackups int `json:"max_backups"`
MaxAge int `json:"max_age"`
}
// Local .
type Local struct {
Address string `json:"address"`
Debug bool `json:"debug"`
log log `json:"log"`
}
// // SwaggerConfig .
// type SwaggerConfig struct {
// Show bool `json:"show"`
// Protocol string `json:"protocol"`
// Host string `json:"host"`
// Address string `json:"address"`
// }
// Answer .
type Answer struct {
MaxIdle int `json:"max_idle"`
MaxOpen int `json:"max_open"`
IsDebug bool `json:"is_debug"`
Datasource string `json:"datasource"`
}
// Mysql .
type Mysql struct {
Answer Answer `json:"answer"`
}

View File

@ -0,0 +1,51 @@
package constant
import "time"
const (
Default_PageSize = 20 //Default number of pages
Key_UserID = "_UserID" //session userid
LoginUserID = "login_user_id"
LoginUserVerify = "login_user_verify"
UserStatusChangedCacheKey = "answer:user:status:"
UserStatusChangedCacheTime = 7 * 24 * time.Hour
UserTokenCacheKey = "answer:user:token:"
UserTokenCacheTime = 7 * 24 * time.Hour
AdminTokenCacheKey = "answer:admin:token:"
AdminTokenCacheTime = 7 * 24 * time.Hour
)
const (
QuestionObjectType = "question"
AnswerObjectType = "answer"
TagObjectType = "tag"
UserObjectType = "user"
CollectionObjectType = "collection"
CommentObjectType = "comment"
ReportObjectType = "report"
)
// ObjectTypeStrMapping key => value
// object TagID AnswerList
// key equal database's table name
var (
ObjectTypeStrMapping = map[string]int{
QuestionObjectType: 1,
AnswerObjectType: 2,
TagObjectType: 3,
UserObjectType: 4,
CollectionObjectType: 6,
CommentObjectType: 7,
ReportObjectType: 8,
}
ObjectTypeNumberMapping = map[int]string{
1: QuestionObjectType,
2: AnswerObjectType,
3: TagObjectType,
4: UserObjectType,
6: CollectionObjectType,
7: CommentObjectType,
8: ReportObjectType,
}
)

View File

@ -0,0 +1,28 @@
package constant
const (
// UpdateQuestion update question
UpdateQuestion = "notification.action.update_question"
// AnswerTheQuestion answer the question
AnswerTheQuestion = "notification.action.answer_the_question"
// UpdateAnswer update answer
UpdateAnswer = "notification.action.update_answer"
// AdoptAnswer adopt answer
AdoptAnswer = "notification.action.adopt_answer"
// CommentQuestion comment question
CommentQuestion = "notification.action.comment_question"
// CommentAnswer comment answer
CommentAnswer = "notification.action.comment_answer"
// ReplyToYou reply to you
ReplyToYou = "notification.action.reply_to_you"
// MentionYou mention you
MentionYou = "notification.action.mention_you"
// YourQuestionIsClosed your question is closed
YourQuestionIsClosed = "notification.action.your_question_is_closed"
// YourQuestionWasDeleted your question was deleted
YourQuestionWasDeleted = "notification.action.your_question_was_deleted"
// YourAnswerWasDeleted your answer was deleted
YourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
// YourCommentWasDeleted your comment was deleted
YourCommentWasDeleted = "notification.action.your_comment_was_deleted"
)

View File

@ -0,0 +1,34 @@
package constant
const (
ReportSpamName = "report.spam.name"
ReportSpamDescription = "report.spam.description"
ReportRudeName = "report.rude.name"
ReportRudeDescription = "report.rude.description"
ReportDuplicateName = "report.duplicate.name"
ReportDuplicateDescription = "report.duplicate.description"
ReportOtherName = "report.other.name"
ReportOtherDescription = "report.other.description"
ReportNotAnswerName = "report.not_answer.name"
ReportNotAnswerDescription = "report.not_answer.description"
ReportNotNeedName = "report.not_need.name"
ReportNotNeedDescription = "report.not_need.description"
//question close
QuestionCloseDuplicateName = "question.close.duplicate.name"
QuestionCloseDuplicateDescription = "question.close.duplicate.description"
QuestionCloseGuidelineName = "question.close.guideline.name"
QuestionCloseGuidelineDescription = "question.close.guideline.description"
QuestionCloseMultipleName = "question.close.multiple.name"
QuestionCloseMultipleDescription = "question.close.multiple.description"
QuestionCloseOtherName = "question.close.other.name"
QuestionCloseOtherDescription = "question.close.other.description"
)
const (
// TODO put this in database
// TODO need reason controller to resolve
QuestionCloseJson = `[{"name":"question.close.duplicate.name","description":"question.close.duplicate.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"question.close.guideline.name","description":"question.close.guideline.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"question.close.multiple.name","description":"question.close.multiple.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"question.close.other.name","description":"question.close.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]`
QuestionReportJson = `[{"name":"report.spam.name","description":"report.spam.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"report.duplicate.name","description":"report.duplicate.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]`
AnswerReportJson = `[{"name":"report.spam.name","description":"report.spam.description","source":"answer","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"answer","type":2,"have_content":false,"content_type":""},{"name":"report.not_answer.name","description":"report.not_answer.description","source":"answer","type":3,"have_content":false,"content_type":""},{"name":"report.other.name","description":"report.other.description","source":"answer","type":4,"have_content":true,"content_type":"textarea"}]`
CommentReportJson = `[{"name":"report.spam.name","description":"report.spam.description","source":"comment","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"comment","type":2,"have_content":false,"content_type":""},{"name":"report.not_need.name","description":"report.not_need.description","source":"comment","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"comment","type":4,"have_content":true,"content_type":"textarea"}]`
)

View File

@ -0,0 +1,14 @@
package data
// Database database config
type Database struct {
Connection string `json:"connection" mapstructure:"connection"`
ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time"`
MaxOpenConn int `json:"max_open_conn" mapstructure:"max_open_conn"`
MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn"`
}
// CacheConf cache
type CacheConf struct {
FilePath string `json:"file_path" mapstructure:"file_path"`
}

View File

@ -0,0 +1,83 @@
package data
import (
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/segmentfault/pacman/cache"
"github.com/segmentfault/pacman/contrib/cache/memory"
"github.com/segmentfault/pacman/log"
"xorm.io/core"
"xorm.io/xorm"
)
// Data data
type Data struct {
DB *xorm.Engine
Cache cache.Cache
}
// NewData new data instance
func NewData(db *xorm.Engine, cache cache.Cache) (*Data, func(), error) {
cleanup := func() {
log.Info("closing the data resources")
db.Close()
}
return &Data{DB: db, Cache: cache}, cleanup, nil
}
// NewDB new database instance
func NewDB(debug bool, dataConf *Database) *xorm.Engine {
engine, err := xorm.NewEngine("mysql", dataConf.Connection)
if err != nil {
panic(err)
}
if err = engine.Ping(); err != nil {
panic(err)
}
if debug {
engine.ShowSQL(true)
}
if dataConf.MaxIdleConn > 0 {
engine.SetMaxIdleConns(dataConf.MaxIdleConn)
}
if dataConf.MaxOpenConn > 0 {
engine.SetMaxOpenConns(dataConf.MaxOpenConn)
}
if dataConf.ConnMaxLifeTime > 0 {
engine.SetConnMaxLifetime(time.Duration(dataConf.ConnMaxLifeTime) * time.Second)
}
engine.SetColumnMapper(core.GonicMapper{})
return engine
}
// NewCache new cache instance
func NewCache(c *CacheConf) (cache.Cache, func(), error) {
// TODO What cache type should be initialized according to the configuration file
memCache := memory.NewCache()
if len(c.FilePath) > 0 {
log.Infof("try to load cache file from %s", c.FilePath)
if err := memory.Load(memCache, c.FilePath); err != nil {
log.Warn(err)
}
go func() {
ticker := time.Tick(time.Minute)
for range ticker {
if err := memory.Save(memCache, c.FilePath); err != nil {
log.Warn(err)
}
}
}()
}
cleanup := func() {
log.Infof("try to save cache file to %s", c.FilePath)
if err := memory.Save(memCache, c.FilePath); err != nil {
log.Warn(err)
}
}
return memCache, cleanup, nil
}

View File

@ -0,0 +1,59 @@
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/base/validator"
myErrors "github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// HandleResponse Handle response body
func HandleResponse(ctx *gin.Context, err error, data interface{}) {
lang := GetLang(ctx)
// no error
if err == nil {
ctx.JSON(http.StatusOK, NewRespBodyData(http.StatusOK, reason.Success, data).TrMsg(lang))
return
}
var myErr *myErrors.Error
// unknown error
if !errors.As(err, &myErr) {
log.Error(err, "\n", myErrors.LogStack(2, 5))
ctx.JSON(http.StatusInternalServerError, NewRespBody(
http.StatusInternalServerError, reason.UnknownError).TrMsg(lang))
return
}
// log internal server error
if myErrors.IsInternalServer(myErr) {
log.Error(myErr)
}
respBody := NewRespBodyFromError(myErr).TrMsg(lang)
if data != nil {
respBody.Data = data
}
ctx.JSON(myErr.Code, respBody)
return
}
// BindAndCheck bind request and check
func BindAndCheck(ctx *gin.Context, data interface{}) bool {
if err := ctx.ShouldBind(data); err != nil {
log.Errorf("http_handle BindAndCheck fail, %s", err.Error())
HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil)
return true
}
errField, err := validator.GetValidatorByLang(GetLang(ctx).Abbr()).Check(data)
if err != nil {
HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError).WithMsg(err.Error()), errField)
return true
}
return false
}

View File

@ -0,0 +1,19 @@
package handler
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/i18n"
)
// GetLang get language from header
func GetLang(ctx *gin.Context) i18n.Language {
acceptLanguage := ctx.GetHeader("Accept-Language")
switch i18n.Language(acceptLanguage) {
case i18n.LanguageChinese:
return i18n.LanguageChinese
case i18n.LanguageEnglish:
return i18n.LanguageEnglish
default:
return i18n.DefaultLang
}
}

View File

@ -0,0 +1,53 @@
package handler
import (
"github.com/segmentfault/answer/internal/base/translator"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
)
// RespBody response body.
type RespBody struct {
// http code
Code int `json:"code"`
// reason key
Reason string `json:"reason"`
// response message
Message string `json:"msg"`
// response data
Data interface{} `json:"data"`
}
// TrMsg translate the reason cause as a message
func (r *RespBody) TrMsg(lang i18n.Language) *RespBody {
if len(r.Message) == 0 {
r.Message = translator.GlobalTrans.Tr(lang, r.Reason)
}
return r
}
// NewRespBody new response body
func NewRespBody(code int, reason string) *RespBody {
return &RespBody{
Code: code,
Reason: reason,
}
}
// NewRespBodyFromError new response body from error
func NewRespBodyFromError(e *errors.Error) *RespBody {
return &RespBody{
Code: e.Code,
Reason: e.Reason,
Message: e.Message,
}
}
// NewRespBodyData new response body with data
func NewRespBodyData(code int, reason string, data interface{}) *RespBody {
return &RespBody{
Code: code,
Reason: reason,
Data: data,
}
}

View File

@ -0,0 +1,177 @@
package middleware
import (
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/segmentfault/answer/internal/schema"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/entity"
"github.com/segmentfault/answer/internal/service/auth"
"github.com/segmentfault/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
)
var (
ctxUuidKey = "ctxUuidKey"
)
// AuthUserMiddleware auth user middleware
type AuthUserMiddleware struct {
authService *auth.AuthService
}
// NewAuthUserMiddleware new auth user middleware
func NewAuthUserMiddleware(authService *auth.AuthService) *AuthUserMiddleware {
return &AuthUserMiddleware{
authService: authService,
}
}
// Auth get token and auth user, set user info to context if user is already login
func (am *AuthUserMiddleware) Auth() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := ExtractToken(ctx)
if len(token) == 0 {
ctx.Next()
return
}
if token == "888" {
userInfo := &entity.UserCacheInfo{}
userInfo.UserID = "2"
spew.Dump("开发环境 Auth", userInfo)
ctx.Set(ctxUuidKey, userInfo)
} else {
userInfo, err := am.authService.GetUserCacheInfo(ctx, token)
if err != nil {
ctx.Next()
return
}
if userInfo != nil {
ctx.Set(ctxUuidKey, userInfo)
}
}
ctx.Next()
}
}
// MustAuth auth user info. If the user does not log in, an unauthenticated error is displayed
func (am *AuthUserMiddleware) MustAuth() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := ExtractToken(ctx)
if len(token) == 0 {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
if token == "888" {
userInfo := &entity.UserCacheInfo{}
userInfo.UserID = "2"
spew.Dump("开发环境 MustAuth", userInfo)
ctx.Set(ctxUuidKey, userInfo)
} else {
userInfo, err := am.authService.GetUserCacheInfo(ctx, token)
spew.Dump(userInfo, err)
if err != nil || userInfo == nil {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
if userInfo.EmailStatus != entity.EmailStatusAvailable {
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailNeedToBeVerified),
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeInactive})
ctx.Abort()
return
}
if userInfo.UserStatus == entity.UserStatusSuspended {
handler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended),
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended})
ctx.Abort()
return
}
if userInfo.UserStatus == entity.UserStatusDeleted {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
ctx.Set(ctxUuidKey, userInfo)
}
ctx.Next()
}
}
func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := ExtractToken(ctx)
if len(token) == 0 {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
if token == "888" {
userInfo := &entity.UserCacheInfo{}
userInfo.UserID = "2"
spew.Dump("开发环境 CmsAuth", userInfo)
ctx.Set(ctxUuidKey, userInfo)
} else {
userInfo, err := am.authService.GetCmsUserCacheInfo(ctx, token)
if err != nil {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
if userInfo != nil {
if userInfo.UserStatus == entity.UserStatusDeleted {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
ctx.Set(ctxUuidKey, userInfo)
}
}
ctx.Next()
}
}
// GetLoginUserIDFromContext get user id from context
func GetLoginUserIDFromContext(ctx *gin.Context) (userID string) {
userInfo, exist := ctx.Get(ctxUuidKey)
if !exist {
return ""
}
u, ok := userInfo.(*entity.UserCacheInfo)
if !ok {
return ""
}
return u.UserID
}
// GetUserInfoFromContext get user info from context
func GetUserInfoFromContext(ctx *gin.Context) (u *entity.UserCacheInfo) {
userInfo, exist := ctx.Get(ctxUuidKey)
if !exist {
return nil
}
u, ok := userInfo.(*entity.UserCacheInfo)
if !ok {
return nil
}
return u
}
func GetLoginUserIDInt64FromContext(ctx *gin.Context) (userID int64) {
userIDStr := GetLoginUserIDFromContext(ctx)
return converter.StringToInt64(userIDStr)
}
// ExtractToken extract token from context
func ExtractToken(ctx *gin.Context) (token string) {
token = ctx.GetHeader("Authorization")
if len(token) == 0 {
token = ctx.Query("Authorization")
}
return strings.TrimPrefix(token, "Bearer ")
}

View File

@ -0,0 +1,10 @@
package middleware
import (
"github.com/google/wire"
)
// ProviderSetMiddleware is providers.
var ProviderSetMiddleware = wire.NewSet(
NewAuthUserMiddleware,
)

View File

@ -0,0 +1,21 @@
package pager
import (
"errors"
"reflect"
"xorm.io/xorm"
)
// Help xorm page helper
func Help(page, pageSize int, rowsSlicePtr interface{}, rowElement interface{}, session *xorm.Session) (total int64, err error) {
page, pageSize = ValPageAndPageSize(page, pageSize)
sliceValue := reflect.Indirect(reflect.ValueOf(rowsSlicePtr))
if sliceValue.Kind() != reflect.Slice {
return 0, errors.New("not a slice")
}
startNum := (page - 1) * pageSize
return session.Limit(pageSize, startNum).FindAndCount(rowsSlicePtr, rowElement)
}

View File

@ -0,0 +1,45 @@
package pager
import (
"reflect"
)
// PageModel page model
type PageModel struct {
Count int64 `json:"count"`
List interface{} `json:"list"`
}
// PageCond page condition
type PageCond struct {
Page int
PageSize int
}
// NewPageModel new page model
func NewPageModel(page, pageSize int, totalRecords int64, records interface{}) *PageModel {
sliceValue := reflect.Indirect(reflect.ValueOf(records))
if sliceValue.Kind() != reflect.Slice {
panic("not a slice")
}
if totalRecords < 0 {
totalRecords = 0
}
return &PageModel{
Count: totalRecords,
List: records,
}
}
// ValPageAndPageSize validate page pageSize
func ValPageAndPageSize(page, pageSize int) (int, int) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
return page, pageSize
}

View File

@ -0,0 +1,38 @@
package reason
const (
// Success .
Success = "base.success"
// UnknownError unknown error
UnknownError = "base.unknown"
// RequestFormatError request format error
RequestFormatError = "base.request_format_error"
// UnauthorizedError unauthorized error
UnauthorizedError = "base.unauthorized_error"
// DatabaseError database error
DatabaseError = "base.database_error"
)
const (
EmailOrPasswordWrong = "error.user.email_or_password_wrong"
CommentNotFound = "error.comment.not_found"
QuestionNotFound = "error.question.not_found"
AnswerNotFound = "error.answer.not_found"
CommentEditWithoutPermission = "error.comment.edit_without_permission"
DisallowVote = "error.object.disallow_vote"
DisallowFollow = "error.object.disallow_follow"
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
UserNotFound = "error.user.not_found"
EmailDuplicate = "error.email.duplicate"
EmailVerifyUrlExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"
UserSuspended = "error.user.suspended"
ObjectNotFound = "error.object.not_found"
TagNotFound = "error.tag.not_found"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
)

View File

@ -0,0 +1,6 @@
package server
// HTTP http config
type HTTP struct {
Addr string `json:"addr" mapstructure:"addr"`
}

View File

@ -0,0 +1,46 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/router"
)
// NewHTTPServer new http server.
func NewHTTPServer(debug bool,
staticRouter *router.StaticRouter,
answerRouter *router.AnswerAPIRouter,
swaggerRouter *router.SwaggerRouter,
viewRouter *router.ViewRouter,
authUserMiddleware *middleware.AuthUserMiddleware) *gin.Engine {
if debug {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
viewRouter.RegisterViewRouter(r)
rootGroup := r.Group("")
swaggerRouter.Register(rootGroup)
staticRouter.RegisterStaticRouter(rootGroup)
// register api that no need to login
unAuthV1 := r.Group("/answer/api/v1")
unAuthV1.Use(authUserMiddleware.Auth())
answerRouter.RegisterUnAuthAnswerAPIRouter(unAuthV1)
// register api that must be authenticated
authV1 := r.Group("/answer/api/v1")
authV1.Use(authUserMiddleware.MustAuth())
answerRouter.RegisterAnswerAPIRouter(authV1)
cmsauthV1 := r.Group("/answer/admin/api")
cmsauthV1.Use(authUserMiddleware.CmsAuth())
answerRouter.RegisterAnswerCmsAPIRouter(cmsauthV1)
return r
}

View File

@ -0,0 +1,6 @@
package server
import "github.com/google/wire"
// ProviderSetServer is providers.
var ProviderSetServer = wire.NewSet(NewHTTPServer)

View File

@ -0,0 +1,6 @@
package translator
// I18n i18n config
type I18n struct {
BundleDir string `json:"bundle_dir" mapstructure:"bundle_dir"`
}

View File

@ -0,0 +1,17 @@
package translator
import (
"github.com/google/wire"
myTran "github.com/segmentfault/pacman/contrib/i18n"
"github.com/segmentfault/pacman/i18n"
)
// ProviderSet is providers.
var ProviderSet = wire.NewSet(NewTranslator)
var GlobalTrans i18n.Translator
// NewTranslator new a translator
func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
GlobalTrans, err = myTran.NewTranslator(c.BundleDir)
return GlobalTrans, err
}

View File

@ -0,0 +1,117 @@
package validator
import (
"errors"
"reflect"
"github.com/go-playground/locales"
english "github.com/go-playground/locales/en"
zhongwen "github.com/go-playground/locales/zh"
"github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"github.com/go-playground/validator/v10/translations/en"
"github.com/go-playground/validator/v10/translations/zh"
"github.com/segmentfault/answer/internal/base/translator"
"github.com/segmentfault/pacman/i18n"
)
// MyValidator my validator
type MyValidator struct {
Validate *validator.Validate
Tran ut.Translator
Lang i18n.Language
}
// ErrorField error field
type ErrorField struct {
Key string `json:"key"`
Value string `json:"value"`
}
var (
// GlobalValidatorMapping is a mapping from validator to translator used
GlobalValidatorMapping = make(map[string]*MyValidator, 0)
)
func init() {
zhTran, zhVal := getTran(zhongwen.New(), i18n.LanguageChinese.Abbr()), createDefaultValidator(i18n.LanguageChinese)
if err := zh.RegisterDefaultTranslations(zhVal, zhTran); err != nil {
panic(err)
}
GlobalValidatorMapping[i18n.LanguageChinese.Abbr()] = &MyValidator{Validate: zhVal, Tran: zhTran, Lang: i18n.LanguageChinese}
enTran, enVal := getTran(english.New(), i18n.LanguageEnglish.Abbr()), createDefaultValidator(i18n.LanguageEnglish)
if err := en.RegisterDefaultTranslations(enVal, enTran); err != nil {
panic(err)
}
GlobalValidatorMapping[i18n.LanguageEnglish.Abbr()] = &MyValidator{Validate: enVal, Tran: enTran, Lang: i18n.LanguageEnglish}
}
func getTran(lo locales.Translator, la string) ut.Translator {
tran, ok := ut.New(lo, lo).GetTranslator(la)
if !ok {
panic(ok)
}
return tran
}
func createDefaultValidator(la i18n.Language) *validator.Validate {
validate := validator.New()
validate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) {
defer func() {
if len(res) > 0 {
res = translator.GlobalTrans.Tr(la, res)
}
}()
if jsonTag := fld.Tag.Get("json"); len(jsonTag) > 0 {
if jsonTag == "-" {
return ""
}
return jsonTag
}
if formTag := fld.Tag.Get("form"); len(formTag) > 0 {
return formTag
}
return fld.Name
})
return validate
}
func GetValidatorByLang(la string) *MyValidator {
if GlobalValidatorMapping[la] != nil {
return GlobalValidatorMapping[la]
}
return GlobalValidatorMapping[i18n.DefaultLang.Abbr()]
}
// Check /
func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error) {
err = m.Validate.Struct(value)
if err != nil {
var valErrors validator.ValidationErrors
if !errors.As(err, &valErrors) {
return nil, errors.New("validate check exception")
}
for _, fieldError := range valErrors {
errField = &ErrorField{
Key: translator.GlobalTrans.Tr(m.Lang, fieldError.Field()),
Value: fieldError.Translate(m.Tran),
}
return errField, errors.New(fieldError.Translate(m.Tran))
}
}
if v, ok := value.(Checker); ok {
errField, err = v.Check()
if err != nil {
return errField, err
}
}
return nil, nil
}
// Checker .
type Checker interface {
Check() (errField *ErrorField, err error)
}

View File

@ -0,0 +1,244 @@
package controller
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/entity"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/answer/internal/service/rank"
"github.com/segmentfault/pacman/errors"
)
// AnswerController answer controller
type AnswerController struct {
answerService *service.AnswerService
rankService *rank.RankService
}
// NewAnswerController new controller
func NewAnswerController(answerService *service.AnswerService, rankService *rank.RankService) *AnswerController {
return &AnswerController{answerService: answerService, rankService: rankService}
}
// RemoveAnswer delete answer
// @Summary delete answer
// @Description delete answer
// @Tags api-answer
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.RemoveAnswerReq true "answer"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/answer [delete]
func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) {
req := &schema.RemoveAnswerReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerDeleteRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
err := ac.answerService.RemoveAnswer(ctx, req.ID)
handler.HandleResponse(ctx, err, nil)
}
// Get godoc
// @Summary Get Answer
// @Description Get Answer
// @Tags api-answer
// @Accept json
// @Produce json
// @Param id query string true "Answer TagID" default(1)
// @Router /answer/api/v1/answer/info [get]
// @Success 200 {string} string ""
func (ac *AnswerController) Get(ctx *gin.Context) {
id := ctx.Query("id")
userId := middleware.GetLoginUserIDFromContext(ctx)
info, questionInfo, has, err := ac.answerService.Get(ctx, id, userId)
if err != nil {
handler.HandleResponse(ctx, err, gin.H{})
return
}
if !has {
handler.HandleResponse(ctx, fmt.Errorf(""), gin.H{})
return
}
handler.HandleResponse(ctx, err, gin.H{
"info": info,
"question": questionInfo,
})
}
// Add godoc
// @Summary Insert Answer
// @Description Insert Answer
// @Tags api-answer
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.AnswerAddReq true "AnswerAddReq"
// @Success 200 {string} string ""
// @Router /answer/api/v1/answer [post]
func (ac *AnswerController) Add(ctx *gin.Context) {
req := &schema.AnswerAddReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerAddRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
answerId, err := ac.answerService.Insert(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
info, questionInfo, has, err := ac.answerService.Get(ctx, answerId, req.UserID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !has {
//todo !has
handler.HandleResponse(ctx, nil, nil)
return
}
handler.HandleResponse(ctx, nil, gin.H{
"info": info,
"question": questionInfo,
})
}
// Update godoc
// @Summary Update Answer
// @Description Update Answer
// @Tags api-answer
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.AnswerUpdateReq true "AnswerUpdateReq"
// @Success 200 {string} string ""
// @Router /answer/api/v1/answer [put]
func (ac *AnswerController) Update(ctx *gin.Context) {
req := &schema.AnswerUpdateReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
_, err := ac.answerService.Update(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
info, questionInfo, has, err := ac.answerService.Get(ctx, req.ID, req.UserID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !has {
//todo !has
handler.HandleResponse(ctx, nil, nil)
return
}
handler.HandleResponse(ctx, nil, gin.H{
"info": info,
"question": questionInfo,
})
}
// AnswerList godoc
// @Summary AnswerList
// @Description AnswerList <br> <b>order</b> (default or updated)
// @Tags api-answer
// @Security ApiKeyAuth
// @Accept json
// @Produce json
// @Param data body schema.AnswerList true "AnswerList"
// @Success 200 {string} string ""
// @Router /answer/api/v1/answer/list [post]
func (ac *AnswerController) AnswerList(c *gin.Context) {
input := new(schema.AnswerList)
err := c.BindJSON(input)
if err != nil {
handler.HandleResponse(c, err, nil)
return
}
ctx := context.Background()
userId := middleware.GetLoginUserIDFromContext(c)
input.LoginUserID = userId
list, count, err := ac.answerService.SearchList(ctx, input)
if err != nil {
handler.HandleResponse(c, err, nil)
return
}
handler.HandleResponse(c, nil, gin.H{
"list": list,
"count": count,
})
}
// Adopted godoc
// @Summary Adopted
// @Description Adopted
// @Tags api-answer
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.AnswerAdoptedReq true "AnswerAdoptedReq"
// @Success 200 {string} string ""
// @Router /answer/api/v1/answer/acceptance [post]
func (ac *AnswerController) Adopted(ctx *gin.Context) {
req := &schema.AnswerAdoptedReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerAcceptRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
err := ac.answerService.UpdateAdopted(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// AdminSetAnswerStatus godoc
// @Summary AdminSetAnswerStatus
// @Description Status:[available,deleted]
// @Tags admin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body entity.AdminSetAnswerStatusRequest true "AdminSetAnswerStatusRequest"
// @Router /answer/admin/api/answer/status [put]
// @Success 200 {object} handler.RespBody
func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) {
req := &entity.AdminSetAnswerStatusRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
err := ac.answerService.AdminSetAnswerStatus(ctx, req.AnswerID, req.StatusStr)
handler.HandleResponse(ctx, err, gin.H{})
}

View File

@ -0,0 +1,56 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
)
// CollectionController collection controller
type CollectionController struct {
collectionService *service.CollectionService
}
// NewCollectionController new controller
func NewCollectionController(collectionService *service.CollectionService) *CollectionController {
return &CollectionController{collectionService: collectionService}
}
// CollectionSwitch add collection
// @Summary add collection
// @Description add collection
// @Tags Collection
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.CollectionSwitchReq true "collection"
// @Success 200 {object} handler.RespBody{data=schema.CollectionSwitchResp}
// @Router /answer/api/v1/collection/switch [post]
func (cc *CollectionController) CollectionSwitch(ctx *gin.Context) {
req := &schema.CollectionSwitchReq{}
if handler.BindAndCheck(ctx, req) {
return
}
dto := &schema.CollectionSwitchDTO{}
_ = copier.Copy(dto, req)
dto.UserID = middleware.GetLoginUserIDFromContext(ctx)
if converter.StringToInt64(req.ObjectID) < 1 {
return
}
if converter.StringToInt64(dto.UserID) < 1 {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
return
}
resp, err := cc.collectionService.CollectionSwitch(ctx, dto)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,168 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/comment"
"github.com/segmentfault/answer/internal/service/rank"
"github.com/segmentfault/pacman/errors"
)
// CommentController comment controller
type CommentController struct {
commentService *comment.CommentService
rankService *rank.RankService
}
// NewCommentController new controller
func NewCommentController(
commentService *comment.CommentService,
rankService *rank.RankService) *CommentController {
return &CommentController{commentService: commentService, rankService: rankService}
}
// AddComment add comment
// @Summary add comment
// @Description add comment
// @Tags Comment
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.AddCommentReq true "comment"
// @Success 200 {object} handler.RespBody{data=schema.GetCommentResp}
// @Router /answer/api/v1/comment [post]
func (cc *CommentController) AddComment(ctx *gin.Context) {
req := &schema.AddCommentReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentAddRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
resp, err := cc.commentService.AddComment(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// RemoveComment remove comment
// @Summary remove comment
// @Description remove comment
// @Tags Comment
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.RemoveCommentReq true "comment"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/comment [delete]
func (cc *CommentController) RemoveComment(ctx *gin.Context) {
req := &schema.RemoveCommentReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentDeleteRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
err := cc.commentService.RemoveComment(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UpdateComment update comment
// @Summary update comment
// @Description update comment
// @Tags Comment
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UpdateCommentReq true "comment"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/comment [put]
func (cc *CommentController) UpdateComment(ctx *gin.Context) {
req := &schema.UpdateCommentReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := cc.rankService.CheckRankPermission(ctx, req.UserID, rank.CommentEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
err := cc.commentService.UpdateComment(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetCommentWithPage get comment page
// @Summary get comment page
// @Description get comment page
// @Tags Comment
// @Produce json
// @Param page query int false "page"
// @Param page_size query int false "page size"
// @Param object_id query string true "object id"
// @Param query_cond query string false "query condition" Enums(vote)
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentResp}}
// @Router /answer/api/v1/comment/page [get]
func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {
req := &schema.GetCommentWithPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := cc.commentService.GetCommentWithPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// GetCommentPersonalWithPage user personal comment list
// @Summary user personal comment list
// @Description user personal comment list
// @Tags Comment
// @Produce json
// @Param page query int false "page"
// @Param page_size query int false "page size"
// @Param username query string false "username"
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentPersonalWithPageResp}}
// @Router /answer/api/v1/personal/comment/page [get]
func (cc *CommentController) GetCommentPersonalWithPage(ctx *gin.Context) {
req := &schema.GetCommentPersonalWithPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := cc.commentService.GetCommentPersonalWithPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// GetComment godoc
// @Summary get comment by id
// @Description get comment by id
// @Tags Comment
// @Produce json
// @Param id query string true "id"
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetCommentResp}}
// @Router /answer/api/v1/comment [get]
func (cc *CommentController) GetComment(ctx *gin.Context) {
req := &schema.GetCommentReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := cc.commentService.GetComment(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,23 @@
package controller
import "github.com/google/wire"
// ProviderSetController is controller providers.
var ProviderSetController = wire.NewSet(
NewLangController,
NewCommentController,
NewReportController,
NewVoteController,
NewTagController,
NewFollowController,
NewCollectionController,
NewUserController,
NewQuestionController,
NewAnswerController,
NewSearchController,
NewRevisionController,
NewRankController,
NewReasonController,
NewNotificationController,
NewSiteinfoController,
)

View File

@ -0,0 +1,70 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/follow"
)
// FollowController activity controller
type FollowController struct {
followService *follow.FollowService
}
// NewFollowController new controller
func NewFollowController(followService *follow.FollowService) *FollowController {
return &FollowController{followService: followService}
}
// Follow godoc
// @Summary follow object or cancel follow operation
// @Description follow object or cancel follow operation
// @Tags Activity
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.FollowReq true "follow"
// @Success 200 {object} handler.RespBody{data=schema.FollowResp}
// @Router /answer/api/v1/follow [post]
func (fc *FollowController) Follow(ctx *gin.Context) {
req := &schema.FollowReq{}
if handler.BindAndCheck(ctx, req) {
return
}
dto := &schema.FollowDTO{}
_ = copier.Copy(dto, req)
dto.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := fc.followService.Follow(ctx, dto)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeToast)
} else {
handler.HandleResponse(ctx, err, resp)
}
}
// UpdateFollowTags update user follow tags
// @Summary update user follow tags
// @Description update user follow tags
// @Tags Activity
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UpdateFollowTagsReq true "follow"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/follow/tags [put]
func (fc *FollowController) UpdateFollowTags(ctx *gin.Context) {
req := &schema.UpdateFollowTagsReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
err := fc.followService.UpdateFollowTags(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -0,0 +1,47 @@
package controller
import (
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/pacman/i18n"
)
type LangController struct {
translator i18n.Translator
}
// NewLangController new language controller.
func NewLangController(tr i18n.Translator) *LangController {
return &LangController{translator: tr}
}
// GetLangMapping get language config mapping
// @Summary get language config mapping
// @Description get language config mapping
// @Tags Lang
// @Param Accept-Language header string true "Accept-Language"
// @Produce json
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/language/config [get]
func (u *LangController) GetLangMapping(ctx *gin.Context) {
data, _ := u.translator.Dump(handler.GetLang(ctx))
var resp map[string]any
_ = json.Unmarshal(data, &resp)
handler.HandleResponse(ctx, nil, resp)
}
// GetLangOptions Get language options
// @Summary Get language options
// @Description Get language options
// @Security ApiKeyAuth
// @Tags Lang
// @Produce json
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/language/options [get]
// @Router /answer/admin/api/language/options [get]
func (u *LangController) GetLangOptions(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, schema.GetLangOptions)
}

View File

@ -0,0 +1,119 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/notification"
)
// NotificationController notification controller
type NotificationController struct {
notificationService *notification.NotificationService
}
// NewNotificationController new controller
func NewNotificationController(notificationService *notification.NotificationService) *NotificationController {
return &NotificationController{notificationService: notificationService}
}
// GetRedDot
// @Summary GetRedDot
// @Description GetRedDot
// @Tags Notification
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/notification/status [get]
func (nc *NotificationController) GetRedDot(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
RedDot, err := nc.notificationService.GetRedDot(ctx, userID)
handler.HandleResponse(ctx, err, RedDot)
}
// ClearRedDot
// @Summary DelRedDot
// @Description DelRedDot
// @Tags Notification
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.NotificationClearRequest true "NotificationClearRequest"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/notification/status [put]
func (nc *NotificationController) ClearRedDot(ctx *gin.Context) {
req := &schema.NotificationClearRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
RedDot, err := nc.notificationService.ClearRedDot(ctx, userID, req.TypeStr)
handler.HandleResponse(ctx, err, RedDot)
}
// ClearUnRead
// @Summary ClearUnRead
// @Description ClearUnRead
// @Tags Notification
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.NotificationClearRequest true "NotificationClearRequest"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/notification/read/state/all [put]
func (nc *NotificationController) ClearUnRead(ctx *gin.Context) {
req := &schema.NotificationClearRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
err := nc.notificationService.ClearUnRead(ctx, userID, req.TypeStr)
handler.HandleResponse(ctx, err, gin.H{})
}
// ClearIDUnRead
// @Summary ClearUnRead
// @Description ClearUnRead
// @Tags Notification
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.NotificationClearIDRequest true "NotificationClearIDRequest"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/notification/read/state [put]
func (nc *NotificationController) ClearIDUnRead(ctx *gin.Context) {
req := &schema.NotificationClearIDRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
err := nc.notificationService.ClearIDUnRead(ctx, userID, req.ID)
handler.HandleResponse(ctx, err, gin.H{})
}
// GetList
// @Summary GetRedDot
// @Description GetRedDot
// @Tags Notification
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Param type query string false "type" Enums(inbox,achievement)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/notification/page [get]
func (nc *NotificationController) GetList(ctx *gin.Context) {
req := &schema.NotificationSearch{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
list, count, err := nc.notificationService.GetList(ctx, req)
handler.HandleResponse(ctx, err, gin.H{
"list": list,
"count": count,
})
}

View File

@ -0,0 +1,425 @@
package controller
import (
"context"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/entity"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/answer/internal/service/rank"
"github.com/segmentfault/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
)
// QuestionController question controller
type QuestionController struct {
questionService *service.QuestionService
rankService *rank.RankService
}
// NewQuestionController new controller
func NewQuestionController(questionService *service.QuestionService, rankService *rank.RankService) *QuestionController {
return &QuestionController{questionService: questionService, rankService: rankService}
}
// RemoveQuestion delete question
// @Summary delete question
// @Description delete question
// @Tags api-question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.RemoveQuestionReq true "question"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question [delete]
func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
req := &schema.RemoveQuestionReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionDeleteRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
err := qc.questionService.RemoveQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// CloseQuestion Close question
// @Summary Close question
// @Description Close question
// @Tags api-question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.CloseQuestionReq true "question"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question/status [put]
func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
req := &schema.CloseQuestionReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
err := qc.questionService.CloseQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetQuestion godoc
// @Summary GetQuestion Question
// @Description GetQuestion Question
// @Tags api-question
// @Security ApiKeyAuth
// @Accept json
// @Produce json
// @Param id query string true "Question TagID" default(1)
// @Success 200 {string} string ""
// @Router /answer/api/v1/question/info [get]
func (qc *QuestionController) GetQuestion(c *gin.Context) {
id := c.Query("id")
ctx := context.Background()
userID := middleware.GetLoginUserIDFromContext(c)
info, err := qc.questionService.GetQuestion(ctx, id, userID)
if err != nil {
handler.HandleResponse(c, err, nil)
return
}
handler.HandleResponse(c, nil, info)
}
// SimilarQuestion godoc
// @Summary Search Similar Question
// @Description Search Similar Question
// @Tags api-question
// @Accept json
// @Produce json
// @Param question_id query string true "question_id" default()
// @Success 200 {string} string ""
// @Router /answer/api/v1/question/similar/tag [get]
func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) {
questionID := ctx.Query("question_id")
userID := middleware.GetLoginUserIDFromContext(ctx)
list, count, err := qc.questionService.SimilarQuestion(ctx, questionID, userID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
handler.HandleResponse(ctx, nil, gin.H{
"list": list,
"count": count,
})
}
// Index godoc
// @Summary SearchQuestionList
// @Description SearchQuestionList <br> "order" Enums(newest, active,frequent,score,unanswered)
// @Tags api-question
// @Accept json
// @Produce json
// @Param data body schema.QuestionSearch true "QuestionSearch"
// @Success 200 {string} string ""
// @Router /answer/api/v1/question/page [post]
func (qc *QuestionController) Index(ctx *gin.Context) {
req := &schema.QuestionSearch{}
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
list, count, err := qc.questionService.SearchList(ctx, req, userID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
handler.HandleResponse(ctx, nil, gin.H{
"list": list,
"count": count,
})
}
// SearchList godoc
// @Summary SearchQuestionList
// @Description SearchQuestionList
// @Tags api-question
// @Accept json
// @Produce json
// @Param data body schema.QuestionSearch true "QuestionSearch"
// @Router /answer/api/v1/question/search [post]
// @Success 200 {string} string ""
func (qc *QuestionController) SearchList(c *gin.Context) {
Request := new(schema.QuestionSearch)
err := c.BindJSON(Request)
if err != nil {
handler.HandleResponse(c, err, nil)
return
}
ctx := context.Background()
userID := middleware.GetLoginUserIDFromContext(c)
list, count, err := qc.questionService.SearchList(ctx, Request, userID)
if err != nil {
handler.HandleResponse(c, err, nil)
return
}
handler.HandleResponse(c, nil, gin.H{
"list": list,
"count": count,
})
}
// AddQuestion add question
// @Summary add question
// @Description add question
// @Tags api-question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.QuestionAdd true "question"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question [post]
func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
req := &schema.QuestionAdd{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionAddRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
resp, err := qc.questionService.AddQuestion(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UpdateQuestion update question
// @Summary update question
// @Description update question
// @Tags api-question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.QuestionUpdate true "question"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question [put]
func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
req := &schema.QuestionUpdate{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
resp, err := qc.questionService.UpdateQuestion(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// CloseMsgList close question msg list
// @Summary close question msg list
// @Description close question msg list
// @Tags api-question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question/closemsglist [get]
func (qc *QuestionController) CloseMsgList(ctx *gin.Context) {
resp, err := qc.questionService.CloseMsgList(ctx, handler.GetLang(ctx))
handler.HandleResponse(ctx, err, resp)
}
// SearchByTitleLike add question title like
// @Summary add question title like
// @Description add question title like
// @Tags api-question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param title query string true "title" default(string)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question/similar [get]
func (qc *QuestionController) SearchByTitleLike(ctx *gin.Context) {
title := ctx.Query("title")
userID := middleware.GetLoginUserIDFromContext(ctx)
resp, err := qc.questionService.SearchByTitleLike(ctx, title, userID)
handler.HandleResponse(ctx, err, resp)
}
// UserTop godoc
// @Summary UserTop
// @Description UserTop
// @Tags api-question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "username" default(string)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/personal/qa/top [get]
func (qc *QuestionController) UserTop(ctx *gin.Context) {
userName := ctx.Query("username")
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, answerList, err := qc.questionService.SearchUserTopList(ctx, userName, userID)
handler.HandleResponse(ctx, err, gin.H{
"question": questionList,
"answer": answerList,
})
}
// UserList godoc
// @Summary UserList
// @Description UserList
// @Tags api-question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "username" default(string)
// @Param order query string true "order" Enums(newest,score)
// @Param page query string true "page" default(0)
// @Param pagesize query string true "pagesize" default(20)
// @Success 200 {object} handler.RespBody
// @Router /personal/question/page [get]
func (qc *QuestionController) UserList(ctx *gin.Context) {
userName := ctx.Query("username")
order := ctx.Query("order")
pageStr := ctx.Query("page")
pageSizeStr := ctx.Query("pagesize")
page := converter.StringToInt(pageStr)
pageSize := converter.StringToInt(pageSizeStr)
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.SearchUserList(ctx, userName, order, page, pageSize, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
}
// UserAnswerList godoc
// @Summary UserAnswerList
// @Description UserAnswerList
// @Tags api-answer
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "username" default(string)
// @Param order query string true "order" Enums(newest,score)
// @Param page query string true "page" default(0)
// @Param pagesize query string true "pagesize" default(20)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/personal/answer/page [get]
func (qc *QuestionController) UserAnswerList(ctx *gin.Context) {
userName := ctx.Query("username")
order := ctx.Query("order")
pageStr := ctx.Query("page")
pageSizeStr := ctx.Query("pagesize")
page := converter.StringToInt(pageStr)
pageSize := converter.StringToInt(pageSizeStr)
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.SearchUserAnswerList(ctx, userName, order, page, pageSize, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
}
// UserCollectionList godoc
// @Summary UserCollectionList
// @Description UserCollectionList
// @Tags Collection
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param page query string true "page" default(0)
// @Param pagesize query string true "pagesize" default(20)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/personal/collection/page [get]
func (qc *QuestionController) UserCollectionList(ctx *gin.Context) {
pageStr := ctx.Query("page")
pageSizeStr := ctx.Query("pagesize")
page := converter.StringToInt(pageStr)
pageSize := converter.StringToInt(pageSizeStr)
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.SearchUserCollectionList(ctx, page, pageSize, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
}
// CmsSearchList godoc
// @Summary CmsSearchList
// @Description Status:[available,closed,deleted]
// @Tags admin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Param status query string false "user status" Enums(available, closed, deleted)
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/question/page [get]
func (qc *QuestionController) CmsSearchList(ctx *gin.Context) {
req := &schema.CmsQuestionSearch{}
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.CmsSearchList(ctx, req, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
}
// CmsSearchAnswerList godoc
// @Summary CmsSearchList
// @Description Status:[available,deleted]
// @Tags admin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Param status query string false "user status" Enums(available,deleted)
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/answer/page [get]
func (qc *QuestionController) CmsSearchAnswerList(ctx *gin.Context) {
req := &entity.CmsAnswerSearch{}
if handler.BindAndCheck(ctx, req) {
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.CmsSearchAnswerList(ctx, req, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
}
// AdminSetQuestionStatus godoc
// @Summary AdminSetQuestionStatus
// @Description Status:[available,closed,deleted]
// @Tags admin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.AdminSetQuestionStatusRequest true "AdminSetQuestionStatusRequest"
// @Router /answer/admin/api/question/status [put]
// @Success 200 {object} handler.RespBody
func (qc *QuestionController) AdminSetQuestionStatus(ctx *gin.Context) {
req := &schema.AdminSetQuestionStatusRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
err := qc.questionService.AdminSetQuestionStatus(ctx, req.QuestionID, req.StatusStr)
handler.HandleResponse(ctx, err, gin.H{})
}

View File

@ -0,0 +1,42 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/rank"
)
// RankController rank controller
type RankController struct {
rankService *rank.RankService
}
// NewRankController new controller
func NewRankController(
rankService *rank.RankService) *RankController {
return &RankController{rankService: rankService}
}
// GetRankPersonalWithPage user personal rank list
// @Summary user personal rank list
// @Description user personal rank list
// @Tags Rank
// @Produce json
// @Param page query int false "page"
// @Param page_size query int false "page size"
// @Param username query string false "username"
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetRankPersonalWithPageResp}}
// @Router /answer/api/v1/personal/rank/page [get]
func (cc *RankController) GetRankPersonalWithPage(ctx *gin.Context) {
req := &schema.GetRankPersonalWithPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := cc.rankService.GetRankPersonalWithPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,42 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/reason"
)
// ReasonController answer controller
type ReasonController struct {
reasonService *reason.ReasonService
}
// NewReasonController new controller
func NewReasonController(answerService *reason.ReasonService) *ReasonController {
return &ReasonController{reasonService: answerService}
}
// Reasons godoc
// @Summary get reasons by object type and action
// @Description get reasons by object type and action
// @Tags reason
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param object_type query string true "object_type" Enums(question, answer, comment, user)
// @Param action query string true "action" Enums(status, close, flag, review)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/reasons [get]
// @Router /answer/admin/api/reasons [get]
func (rc *ReasonController) Reasons(ctx *gin.Context) {
req := &schema.ReasonReq{}
if handler.BindAndCheck(ctx, req) {
return
}
reasons, err := rc.reasonService.GetReasons(ctx, *req)
if err != nil {
err = nil
}
handler.HandleResponse(ctx, err, reasons)
}

View File

@ -0,0 +1,68 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/rank"
"github.com/segmentfault/answer/internal/service/report"
"github.com/segmentfault/pacman/errors"
)
// ReportController report controller
type ReportController struct {
reportService *report.ReportService
rankService *rank.RankService
}
// NewReportController new controller
func NewReportController(reportService *report.ReportService, rankService *rank.RankService) *ReportController {
return &ReportController{reportService: reportService, rankService: rankService}
}
// AddReport add report
// @Summary add report
// @Description add report <br> source (question, answer, comment, user)
// @Security ApiKeyAuth
// @Tags Report
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.AddReportReq true "report"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/report [post]
func (rc *ReportController) AddReport(ctx *gin.Context) {
req := &schema.AddReportReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := rc.rankService.CheckRankPermission(ctx, req.UserID, rank.ReportAddRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
err := rc.reportService.AddReport(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetReportTypeList get report type list
// @Summary get report type list
// @Description get report type list
// @Tags Report
// @Produce json
// @Param source query string true "report source" Enums(question, answer, comment, user)
// @Success 200 {object} handler.RespBody{data=[]schema.GetReportTypeResp}
// @Router /answer/api/v1/report/type/list [get]
func (rc *ReportController) GetReportTypeList(ctx *gin.Context) {
req := &schema.GetReportListReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := rc.reportService.GetReportTypeList(ctx, handler.GetLang(ctx), req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,63 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/pacman/errors"
)
// RevisionController revision controller
type RevisionController struct {
revisionListService *service.RevisionService
}
// NewRevisionController new controller
func NewRevisionController(revisionListService *service.RevisionService) *RevisionController {
return &RevisionController{revisionListService: revisionListService}
}
// GetRevision get revision one
// @Summary get revision one
// @Description get revision one
// @Tags Revision
// @Accept json
// @Produce json
// @Param id path int true "revisionid"
// @Success 200 {object} handler.RespBody{data=schema.GetRevisionResp}
// Router /revision/{id} [get]
func (rc *RevisionController) GetRevision(ctx *gin.Context) {
id := ctx.Param("id")
if id == "0" {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
resp, err := rc.revisionListService.GetRevision(ctx, id)
handler.HandleResponse(ctx, err, resp)
}
// GetRevisionList godoc
// @Summary get revision list
// @Description get revision list
// @Tags Revision
// @Produce json
// @Param object_id query string true "object id"
// @Success 200 {object} handler.RespBody{data=[]schema.GetRevisionResp}
// @Router /answer/api/v1/revisions [get]
func (rc *RevisionController) GetRevisionList(ctx *gin.Context) {
objectID := ctx.Query("object_id")
if objectID == "0" || objectID == "" {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
req := &schema.GetRevisionListReq{
ObjectID: objectID,
}
resp, err := rc.revisionListService.GetRevisionList(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,69 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
)
// SearchController tag controller
type SearchController struct {
searchService *service.SearchService
}
// NewSearchController new controller
func NewSearchController(searchService *service.SearchService) *SearchController {
return &SearchController{searchService: searchService}
}
// Search godoc
// @Summary search object
// @Description search object
// @Tags Search
// @Produce json
// @Security ApiKeyAuth
// @Param q query string true "query string"
// @Success 200 {object} handler.RespBody{data=schema.SearchListResp}
// @Router /answer/api/v1/search [get]
func (sc *SearchController) Search(ctx *gin.Context) {
var (
q string
page string
size string
ok bool
dto schema.SearchDTO
)
q, ok = ctx.GetQuery("q")
if len(q) == 0 || !ok {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), q)
return
}
page, ok = ctx.GetQuery("page")
if !ok {
page = "1"
}
size, ok = ctx.GetQuery("size")
if !ok {
size = "30"
}
dto = schema.SearchDTO{
Query: q,
Page: converter.StringToInt(page),
Size: converter.StringToInt(size),
UserID: middleware.GetLoginUserIDFromContext(ctx),
}
resp, total, extra, err := sc.searchService.Search(ctx, &dto)
handler.HandleResponse(ctx, err, schema.SearchListResp{
Total: total,
SearchResp: resp,
Extra: extra,
})
}

View File

@ -0,0 +1,47 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
)
type SiteinfoController struct {
siteInfoService *service.SiteInfoService
}
// NewSiteinfoController new siteinfo controller.
func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController {
return &SiteinfoController{
siteInfoService: siteInfoService,
}
}
// GetInfo godoc
// @Summary Get siteinfo
// @Description Get siteinfo
// @Tags site
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp}
// @Router /answer/api/v1/siteinfo [get]
func (sc *SiteinfoController) GetInfo(ctx *gin.Context) {
var (
resp = &schema.SiteInfoResp{}
general schema.SiteGeneralResp
face schema.SiteInterfaceResp
err error
)
general, err = sc.siteInfoService.GetSiteGeneral(ctx)
resp.General = &general
if err != nil {
handler.HandleResponse(ctx, err, resp)
return
}
face, err = sc.siteInfoService.GetSiteInterface(ctx)
resp.Face = &face
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,194 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/rank"
"github.com/segmentfault/answer/internal/service/tag"
"github.com/segmentfault/pacman/errors"
)
// TagController tag controller
type TagController struct {
tagService *tag.TagService
rankService *rank.RankService
}
// NewTagController new controller
func NewTagController(tagService *tag.TagService, rankService *rank.RankService) *TagController {
return &TagController{tagService: tagService, rankService: rankService}
}
// SearchTagLike get tag list
// @Summary get tag list
// @Description get tag list
// @Tags Tag
// @Produce json
// @Security ApiKeyAuth
// @Param tag query string false "tag"
// @Success 200 {object} handler.RespBody{data=[]schema.GetTagResp}
// @Router /answer/api/v1/question/tags [get]
func (tc *TagController) SearchTagLike(ctx *gin.Context) {
req := &schema.SearchTagLikeReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := tc.tagService.SearchTagLike(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// RemoveTag delete tag
// @Summary delete tag
// @Description delete tag
// @Tags Tag
// @Accept json
// @Produce json
// @Param data body schema.RemoveTagReq true "tag"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/tag [delete]
func (tc *TagController) RemoveTag(ctx *gin.Context) {
req := &schema.RemoveTagReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagDeleteRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
err := tc.tagService.RemoveTag(ctx, req.TagID)
handler.HandleResponse(ctx, err, nil)
}
// UpdateTag update tag
// @Summary update tag
// @Description update tag
// @Tags Tag
// @Accept json
// @Produce json
// @Param data body schema.UpdateTagReq true "tag"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/tag [put]
func (tc *TagController) UpdateTag(ctx *gin.Context) {
req := &schema.UpdateTagReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
err := tc.tagService.UpdateTag(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetTagInfo get tag one
// @Summary get tag one
// @Description get tag one
// @Tags Tag
// @Accept json
// @Produce json
// @Param tag_id query string true "tag id"
// @Param tag_name query string true "tag name"
// @Success 200 {object} handler.RespBody{data=schema.GetTagResp}
// @Router /answer/api/v1/tag [get]
func (tc *TagController) GetTagInfo(ctx *gin.Context) {
req := &schema.GetTagInfoReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := tc.tagService.GetTagInfo(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// GetTagWithPage get tag page
// @Summary get tag page
// @Description get tag page
// @Tags Tag
// @Produce json
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Param slug_name query string false "slug_name"
// @Param query_cond query string false "query condition" Enums(popular, name, newest)
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetTagPageResp}}
// @Router /answer/api/v1/tags/page [get]
func (tc *TagController) GetTagWithPage(ctx *gin.Context) {
req := &schema.GetTagWithPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := tc.tagService.GetTagWithPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// GetFollowingTags get following tag list
// @Summary get following tag list
// @Description get following tag list
// @Security ApiKeyAuth
// @Tags Tag
// @Produce json
// @Success 200 {object} handler.RespBody{data=[]schema.GetFollowingTagsResp}
// @Router /answer/api/v1/tags/following [get]
func (tc *TagController) GetFollowingTags(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
resp, err := tc.tagService.GetFollowingTags(ctx, userID)
handler.HandleResponse(ctx, err, resp)
}
// GetTagSynonyms get tag synonyms
// @Summary get tag synonyms
// @Description get tag synonyms
// @Tags Tag
// @Produce json
// @Param tag_id query int true "tag id"
// @Success 200 {object} handler.RespBody{data=[]schema.GetTagSynonymsResp}
// @Router /answer/api/v1/tag/synonyms [get]
func (tc *TagController) GetTagSynonyms(ctx *gin.Context) {
req := &schema.GetTagSynonymsReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := tc.tagService.GetTagSynonyms(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UpdateTagSynonym update tag
// @Summary update tag
// @Description update tag
// @Tags Tag
// @Accept json
// @Produce json
// @Param data body schema.UpdateTagSynonymReq true "tag"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/tag/synonym [put]
func (tc *TagController) UpdateTagSynonym(ctx *gin.Context) {
req := &schema.UpdateTagSynonymReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := tc.rankService.CheckRankPermission(ctx, req.UserID, rank.TagSynonymRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
err := tc.tagService.UpdateTagSynonym(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -0,0 +1,58 @@
package useless
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/pacman/log"
)
// CollectionGroupController collectionGroup controller
type CollectionGroupController struct {
log log.log
collectionGroupService *service.CollectionGroupService
}
// NewCollectionGroupController new controller
func NewCollectionGroupController(collectionGroupService *service.CollectionGroupService) *CollectionGroupController {
return &CollectionGroupController{collectionGroupService: collectionGroupService}
}
// AddCollectionGroup add collection group
// @Summary add collection group
// @Description add collection group
// @Tags CollectionGroup
// @Accept json
// @Produce json
// @Param data body schema.AddCollectionGroupReq true "collection group"
// @Success 200 {object} handler.RespBody
// Router /collection-group [post]
func (cc *CollectionGroupController) AddCollectionGroup(ctx *gin.Context) {
req := &schema.AddCollectionGroupReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := cc.collectionGroupService.AddCollectionGroup(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UpdateCollectionGroup update collection group
// @Summary update collection group
// @Description update collection group
// @Tags CollectionGroup
// @Accept json
// @Produce json
// @Param data body schema.UpdateCollectionGroupReq true "collection group"
// @Success 200 {object} handler.RespBody
// Router /collection-group [put]
func (cc *CollectionGroupController) UpdateCollectionGroup(ctx *gin.Context) {
req := &schema.UpdateCollectionGroupReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := cc.collectionGroupService.UpdateCollectionGroup(ctx, req, []string{})
handler.HandleResponse(ctx, err, nil)
}

View File

@ -0,0 +1,143 @@
package useless
import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/notification"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// NotificationReadController notificationRead controller
type NotificationReadController struct {
log log.log
notificationReadService *notification.NotificationReadService
}
// NewNotificationReadController new controller
func NewNotificationReadController(notificationReadService *notification.NotificationReadService) *NotificationReadController {
return &NotificationReadController{notificationReadService: notificationReadService}
}
// AddNotificationRead add notification read record
// @Summary add notification read record
// @Description add notification read record
// @Tags NotificationRead
// @Accept json
// @Produce json
// @Param data body schema.AddNotificationReadReq true "notification read record"
// @Success 200 {object} handler.RespBody
// Router /notification-read [post]
func (nc *NotificationReadController) AddNotificationRead(ctx *gin.Context) {
req := &schema.AddNotificationReadReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := nc.notificationReadService.AddNotificationRead(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// RemoveNotificationRead delete notification read record
// @Summary delete notification read record
// @Description delete notification read record
// @Tags NotificationRead
// @Accept json
// @Produce json
// @Param data body schema.RemoveNotificationReadReq true "notification read record"
// @Success 200 {object} handler.RespBody
// Router /notification-read [delete]
func (nc *NotificationReadController) RemoveNotificationRead(ctx *gin.Context) {
req := &schema.RemoveNotificationReadReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := nc.notificationReadService.RemoveNotificationRead(ctx, req.ID)
handler.HandleResponse(ctx, err, nil)
}
// UpdateNotificationRead update notification read record
// @Summary update notification read record
// @Description update notification read record
// @Tags NotificationRead
// @Accept json
// @Produce json
// @Param data body schema.UpdateNotificationReadReq true "notification read record"
// @Success 200 {object} handler.RespBody
// Router /notification-read [put]
func (nc *NotificationReadController) UpdateNotificationRead(ctx *gin.Context) {
req := &schema.UpdateNotificationReadReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := nc.notificationReadService.UpdateNotificationRead(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetNotificationRead get notification read record one
// @Summary get notification read record one
// @Description get notification read record one
// @Tags NotificationRead
// @Accept json
// @Produce json
// @Param id path int true "notification read recordid"
// @Success 200 {object} handler.RespBody{data=schema.GetNotificationReadResp}
// Router /notification-read/{id} [get]
func (nc *NotificationReadController) GetNotificationRead(ctx *gin.Context) {
id, _ := strconv.Atoi(ctx.Param("id"))
if id == 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
resp, err := nc.notificationReadService.GetNotificationRead(ctx, id)
handler.HandleResponse(ctx, err, resp)
}
// GetNotificationReadList get notification read record list
// @Summary get notification read record list
// @Description get notification read record list
// @Tags NotificationRead
// @Produce json
// @Param user_id query string false "user id"
// @Param message_id query string false "message id"
// @Param is_read query string false "read status(unread: 1; read 2)"
// @Success 200 {object} handler.RespBody{data=[]schema.GetNotificationReadResp}
// Router /notification-reads [get]
func (nc *NotificationReadController) GetNotificationReadList(ctx *gin.Context) {
req := &schema.GetNotificationReadListReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := nc.notificationReadService.GetNotificationReadList(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// GetNotificationReadWithPage get notification read record page
// @Summary get notification read record page
// @Description get notification read record page
// @Tags NotificationRead
// @Produce json
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Param user_id query string false "user id"
// @Param message_id query string false "message id"
// @Param is_read query string false "read status(unread: 1; read 2)"
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetNotificationReadResp}}
// Router /notification-reads/page [get]
func (nc *NotificationReadController) GetNotificationReadWithPage(ctx *gin.Context) {
req := &schema.GetNotificationReadWithPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := nc.notificationReadService.GetNotificationReadWithPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,137 @@
package useless
import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// UserGroupController userGroup controller
type UserGroupController struct {
log log.log
userGroupService *service.UserGroupService
}
// NewUserGroupController new controller
func NewUserGroupController(userGroupService *service.UserGroupService) *UserGroupController {
return &UserGroupController{userGroupService: userGroupService}
}
// AddUserGroup add user group
// @Summary add user group
// @Description add user group
// @Tags UserGroup
// @Accept json
// @Produce json
// @Param data body schema.AddUserGroupReq true "user group"
// @Success 200 {object} handler.RespBody
// Router /user-group [post]
func (uc *UserGroupController) AddUserGroup(ctx *gin.Context) {
req := &schema.AddUserGroupReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := uc.userGroupService.AddUserGroup(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// RemoveUserGroup delete user group
// @Summary delete user group
// @Description delete user group
// @Tags UserGroup
// @Accept json
// @Produce json
// @Param data body schema.RemoveUserGroupReq true "user group"
// @Success 200 {object} handler.RespBody
// Router /user-group [delete]
func (uc *UserGroupController) RemoveUserGroup(ctx *gin.Context) {
req := &schema.RemoveUserGroupReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := uc.userGroupService.RemoveUserGroup(ctx, int(req.ID))
handler.HandleResponse(ctx, err, nil)
}
// UpdateUserGroup update user group
// @Summary update user group
// @Description update user group
// @Tags UserGroup
// @Accept json
// @Produce json
// @Param data body schema.UpdateUserGroupReq true "user group"
// @Success 200 {object} handler.RespBody
// Router /user-group [put]
func (uc *UserGroupController) UpdateUserGroup(ctx *gin.Context) {
req := &schema.UpdateUserGroupReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := uc.userGroupService.UpdateUserGroup(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetUserGroup get user group one
// @Summary get user group one
// @Description get user group one
// @Tags UserGroup
// @Accept json
// @Produce json
// @Param id path int true "user groupid"
// @Success 200 {object} handler.RespBody{data=schema.GetUserGroupResp}
// Router /user-group/{id} [get]
func (uc *UserGroupController) GetUserGroup(ctx *gin.Context) {
id, _ := strconv.Atoi(ctx.Param("id"))
if id == 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
resp, err := uc.userGroupService.GetUserGroup(ctx, id)
handler.HandleResponse(ctx, err, resp)
}
// GetUserGroupList get user group list
// @Summary get user group list
// @Description get user group list
// @Tags UserGroup
// @Produce json
// @Success 200 {object} handler.RespBody{data=[]schema.GetUserGroupResp}
// Router /user-groups [get]
func (uc *UserGroupController) GetUserGroupList(ctx *gin.Context) {
req := &schema.GetUserGroupListReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := uc.userGroupService.GetUserGroupList(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// GetUserGroupWithPage get user group page
// @Summary get user group page
// @Description get user group page
// @Tags UserGroup
// @Produce json
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetUserGroupResp}}
// Router /user-groups/page [get]
func (uc *UserGroupController) GetUserGroupWithPage(ctx *gin.Context) {
req := &schema.GetUserGroupWithPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := uc.userGroupService.GetUserGroupWithPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,479 @@
package controller
import (
"net/http"
"path"
"strings"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
"github.com/segmentfault/answer/internal/service/action"
"github.com/segmentfault/answer/internal/service/auth"
"github.com/segmentfault/answer/internal/service/export"
"github.com/segmentfault/answer/internal/service/uploader"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// UserController user controller
type UserController struct {
userService *service.UserService
authService *auth.AuthService
actionService *action.CaptchaService
uploaderService *uploader.UploaderService
emailService *export.EmailService
}
// NewUserController new controller
func NewUserController(
authService *auth.AuthService,
userService *service.UserService,
actionService *action.CaptchaService,
emailService *export.EmailService,
uploaderService *uploader.UploaderService) *UserController {
return &UserController{
authService: authService,
userService: userService,
actionService: actionService,
uploaderService: uploaderService,
emailService: emailService,
}
}
// GetUserInfoByUserID godoc
// @Summary GetUserInfoByUserID
// @Description GetUserInfoByUserID
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/info [get]
func (uc *UserController) GetUserInfoByUserID(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
token := middleware.ExtractToken(ctx)
resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userID)
handler.HandleResponse(ctx, err, resp)
}
// GetOtherUserInfoByUsername godoc
// @Summary GetOtherUserInfoByUsername
// @Description GetOtherUserInfoByUsername
// @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/personal/user/info [get]
func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) {
req := &schema.GetOtherUserInfoByUsernameReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := uc.userService.GetOtherUserInfoByUsername(ctx, req.Username)
handler.HandleResponse(ctx, err, resp)
}
// UserEmailLogin godoc
// @Summary UserEmailLogin
// @Description UserEmailLogin
// @Tags User
// @Accept json
// @Produce json
// @Param data body schema.UserEmailLogin true "UserEmailLogin"
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/login/email [post]
func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
req := &schema.UserEmailLogin{}
if handler.BindAndCheck(ctx, req) {
return
}
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), gin.H{
"key": "captcha_code",
"value": "verification failed",
})
return
}
resp, err := uc.userService.EmailLogin(ctx, req)
if err != nil {
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP())
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), gin.H{
"key": "e_mail",
"value": "Email or password incorrect",
})
return
}
uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP())
handler.HandleResponse(ctx, nil, resp)
}
// RetrievePassWord godoc
// @Summary RetrievePassWord
// @Description RetrievePassWord
// @Tags User
// @Accept json
// @Produce json
// @Param data body schema.UserRetrievePassWordRequest true "UserRetrievePassWordRequest"
// @Success 200 {string} string ""
// @Router /answer/api/v1/user/password/reset [post]
func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
req := &schema.UserRetrievePassWordRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), gin.H{
"key": "captcha_code",
"value": "verification failed",
})
return
}
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP())
code, err := uc.userService.RetrievePassWord(ctx, req)
handler.HandleResponse(ctx, err, code)
}
// UseRePassWord godoc
// @Summary UseRePassWord
// @Description UseRePassWord
// @Tags User
// @Accept json
// @Produce json
// @Param data body schema.UserRePassWordRequest true "UserRePassWordRequest"
// @Success 200 {string} string ""
// @Router /answer/api/v1/user/password/replacement [post]
func (uc *UserController) UseRePassWord(ctx *gin.Context) {
req := &schema.UserRePassWordRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)
if len(req.Content) == 0 {
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyUrlExpired),
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUrlExpired})
return
}
resp, err := uc.userService.UseRePassWord(ctx, req)
uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP())
handler.HandleResponse(ctx, err, resp)
}
// UserLogout user logout
// @Summary user logout
// @Description user logout
// @Tags User
// @Accept json
// @Produce json
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/user/logout [get]
func (uc *UserController) UserLogout(ctx *gin.Context) {
accessToken := middleware.ExtractToken(ctx)
_ = uc.authService.RemoveUserCacheInfo(ctx, accessToken)
handler.HandleResponse(ctx, nil, nil)
}
// UserRegisterByEmail godoc
// @Summary UserRegisterByEmail
// @Description UserRegisterByEmail
// @Tags User
// @Accept json
// @Produce json
// @Param data body schema.UserRegister true "UserRegister"
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/register/email [post]
func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
req := &schema.UserRegister{}
if handler.BindAndCheck(ctx, req) {
return
}
req.IP = ctx.ClientIP()
resp, err := uc.userService.UserRegisterByEmail(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UserVerifyEmail godoc
// @Summary UserVerifyEmail
// @Description UserVerifyEmail
// @Tags User
// @Accept json
// @Produce json
// @Param code query string true "code" default()
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/email/verification [post]
func (uc *UserController) UserVerifyEmail(ctx *gin.Context) {
req := &schema.UserVerifyEmailReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)
if len(req.Content) == 0 {
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyUrlExpired),
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUrlExpired})
return
}
resp, err := uc.userService.UserVerifyEmail(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP())
handler.HandleResponse(ctx, err, resp)
}
// UserVerifyEmailSend godoc
// @Summary UserVerifyEmailSend
// @Description UserVerifyEmailSend
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param captcha_id query string false "captcha_id" default()
// @Param captcha_code query string false "captcha_code" default()
// @Success 200 {string} string ""
// @Router /answer/api/v1/user/email/verification/send [post]
func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
req := &schema.UserVerifyEmailSendReq{}
if handler.BindAndCheck(ctx, req) {
return
}
userInfo := middleware.GetUserInfoFromContext(ctx)
if userInfo == nil {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
return
}
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP(),
req.CaptchaID, req.CaptchaCode)
if !captchaPass {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), gin.H{
"key": "captcha_code",
"value": "verification failed",
})
return
}
uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP())
err := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID)
handler.HandleResponse(ctx, err, nil)
}
// UserModifyPassWord godoc
// @Summary UserModifyPassWord
// @Description UserModifyPassWord
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UserModifyPassWordRequest true "UserModifyPassWordRequest"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/user/password [put]
func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
req := &schema.UserModifyPassWordRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !oldPassVerification {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), gin.H{
"key": "old_pass",
"value": "the old password verification failed",
})
return
}
if req.OldPass == req.Pass {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), gin.H{
"key": "pass",
"value": "The new password is the same as the previous setting",
})
return
}
err = uc.userService.UserModifyPassWord(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UserUpdateInfo godoc
// @Summary UserUpdateInfo
// @Description UserUpdateInfo
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param Authorization header string true "access-token"
// @Param data body schema.UpdateInfoRequest true "UpdateInfoRequest"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/user/info [put]
func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
req := &schema.UpdateInfoRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
err := uc.userService.UpdateInfo(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UploadUserAvatar godoc
// @Summary UserUpdateInfo
// @Description UserUpdateInfo
// @Tags User
// @Accept multipart/form-data
// @Security ApiKeyAuth
// @Param file formData file true "file"
// @Success 200 {object} handler.RespBody{data=string}
// @Router /answer/api/v1/user/avatar/upload [post]
func (uc *UserController) UploadUserAvatar(ctx *gin.Context) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, header, err := ctx.Request.FormFile("file")
if err != nil {
log.Error(err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(header.Filename))
if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" {
log.Errorf("upload file format is not supported: %s", fileExt)
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
url, err := uc.uploaderService.UploadAvatarFile(ctx, header, fileExt)
handler.HandleResponse(ctx, err, url)
}
// UploadUserPostFile godoc
// @Summary upload user post file
// @Description upload user post file
// @Tags User
// @Accept multipart/form-data
// @Security ApiKeyAuth
// @Param file formData file true "file"
// @Success 200 {object} handler.RespBody{data=string}
// @Router /answer/api/v1/user/post/file [post]
func (uc *UserController) UploadUserPostFile(ctx *gin.Context) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, header, err := ctx.Request.FormFile("file")
if err != nil {
log.Error(err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(header.Filename))
if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" {
log.Errorf("upload file format is not supported: %s", fileExt)
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
url, err := uc.uploaderService.UploadPostFile(ctx, header, fileExt)
handler.HandleResponse(ctx, err, url)
}
// ActionRecord godoc
// @Summary ActionRecord
// @Description ActionRecord
// @Tags User
// @Param action query string true "action" Enums(login, e_mail, find_pass)
// @Security ApiKeyAuth
// @Success 200 {object} handler.RespBody{data=schema.ActionRecordResp}
// @Router /answer/api/v1/user/action/record [get]
func (uc *UserController) ActionRecord(ctx *gin.Context) {
req := &schema.ActionRecordReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.Ip = ctx.ClientIP()
resp, err := uc.actionService.ActionRecord(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UserNoticeSet godoc
// @Summary UserNoticeSet
// @Description UserNoticeSet
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UserNoticeSetRequest true "UserNoticeSetRequest"
// @Success 200 {object} handler.RespBody{data=schema.UserNoticeSetResp}
// @Router /answer/api/v1/user/notice/set [post]
func (uc *UserController) UserNoticeSet(ctx *gin.Context) {
req := &schema.UserNoticeSetRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
resp, err := uc.userService.UserNoticeSet(ctx, req.UserId, req.NoticeSwitch)
handler.HandleResponse(ctx, err, resp)
}
// UserChangeEmailSendCode send email to the user email then change their email
// @Summary send email to the user email then change their email
// @Description send email to the user email then change their email
// @Tags User
// @Accept json
// @Produce json
// @Param data body schema.UserChangeEmailSendCodeReq true "UserChangeEmailSendCodeReq"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/user/email/change/code [post]
func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
req := &schema.UserChangeEmailSendCodeReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
err := uc.userService.UserChangeEmailSendCode(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UserChangeEmailVerify user change email verification
// @Summary user change email verification
// @Description user change email verification
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UserChangeEmailVerifyReq true "UserChangeEmailVerifyReq"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/user/email [put]
func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) {
req := &schema.UserChangeEmailVerifyReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code)
if len(req.Content) == 0 {
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyUrlExpired),
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUrlExpired})
return
}
err := uc.userService.UserChangeEmailVerify(ctx, req.Content)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -0,0 +1,105 @@
package controller
import (
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
)
// VoteController activity controller
type VoteController struct {
VoteService *service.VoteService
}
// NewVoteController new controller
func NewVoteController(voteService *service.VoteService) *VoteController {
return &VoteController{VoteService: voteService}
}
// VoteUp godoc
// @Summary vote up
// @Description add vote
// @Tags Activity
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.VoteReq true "vote"
// @Success 200 {object} handler.RespBody{data=schema.VoteResp}
// @Router /answer/api/v1/vote/up [post]
func (vc *VoteController) VoteUp(ctx *gin.Context) {
req := &schema.VoteReq{}
if handler.BindAndCheck(ctx, req) {
return
}
dto := &schema.VoteDTO{}
_ = copier.Copy(dto, req)
dto.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := vc.VoteService.VoteUp(ctx, dto)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeToast)
} else {
handler.HandleResponse(ctx, err, resp)
}
}
// VoteDown godoc
// @Summary vote down
// @Description add vote
// @Tags Activity
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.VoteReq true "vote"
// @Success 200 {object} handler.RespBody{data=schema.VoteResp}
// @Router /answer/api/v1/vote/down [post]
func (vc *VoteController) VoteDown(ctx *gin.Context) {
req := &schema.VoteReq{}
if handler.BindAndCheck(ctx, req) {
return
}
dto := &schema.VoteDTO{}
_ = copier.Copy(dto, req)
dto.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := vc.VoteService.VoteDown(ctx, dto)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeToast)
} else {
handler.HandleResponse(ctx, err, resp)
}
}
// UserVotes godoc
// @Summary user's votes
// @Description user's vote
// @Tags Activity
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Success 200 {object} handler.RespBody{data=pager.PageModel{list=[]schema.GetVoteWithPageResp}}
// @Router /answer/api/v1/personal/vote/page [get]
func (vc *VoteController) UserVotes(ctx *gin.Context) {
req := schema.GetVoteWithPageReq{}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if handler.BindAndCheck(ctx, &req) {
return
}
if req.Page == 0 {
req.Page = 1
}
if req.PageSize == 0 {
req.PageSize = 30
}
resp, err := vc.VoteService.ListUserVotes(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeModal)
} else {
handler.HandleResponse(ctx, err, resp)
}
}

View File

@ -0,0 +1,11 @@
package controller_backyard
import "github.com/google/wire"
// ProviderSetController is controller providers.
var ProviderSetController = wire.NewSet(
NewReportController,
NewUserBackyardController,
NewThemeController,
NewSiteInfoController,
)

View File

@ -0,0 +1,77 @@
package controller_backyard
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/report_backyard"
"github.com/segmentfault/answer/pkg/converter"
)
// ReportController report controller
type ReportController struct {
reportService *report_backyard.ReportBackyardService
}
// NewReportController new controller
func NewReportController(reportService *report_backyard.ReportBackyardService) *ReportController {
return &ReportController{reportService: reportService}
}
// ListReportPage godoc
// @Summary list report page
// @Description list report records
// @Security ApiKeyAuth
// @Tags admin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param status query string true "status" Enums(pending, completed)
// @Param object_type query string true "object_type" Enums(all, question,answer,comment)
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/reports/page [get]
func (rc *ReportController) ListReportPage(ctx *gin.Context) {
var (
objectType = ctx.Query("object_type")
status = ctx.Query("status")
page = converter.StringToInt(ctx.DefaultQuery("page", "1"))
pageSize = converter.StringToInt(ctx.DefaultQuery("page_size", "20"))
)
dto := schema.GetReportListPageDTO{
ObjectType: objectType,
Status: status,
Page: page,
PageSize: pageSize,
}
resp, err := rc.reportService.ListReportPage(ctx, dto)
if err != nil {
handler.HandleResponse(ctx, err, schema.ErrTypeModal)
} else {
handler.HandleResponse(ctx, err, resp)
}
}
// Handle godoc
// @Summary handle flag
// @Description handle flag
// @Security ApiKeyAuth
// @Tags admin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.ReportHandleReq true "flag"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/report/ [put]
func (rc *ReportController) Handle(ctx *gin.Context) {
req := schema.ReportHandleReq{}
if handler.BindAndCheck(ctx, &req) {
return
}
err := rc.reportService.HandleReported(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -0,0 +1,78 @@
package controller_backyard
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service"
)
type SiteInfoController struct {
siteInfoService *service.SiteInfoService
}
// NewSiteInfoController new siteinfo controller.
func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController {
return &SiteInfoController{
siteInfoService: siteInfoService,
}
}
// GetGeneral godoc
// @Summary Get siteinfo general
// @Description Get siteinfo general
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp}
// @Router /answer/admin/api/siteinfo/general [get]
func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteGeneral(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetInterface godoc
// @Summary Get siteinfo interface
// @Description Get siteinfo interface
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp}
// @Router /answer/admin/api/siteinfo/interface [get]
// @Param data body schema.AddCommentReq true "general"
func (sc *SiteInfoController) GetInterface(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteInterface(ctx)
handler.HandleResponse(ctx, err, resp)
}
// UpdateGeneral godoc
// @Summary Get siteinfo interface
// @Description Get siteinfo interface
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteGeneralReq true "general"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/general [put]
func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) {
req := schema.SiteGeneralReq{}
handler.BindAndCheck(ctx, &req)
err := sc.siteInfoService.SaveSiteGeneral(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UpdateInterface godoc
// @Summary Get siteinfo interface
// @Description Get siteinfo interface
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteInterfaceReq true "general"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/interface [put]
func (sc *SiteInfoController) UpdateInterface(ctx *gin.Context) {
req := schema.SiteInterfaceReq{}
handler.BindAndCheck(ctx, &req)
err := sc.siteInfoService.SaveSiteInterface(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -0,0 +1,26 @@
package controller_backyard
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/schema"
)
type ThemeController struct{}
// NewThemeController new theme controller.
func NewThemeController() *ThemeController {
return &ThemeController{}
}
// GetThemeOptions godoc
// @Summary Get theme options
// @Description Get theme options
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/theme/options [get]
func (t *ThemeController) GetThemeOptions(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, schema.GetThemeOptions)
}

View File

@ -0,0 +1,84 @@
package controller_backyard
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/handler"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/user_backyard"
"github.com/segmentfault/pacman/errors"
)
// UserBackyardController user controller
type UserBackyardController struct {
userService *user_backyard.UserBackyardService
}
// NewUserBackyardController new controller
func NewUserBackyardController(userService *user_backyard.UserBackyardService) *UserBackyardController {
return &UserBackyardController{userService: userService}
}
// UpdateUserStatus update user
// @Summary update user
// @Description update user
// @Security ApiKeyAuth
// @Tags admin
// @Accept json
// @Produce json
// @Param data body schema.UpdateUserStatusReq true "user"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/status [put]
func (uc *UserBackyardController) UpdateUserStatus(ctx *gin.Context) {
req := &schema.UpdateUserStatusReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := uc.userService.UpdateUserStatus(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetUserInfo get user one
// @Summary get user one
// @Description get user one
// @Security ApiKeyAuth
// @Tags admin
// @Accept json
// @Produce json
// @Param id path int true "userid"
// @Success 200 {object} handler.RespBody{data=schema.GetUserInfoResp}
// Router /user/{id} [get]
func (uc *UserBackyardController) GetUserInfo(ctx *gin.Context) {
userID := ctx.Query("user_id")
if len(userID) == 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
resp, err := uc.userService.GetUserInfo(ctx, userID)
handler.HandleResponse(ctx, err, resp)
}
// GetUserPage get user page
// @Summary get user page
// @Description get user page
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Param username query string false "username"
// @Param e_mail query string false "email"
// @Param status query string false "user status" Enums(normal, suspended, deleted, inactive)
// @Success 200 {object} handler.RespBody{data=pager.PageModel{records=[]schema.GetUserPageResp}}
// @Router /answer/admin/api/users/page [get]
func (uc *UserBackyardController) GetUserPage(ctx *gin.Context) {
req := &schema.GetUserPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := uc.userService.GetUserPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -0,0 +1,31 @@
package entity
import "time"
const (
ActivityAvailable = 0
ActivityCancelled = 1
)
// Activity activity
type Activity struct {
ID string `xorm:"not null pk autoincr comment('Activity TagID autoincrement') BIGINT(20) id"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated comment('update time') TIMESTAMP updated_at"`
UserID string `xorm:"not null comment('the user ID that generated the activity or affected by the activity') index BIGINT(20) user_id"`
TriggerUserID int64 `xorm:"not null default 0 comment('the trigger user TagID that generated the activity or affected by the activity') index BIGINT(20) trigger_user_id"`
ObjectID string `xorm:"not null default 0 comment('the object TagID that affected by the activity') index BIGINT(20) object_id"`
ActivityType int `xorm:"not null comment('activity type, correspond to config id') INT(11) activity_type"`
Cancelled int `xorm:"not null default 0 comment('mark this activity if cancelled or not,default 0(not cancelled)') TINYINT(4) cancelled"`
Rank int `xorm:"not null default 0 comment('rank of current operating user affected') INT(11) rank"`
HasRank int `xorm:"not null default 0 comment('this activity has rank or not') TINYINT(4) has_rank"`
}
type ActivityRunkSum struct {
Rank int `xorm:"not null default 0 comment('rank of current operating user affected') INT(11) rank"`
}
// TableName activity table name
func (Activity) TableName() string {
return "activity"
}

View File

@ -0,0 +1,57 @@
package entity
import "time"
const (
Answer_Search_OrderBy_Default = "default"
Answer_Search_OrderBy_Time = "updated"
Answer_Search_OrderBy_Vote = "vote"
AnswerStatusAvailable = 1
AnswerStatusDeleted = 10
)
var CmsAnswerSearchStatus = map[string]int{
"available": AnswerStatusAvailable,
"deleted": AnswerStatusDeleted,
}
// Answer answer
type Answer struct {
ID string `xorm:"not null pk autoincr comment('answer id') BIGINT(20) id"`
CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"`
QuestionID string `xorm:"not null default 0 comment('question id') BIGINT(20) question_id"`
UserID string `xorm:"not null default 0 comment('answer user id') BIGINT(20) user_id"`
OriginalText string `xorm:"not null comment('original content') MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null comment('parsed content') MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 comment(' answer status(available: 1; deleted: 10)') INT(11) status"`
Adopted int `xorm:"not null default 1 comment('adopted (1 failed 2 adopted)') INT(11) adopted"`
CommentCount int `xorm:"not null default 0 comment('comment count') INT(11) comment_count"`
VoteCount int `xorm:"not null default 0 comment('vote count') INT(11) vote_count"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
type AnswerSearch struct {
Answer
Order string `json:"order_by" ` // default or updated
Page int `json:"page" form:"page"` //Query number of pages
PageSize int `json:"page_size" form:"page_size"` //Search page size
}
type CmsAnswerSearch 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
}
type AdminSetAnswerStatusRequest struct {
StatusStr string `json:"status" form:"status"`
AnswerID string `json:"answer_id" form:"answer_id"`
}
// TableName answer table name
func (Answer) TableName() string {
return "answer"
}

View File

@ -0,0 +1,8 @@
package entity
// UserCacheInfo 用户缓存信息
type UserCacheInfo struct {
UserID string `json:"user_id"`
UserStatus int `json:"user_status"`
EmailStatus int `json:"email_status"`
}

View File

@ -0,0 +1,26 @@
package entity
import "time"
const ()
// Collection collection
type Collection struct {
ID string `xorm:"not null pk default 0 comment('collection id') BIGINT(20) id"`
UserID string `xorm:"not null default 0 comment('user id') BIGINT(20) user_id"`
ObjectID string `xorm:"not null default 0 comment('object id') BIGINT(20) object_id"`
UserCollectionGroupID string `xorm:"not null default 0 comment('user collection group id') BIGINT(20) user_collection_group_id"`
CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"`
}
type CollectionSearch struct {
Collection
Page int `json:"page" form:"page"` //Query number of pages
PageSize int `json:"page_size" form:"page_size"` //Search page size
}
// TableName collection table name
func (Collection) TableName() string {
return "collection"
}

View File

@ -0,0 +1,18 @@
package entity
import "time"
// CollectionGroup collection group
type CollectionGroup struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
Name string `xorm:"not null default '' comment('the collection group name') VARCHAR(50) name"`
DefaultGroup int `xorm:"not null default 1 comment('mark this group is default, default 1') INT(11) default_group"`
CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"`
}
// TableName collection group table name
func (CollectionGroup) TableName() string {
return "collection_group"
}

View File

@ -0,0 +1,69 @@
package entity
import (
"database/sql"
"fmt"
"time"
"github.com/segmentfault/answer/pkg/converter"
)
const (
CommentStatusAvailable = 1
CommentStatusDeleted = 10
)
// Comment comment
type Comment struct {
ID string `xorm:"not null pk autoincr comment('comment id') BIGINT(20) id"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated comment('update time') TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 comment('user id') BIGINT(20) user_id"`
ReplyUserID sql.NullInt64 `xorm:"comment('reply user id') BIGINT(20) reply_user_id"`
ReplyCommentID sql.NullInt64 `xorm:"comment('reply comment id') BIGINT(20) reply_comment_id"`
ObjectID string `xorm:"not null default 0 comment('object id') BIGINT(20) object_id"`
QuestionID string `xorm:"not null default 0 comment('question id') BIGINT(20) question_id"`
VoteCount int `xorm:"not null default 0 comment('user vote amount') INT(11) vote_count"`
Status int `xorm:"not null default 0 comment('comment status(available: 1; deleted: 10)') TINYINT(4) status"`
OriginalText string `xorm:"not null comment('original comment content') MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null comment('parsed comment content') MEDIUMTEXT parsed_text"`
}
// TableName comment table name
func (c *Comment) TableName() string {
return "comment"
}
// GetReplyUserID get reply user id
func (c *Comment) GetReplyUserID() string {
if c.ReplyUserID.Valid {
return fmt.Sprintf("%d", c.ReplyUserID.Int64)
}
return ""
}
// GetReplyCommentID get reply comment id
func (c *Comment) GetReplyCommentID() string {
if c.ReplyCommentID.Valid {
return fmt.Sprintf("%d", c.ReplyCommentID.Int64)
}
return ""
}
// SetReplyUserID set reply user id
func (c *Comment) SetReplyUserID(str string) {
if len(str) > 0 {
c.ReplyUserID = sql.NullInt64{Int64: converter.StringToInt64(str), Valid: true}
} else {
c.ReplyUserID = sql.NullInt64{Valid: false}
}
}
// SetReplyCommentID set reply comment id
func (c *Comment) SetReplyCommentID(str string) {
if len(str) > 0 {
c.ReplyCommentID = sql.NullInt64{Int64: converter.StringToInt64(str), Valid: true}
} else {
c.ReplyCommentID = sql.NullInt64{Valid: false}
}
}

View File

@ -0,0 +1,13 @@
package entity
// Config config
type Config struct {
ID int `xorm:"not null pk autoincr comment('config id') INT(11) id"`
Key string `xorm:"comment('the config key') unique VARCHAR(32) key"`
Value string `xorm:"comment('the config value, custom data structures and types') VARCHAR(128) value"`
}
// TableName config table name
func (Config) TableName() string {
return "config"
}

View File

@ -0,0 +1,25 @@
package entity
import "time"
const (
QuestionEditSummaryKey = "question.edit.summary"
QuestionCloseReasonKey = "question.close.reason"
AnswerEditSummaryKey = "answer.edit.summary"
TagEditSummaryKey = "tag.edit.summary"
)
// Meta meta
type Meta struct {
ID int `xorm:"not null pk autoincr comment('id') INT(10) id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP created comment('created time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP updated comment('updated time') TIMESTAMP updated_at"`
ObjectID string `xorm:"not null default 0 comment('object id') BIGINT(20) object_id"`
Key string `xorm:"not null comment('key') VARCHAR(100) key"`
Value string `xorm:"not null comment('value') MEDIUMTEXT value"`
}
// TableName meta table name
func (Meta) TableName() string {
return "meta"
}

View File

@ -0,0 +1,21 @@
package entity
import "time"
// Notification notification
type Notification struct {
ID string `xorm:"not null pk autoincr comment('notification id') BIGINT(20) id"`
UserID string `xorm:"not null default 0 comment('user id') BIGINT(20) user_id"`
ObjectID string `xorm:"not null default 0 comment('object id') index BIGINT(20) object_id"`
Content string `xorm:"not null comment('notification content') TEXT content"`
Type int `xorm:"not null default 0 comment('notification type( 1 inbox 2 achievement)') INT(11) type"`
IsRead int `xorm:"not null default 1 comment('read status(unread: 1; read 2)') INT(11) is_read"`
Status int `xorm:"not null default 1 comment('notification status(normal: 1; delete 2)') INT(11) status"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"comment('update time') TIMESTAMP updated_at"`
}
// TableName notification table name
func (Notification) TableName() string {
return "notification"
}

View File

@ -0,0 +1,18 @@
package entity
import "time"
// NotificationRead notification read record
type NotificationRead struct {
ID int `xorm:"not null pk autoincr comment('id') INT(11) id"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated comment('update time') TIMESTAMP updated_at"`
UserID int64 `xorm:"not null default 0 comment('user id') BIGINT(20) user_id"`
MessageID int64 `xorm:"not null default 0 comment('message id') BIGINT(20) message_id"`
IsRead int `xorm:"not null default 1 comment('read status(unread: 1; read 2)') INT(11) is_read"`
}
// TableName notification read record table name
func (NotificationRead) TableName() string {
return "notification_read"
}

View File

@ -0,0 +1,49 @@
package entity
import (
"time"
)
const (
QuestionStatusAvailable = 1
QuestionStatusclosed = 2
QuestionStatusDeleted = 10
)
var CmsQuestionSearchStatus = map[string]int{
"available": QuestionStatusAvailable,
"closed": QuestionStatusclosed,
"deleted": QuestionStatusDeleted,
}
type QuestionTag struct {
Question `xorm:"extends"`
TagRel `xorm:"extends"`
}
// Question question
type Question struct {
ID string `xorm:"not null pk comment('question id') BIGINT(20) id"`
UserID string `xorm:"not null default 0 comment('user id') BIGINT(20) user_id"`
Title string `xorm:"not null default '' comment('question title') VARCHAR(255) title"`
OriginalText string `xorm:"not null comment('original content') MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null comment('parsed content') MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 comment(' question status(available: 1; deleted: 10)') INT(11) status"`
ViewCount int `xorm:"not null default 0 comment('view count') INT(11) view_count"`
UniqueViewCount int `xorm:"not null default 0 comment('unique view count') INT(11) unique_view_count"`
VoteCount int `xorm:"not null default 0 comment('vote count') INT(11) vote_count"`
AnswerCount int `xorm:"not null default 0 comment('answer count') INT(11) answer_count"`
CollectionCount int `xorm:"not null default 0 comment('collection count') INT(11) collection_count"`
FollowCount int `xorm:"not null default 0 comment('follow count') INT(11) follow_count"`
AcceptedAnswerID string `xorm:"not null default 0 comment('accepted answer id') BIGINT(20) accepted_answer_id"`
LastAnswerID string `xorm:"not null default 0 comment('last answer id') BIGINT(20) last_answer_id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP comment('update time') TIMESTAMP updated_at"`
PostUpdateTime time.Time `xorm:"default CURRENT_TIMESTAMP comment('answer the last update time') TIMESTAMP post_update_time"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
// TableName question table name
func (Question) TableName() string {
return "question"
}

View File

@ -0,0 +1,38 @@
package entity
import "time"
const (
ReportStatusPending = 1
ReportStatusCompleted = 2
ReportStatusDeleted = 10
)
var (
ReportStatus = map[string]int{
"pending": ReportStatusPending,
"completed": ReportStatusCompleted,
"deleted": ReportStatusDeleted,
}
)
// Report report
type Report struct {
ID string `xorm:"not null pk autoincr comment('id') BIGINT(20) id"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated comment('update time') TIMESTAMP updated_at"`
UserID string `xorm:"not null comment('reporter user id') BIGINT(20) user_id"`
ReportedUserID string `xorm:"not null comment('reported user id') BIGINT(20) reported_user_id"`
ObjectID string `xorm:"not null comment('object id') BIGINT(20) object_id"`
ObjectType int `xorm:"not null default 0 comment('revision type') INT(11) object_type"`
ReportType int `xorm:"not null default 0 comment('report type') INT(11) report_type"`
Content string `xorm:"not null comment('report content') TEXT content"`
FlagedType int `xorm:"not null default 0 comment('flaged type') INT(11) flaged_type"`
FlagedContent string `xorm:"not null comment('flaged content') TEXT flaged_content"`
Status int `xorm:"not null default 1 comment('status(normal: 1; delete 2)') INT(11) status"`
}
// TableName report table name
func (Report) TableName() string {
return "report"
}

View File

@ -0,0 +1,22 @@
package entity
import "time"
// Revision revision
type Revision struct {
ID string `xorm:"not null pk autoincr comment('id') BIGINT(20) id"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated comment('update time') TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 comment('user id') BIGINT(20) user_id"`
ObjectType int `xorm:"not null default 0 comment('revision type(question: 1; answer 2; tag 3)') INT(11) object_type"`
ObjectID string `xorm:"not null default 0 comment('object id') BIGINT(20) object_id"`
Title string `xorm:"not null default '' comment('title') VARCHAR(255) title"`
Content string `xorm:"not null comment('content') TEXT content"`
Log string `xorm:"comment('log') VARCHAR(255) log"`
Status int `xorm:"not null default 1 comment('revision status(normal: 1; delete 2)') INT(11) status"`
}
// TableName revision table name
func (Revision) TableName() string {
return "revision"
}

View File

@ -0,0 +1,18 @@
package entity
import "time"
// SiteInfo site information setting
type SiteInfo struct {
ID string `xorm:"not null pk autoincr comment('id') INT(11) id"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated comment('update time') TIMESTAMP updated_at"`
Type string `xorm:"not null comment('content') VARCHAR(64) type"`
Content string `xorm:"not null comment('content') MEDIUMTEXT content"`
Status int `xorm:"not null default 1 comment('site info status(available: 1; deleted: 10)') INT(11) status"`
}
// TableName table name
func (*SiteInfo) TableName() string {
return "site_info"
}

View File

@ -0,0 +1,30 @@
package entity
import "time"
const (
TagStatusAvailable = 1
TagStatusDeleted = 10
)
// Tag tag
type Tag struct {
ID string `xorm:"not null comment('tag_id') BIGINT(20) id"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated comment('update time') TIMESTAMP updated_at"`
MainTagID int64 `xorm:"not null default 0 comment('main tag id') BIGINT(20) main_tag_id"`
MainTagSlugName string `xorm:"default '' comment('main_tag_slug_name') VARCHAR(50) main_tag_slug_name"`
SlugName string `xorm:"default '' comment('slug_name') unique VARCHAR(50) slug_name"`
DisplayName string `xorm:"not null default '' comment('display_name') VARCHAR(50) display_name"`
OriginalText string `xorm:"not null comment('original comment content') MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null comment('parsed comment content') MEDIUMTEXT parsed_text"`
FollowCount int `xorm:"not null default 0 comment('associated follow count') INT(11) follow_count"`
QuestionCount int `xorm:"not null default 0 comment('associated question count') INT(11) question_count"`
Status int `xorm:"not null default 1 comment('tag status(available: 1; deleted: 10)') INT(11) status"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
// TableName tag table name
func (Tag) TableName() string {
return "tag"
}

View File

@ -0,0 +1,23 @@
package entity
import "time"
const (
TagRelStatusAvailable = 1
TagRelStatusDeleted = 10
)
// TagRel tag relation
type TagRel struct {
ID int64 `xorm:"not null pk autoincr comment('tag_list_id') BIGINT(20) id"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated comment('update time') TIMESTAMP updated_at"`
ObjectID string `xorm:"not null comment('object_id') index BIGINT(20) object_id"`
TagID string `xorm:"not null comment('tag_id') index BIGINT(20) tag_id"`
Status int `xorm:"not null default 1 comment('tag_list_status(available: 1; deleted: 10)') INT(11) status"`
}
// TableName tag list table name
func (TagRel) TableName() string {
return "tag_rel"
}

View File

@ -0,0 +1,12 @@
package entity
// Uniqid uniqid
type Uniqid struct {
ID int64 `xorm:"not null pk autoincr comment('uniqid_id') BIGINT(20) id"`
UniqidType int `xorm:"not null default 0 comment('uniqid_type') INT(11) uniqid_type"`
}
// TableName uniqid table name
func (Uniqid) TableName() string {
return "uniqid"
}

View File

@ -0,0 +1,59 @@
package entity
import "time"
const (
UserStatusAvailable = 1
UserStatusSuspended = 9
UserStatusDeleted = 10
)
const (
EmailStatusAvailable = 1
EmailStatusToBeVerified = 2
)
const (
UserAdminFlag = 1
)
// User user
type User struct {
ID string `xorm:"not null pk autoincr comment('user id') BIGINT(20) id"`
CreatedAt time.Time `xorm:"created comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated comment('update time') TIMESTAMP updated_at"`
SuspendedAt time.Time `xorm:"comment('suspended time') TIMESTAMP suspended_at"`
DeletedAt time.Time `xorm:"comment('delete time') TIMESTAMP deleted_at"`
LastLoginDate time.Time `xorm:"comment('last_login_date') TIMESTAMP last_login_date"`
Username string `xorm:"not null default '' comment('username') VARCHAR(50) username"`
Pass string `xorm:"not null default '' comment('password') VARCHAR(255) pass"`
EMail string `xorm:"not null comment('email') VARCHAR(100) e_mail"`
MailStatus int `xorm:"not null default 2 comment('mail status(1 pass 2 to be verified)') TINYINT(4) mail_status"`
NoticeStatus int `xorm:"not null default 2 comment('notice status(1 on 2off)') INT(11) notice_status"`
FollowCount int `xorm:"not null default 0 comment('follow count') INT(11) follow_count"`
AnswerCount int `xorm:"not null default 0 comment('answer count') INT(11) answer_count"`
QuestionCount int `xorm:"not null default 0 comment('question count') INT(11) question_count"`
Rank int `xorm:"not null default 0 comment('rank') INT(11) rank"`
Status int `xorm:"not null default 1 comment('user status(available: 1; deleted: 10)') INT(11) status"`
AuthorityGroup int `xorm:"not null default 1 comment('authority group') INT(11) authority_group"`
DisplayName string `xorm:"not null default '' comment('display name') VARCHAR(50) display_name"`
Avatar string `xorm:"not null default '' comment('avatar') VARCHAR(255) avatar"`
Mobile string `xorm:"not null comment('mobile') VARCHAR(20) mobile"`
Bio string `xorm:"not null comment('bio markdown') TEXT bio"`
BioHtml string `xorm:"not null comment('bio html') TEXT bio_html"`
Website string `xorm:"not null default '' comment('website') VARCHAR(255) website"`
Location string `xorm:"not null default '' comment('location') VARCHAR(100) location"`
IPInfo string `xorm:"not null default '' comment('ip info') VARCHAR(255) ip_info"`
IsAdmin bool `xorm:"not null default 0 comment('admin flag') INT(11) is_admin"`
}
// TableName user table name
func (User) TableName() string {
return "user"
}
type UserSearch struct {
User
Page int `json:"page" form:"page"` //Query number of pages
PageSize int `json:"page_size" form:"page_size"` //Search page size
}

View File

@ -0,0 +1,11 @@
package entity
// UserGroup user group
type UserGroup struct {
ID int64 `xorm:"not null pk autoincr comment('user group id') unique BIGINT(20) id"`
}
// TableName user group table name
func (UserGroup) TableName() string {
return "user_group"
}

View File

@ -0,0 +1,336 @@
package activity
import (
"context"
"github.com/segmentfault/answer/internal/base/constant"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/entity"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/activity"
"github.com/segmentfault/answer/internal/service/activity_common"
"github.com/segmentfault/answer/internal/service/notice_queue"
"github.com/segmentfault/answer/internal/service/rank"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
// AnswerActivityRepo answer accepted
type AnswerActivityRepo struct {
data *data.Data
activityRepo activity_common.ActivityRepo
userRankRepo rank.UserRankRepo
}
const (
acceptAction = "accept"
acceptedAction = "accepted"
acceptCancelAction = "accept_cancel"
acceptedCancelAction = "accepted_cancel"
)
var (
acceptActionList = []string{acceptAction, acceptedAction}
acceptCancelActionList = []string{acceptCancelAction, acceptedCancelAction}
)
// NewAnswerActivityRepo new repository
func NewAnswerActivityRepo(
data *data.Data,
activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo,
) activity.AnswerActivityRepo {
return &AnswerActivityRepo{
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
}
}
// NewQuestionActivityRepo new repository
func NewQuestionActivityRepo(
data *data.Data,
activityRepo activity_common.ActivityRepo,
userRankRepo rank.UserRankRepo,
) activity.QuestionActivityRepo {
return &AnswerActivityRepo{
data: data,
activityRepo: activityRepo,
userRankRepo: userRankRepo,
}
}
func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID string) (err error) {
questionInfo := &entity.Question{}
exist, err := ar.data.DB.Where("id = ?", questionID).Get(questionInfo)
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.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) {
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`").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
return nil, nil
})
if err != nil {
return err
}
// get all answers
answerList := make([]*entity.Answer, 0)
err = ar.data.DB.Find(&answerList, &entity.Answer{QuestionID: questionID})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
for _, answerInfo := range answerList {
err = ar.DeleteAnswer(ctx, answerInfo.ID)
if err != nil {
log.Error(err)
}
}
return
}
// AcceptAnswer accept other answer
func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string, isSelf bool) (err error) {
addActivityList := make([]*entity.Activity, 0)
for _, action := range acceptActionList {
// get accept answer need add rank amount
activityType, deltaRank, hasRank, e := ar.activityRepo.GetActivityTypeByObjID(ctx, answerObjID, action)
if e != nil {
return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
addActivity := &entity.Activity{
ObjectID: answerObjID,
ActivityType: activityType,
Rank: deltaRank,
HasRank: hasRank,
}
if action == acceptAction {
addActivity.UserID = questionUserID
} else {
addActivity.UserID = answerUserID
}
if isSelf {
addActivity.Rank = 0
}
addActivityList = append(addActivityList, addActivity)
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
for _, addActivity := range addActivityList {
existsActivity, exists, e := ar.activityRepo.GetActivity(
ctx, session, answerObjID, addActivity.UserID, addActivity.ActivityType)
if e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if exists && existsActivity.Cancelled == entity.ActivityAvailable {
continue
}
reachStandard, e := ar.userRankRepo.TriggerUserRank(
ctx, session, addActivity.UserID, addActivity.Rank, addActivity.ActivityType)
if e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
if reachStandard {
addActivity.Rank = 0
}
if exists {
if _, e := session.Where("id = ?", existsActivity.ID).Cols("`cancelled`").
Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
} else {
if _, e = session.Insert(addActivity); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
}
return nil, nil
})
if err != nil {
return err
}
for _, act := range addActivityList {
msg := &schema.NotificationMsg{
Type: schema.NotificationTypeAchievement,
ObjectID: act.ObjectID,
ReceiverUserID: act.UserID,
}
if act.UserID == questionUserID {
msg.TriggerUserID = answerUserID
msg.ObjectType = constant.QuestionObjectType
} else {
msg.TriggerUserID = questionUserID
msg.ObjectType = constant.AnswerObjectType
}
notice_queue.AddNotification(msg)
}
for _, act := range addActivityList {
msg := &schema.NotificationMsg{
ReceiverUserID: act.UserID,
Type: schema.NotificationTypeInbox,
ObjectID: act.ObjectID,
}
if act.UserID != questionUserID {
msg.TriggerUserID = questionUserID
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.AdoptAnswer
notice_queue.AddNotification(msg)
}
}
return err
}
// CancelAcceptAnswer accept other answer
func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
answerObjID, questionUserID, answerUserID string) (err error) {
addActivityList := make([]*entity.Activity, 0)
for _, action := range acceptActionList {
// get accept answer need add rank amount
activityType, deltaRank, hasRank, e := ar.activityRepo.GetActivityTypeByObjID(ctx, answerObjID, action)
if e != nil {
return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
addActivity := &entity.Activity{
ObjectID: answerObjID,
ActivityType: activityType,
Rank: -deltaRank,
HasRank: hasRank,
}
if action == acceptAction {
addActivity.UserID = questionUserID
} else {
addActivity.UserID = answerUserID
}
addActivityList = append(addActivityList, addActivity)
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
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
}
_, 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`").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); 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{
ReceiverUserID: act.UserID,
Type: schema.NotificationTypeAchievement,
ObjectID: act.ObjectID,
}
if act.UserID == questionUserID {
msg.TriggerUserID = answerUserID
msg.ObjectType = constant.QuestionObjectType
} else {
msg.TriggerUserID = questionUserID
msg.ObjectType = constant.AnswerObjectType
}
notice_queue.AddNotification(msg)
}
return err
}
func (ar *AnswerActivityRepo) DeleteAnswer(ctx context.Context, answerID string) (err error) {
answerInfo := &entity.Answer{}
exist, err := ar.data.DB.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.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) {
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`").
Update(&entity.Activity{Cancelled: entity.ActivityCancelled}); e != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
}
}
return nil, nil
})
if err != nil {
return err
}
return
}

View File

@ -0,0 +1,152 @@
package activity
import (
"context"
"github.com/segmentfault/answer/internal/service/activity_common"
"github.com/segmentfault/answer/internal/service/follow"
"github.com/segmentfault/answer/pkg/obj"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/entity"
"github.com/segmentfault/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm"
)
// FollowRepo activity repository
type FollowRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
activityRepo activity_common.ActivityRepo
}
// NewFollowRepo new repository
func NewFollowRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
activityRepo activity_common.ActivityRepo,
) follow.FollowRepo {
return &FollowRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
activityRepo: activityRepo,
}
}
func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error {
activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(nil, objectId, "follow")
if err != nil {
return err
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
var (
existsActivity entity.Activity
has bool
)
result = nil
has, err = session.Where(builder.Eq{"activity_type": activityType}).
And(builder.Eq{"user_id": userId}).
And(builder.Eq{"object_id": objectId}).
Get(&existsActivity)
if err != nil {
return
}
if has && existsActivity.Cancelled == 0 {
return
}
if has {
_, err = session.Where(builder.Eq{"id": existsActivity.ID}).
Cols(`cancelled`).
Update(&entity.Activity{
Cancelled: 0,
})
} else {
// update existing activity with new user id and u object id
_, err = session.Insert(&entity.Activity{
UserID: userId,
ObjectID: objectId,
ActivityType: activityType,
Cancelled: 0,
Rank: 0,
HasRank: 0,
})
}
if err != nil {
log.Error(err)
return
}
// start update followers when everything is fine
err = ar.updateFollows(ctx, session, objectId, 1)
if err != nil {
log.Error(err)
}
return
})
return err
}
func (ar *FollowRepo) FollowCancel(ctx context.Context, objectId, userId string) error {
activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(nil, objectId, "follow")
if err != nil {
return err
}
_, err = ar.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
var (
existsActivity entity.Activity
has bool
)
result = nil
has, err = session.Where(builder.Eq{"activity_type": activityType}).
And(builder.Eq{"user_id": userId}).
And(builder.Eq{"object_id": objectId}).
Get(&existsActivity)
if err != nil || !has {
return
}
if has && existsActivity.Cancelled == 1 {
return
}
if _, err = session.Where("id = ?", existsActivity.ID).
Cols("cancelled").
Update(&entity.Activity{
Cancelled: 1,
}); err != nil {
return
}
err = ar.updateFollows(ctx, session, objectId, -1)
return
})
return err
}
func (ar *FollowRepo) updateFollows(ctx context.Context, session *xorm.Session, objectId string, follows int) error {
objectType, err := obj.GetObjectTypeStrByObjectID(objectId)
switch objectType {
case "question":
_, err = session.Where("id = ?", objectId).Incr("follow_count", follows).Update(&entity.Question{})
case "user":
_, err = session.Where("id = ?", objectId).Incr("follow_count", follows).Update(&entity.User{})
case "tag":
_, err = session.Where("id = ?", objectId).Incr("follow_count", follows).Update(&entity.Tag{})
default:
err = errors.InternalServer(reason.DisallowFollow).WithMsg("this object can't be followed")
}
return err
}

Some files were not shown because too many files have changed in this diff Show More