mirror of https://gitee.com/answerdev/answer.git
Merge branch 'release/0.3.0' into 'main'
Release/0.3.0 See merge request opensource/answer!227
This commit is contained in:
commit
2b1ad94791
|
@ -9,7 +9,7 @@
|
|||
/.fleet
|
||||
/.vscode/*.log
|
||||
/cmd/answer/*.sh
|
||||
/cmd/answer/upfiles/*
|
||||
/cmd/answer/uploads/*
|
||||
/cmd/logs
|
||||
/configs/config-dev.yaml
|
||||
/go.work*
|
||||
|
|
|
@ -29,7 +29,7 @@ RUN apk --no-cache add build-base git \
|
|||
&& make clean build \
|
||||
&& cp answer /usr/bin/answer
|
||||
|
||||
RUN mkdir -p /data/upfiles && chmod 777 /data/upfiles \
|
||||
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
|
||||
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
|
||||
|
||||
# stage3 copy the binary and resource files into fresh container
|
||||
|
|
|
@ -91,7 +91,7 @@ swaggerui:
|
|||
service_config:
|
||||
secret_key: "answer" #encryption key
|
||||
web_host: "http://127.0.0.1" #Page access using domain name address
|
||||
upload_path: "./upfiles" #upload directory
|
||||
upload_path: "./uploads" #upload directory
|
||||
```
|
||||
|
||||
## Compile the image
|
||||
|
@ -100,4 +100,4 @@ If you have modified the source files and want to repackage the image, you can u
|
|||
docker build -t answer:v1.0.0 .
|
||||
```
|
||||
## common problem
|
||||
1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`upfiles`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and upfiles directories.
|
||||
1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`uploads`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and uploads directories.
|
||||
|
|
108
INSTALL_CN.md
108
INSTALL_CN.md
|
@ -1,80 +1,54 @@
|
|||
# Answer 安装指引
|
||||
|
||||
安装 Answer 之前,您需要先安装基本环境。
|
||||
- 数据库
|
||||
- [MySQL](http://dev.mysql.com):版本 >= 5.7
|
||||
|
||||
然后,您可以通过以下几种方式来安装 Answer:
|
||||
|
||||
- 采用 Docker 部署
|
||||
- 二进制安装
|
||||
- 源码安装
|
||||
|
||||
## 使用 Docker-compose 安装 Answer
|
||||
## 使用 docker 安装
|
||||
### 步骤 1: 使用 docker 命令启动项目
|
||||
```bash
|
||||
$ mkdir answer && cd answer
|
||||
$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml
|
||||
$ docker-compose up
|
||||
docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
|
||||
```
|
||||
### 步骤 2: 访问安装路径进行项目安装
|
||||
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
|
||||
|
||||
选择语言后点击下一步选择合适的数据库,如果当前只是想体验,建议直接选择 sqlite 作为数据库,如下图所示
|
||||
|
||||

|
||||
|
||||
然后点击下一步会进行配置文件创建等操作,点击下一步输入网站基本信息和管理员信息,如下图所示
|
||||
|
||||

|
||||
|
||||
点击下一步即可安装完成
|
||||
|
||||
### 步骤 3:安装完成后访问项目路径开始使用
|
||||
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
|
||||
|
||||
使用刚才创建的管理员用户名密码即可登录。
|
||||
|
||||
## 使用 docker-compose 安装
|
||||
### 步骤 1: 使用 docker-compose 命令启动项目
|
||||
```bash
|
||||
mkdir answer && cd answer
|
||||
wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
启动完成后使用浏览器访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/).
|
||||
### 步骤 2: 访问安装路径进行项目安装
|
||||
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
|
||||
|
||||
你可以使用默认的用户名:( **`admin@admin.com`** ) 和密码:( **`admin`** ) 进行登录.
|
||||
具体配置与 docker 使用时相同
|
||||
|
||||
## 使用Docker 安装 Answer
|
||||
可以从 Docker Hub 或者 GitHub Container registry 下载最新的 tags 镜像
|
||||
### 步骤 3:安装完成后访问项目路径开始使用
|
||||
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
|
||||
|
||||
### 用法
|
||||
将配置和存储目录挂在到镜像之外 volume (/var/data -> /data),你可以修改外部挂载地址
|
||||
## 使用 二进制 安装
|
||||
### 步骤 1: 下载二进制文件
|
||||
[https://github.com/answerdev/answer/releases](https://github.com/answerdev/answer/releases)
|
||||
请下载您当下系统所需要的对应版本
|
||||
|
||||
```
|
||||
# 将镜像从 docker hub 拉到本地
|
||||
$ docker pull answerdev/answer:latest
|
||||
|
||||
# 创建一个挂载目录
|
||||
$ mkdir -p /var/data
|
||||
|
||||
# 先运行一遍镜像
|
||||
$ docker run --name=answer -p 9080:80 -v /var/data:/data answerdev/answer
|
||||
|
||||
# 第一次启动后会在/var/data 目录下生成配置文件
|
||||
# /var/data/conf/config.yaml
|
||||
# 需要修改配置文件中的Mysql 数据库地址
|
||||
vim /var/data/conf/config.yaml
|
||||
|
||||
# 修改数据库连接 connection: [username]:[password]@tcp([host]:[port])/[DbName]
|
||||
...
|
||||
|
||||
# 配置好配置文件后可以再次启动镜像即可启动服务
|
||||
$ docker start answer
|
||||
```
|
||||
|
||||
## 使用二进制 安装 Answer
|
||||
可以使用编译完成的各个平台的二进制文件运行 Answer 项目
|
||||
### 用法
|
||||
从 GitHub 最新版本的tag中下载对应平台的二进制文件压缩包
|
||||
|
||||
1. 解压压缩包
|
||||
2. 使用命令 cd 进入到刚刚创建的目录
|
||||
3. 执行命令 ./answer init
|
||||
4. Answer 会在当前目录生成 ./data 目录
|
||||
5. 进入 data 目录修改 config.yaml 文件
|
||||
6. 将数据库连接地址修改为你的数据库连接地址
|
||||
|
||||
connection: [username]:[password]@tcp([host]:[port])/[DbName]
|
||||
7. 退出 data 目录,执行 ./answer run -c ./data/conf/config.yaml
|
||||
|
||||
## 当前支持的命令
|
||||
用法: answer [command]
|
||||
|
||||
- help: 帮助
|
||||
- init: 初始化环境
|
||||
- run: 启动
|
||||
- check: 环境依赖检查
|
||||
- dump: 备份数据
|
||||
|
||||
## 配置文件 config.yaml 参数说明
|
||||
### 步骤 2: 使用命令行安装
|
||||
> 以下命令中 -C 指定的是 answer 所需的数据目录,您可以根据实际需要进行修改
|
||||
|
||||
```bash
|
||||
./answer init -C ./answer-data/
|
||||
```
|
||||
server:
|
||||
http:
|
||||
|
|
|
@ -4,14 +4,14 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/install"
|
||||
"github.com/answerdev/answer/internal/migrations"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// configFilePath is the config file path
|
||||
configFilePath string
|
||||
// dataDirPath save all answer application data in this directory. like config file, upload file...
|
||||
dataDirPath string
|
||||
// dumpDataPath dump data path
|
||||
|
@ -21,9 +21,7 @@ var (
|
|||
func init() {
|
||||
rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time)
|
||||
|
||||
initCmd.Flags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&configFilePath, "config", "c", "", "config path, eg: -c config.yaml")
|
||||
rootCmd.PersistentFlags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/")
|
||||
|
||||
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
|
||||
|
||||
|
@ -49,6 +47,9 @@ To run answer, use:
|
|||
Short: "Run the application",
|
||||
Long: `Run the application`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
fmt.Println("config file path: ", cli.GetConfigFilePath())
|
||||
fmt.Println("Answer is string..........................")
|
||||
runApp()
|
||||
},
|
||||
}
|
||||
|
@ -59,18 +60,27 @@ To run answer, use:
|
|||
Short: "init answer application",
|
||||
Long: `init answer application`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
// check config file and database. if config file exists and database is already created, init done
|
||||
cli.InstallAllInitialEnvironment(dataDirPath)
|
||||
c, err := readConfig()
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
|
||||
configFileExist := cli.CheckConfigFile(cli.GetConfigFilePath())
|
||||
if configFileExist {
|
||||
fmt.Println("config file exists, try to read the config...")
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("config file read successfully, try to connect database...")
|
||||
if cli.CheckDBTableExist(c.Data.Database) {
|
||||
fmt.Println("connect to database successfully and table already exists, do nothing.")
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Println("read config successfully")
|
||||
if err := migrations.InitDB(c.Data.Database); err != nil {
|
||||
fmt.Println("init database error: ", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println("init database successfully")
|
||||
|
||||
// start installation server to install
|
||||
install.Run(cli.GetConfigFilePath())
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -80,7 +90,8 @@ To run answer, use:
|
|||
Short: "upgrade Answer version",
|
||||
Long: `upgrade Answer version`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
c, err := readConfig()
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
|
@ -100,7 +111,8 @@ To run answer, use:
|
|||
Long: `back up data`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
fmt.Println("Answer is backing up data")
|
||||
c, err := readConfig()
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
|
@ -120,8 +132,9 @@ To run answer, use:
|
|||
Short: "checking the required environment",
|
||||
Long: `Check if the current environment meets the startup requirements`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
fmt.Println("Start checking the required environment...")
|
||||
if cli.CheckConfigFile(configFilePath) {
|
||||
if cli.CheckConfigFile(cli.GetConfigFilePath()) {
|
||||
fmt.Println("config file exists [✔]")
|
||||
} else {
|
||||
fmt.Println("config file not exists [x]")
|
||||
|
@ -133,13 +146,13 @@ To run answer, use:
|
|||
fmt.Println("upload directory not exists [x]")
|
||||
}
|
||||
|
||||
c, err := readConfig()
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if cli.CheckDB(c.Data.Database) {
|
||||
if cli.CheckDBConnection(c.Data.Database) {
|
||||
fmt.Println("db connection successfully [✔]")
|
||||
} else {
|
||||
fmt.Println("db connection failed [x]")
|
||||
|
|
|
@ -2,13 +2,14 @@ package main
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/gin-gonic/gin"
|
||||
"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"
|
||||
|
@ -40,8 +41,7 @@ func main() {
|
|||
func runApp() {
|
||||
log.SetLogger(zap.NewLogger(
|
||||
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
|
||||
|
||||
c, err := readConfig()
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -50,27 +50,15 @@ func runApp() {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
constant.Version = Version
|
||||
schema.AppStartTime = time.Now()
|
||||
|
||||
defer cleanup()
|
||||
if err := app.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func readConfig() (c *conf.AllConfig, err error) {
|
||||
if len(configFilePath) == 0 {
|
||||
configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName)
|
||||
}
|
||||
c = &conf.AllConfig{}
|
||||
config, err := viper.NewWithPath(configFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = config.Parse(&c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application {
|
||||
return pacman.NewApp(
|
||||
pacman.WithName(Name),
|
||||
|
|
|
@ -44,6 +44,7 @@ import (
|
|||
auth2 "github.com/answerdev/answer/internal/service/auth"
|
||||
"github.com/answerdev/answer/internal/service/collection_common"
|
||||
comment2 "github.com/answerdev/answer/internal/service/comment"
|
||||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
export2 "github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/follow"
|
||||
meta2 "github.com/answerdev/answer/internal/service/meta"
|
||||
|
@ -58,6 +59,8 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/report_handle_backyard"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/internal/service/service_config"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
tag2 "github.com/answerdev/answer/internal/service/tag"
|
||||
"github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
|
@ -76,7 +79,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
langController := controller.NewLangController(i18nTranslator)
|
||||
engine, err := data.NewDB(debug, dbConf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -90,6 +92,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
siteInfoRepo := site_info.NewSiteInfo(dataData)
|
||||
siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo)
|
||||
langController := controller.NewLangController(i18nTranslator, siteInfoCommonService)
|
||||
authRepo := auth.NewAuthRepo(dataData)
|
||||
authService := auth2.NewAuthService(authRepo)
|
||||
configRepo := config.NewConfigRepo(dataData)
|
||||
|
@ -99,12 +104,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
userRankRepo := rank.NewUserRankRepo(dataData, configRepo)
|
||||
userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo)
|
||||
emailRepo := export.NewEmailRepo(dataData)
|
||||
siteInfoRepo := site_info.NewSiteInfo(dataData)
|
||||
emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo)
|
||||
userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf)
|
||||
userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService)
|
||||
captchaRepo := captcha.NewCaptchaRepo(dataData)
|
||||
captchaService := action.NewCaptchaService(captchaRepo)
|
||||
uploaderService := uploader.NewUploaderService(serviceConf)
|
||||
uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
|
||||
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService)
|
||||
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
|
||||
commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo)
|
||||
|
@ -148,7 +152,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
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)
|
||||
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
|
||||
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
|
||||
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
|
||||
searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo)
|
||||
searchController := controller.NewSearchController(searchService)
|
||||
|
@ -166,14 +171,15 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
reasonService := reason2.NewReasonService(reasonRepo)
|
||||
reasonController := controller.NewReasonController(reasonService)
|
||||
themeController := controller_backyard.NewThemeController()
|
||||
siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService)
|
||||
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService)
|
||||
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
|
||||
siteinfoController := controller.NewSiteinfoController(siteInfoService)
|
||||
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
|
||||
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)
|
||||
dashboardController := controller.NewDashboardController(dashboardService)
|
||||
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController)
|
||||
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
|
||||
uiRouter := router.NewUIRouter()
|
||||
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)
|
||||
|
|
|
@ -17,4 +17,4 @@ swaggerui:
|
|||
service_config:
|
||||
secret_key: "answer"
|
||||
web_host: "http://127.0.0.1:9080"
|
||||
upload_path: "/data/upfiles"
|
||||
upload_path: "/data/uploads"
|
||||
|
|
103
docs/docs.go
103
docs/docs.go
|
@ -62,12 +62,6 @@ const docTemplate = `{
|
|||
"description": "answer id or question title",
|
||||
"name": "query",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "question id",
|
||||
"name": "question_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -119,6 +113,34 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/dashboard": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "DashboardInfo",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "DashboardInfo",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/language/options": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -487,14 +509,14 @@ const docTemplate = `{
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get siteinfo general",
|
||||
"description": "get site general information",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Get siteinfo general",
|
||||
"summary": "get site general information",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
@ -522,14 +544,14 @@ const docTemplate = `{
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get siteinfo interface",
|
||||
"description": "update site general information",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Get siteinfo interface",
|
||||
"summary": "update site general information",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "general",
|
||||
|
@ -558,25 +580,14 @@ const docTemplate = `{
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get siteinfo interface",
|
||||
"description": "get site interface",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Get siteinfo interface",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "general",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.AddCommentReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": "get site interface",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
@ -604,14 +615,14 @@ const docTemplate = `{
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get siteinfo interface",
|
||||
"description": "update site info interface",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Get siteinfo interface",
|
||||
"summary": "update site info interface",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "general",
|
||||
|
@ -2710,14 +2721,14 @@ const docTemplate = `{
|
|||
},
|
||||
"/answer/api/v1/siteinfo": {
|
||||
"get": {
|
||||
"description": "Get siteinfo",
|
||||
"description": "get site info",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"site"
|
||||
],
|
||||
"summary": "Get siteinfo",
|
||||
"summary": "get site info",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
@ -5281,11 +5292,17 @@ const docTemplate = `{
|
|||
"schema.SiteGeneralReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"contact_email",
|
||||
"description",
|
||||
"name",
|
||||
"short_description"
|
||||
"short_description",
|
||||
"site_url"
|
||||
],
|
||||
"properties": {
|
||||
"contact_email": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 2000
|
||||
|
@ -5297,17 +5314,27 @@ const docTemplate = `{
|
|||
"short_description": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"site_url": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.SiteGeneralResp": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"contact_email",
|
||||
"description",
|
||||
"name",
|
||||
"short_description"
|
||||
"short_description",
|
||||
"site_url"
|
||||
],
|
||||
"properties": {
|
||||
"contact_email": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 2000
|
||||
|
@ -5319,6 +5346,10 @@ const docTemplate = `{
|
|||
"short_description": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"site_url": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5326,7 +5357,8 @@ const docTemplate = `{
|
|||
"type": "object",
|
||||
"required": [
|
||||
"language",
|
||||
"theme"
|
||||
"theme",
|
||||
"time_zone"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
|
@ -5340,6 +5372,10 @@ const docTemplate = `{
|
|||
"theme": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"time_zone": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5347,7 +5383,8 @@ const docTemplate = `{
|
|||
"type": "object",
|
||||
"required": [
|
||||
"language",
|
||||
"theme"
|
||||
"theme",
|
||||
"time_zone"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
|
@ -5361,6 +5398,10 @@ const docTemplate = `{
|
|||
"theme": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"time_zone": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
|
@ -50,12 +50,6 @@
|
|||
"description": "answer id or question title",
|
||||
"name": "query",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "question id",
|
||||
"name": "question_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -107,6 +101,34 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/dashboard": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "DashboardInfo",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "DashboardInfo",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/language/options": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -475,14 +497,14 @@
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get siteinfo general",
|
||||
"description": "get site general information",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Get siteinfo general",
|
||||
"summary": "get site general information",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
@ -510,14 +532,14 @@
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get siteinfo interface",
|
||||
"description": "update site general information",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Get siteinfo interface",
|
||||
"summary": "update site general information",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "general",
|
||||
|
@ -546,25 +568,14 @@
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get siteinfo interface",
|
||||
"description": "get site interface",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Get siteinfo interface",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "general",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.AddCommentReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": "get site interface",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
@ -592,14 +603,14 @@
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Get siteinfo interface",
|
||||
"description": "update site info interface",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"admin"
|
||||
],
|
||||
"summary": "Get siteinfo interface",
|
||||
"summary": "update site info interface",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "general",
|
||||
|
@ -2698,14 +2709,14 @@
|
|||
},
|
||||
"/answer/api/v1/siteinfo": {
|
||||
"get": {
|
||||
"description": "Get siteinfo",
|
||||
"description": "get site info",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"site"
|
||||
],
|
||||
"summary": "Get siteinfo",
|
||||
"summary": "get site info",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
@ -5269,11 +5280,17 @@
|
|||
"schema.SiteGeneralReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"contact_email",
|
||||
"description",
|
||||
"name",
|
||||
"short_description"
|
||||
"short_description",
|
||||
"site_url"
|
||||
],
|
||||
"properties": {
|
||||
"contact_email": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 2000
|
||||
|
@ -5285,17 +5302,27 @@
|
|||
"short_description": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"site_url": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.SiteGeneralResp": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"contact_email",
|
||||
"description",
|
||||
"name",
|
||||
"short_description"
|
||||
"short_description",
|
||||
"site_url"
|
||||
],
|
||||
"properties": {
|
||||
"contact_email": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 2000
|
||||
|
@ -5307,6 +5334,10 @@
|
|||
"short_description": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"site_url": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5314,7 +5345,8 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"language",
|
||||
"theme"
|
||||
"theme",
|
||||
"time_zone"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
|
@ -5328,6 +5360,10 @@
|
|||
"theme": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"time_zone": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5335,7 +5371,8 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"language",
|
||||
"theme"
|
||||
"theme",
|
||||
"time_zone"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
|
@ -5349,6 +5386,10 @@
|
|||
"theme": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
},
|
||||
"time_zone": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -983,6 +983,9 @@ definitions:
|
|||
type: object
|
||||
schema.SiteGeneralReq:
|
||||
properties:
|
||||
contact_email:
|
||||
maxLength: 512
|
||||
type: string
|
||||
description:
|
||||
maxLength: 2000
|
||||
type: string
|
||||
|
@ -992,13 +995,21 @@ definitions:
|
|||
short_description:
|
||||
maxLength: 255
|
||||
type: string
|
||||
site_url:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- contact_email
|
||||
- description
|
||||
- name
|
||||
- short_description
|
||||
- site_url
|
||||
type: object
|
||||
schema.SiteGeneralResp:
|
||||
properties:
|
||||
contact_email:
|
||||
maxLength: 512
|
||||
type: string
|
||||
description:
|
||||
maxLength: 2000
|
||||
type: string
|
||||
|
@ -1008,10 +1019,15 @@ definitions:
|
|||
short_description:
|
||||
maxLength: 255
|
||||
type: string
|
||||
site_url:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- contact_email
|
||||
- description
|
||||
- name
|
||||
- short_description
|
||||
- site_url
|
||||
type: object
|
||||
schema.SiteInterfaceReq:
|
||||
properties:
|
||||
|
@ -1024,9 +1040,13 @@ definitions:
|
|||
theme:
|
||||
maxLength: 128
|
||||
type: string
|
||||
time_zone:
|
||||
maxLength: 128
|
||||
type: string
|
||||
required:
|
||||
- language
|
||||
- theme
|
||||
- time_zone
|
||||
type: object
|
||||
schema.SiteInterfaceResp:
|
||||
properties:
|
||||
|
@ -1039,9 +1059,13 @@ definitions:
|
|||
theme:
|
||||
maxLength: 128
|
||||
type: string
|
||||
time_zone:
|
||||
maxLength: 128
|
||||
type: string
|
||||
required:
|
||||
- language
|
||||
- theme
|
||||
- time_zone
|
||||
type: object
|
||||
schema.TagItem:
|
||||
properties:
|
||||
|
@ -1394,10 +1418,6 @@ paths:
|
|||
in: query
|
||||
name: query
|
||||
type: string
|
||||
- description: question id
|
||||
in: query
|
||||
name: question_id
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -1434,6 +1454,23 @@ paths:
|
|||
summary: AdminSetAnswerStatus
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/dashboard:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: DashboardInfo
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: DashboardInfo
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/language/options:
|
||||
get:
|
||||
description: Get language options
|
||||
|
@ -1662,7 +1699,7 @@ paths:
|
|||
- admin
|
||||
/answer/admin/api/siteinfo/general:
|
||||
get:
|
||||
description: Get siteinfo general
|
||||
description: get site general information
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -1677,11 +1714,11 @@ paths:
|
|||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get siteinfo general
|
||||
summary: get site general information
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
description: Get siteinfo interface
|
||||
description: update site general information
|
||||
parameters:
|
||||
- description: general
|
||||
in: body
|
||||
|
@ -1698,19 +1735,12 @@ paths:
|
|||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get siteinfo interface
|
||||
summary: update site general information
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/siteinfo/interface:
|
||||
get:
|
||||
description: Get siteinfo interface
|
||||
parameters:
|
||||
- description: general
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.AddCommentReq'
|
||||
description: get site interface
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -1725,11 +1755,11 @@ paths:
|
|||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get siteinfo interface
|
||||
summary: get site interface
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
description: Get siteinfo interface
|
||||
description: update site info interface
|
||||
parameters:
|
||||
- description: general
|
||||
in: body
|
||||
|
@ -1746,7 +1776,7 @@ paths:
|
|||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get siteinfo interface
|
||||
summary: update site info interface
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/theme/options:
|
||||
|
@ -3014,7 +3044,7 @@ paths:
|
|||
- Search
|
||||
/answer/api/v1/siteinfo:
|
||||
get:
|
||||
description: Get siteinfo
|
||||
description: get site info
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -3027,7 +3057,7 @@ paths:
|
|||
data:
|
||||
$ref: '#/definitions/schema.SiteGeneralResp'
|
||||
type: object
|
||||
summary: Get siteinfo
|
||||
summary: get site info
|
||||
tags:
|
||||
- site
|
||||
/answer/api/v1/tag:
|
||||
|
|
5
go.mod
5
go.mod
|
@ -15,6 +15,7 @@ require (
|
|||
github.com/goccy/go-json v0.9.11
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/wire v0.5.0
|
||||
github.com/grokify/html-strip-tags-go v0.0.1
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/jinzhu/now v1.1.5
|
||||
github.com/lib/pq v1.10.7
|
||||
|
@ -24,7 +25,7 @@ require (
|
|||
github.com/segmentfault/pacman v1.0.1
|
||||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632
|
||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/spf13/cobra v1.6.1
|
||||
|
@ -35,6 +36,7 @@ require (
|
|||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/net v0.1.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
xorm.io/builder v0.3.12
|
||||
xorm.io/core v0.7.3
|
||||
xorm.io/xorm v1.3.2
|
||||
|
@ -110,6 +112,5 @@ require (
|
|||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // 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
|
||||
)
|
||||
|
|
6
go.sum
6
go.sum
|
@ -299,6 +299,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
|
|||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0=
|
||||
github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
|
@ -594,8 +596,8 @@ github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1
|
|||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk=
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8=
|
||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc=
|
||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk=
|
||||
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A=
|
||||
|
|
1268
i18n/en_US.yaml
1268
i18n/en_US.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
|||
# all support language
|
||||
language_options:
|
||||
- label: "简体中文(CN)"
|
||||
value: "zh_CN"
|
||||
- label: "English(US)"
|
||||
value: "en_US"
|
312
i18n/it_IT.yaml
312
i18n/it_IT.yaml
|
@ -1,170 +1,172 @@
|
|||
base:
|
||||
success:
|
||||
other: "Successo"
|
||||
unknown:
|
||||
other: "Errore sconosciuto"
|
||||
request_format_error:
|
||||
other: "Il formato della richiesta non è valido"
|
||||
unauthorized_error:
|
||||
other: "Non autorizzato"
|
||||
database_error:
|
||||
other: "Errore server dati"
|
||||
# The following fields are used for back-end
|
||||
backend:
|
||||
base:
|
||||
success:
|
||||
other: "Successo"
|
||||
unknown:
|
||||
other: "Errore sconosciuto"
|
||||
request_format_error:
|
||||
other: "Il formato della richiesta non è valido"
|
||||
unauthorized_error:
|
||||
other: "Non autorizzato"
|
||||
database_error:
|
||||
other: "Errore server dati"
|
||||
|
||||
email:
|
||||
other: "email"
|
||||
password:
|
||||
other: "password"
|
||||
|
||||
email_or_password_wrong_error: &email_or_password_wrong
|
||||
other: "Email o password errati"
|
||||
|
||||
error:
|
||||
admin:
|
||||
email_or_password_wrong: *email_or_password_wrong
|
||||
answer:
|
||||
not_found:
|
||||
other: "Risposta non trovata"
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: "Non si hanno di privilegi sufficienti per modificare il commento"
|
||||
not_found:
|
||||
other: "Commento non trovato"
|
||||
email:
|
||||
duplicate:
|
||||
other: "email già esistente"
|
||||
need_to_be_verified:
|
||||
other: "email deve essere verificata"
|
||||
verify_url_expired:
|
||||
other: "l'url di verifica email è scaduto, si prega di reinviare la email"
|
||||
lang:
|
||||
not_found:
|
||||
other: "lingua non trovata"
|
||||
object:
|
||||
captcha_verification_failed:
|
||||
other: "captcha errato"
|
||||
disallow_follow:
|
||||
other: "Non sei autorizzato a seguire"
|
||||
disallow_vote:
|
||||
other: "non sei autorizzato a votare"
|
||||
disallow_vote_your_self:
|
||||
other: "Non puoi votare un tuo post!"
|
||||
not_found:
|
||||
other: "oggetto non trovato"
|
||||
verification_failed:
|
||||
other: "verifica fallita"
|
||||
email_or_password_incorrect:
|
||||
other: "email o password incorretti"
|
||||
old_password_verification_failed:
|
||||
other: "la verifica della vecchia password è fallita"
|
||||
new_password_same_as_previous_setting:
|
||||
other: "La nuova password è identica alla precedente"
|
||||
question:
|
||||
not_found:
|
||||
other: "domanda non trovata"
|
||||
rank:
|
||||
fail_to_meet_the_condition:
|
||||
other: "Condizioni non valide per il grado"
|
||||
other: "email"
|
||||
password:
|
||||
other: "password"
|
||||
|
||||
email_or_password_wrong_error: &email_or_password_wrong
|
||||
other: "Email o password errati"
|
||||
|
||||
error:
|
||||
admin:
|
||||
email_or_password_wrong: *email_or_password_wrong
|
||||
answer:
|
||||
not_found:
|
||||
other: "Risposta non trovata"
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: "Non si hanno di privilegi sufficienti per modificare il commento"
|
||||
not_found:
|
||||
other: "Commento non trovato"
|
||||
email:
|
||||
duplicate:
|
||||
other: "email già esistente"
|
||||
need_to_be_verified:
|
||||
other: "email deve essere verificata"
|
||||
verify_url_expired:
|
||||
other: "l'url di verifica email è scaduto, si prega di reinviare la email"
|
||||
lang:
|
||||
not_found:
|
||||
other: "lingua non trovata"
|
||||
object:
|
||||
captcha_verification_failed:
|
||||
other: "captcha errato"
|
||||
disallow_follow:
|
||||
other: "Non sei autorizzato a seguire"
|
||||
disallow_vote:
|
||||
other: "non sei autorizzato a votare"
|
||||
disallow_vote_your_self:
|
||||
other: "Non puoi votare un tuo post!"
|
||||
not_found:
|
||||
other: "oggetto non trovato"
|
||||
verification_failed:
|
||||
other: "verifica fallita"
|
||||
email_or_password_incorrect:
|
||||
other: "email o password incorretti"
|
||||
old_password_verification_failed:
|
||||
other: "la verifica della vecchia password è fallita"
|
||||
new_password_same_as_previous_setting:
|
||||
other: "La nuova password è identica alla precedente"
|
||||
question:
|
||||
not_found:
|
||||
other: "domanda non trovata"
|
||||
rank:
|
||||
fail_to_meet_the_condition:
|
||||
other: "Condizioni non valide per il grado"
|
||||
report:
|
||||
handle_failed:
|
||||
other: "Gestione del report fallita"
|
||||
not_found:
|
||||
other: "Report non trovato"
|
||||
tag:
|
||||
not_found:
|
||||
other: "Etichetta non trovata"
|
||||
theme:
|
||||
not_found:
|
||||
other: "tema non trovato"
|
||||
user:
|
||||
email_or_password_wrong:
|
||||
other: *email_or_password_wrong
|
||||
not_found:
|
||||
other: "utente non trovato"
|
||||
suspended:
|
||||
other: "utente sospeso"
|
||||
username_invalid:
|
||||
other: "utente non valido"
|
||||
username_duplicate:
|
||||
other: "utente già in uso"
|
||||
|
||||
report:
|
||||
handle_failed:
|
||||
other: "Gestione del report fallita"
|
||||
not_found:
|
||||
other: "Report non trovato"
|
||||
tag:
|
||||
not_found:
|
||||
other: "Etichetta non trovata"
|
||||
theme:
|
||||
not_found:
|
||||
other: "tema non trovato"
|
||||
user:
|
||||
email_or_password_wrong:
|
||||
other: *email_or_password_wrong
|
||||
not_found:
|
||||
other: "utente non trovato"
|
||||
suspended:
|
||||
other: "utente sospeso"
|
||||
username_invalid:
|
||||
other: "utente non valido"
|
||||
username_duplicate:
|
||||
other: "utente già in uso"
|
||||
|
||||
report:
|
||||
spam:
|
||||
name:
|
||||
other: "spam"
|
||||
description:
|
||||
other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente"
|
||||
rude:
|
||||
name:
|
||||
other: "scortese o violento"
|
||||
description:
|
||||
other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso"
|
||||
duplicate:
|
||||
name:
|
||||
other: "duplicato"
|
||||
description:
|
||||
other: "Questa domanda è già stata posta e ha già una risposta."
|
||||
not_answer:
|
||||
name:
|
||||
other: "non è una risposta"
|
||||
description:
|
||||
other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto."
|
||||
not_need:
|
||||
name:
|
||||
other: "non più necessario"
|
||||
description:
|
||||
other: "Questo commento è datato, conversazionale o non rilevante a questo articolo."
|
||||
other:
|
||||
name:
|
||||
other: "altro"
|
||||
description:
|
||||
other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra."
|
||||
|
||||
question:
|
||||
close:
|
||||
duplicate:
|
||||
spam:
|
||||
name:
|
||||
other: "spam"
|
||||
description:
|
||||
other: "Questa domanda è già stata chiesta o ha già una risposta."
|
||||
guideline:
|
||||
other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente"
|
||||
rude:
|
||||
name:
|
||||
other: "motivo legato alla community"
|
||||
other: "scortese o violento"
|
||||
description:
|
||||
other: "Questa domanda non soddisfa le linee guida della comunità."
|
||||
multiple:
|
||||
other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso"
|
||||
duplicate:
|
||||
name:
|
||||
other: "richiede maggiori dettagli o chiarezza"
|
||||
other: "duplicato"
|
||||
description:
|
||||
other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema."
|
||||
other: "Questa domanda è già stata posta e ha già una risposta."
|
||||
not_answer:
|
||||
name:
|
||||
other: "non è una risposta"
|
||||
description:
|
||||
other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto."
|
||||
not_need:
|
||||
name:
|
||||
other: "non più necessario"
|
||||
description:
|
||||
other: "Questo commento è datato, conversazionale o non rilevante a questo articolo."
|
||||
other:
|
||||
name:
|
||||
other: "altro"
|
||||
description:
|
||||
other: "Questo articolo richiede un'altro motivo non listato sopra."
|
||||
other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra."
|
||||
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
other: "domanda aggiornata"
|
||||
answer_the_question:
|
||||
other: "domanda risposta"
|
||||
update_answer:
|
||||
other: "risposta aggiornata"
|
||||
adopt_answer:
|
||||
other: "risposta accettata"
|
||||
comment_question:
|
||||
other: "domanda commentata"
|
||||
comment_answer:
|
||||
other: "risposta commentata"
|
||||
reply_to_you:
|
||||
other: "hai ricevuto risposta"
|
||||
mention_you:
|
||||
other: "sei stato menzionato"
|
||||
your_question_is_closed:
|
||||
other: "la tua domanda è stata chiusa"
|
||||
your_question_was_deleted:
|
||||
other: "la tua domanda è stata rimossa"
|
||||
your_answer_was_deleted:
|
||||
other: "la tua risposta è stata rimossa"
|
||||
your_comment_was_deleted:
|
||||
other: "il tuo commento è stato rimosso"
|
||||
question:
|
||||
close:
|
||||
duplicate:
|
||||
name:
|
||||
other: "spam"
|
||||
description:
|
||||
other: "Questa domanda è già stata chiesta o ha già una risposta."
|
||||
guideline:
|
||||
name:
|
||||
other: "motivo legato alla community"
|
||||
description:
|
||||
other: "Questa domanda non soddisfa le linee guida della comunità."
|
||||
multiple:
|
||||
name:
|
||||
other: "richiede maggiori dettagli o chiarezza"
|
||||
description:
|
||||
other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema."
|
||||
other:
|
||||
name:
|
||||
other: "altro"
|
||||
description:
|
||||
other: "Questo articolo richiede un'altro motivo non listato sopra."
|
||||
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
other: "domanda aggiornata"
|
||||
answer_the_question:
|
||||
other: "domanda risposta"
|
||||
update_answer:
|
||||
other: "risposta aggiornata"
|
||||
adopt_answer:
|
||||
other: "risposta accettata"
|
||||
comment_question:
|
||||
other: "domanda commentata"
|
||||
comment_answer:
|
||||
other: "risposta commentata"
|
||||
reply_to_you:
|
||||
other: "hai ricevuto risposta"
|
||||
mention_you:
|
||||
other: "sei stato menzionato"
|
||||
your_question_is_closed:
|
||||
other: "la tua domanda è stata chiusa"
|
||||
your_question_was_deleted:
|
||||
other: "la tua domanda è stata rimossa"
|
||||
your_answer_was_deleted:
|
||||
other: "la tua risposta è stata rimossa"
|
||||
your_comment_was_deleted:
|
||||
other: "il tuo commento è stato rimosso"
|
||||
|
|
1061
i18n/zh_CN.yaml
1061
i18n/zh_CN.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,30 +1,64 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/server"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/router"
|
||||
"github.com/answerdev/answer/internal/service/service_config"
|
||||
"github.com/answerdev/answer/pkg/writer"
|
||||
"github.com/segmentfault/pacman/contrib/conf/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Debug bool `json:"debug" mapstructure:"debug" yaml:"debug"`
|
||||
Server *Server `json:"server" mapstructure:"server" yaml:"server"`
|
||||
Data *Data `json:"data" mapstructure:"data" yaml:"data"`
|
||||
I18n *translator.I18n `json:"i18n" mapstructure:"i18n" yaml:"i18n"`
|
||||
ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config" yaml:"service_config"`
|
||||
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui" yaml:"swaggerui"`
|
||||
}
|
||||
|
||||
// Server server config
|
||||
type Server struct {
|
||||
HTTP *server.HTTP `json:"http" mapstructure:"http"`
|
||||
HTTP *server.HTTP `json:"http" mapstructure:"http" yaml:"http"`
|
||||
}
|
||||
|
||||
// Data data config
|
||||
type Data struct {
|
||||
Database *data.Database `json:"database" mapstructure:"database"`
|
||||
Cache *data.CacheConf `json:"cache" mapstructure:"cache"`
|
||||
Database *data.Database `json:"database" mapstructure:"database" yaml:"database"`
|
||||
Cache *data.CacheConf `json:"cache" mapstructure:"cache" yaml:"cache"`
|
||||
}
|
||||
|
||||
// ReadConfig read config
|
||||
func ReadConfig(configFilePath string) (c *AllConfig, err error) {
|
||||
if len(configFilePath) == 0 {
|
||||
configFilePath = filepath.Join(cli.ConfigFileDir, cli.DefaultConfigFileName)
|
||||
}
|
||||
c = &AllConfig{}
|
||||
config, err := viper.NewWithPath(configFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = config.Parse(&c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// RewriteConfig rewrite config file path
|
||||
func RewriteConfig(configFilePath string, allConfig *AllConfig) error {
|
||||
buf := bytes.Buffer{}
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
if err := enc.Encode(allConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
return writer.ReplaceFile(configFilePath, buf.String())
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ const (
|
|||
// object TagID AnswerList
|
||||
// key equal database's table name
|
||||
var (
|
||||
Version string = ""
|
||||
|
||||
ObjectTypeStrMapping = map[string]int{
|
||||
QuestionObjectType: 1,
|
||||
AnswerObjectType: 2,
|
||||
|
@ -47,3 +49,8 @@ var (
|
|||
8: ReportObjectType,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
SiteTypeGeneral = "general"
|
||||
SiteTypeInterface = "interface"
|
||||
)
|
||||
|
|
|
@ -2,14 +2,14 @@ package data
|
|||
|
||||
// Database database config
|
||||
type Database struct {
|
||||
Driver string `json:"driver" mapstructure:"driver"`
|
||||
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"`
|
||||
Driver string `json:"driver" mapstructure:"driver" yaml:"driver"`
|
||||
Connection string `json:"connection" mapstructure:"connection" yaml:"connection"`
|
||||
ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time" yaml:"conn_max_life_time,omitempty"`
|
||||
MaxOpenConn int `json:"max_open_conn" mapstructure:"max_open_conn" yaml:"max_open_conn,omitempty"`
|
||||
MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn" yaml:"max_idle_conn,omitempty"`
|
||||
}
|
||||
|
||||
// CacheConf cache
|
||||
type CacheConf struct {
|
||||
FilePath string `json:"file_path" mapstructure:"file_path"`
|
||||
FilePath string `json:"file_path" mapstructure:"file_path" yaml:"file_path"`
|
||||
}
|
||||
|
|
|
@ -38,4 +38,9 @@ const (
|
|||
LangNotFound = "error.lang.not_found"
|
||||
ReportHandleFailed = "error.report.handle_failed"
|
||||
ReportNotFound = "error.report.not_found"
|
||||
ReadConfigFailed = "error.config.read_config_failed"
|
||||
DatabaseConnectionFailed = "error.database.connection_failed"
|
||||
InstallCreateTableFailed = "error.database.create_table_failed"
|
||||
InstallConfigFailed = "error.install.create_config_failed"
|
||||
SiteInfoNotFound = "error.site_info.not_found"
|
||||
)
|
||||
|
|
|
@ -2,5 +2,5 @@ package translator
|
|||
|
||||
// I18n i18n config
|
||||
type I18n struct {
|
||||
BundleDir string `json:"bundle_dir" mapstructure:"bundle_dir"`
|
||||
BundleDir string `json:"bundle_dir" mapstructure:"bundle_dir" yaml:"bundle_dir"`
|
||||
}
|
||||
|
|
|
@ -1,17 +1,100 @@
|
|||
package translator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/wire"
|
||||
myTran "github.com/segmentfault/pacman/contrib/i18n"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ProviderSet is providers.
|
||||
var ProviderSet = wire.NewSet(NewTranslator)
|
||||
var GlobalTrans i18n.Translator
|
||||
|
||||
// LangOption language option
|
||||
type LangOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// DefaultLangOption default language option. If user config the language is default, the language option is admin choose.
|
||||
const DefaultLangOption = "Default"
|
||||
|
||||
var (
|
||||
// LanguageOptions language
|
||||
LanguageOptions []*LangOption
|
||||
)
|
||||
|
||||
// NewTranslator new a translator
|
||||
func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
|
||||
GlobalTrans, err = myTran.NewTranslator(c.BundleDir)
|
||||
entries, err := os.ReadDir(c.BundleDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read the Bundle resources file from entries
|
||||
for _, file := range entries {
|
||||
// ignore directory
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
// ignore non-YAML file
|
||||
if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" {
|
||||
continue
|
||||
}
|
||||
buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err)
|
||||
}
|
||||
|
||||
// only parse the backend translation
|
||||
translation := struct {
|
||||
Content map[string]interface{} `yaml:"backend"`
|
||||
}{}
|
||||
if err = yaml.Unmarshal(buf, &translation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := yaml.Marshal(translation.Content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err)
|
||||
}
|
||||
|
||||
// add translator use backend translation
|
||||
if err = myTran.AddTranslator(content, file.Name()); err != nil {
|
||||
return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err)
|
||||
}
|
||||
}
|
||||
GlobalTrans = myTran.GlobalTrans
|
||||
|
||||
i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read i18n file failed: %s", err)
|
||||
}
|
||||
|
||||
s := struct {
|
||||
LangOption []*LangOption `yaml:"language_options"`
|
||||
}{}
|
||||
err = yaml.Unmarshal(i18nFile, &s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("i18n file parsing failed: %s", err)
|
||||
}
|
||||
LanguageOptions = s.LangOption
|
||||
return GlobalTrans, err
|
||||
}
|
||||
|
||||
// CheckLanguageIsValid check user input language is valid
|
||||
func CheckLanguageIsValid(lang string) bool {
|
||||
if lang == DefaultLangOption {
|
||||
return true
|
||||
}
|
||||
for _, option := range LanguageOptions {
|
||||
if option.Value == lang {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package validator
|
|||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
|
@ -97,9 +98,19 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
|
|||
|
||||
for _, fieldError := range valErrors {
|
||||
errField = &ErrorField{
|
||||
Key: translator.GlobalTrans.Tr(m.Lang, fieldError.Field()),
|
||||
Key: fieldError.Field(),
|
||||
Value: fieldError.Translate(m.Tran),
|
||||
}
|
||||
|
||||
// get original tag name from value for set err field key.
|
||||
structNamespace := fieldError.StructNamespace()
|
||||
_, fieldName, found := strings.Cut(structNamespace, ".")
|
||||
if found {
|
||||
originalTag := getObjectTagByFieldName(value, fieldName)
|
||||
if len(originalTag) > 0 {
|
||||
errField.Key = originalTag
|
||||
}
|
||||
}
|
||||
return errField, myErrors.BadRequest(reason.RequestFormatError).WithMsg(fieldError.Translate(m.Tran))
|
||||
}
|
||||
}
|
||||
|
@ -117,3 +128,24 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
|
|||
type Checker interface {
|
||||
Check() (errField *ErrorField, err error)
|
||||
}
|
||||
|
||||
func getObjectTagByFieldName(obj interface{}, fieldName string) (tag string) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
objT := reflect.TypeOf(obj)
|
||||
objT = objT.Elem()
|
||||
|
||||
structField, exists := objT.FieldByName(fieldName)
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
tag = structField.Tag.Get("json")
|
||||
if len(tag) == 0 {
|
||||
return structField.Tag.Get("form")
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
|
|
@ -1,59 +1,71 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/answerdev/answer/configs"
|
||||
"github.com/answerdev/answer/i18n"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
"github.com/answerdev/answer/pkg/writer"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultConfigFileName = "config.yaml"
|
||||
DefaultCacheFileName = "cache.db"
|
||||
)
|
||||
|
||||
var (
|
||||
ConfigFilePath = "/conf/"
|
||||
UploadFilePath = "/upfiles/"
|
||||
ConfigFileDir = "/conf/"
|
||||
UploadFilePath = "/uploads/"
|
||||
I18nPath = "/i18n/"
|
||||
CacheDir = "/cache/"
|
||||
)
|
||||
|
||||
// GetConfigFilePath get config file path
|
||||
func GetConfigFilePath() string {
|
||||
return filepath.Join(ConfigFileDir, DefaultConfigFileName)
|
||||
}
|
||||
|
||||
func FormatAllPath(dataDirPath string) {
|
||||
ConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir)
|
||||
UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
|
||||
I18nPath = filepath.Join(dataDirPath, I18nPath)
|
||||
CacheDir = filepath.Join(dataDirPath, CacheDir)
|
||||
}
|
||||
|
||||
// InstallAllInitialEnvironment install all initial environment
|
||||
func InstallAllInitialEnvironment(dataDirPath string) {
|
||||
ConfigFilePath = filepath.Join(dataDirPath, ConfigFilePath)
|
||||
UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
|
||||
I18nPath = filepath.Join(dataDirPath, I18nPath)
|
||||
|
||||
installConfigFile()
|
||||
FormatAllPath(dataDirPath)
|
||||
installUploadDir()
|
||||
installI18nBundle()
|
||||
fmt.Println("install all initial environment done")
|
||||
}
|
||||
|
||||
func installConfigFile() {
|
||||
fmt.Println("[config-file] try to install...")
|
||||
defaultConfigFile := filepath.Join(ConfigFilePath, DefaultConfigFileName)
|
||||
func InstallConfigFile(configFilePath string) error {
|
||||
if len(configFilePath) == 0 {
|
||||
configFilePath = filepath.Join(ConfigFileDir, DefaultConfigFileName)
|
||||
}
|
||||
fmt.Println("[config-file] try to create at ", configFilePath)
|
||||
|
||||
// if config file already exists do nothing.
|
||||
if CheckConfigFile(defaultConfigFile) {
|
||||
fmt.Printf("[config-file] %s already exists\n", defaultConfigFile)
|
||||
return
|
||||
if CheckConfigFile(configFilePath) {
|
||||
fmt.Printf("[config-file] %s already exists\n", configFilePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := dir.CreateDirIfNotExist(ConfigFilePath); err != nil {
|
||||
if err := dir.CreateDirIfNotExist(ConfigFileDir); err != nil {
|
||||
fmt.Printf("[config-file] create directory fail %s\n", err.Error())
|
||||
return
|
||||
return fmt.Errorf("create directory fail %s", err.Error())
|
||||
}
|
||||
fmt.Printf("[config-file] create directory success, config file is %s\n", defaultConfigFile)
|
||||
fmt.Printf("[config-file] create directory success, config file is %s\n", configFilePath)
|
||||
|
||||
if err := writerFile(defaultConfigFile, string(configs.Config)); err != nil {
|
||||
if err := writer.WriteFile(configFilePath, string(configs.Config)); err != nil {
|
||||
fmt.Printf("[config-file] install fail %s\n", err.Error())
|
||||
return
|
||||
return fmt.Errorf("write file failed %s", err)
|
||||
}
|
||||
fmt.Printf("[config-file] install success\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func installUploadDir() {
|
||||
|
@ -85,7 +97,7 @@ func installI18nBundle() {
|
|||
continue
|
||||
}
|
||||
fmt.Printf("[i18n] install %s bundle...\n", item.Name())
|
||||
err = writerFile(path, string(content))
|
||||
err = writer.WriteFile(path, string(content))
|
||||
if err != nil {
|
||||
fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error())
|
||||
} else {
|
||||
|
@ -93,21 +105,3 @@ func installI18nBundle() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writerFile(filePath, content string) error {
|
||||
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
writer := bufio.NewWriter(file)
|
||||
if _, err := writer.WriteString(content); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
)
|
||||
|
||||
|
@ -13,12 +16,40 @@ func CheckUploadDir() bool {
|
|||
return dir.CheckDirExist(UploadFilePath)
|
||||
}
|
||||
|
||||
func CheckDB(dataConf *data.Database) bool {
|
||||
// CheckDBConnection check database whether the connection is normal
|
||||
func CheckDBConnection(dataConf *data.Database) bool {
|
||||
db, err := data.NewDB(false, dataConf)
|
||||
if err != nil {
|
||||
fmt.Printf("connection database failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
fmt.Printf("connection ping database failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// CheckDBTableExist check database whether the table is already exists
|
||||
func CheckDBTableExist(dataConf *data.Database) bool {
|
||||
db, err := data.NewDB(false, dataConf)
|
||||
if err != nil {
|
||||
fmt.Printf("connection database failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
fmt.Printf("connection ping database failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
exist, err := db.IsTableExist(&entity.Version{})
|
||||
if err != nil {
|
||||
fmt.Printf("check table exist failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
if !exist {
|
||||
fmt.Printf("check table not exist\n")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
@ -16,13 +17,21 @@ import (
|
|||
|
||||
// AnswerController answer controller
|
||||
type AnswerController struct {
|
||||
answerService *service.AnswerService
|
||||
rankService *rank.RankService
|
||||
answerService *service.AnswerService
|
||||
rankService *rank.RankService
|
||||
dashboardService *dashboard.DashboardService
|
||||
}
|
||||
|
||||
// NewAnswerController new controller
|
||||
func NewAnswerController(answerService *service.AnswerService, rankService *rank.RankService) *AnswerController {
|
||||
return &AnswerController{answerService: answerService, rankService: rankService}
|
||||
func NewAnswerController(answerService *service.AnswerService,
|
||||
rankService *rank.RankService,
|
||||
dashboardService *dashboard.DashboardService,
|
||||
) *AnswerController {
|
||||
return &AnswerController{
|
||||
answerService: answerService,
|
||||
rankService: rankService,
|
||||
dashboardService: dashboardService,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAnswer delete answer
|
||||
|
|
|
@ -20,4 +20,5 @@ var ProviderSetController = wire.NewSet(
|
|||
NewReasonController,
|
||||
NewNotificationController,
|
||||
NewSiteinfoController,
|
||||
NewDashboardController,
|
||||
)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
package controller
|
|
@ -0,0 +1,36 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DashboardController struct {
|
||||
dashboardService *dashboard.DashboardService
|
||||
}
|
||||
|
||||
// NewDashboardController new controller
|
||||
func NewDashboardController(
|
||||
dashboardService *dashboard.DashboardService,
|
||||
) *DashboardController {
|
||||
return &DashboardController{
|
||||
dashboardService: dashboardService,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardInfo godoc
|
||||
// @Summary DashboardInfo
|
||||
// @Description DashboardInfo
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /answer/admin/api/dashboard [get]
|
||||
// @Success 200 {object} handler.RespBody
|
||||
func (ac *DashboardController) DashboardInfo(ctx *gin.Context) {
|
||||
info, err := ac.dashboardService.StatisticalByCache(ctx)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"info": info,
|
||||
})
|
||||
}
|
|
@ -4,18 +4,20 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
)
|
||||
|
||||
type LangController struct {
|
||||
translator i18n.Translator
|
||||
translator i18n.Translator
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewLangController new language controller.
|
||||
func NewLangController(tr i18n.Translator) *LangController {
|
||||
return &LangController{translator: tr}
|
||||
func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController {
|
||||
return &LangController{translator: tr, siteInfoService: siteInfoService}
|
||||
}
|
||||
|
||||
// GetLangMapping get language config mapping
|
||||
|
@ -33,15 +35,38 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// GetLangOptions Get language options
|
||||
// GetAdminLangOptions 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)
|
||||
func (u *LangController) GetAdminLangOptions(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
|
||||
}
|
||||
|
||||
// GetUserLangOptions Get language options
|
||||
// @Summary Get language options
|
||||
// @Description Get language options
|
||||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/language/options [get]
|
||||
func (u *LangController) GetUserLangOptions(ctx *gin.Context) {
|
||||
siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
options := translator.LanguageOptions
|
||||
if len(siteInterfaceResp.Language) > 0 {
|
||||
defaultOption := []*translator.LangOption{
|
||||
{Label: translator.DefaultLangOption, Value: translator.DefaultLangOption},
|
||||
}
|
||||
options = append(defaultOption, options...)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, options)
|
||||
}
|
||||
|
|
|
@ -3,45 +3,36 @@ package controller
|
|||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SiteinfoController struct {
|
||||
siteInfoService *service.SiteInfoService
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewSiteinfoController new siteinfo controller.
|
||||
func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController {
|
||||
func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController {
|
||||
return &SiteinfoController{
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo godoc
|
||||
// @Summary Get siteinfo
|
||||
// @Description Get siteinfo
|
||||
// GetSiteInfo get site info
|
||||
// @Summary get site info
|
||||
// @Description get site info
|
||||
// @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
|
||||
func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
|
||||
var err error
|
||||
resp := &schema.SiteInfoResp{}
|
||||
resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
return
|
||||
}
|
||||
|
||||
face, err = sc.siteInfoService.GetSiteInterface(ctx)
|
||||
resp.Face = &face
|
||||
|
||||
resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
|
|
@ -89,22 +89,6 @@ func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetUserStatus get user status info
|
||||
// @Summary get user status info
|
||||
// @Description get user status info
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
|
||||
// @Router /answer/api/v1/user/status [get]
|
||||
func (uc *UserController) GetUserStatus(ctx *gin.Context) {
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
token := middleware.ExtractToken(ctx)
|
||||
resp, err := uc.userService.GetUserStatus(ctx, userID, token)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserEmailLogin godoc
|
||||
// @Summary UserEmailLogin
|
||||
// @Description UserEmailLogin
|
||||
|
@ -373,6 +357,27 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UserUpdateInterface update user interface config
|
||||
// @Summary UserUpdateInterface update user interface config
|
||||
// @Description UserUpdateInterface update user interface config
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param Authorization header string true "access-token"
|
||||
// @Param data body schema.UpdateUserInterfaceRequest true "UpdateInfoRequest"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/user/interface [put]
|
||||
func (uc *UserController) UserUpdateInterface(ctx *gin.Context) {
|
||||
req := &schema.UpdateUserInterfaceRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := uc.userService.UserUpdateInterface(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UploadUserAvatar godoc
|
||||
// @Summary UserUpdateInfo
|
||||
// @Description UserUpdateInfo
|
||||
|
@ -490,6 +495,10 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
|
|||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
// If the user is not logged in, the api cannot be used.
|
||||
// If the user email is not verified, that also can use this api to modify the email.
|
||||
if len(req.UserID) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
|
||||
return
|
||||
}
|
||||
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
|
@ -501,13 +510,15 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.UserID) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
|
||||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
|
||||
resp, err := uc.userService.UserChangeEmailSendCode(ctx, req)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
return
|
||||
}
|
||||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
|
||||
err := uc.userService.UserChangeEmailSendCode(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,24 +3,24 @@ package controller_backyard
|
|||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SiteInfoController struct {
|
||||
siteInfoService *service.SiteInfoService
|
||||
siteInfoService *siteinfo.SiteInfoService
|
||||
}
|
||||
|
||||
// NewSiteInfoController new siteinfo controller.
|
||||
func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController {
|
||||
func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController {
|
||||
return &SiteInfoController{
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetGeneral godoc
|
||||
// @Summary Get siteinfo general
|
||||
// @Description Get siteinfo general
|
||||
// GetGeneral get site general information
|
||||
// @Summary get site general information
|
||||
// @Description get site general information
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
|
@ -31,23 +31,22 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetInterface godoc
|
||||
// @Summary Get siteinfo interface
|
||||
// @Description Get siteinfo interface
|
||||
// GetInterface get site interface
|
||||
// @Summary get site interface
|
||||
// @Description get site 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
|
||||
// UpdateGeneral update site general information
|
||||
// @Summary update site general information
|
||||
// @Description update site general information
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
|
@ -63,9 +62,9 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UpdateInterface godoc
|
||||
// @Summary Get siteinfo interface
|
||||
// @Description Get siteinfo interface
|
||||
// UpdateInterface update site interface
|
||||
// @Summary update site info interface
|
||||
// @Description update site info interface
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
|
|
|
@ -45,6 +45,7 @@ type User struct {
|
|||
Location string `xorm:"not null default '' VARCHAR(100) location"`
|
||||
IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"`
|
||||
IsAdmin bool `xorm:"not null default false BOOL is_admin"`
|
||||
Language string `xorm:"not null default '' VARCHAR(100) language"`
|
||||
}
|
||||
|
||||
// TableName user table name
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/configs"
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/migrations"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// LangOptions get installation language options
|
||||
// @Summary get installation language options
|
||||
// @Description get installation language options
|
||||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]*translator.LangOption}
|
||||
// @Router /installation/language/options [get]
|
||||
func LangOptions(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
|
||||
}
|
||||
|
||||
// CheckConfigFile check config file if exist when installation
|
||||
// @Summary check config file if exist when installation
|
||||
// @Description check config file if exist when installation
|
||||
// @Tags installation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
|
||||
// @Router /installation/config-file/check [post]
|
||||
func CheckConfigFile(ctx *gin.Context) {
|
||||
resp := &CheckConfigFileResp{}
|
||||
resp.ConfigFileExist = cli.CheckConfigFile(confPath)
|
||||
if !resp.ConfigFileExist {
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
return
|
||||
}
|
||||
allConfig, err := conf.ReadConfig(confPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
err = errors.BadRequest(reason.ReadConfigFailed)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
resp.DBConnectionSuccess = cli.CheckDBConnection(allConfig.Data.Database)
|
||||
if resp.DBConnectionSuccess {
|
||||
resp.DbTableExist = cli.CheckDBTableExist(allConfig.Data.Database)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// CheckDatabase check database if exist when installation
|
||||
// @Summary check database if exist when installation
|
||||
// @Description check database if exist when installation
|
||||
// @Tags installation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq"
|
||||
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
|
||||
// @Router /installation/db/check [post]
|
||||
func CheckDatabase(ctx *gin.Context) {
|
||||
req := &CheckDatabaseReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := &CheckDatabaseResp{}
|
||||
dataConf := &data.Database{
|
||||
Driver: req.DbType,
|
||||
Connection: req.GetConnection(),
|
||||
}
|
||||
resp.ConnectionSuccess = cli.CheckDBConnection(dataConf)
|
||||
if !resp.ConnectionSuccess {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// InitEnvironment init environment
|
||||
// @Summary init environment
|
||||
// @Description init environment
|
||||
// @Tags installation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq"
|
||||
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
|
||||
// @Router /installation/init [post]
|
||||
func InitEnvironment(ctx *gin.Context) {
|
||||
req := &CheckDatabaseReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
// check config file if exist
|
||||
if cli.CheckConfigFile(confPath) {
|
||||
log.Debug("config file already exists")
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := cli.InstallConfigFile(confPath); err != nil {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{
|
||||
Success: false,
|
||||
CreateConfigFailed: true,
|
||||
DefaultConfig: string(configs.Config),
|
||||
ErrType: schema.ErrTypeAlert.ErrType,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c, err := conf.ReadConfig(confPath)
|
||||
if err != nil {
|
||||
log.Errorf("read config failed %s", err)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
|
||||
return
|
||||
}
|
||||
c.Data.Database.Driver = req.DbType
|
||||
c.Data.Database.Connection = req.GetConnection()
|
||||
c.Data.Cache.FilePath = filepath.Join(cli.CacheDir, cli.DefaultCacheFileName)
|
||||
c.I18n.BundleDir = cli.I18nPath
|
||||
c.ServiceConfig.UploadPath = cli.UploadFilePath
|
||||
|
||||
if err := conf.RewriteConfig(confPath, c); err != nil {
|
||||
log.Errorf("rewrite config failed %s", err)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
}
|
||||
|
||||
// InitBaseInfo init base info
|
||||
// @Summary init base info
|
||||
// @Description init base info
|
||||
// @Tags installation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body install.InitBaseInfoReq true "InitBaseInfoReq"
|
||||
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
|
||||
// @Router /installation/base-info [post]
|
||||
func InitBaseInfo(ctx *gin.Context) {
|
||||
req := &InitBaseInfoReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.FormatSiteUrl()
|
||||
|
||||
c, err := conf.ReadConfig(confPath)
|
||||
if err != nil {
|
||||
log.Errorf("read config failed %s", err)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if cli.CheckDBTableExist(c.Data.Database) {
|
||||
log.Warnf("database is already initialized")
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := migrations.InitDB(c.Data.Database); err != nil {
|
||||
log.Error("init database error: ", err.Error())
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), schema.ErrTypeAlert)
|
||||
return
|
||||
}
|
||||
|
||||
err = migrations.UpdateInstallInfo(c.Data.Database, req.Language, req.SiteName, req.SiteURL, req.ContactEmail,
|
||||
req.AdminName, req.AdminPassword, req.AdminEmail)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), nil)
|
||||
return
|
||||
}
|
||||
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
os.Exit(0)
|
||||
}()
|
||||
return
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
port = os.Getenv("INSTALL_PORT")
|
||||
confPath = ""
|
||||
)
|
||||
|
||||
func Run(configPath string) {
|
||||
confPath = configPath
|
||||
// initialize translator for return internationalization error when installing.
|
||||
_, err := translator.NewTranslator(&translator.I18n{BundleDir: cli.I18nPath})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
installServer := NewInstallHTTPServer()
|
||||
if len(port) == 0 {
|
||||
port = "80"
|
||||
}
|
||||
fmt.Printf("[SUCCESS] answer installation service will run at: http://localhost:%s/install/ \n", port)
|
||||
if err = installServer.Run(":" + port); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// CheckConfigFileResp check config file if exist or not response
|
||||
type CheckConfigFileResp struct {
|
||||
ConfigFileExist bool `json:"config_file_exist"`
|
||||
DBConnectionSuccess bool `json:"db_connection_success"`
|
||||
DbTableExist bool `json:"db_table_exist"`
|
||||
}
|
||||
|
||||
// CheckDatabaseReq check database
|
||||
type CheckDatabaseReq struct {
|
||||
DbType string `validate:"required,oneof=postgres sqlite3 mysql" json:"db_type"`
|
||||
DbUsername string `json:"db_username"`
|
||||
DbPassword string `json:"db_password"`
|
||||
DbHost string `json:"db_host"`
|
||||
DbName string `json:"db_name"`
|
||||
DbFile string `json:"db_file"`
|
||||
}
|
||||
|
||||
// GetConnection get connection string
|
||||
func (r *CheckDatabaseReq) GetConnection() string {
|
||||
if r.DbType == string(schemas.SQLITE) {
|
||||
return r.DbFile
|
||||
}
|
||||
if r.DbType == string(schemas.MYSQL) {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s)/%s",
|
||||
r.DbUsername, r.DbPassword, r.DbHost, r.DbName)
|
||||
}
|
||||
if r.DbType == string(schemas.POSTGRES) {
|
||||
host, port := parsePgSQLHostPort(r.DbHost)
|
||||
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
host, port, r.DbUsername, r.DbPassword, r.DbName)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parsePgSQLHostPort(dbHost string) (host string, port string) {
|
||||
if strings.Contains(dbHost, ":") {
|
||||
idx := strings.LastIndex(dbHost, ":")
|
||||
host, port = dbHost[:idx], dbHost[idx+1:]
|
||||
} else if len(dbHost) > 0 {
|
||||
host = dbHost
|
||||
}
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
if port == "" {
|
||||
port = "5432"
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
// CheckDatabaseResp check database response
|
||||
type CheckDatabaseResp struct {
|
||||
ConnectionSuccess bool `json:"connection_success"`
|
||||
}
|
||||
|
||||
// InitEnvironmentResp init environment response
|
||||
type InitEnvironmentResp struct {
|
||||
Success bool `json:"success"`
|
||||
CreateConfigFailed bool `json:"create_config_failed"`
|
||||
DefaultConfig string `json:"default_config"`
|
||||
ErrType string `json:"err_type"`
|
||||
}
|
||||
|
||||
// InitBaseInfoReq init base info request
|
||||
type InitBaseInfoReq struct {
|
||||
Language string `validate:"required,gt=0,lte=30" json:"lang"`
|
||||
SiteName string `validate:"required,gt=0,lte=30" json:"site_name"`
|
||||
SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"`
|
||||
ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"`
|
||||
AdminName string `validate:"required,gt=4,lte=30" json:"name"`
|
||||
AdminPassword string `validate:"required,gte=8,lte=32" json:"password"`
|
||||
AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"`
|
||||
}
|
||||
|
||||
func (r *InitBaseInfoReq) FormatSiteUrl() {
|
||||
parsedUrl, err := url.Parse(r.SiteURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r.SiteURL = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package install
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/answerdev/answer/ui"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
const UIStaticPath = "build/static"
|
||||
|
||||
type _resource struct {
|
||||
fs embed.FS
|
||||
}
|
||||
|
||||
// Open to implement the interface by http.FS required
|
||||
func (r *_resource) Open(name string) (fs.File, error) {
|
||||
name = fmt.Sprintf(UIStaticPath+"/%s", name)
|
||||
log.Debugf("open static path %s", name)
|
||||
return r.fs.Open(name)
|
||||
}
|
||||
|
||||
// NewInstallHTTPServer new install http server.
|
||||
func NewInstallHTTPServer() *gin.Engine {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
|
||||
r.StaticFS("/static", http.FS(&_resource{
|
||||
fs: ui.Build,
|
||||
}))
|
||||
|
||||
installApi := r.Group("")
|
||||
installApi.GET("/install", WebPage)
|
||||
installApi.GET("/50x", WebPage)
|
||||
installApi.GET("/installation/language/options", LangOptions)
|
||||
installApi.POST("/installation/db/check", CheckDatabase)
|
||||
installApi.POST("/installation/config-file/check", CheckConfigFile)
|
||||
installApi.POST("/installation/init", InitEnvironment)
|
||||
installApi.POST("/installation/base-info", InitBaseInfo)
|
||||
|
||||
r.NoRoute(func(ctx *gin.Context) {
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func WebPage(c *gin.Context) {
|
||||
filePath := ""
|
||||
var file []byte
|
||||
var err error
|
||||
filePath = "build/index.html"
|
||||
c.Header("content-type", "text/html;charset=utf-8")
|
||||
file, err = ui.Build.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(file))
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
|
@ -55,11 +57,6 @@ func InitDB(dataConf *data.Database) (err error) {
|
|||
return fmt.Errorf("init admin user failed: %s", err)
|
||||
}
|
||||
|
||||
err = initSiteInfo(engine)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init site info failed: %s", err)
|
||||
}
|
||||
|
||||
err = initConfigTable(engine)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init config table: %s", err)
|
||||
|
@ -82,12 +79,79 @@ func initAdminUser(engine *xorm.Engine) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func initSiteInfo(engine *xorm.Engine) error {
|
||||
func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail string) error {
|
||||
interfaceData := map[string]string{
|
||||
"logo": "",
|
||||
"theme": "black",
|
||||
"language": language,
|
||||
}
|
||||
interfaceDataBytes, _ := json.Marshal(interfaceData)
|
||||
_, err := engine.InsertOne(&entity.SiteInfo{
|
||||
Type: "interface",
|
||||
Content: `{"logo":"","theme":"black","language":"en_US"}`,
|
||||
Content: string(interfaceDataBytes),
|
||||
Status: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
generalData := map[string]string{
|
||||
"name": siteName,
|
||||
"site_url": siteURL,
|
||||
"contact_email": contactEmail,
|
||||
}
|
||||
generalDataBytes, _ := json.Marshal(generalData)
|
||||
_, err = engine.InsertOne(&entity.SiteInfo{
|
||||
Type: "general",
|
||||
Content: string(generalDataBytes),
|
||||
Status: 1,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func updateAdminInfo(engine *xorm.Engine, adminName, adminPassword, adminEmail string) error {
|
||||
generateFromPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("")
|
||||
}
|
||||
adminPassword = string(generateFromPassword)
|
||||
|
||||
// update admin info
|
||||
_, err = engine.ID("1").Update(&entity.User{
|
||||
Username: adminName,
|
||||
Pass: adminPassword,
|
||||
EMail: adminEmail,
|
||||
DisplayName: adminName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update admin user info failed: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateInstallInfo update some init data about the admin interface and admin password
|
||||
func UpdateInstallInfo(dataConf *data.Database, language string,
|
||||
siteName string,
|
||||
siteURL string,
|
||||
contactEmail string,
|
||||
adminName string,
|
||||
adminPassword string,
|
||||
adminEmail string) error {
|
||||
|
||||
engine, err := data.NewDB(false, dataConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database connection error: %s", err)
|
||||
}
|
||||
|
||||
err = updateAdminInfo(engine, adminName, adminPassword, adminEmail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update admin info failed: %s", err)
|
||||
}
|
||||
|
||||
err = initSiteInfo(engine, language, siteName, siteURL, contactEmail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init site info failed: %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -125,7 +189,7 @@ func initConfigTable(engine *xorm.Engine) error {
|
|||
{ID: 30, Key: "answer.vote_up", Value: `0`},
|
||||
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
|
||||
{ID: 32, Key: "question.follow", Value: `0`},
|
||||
{ID: 33, Key: "email.config", Value: `{"from_name":"answer","from_email":"answer@answer.com","smtp_host":"smtp.answer.org","smtp_port":465,"smtp_password":"answer","smtp_username":"answer@answer.com","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`},
|
||||
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`},
|
||||
{ID: 35, Key: "tag.follow", Value: `0`},
|
||||
{ID: 36, Key: "rank.question.add", Value: `0`},
|
||||
{ID: 37, Key: "rank.question.edit", Value: `0`},
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
|
@ -43,6 +42,7 @@ var noopMigration = func(_ *xorm.Engine) error { return nil }
|
|||
var migrations = []Migration{
|
||||
// 0->1
|
||||
NewMigration("this is first version, no operation", noopMigration),
|
||||
NewMigration("add user language", addUserLanguage),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
@ -86,17 +86,17 @@ func Migrate(dataConf *data.Database) error {
|
|||
expectedVersion := ExpectedVersion()
|
||||
|
||||
for currentDBVersion < expectedVersion {
|
||||
log.Infof("[migrate] current db version is %d, try to migrate version %d, latest version is %d",
|
||||
fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n",
|
||||
currentDBVersion, currentDBVersion+1, expectedVersion)
|
||||
migrationFunc := migrations[currentDBVersion]
|
||||
log.Infof("[migrate] try to migrate db version %d, description: %s", currentDBVersion+1, migrationFunc.Description())
|
||||
fmt.Printf("[migrate] try to migrate db version %d, description: %s\n", currentDBVersion+1, migrationFunc.Description())
|
||||
if err := migrationFunc.Migrate(engine); err != nil {
|
||||
log.Errorf("[migrate] migrate to db version %d failed: ", currentDBVersion+1, err.Error())
|
||||
fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error())
|
||||
return err
|
||||
}
|
||||
log.Infof("[migrate] migrate to db version %d success", currentDBVersion+1)
|
||||
fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1)
|
||||
if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil {
|
||||
log.Errorf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
|
||||
fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
|
||||
return err
|
||||
}
|
||||
currentDBVersion++
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addUserLanguage(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
Language string `xorm:"not null default '' VARCHAR(100) language"`
|
||||
}
|
||||
return x.Sync(new(User))
|
||||
}
|
|
@ -4,8 +4,10 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// VoteRepo activity repository
|
||||
|
@ -39,3 +41,12 @@ func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string)
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (vr *VoteRepo) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) {
|
||||
list := make([]*entity.Activity, 0)
|
||||
count, err = vr.data.DB.Where("cancelled =0").In("activity_type", activityTypes).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
|
@ -102,6 +103,16 @@ func (ar *answerRepo) GetAnswer(ctx context.Context, id string) (
|
|||
return
|
||||
}
|
||||
|
||||
// GetQuestionCount
|
||||
func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) {
|
||||
list := make([]*entity.Answer, 0)
|
||||
count, err = ar.data.DB.Where("status = ?", entity.AnswerStatusAvailable).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAnswerList get answer list all
|
||||
func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) {
|
||||
answerList = make([]*entity.Answer, 0)
|
||||
|
|
|
@ -79,6 +79,15 @@ func (cr *commentRepo) GetComment(ctx context.Context, commentID string) (
|
|||
return
|
||||
}
|
||||
|
||||
func (cr *commentRepo) GetCommentCount(ctx context.Context) (count int64, err error) {
|
||||
list := make([]*entity.Comment, 0)
|
||||
count, err = cr.data.DB.Where("status = ?", entity.CommentStatusAvailable).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetCommentPage get comment page
|
||||
func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) (
|
||||
commentList []*entity.Comment, total int64, err error,
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
|
@ -162,6 +163,16 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu
|
|||
return
|
||||
}
|
||||
|
||||
func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) {
|
||||
questionList := make([]*entity.Question, 0)
|
||||
|
||||
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}).FindAndCount(&questionList)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetQuestionPage get question page
|
||||
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) {
|
||||
questionList = make([]*entity.Question, 0)
|
||||
|
|
|
@ -94,3 +94,12 @@ func (ar *reportRepo) UpdateByID(
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (vr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) {
|
||||
list := make([]*entity.Report, 0)
|
||||
count, err = vr.data.DB.Where("status =?", entity.ReportStatusPending).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package search_common
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -25,7 +26,7 @@ var (
|
|||
"`question`.`id`",
|
||||
"`question`.`id` as `question_id`",
|
||||
"`title`",
|
||||
"`original_text`",
|
||||
"`parsed_text`",
|
||||
"`question`.`created_at`",
|
||||
"`user_id`",
|
||||
"`vote_count`",
|
||||
|
@ -38,7 +39,7 @@ var (
|
|||
"`answer`.`id` as `id`",
|
||||
"`question_id`",
|
||||
"`question`.`title` as `title`",
|
||||
"`answer`.`original_text` as `original_text`",
|
||||
"`answer`.`parsed_text` as `parsed_text`",
|
||||
"`answer`.`created_at`",
|
||||
"`answer`.`user_id` as `user_id`",
|
||||
"`answer`.`vote_count` as `vote_count`",
|
||||
|
@ -142,13 +143,22 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
|
|||
argsA = append(argsA, votes)
|
||||
}
|
||||
|
||||
b = b.Union("all", ub)
|
||||
|
||||
querySQL, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
|
||||
//b = b.Union("all", ub)
|
||||
ubSQL, _, err := ub.ToSQL()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
|
||||
bSQL, _, err := b.ToSQL()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sql := fmt.Sprintf("(%s UNION ALL %s)", ubSQL, bSQL)
|
||||
|
||||
querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -412,7 +422,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
|
|||
object = schema.SearchObject{
|
||||
ID: string(r["id"]),
|
||||
Title: string(r["title"]),
|
||||
Excerpt: cutOutParsedText(string(r["original_text"])),
|
||||
Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240),
|
||||
CreatedAtParsed: tp.Unix(),
|
||||
UserInfo: userInfo,
|
||||
Tags: tags,
|
||||
|
@ -443,15 +453,6 @@ func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.Us
|
|||
}
|
||||
}
|
||||
|
||||
func cutOutParsedText(parsedText string) string {
|
||||
parsedText = strings.TrimSpace(parsedText)
|
||||
idx := strings.Index(parsedText, "\n")
|
||||
if idx >= 0 {
|
||||
parsedText = parsedText[0:idx]
|
||||
}
|
||||
return parsedText
|
||||
}
|
||||
|
||||
func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) {
|
||||
relevanceRes := []string{}
|
||||
args = []interface{}{}
|
||||
|
|
|
@ -101,6 +101,14 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err
|
|||
return
|
||||
}
|
||||
|
||||
func (ur *userRepo) UpdateLanguage(ctx context.Context, userID, language string) (err error) {
|
||||
_, err = ur.data.DB.Where("id = ?", userID).Update(&entity.User{Language: language})
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateInfo update user info
|
||||
func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {
|
||||
_, err = ur.data.DB.Where("id = ?", userInfo.ID).
|
||||
|
@ -149,3 +157,12 @@ func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *ent
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (vr *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
|
||||
list := make([]*entity.User, 0)
|
||||
count, err = vr.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ type AnswerAPIRouter struct {
|
|||
siteInfoController *controller_backyard.SiteInfoController
|
||||
siteinfoController *controller.SiteinfoController
|
||||
notificationController *controller.NotificationController
|
||||
dashboardController *controller.DashboardController
|
||||
}
|
||||
|
||||
func NewAnswerAPIRouter(
|
||||
|
@ -50,6 +51,7 @@ func NewAnswerAPIRouter(
|
|||
siteInfoController *controller_backyard.SiteInfoController,
|
||||
siteinfoController *controller.SiteinfoController,
|
||||
notificationController *controller.NotificationController,
|
||||
dashboardController *controller.DashboardController,
|
||||
|
||||
) *AnswerAPIRouter {
|
||||
return &AnswerAPIRouter{
|
||||
|
@ -73,13 +75,14 @@ func NewAnswerAPIRouter(
|
|||
siteInfoController: siteInfoController,
|
||||
notificationController: notificationController,
|
||||
siteinfoController: siteinfoController,
|
||||
dashboardController: dashboardController,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
// i18n
|
||||
r.GET("/language/config", a.langController.GetLangMapping)
|
||||
r.GET("/language/options", a.langController.GetLangOptions)
|
||||
r.GET("/language/options", a.langController.GetUserLangOptions)
|
||||
|
||||
// comment
|
||||
r.GET("/comment/page", a.commentController.GetCommentWithPage)
|
||||
|
@ -88,7 +91,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
// user
|
||||
r.GET("/user/info", a.userController.GetUserInfoByUserID)
|
||||
r.GET("/user/status", a.userController.GetUserStatus)
|
||||
r.GET("/user/action/record", a.userController.ActionRecord)
|
||||
r.POST("/user/login/email", a.userController.UserEmailLogin)
|
||||
r.POST("/user/register/email", a.userController.UserRegisterByEmail)
|
||||
|
@ -131,7 +133,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage)
|
||||
|
||||
//siteinfo
|
||||
r.GET("/siteinfo", a.siteinfoController.GetInfo)
|
||||
r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
|
||||
}
|
||||
|
||||
func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
|
@ -177,6 +179,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
// user
|
||||
r.PUT("/user/password", a.userController.UserModifyPassWord)
|
||||
r.PUT("/user/info", a.userController.UserUpdateInfo)
|
||||
r.PUT("/user/interface", a.userController.UserUpdateInterface)
|
||||
r.POST("/user/avatar/upload", a.userController.UploadUserAvatar)
|
||||
r.POST("/user/post/file", a.userController.UploadUserPostFile)
|
||||
r.POST("/user/notice/set", a.userController.UserNoticeSet)
|
||||
|
@ -213,7 +216,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
|
|||
r.GET("/reasons", a.reasonController.Reasons)
|
||||
|
||||
// language
|
||||
r.GET("/language/options", a.langController.GetLangOptions)
|
||||
r.GET("/language/options", a.langController.GetAdminLangOptions)
|
||||
|
||||
// theme
|
||||
r.GET("/theme/options", a.themeController.GetThemeOptions)
|
||||
|
@ -225,4 +228,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
|
|||
r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
|
||||
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
|
||||
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
|
||||
|
||||
//dashboard
|
||||
r.GET("/dashboard", a.dashboardController.DashboardInfo)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package router
|
|||
|
||||
// SwaggerConfig struct describes configure for the Swagger API endpoint
|
||||
type SwaggerConfig struct {
|
||||
Show bool `json:"show"`
|
||||
Protocol string `json:"protocol"`
|
||||
Host string `json:"host"`
|
||||
Address string `json:"address"`
|
||||
Show bool `json:"show" mapstructure:"show" yaml:"show"`
|
||||
Protocol string `json:"protocol" mapstructure:"protocol" yaml:"protocol"`
|
||||
Host string `json:"host" mapstructure:"host" yaml:"host"`
|
||||
Address string `json:"address" mapstructure:"address" yaml:"address"`
|
||||
}
|
||||
|
|
|
@ -78,6 +78,9 @@ func (a *UIRouter) Register(r *gin.Engine) {
|
|||
filePath = UIRootFilePath + name
|
||||
case "/manifest.json":
|
||||
filePath = UIRootFilePath + name
|
||||
case "/install":
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
default:
|
||||
filePath = UIIndexFilePath
|
||||
c.Header("content-type", "text/html;charset=utf-8")
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package schema
|
||||
|
||||
import "time"
|
||||
|
||||
var AppStartTime time.Time
|
||||
|
||||
const (
|
||||
DashBoardCachekey = "answer@dashboard"
|
||||
DashBoardCacheTime = 60 * time.Minute
|
||||
)
|
||||
|
||||
type DashboardInfo struct {
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
AnswerCount int64 `json:"answer_count"`
|
||||
CommentCount int64 `json:"comment_count"`
|
||||
VoteCount int64 `json:"vote_count"`
|
||||
UserCount int64 `json:"user_count"`
|
||||
ReportCount int64 `json:"report_count"`
|
||||
UploadingFiles bool `json:"uploading_files"`
|
||||
SMTP bool `json:"smtp"`
|
||||
HTTPS bool `json:"https"`
|
||||
TimeZone string `json:"time_zone"`
|
||||
OccupyingStorageSpace string `json:"occupying_storage_space"`
|
||||
AppStartTime string `json:"app_start_time"`
|
||||
VersionInfo DashboardInfoVersion `json:"version_info"`
|
||||
}
|
||||
|
||||
type DashboardInfoVersion struct {
|
||||
Version string `json:"version"`
|
||||
RemoteVersion string `json:"remote_version"`
|
||||
}
|
||||
|
||||
type RemoteVersion struct {
|
||||
Release struct {
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
} `json:"release"`
|
||||
}
|
|
@ -7,3 +7,5 @@ type ErrTypeData struct {
|
|||
var ErrTypeModal = ErrTypeData{ErrType: "modal"}
|
||||
|
||||
var ErrTypeToast = ErrTypeData{ErrType: "toast"}
|
||||
|
||||
var ErrTypeAlert = ErrTypeData{ErrType: "alert"}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
package schema
|
||||
|
||||
// GetLangOption get label option
|
||||
type GetLangOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
var GetLangOptions = []*GetLangOption{
|
||||
{
|
||||
Label: "English(US)",
|
||||
Value: "en_US",
|
||||
},
|
||||
{
|
||||
Label: "中文(CN)",
|
||||
Value: "zh_CN",
|
||||
},
|
||||
}
|
|
@ -1,17 +1,33 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// SiteGeneralReq site general request
|
||||
type SiteGeneralReq struct {
|
||||
Name string `validate:"required,gt=1,lte=128" comment:"site name" form:"name" json:"name"`
|
||||
ShortDescription string `validate:"required,gt=3,lte=255" comment:"short site description" form:"short_description" json:"short_description"`
|
||||
Description string `validate:"required,gt=3,lte=2000" comment:"site description" form:"description" json:"description"`
|
||||
Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"`
|
||||
ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"`
|
||||
Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"`
|
||||
SiteUrl string `validate:"required,gt=1,lte=512,url" form:"site_url" json:"site_url"`
|
||||
ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"`
|
||||
}
|
||||
|
||||
func (r *SiteGeneralReq) FormatSiteUrl() {
|
||||
parsedUrl, err := url.Parse(r.SiteUrl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r.SiteUrl = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host)
|
||||
}
|
||||
|
||||
// SiteInterfaceReq site interface request
|
||||
type SiteInterfaceReq struct {
|
||||
Logo string `validate:"omitempty,gt=0,lte=256" comment:"logo" form:"logo" json:"logo"`
|
||||
Theme string `validate:"required,gt=1,lte=128" comment:"theme" form:"theme" json:"theme"`
|
||||
Language string `validate:"required,gt=1,lte=128" comment:"interface language" form:"language" json:"language"`
|
||||
Logo string `validate:"omitempty,gt=0,lte=256" form:"logo" json:"logo"`
|
||||
Theme string `validate:"required,gt=1,lte=128" form:"theme" json:"theme"`
|
||||
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
|
||||
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
|
||||
}
|
||||
|
||||
// SiteGeneralResp site general response
|
||||
|
|
|
@ -62,6 +62,8 @@ type GetUserResp struct {
|
|||
Location string `json:"location"`
|
||||
// ip info
|
||||
IPInfo string `json:"ip_info"`
|
||||
// language
|
||||
Language string `json:"language"`
|
||||
// access token
|
||||
AccessToken string `json:"access_token"`
|
||||
// is admin
|
||||
|
@ -305,6 +307,14 @@ func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error)
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// UpdateUserInterfaceRequest update user interface request
|
||||
type UpdateUserInterfaceRequest struct {
|
||||
// language
|
||||
Language string `validate:"required,gt=1,lte=100" json:"language"`
|
||||
// user id
|
||||
UserId string `json:"-" `
|
||||
}
|
||||
|
||||
type UserRetrievePassWordRequest struct {
|
||||
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail
|
||||
CaptchaID string `json:"captcha_id" ` // captcha_id
|
||||
|
|
|
@ -7,4 +7,5 @@ import (
|
|||
// VoteRepo activity repository
|
||||
type VoteRepo interface {
|
||||
GetVoteStatus(ctx context.Context, objectId, userId string) (status string)
|
||||
GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
)
|
||||
|
||||
type AnswerRepo interface {
|
||||
|
@ -20,6 +21,7 @@ type AnswerRepo interface {
|
|||
SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error)
|
||||
CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error)
|
||||
UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error)
|
||||
GetAnswerCount(ctx context.Context) (count int64, err error)
|
||||
}
|
||||
|
||||
// AnswerCommon user service
|
||||
|
@ -74,11 +76,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer
|
|||
info := schema.AdminAnswerInfo{}
|
||||
info.ID = data.ID
|
||||
info.QuestionID = data.QuestionID
|
||||
info.Description = data.ParsedText
|
||||
info.Adopted = data.Adopted
|
||||
info.VoteCount = data.VoteCount
|
||||
info.CreateTime = data.CreatedAt.Unix()
|
||||
info.UpdateTime = data.UpdatedAt.Unix()
|
||||
info.UserID = data.UserID
|
||||
info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240)
|
||||
return &info
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
// CommentCommonRepo comment repository
|
||||
type CommentCommonRepo interface {
|
||||
GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)
|
||||
GetCommentCount(ctx context.Context) (count int64, err error)
|
||||
}
|
||||
|
||||
// CommentCommonService user service
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
answercommon "github.com/answerdev/answer/internal/service/answer_common"
|
||||
"github.com/answerdev/answer/internal/service/comment_common"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
questioncommon "github.com/answerdev/answer/internal/service/question_common"
|
||||
"github.com/answerdev/answer/internal/service/report_common"
|
||||
"github.com/answerdev/answer/internal/service/service_config"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
type DashboardService struct {
|
||||
questionRepo questioncommon.QuestionRepo
|
||||
answerRepo answercommon.AnswerRepo
|
||||
commentRepo comment_common.CommentCommonRepo
|
||||
voteRepo activity_common.VoteRepo
|
||||
userRepo usercommon.UserRepo
|
||||
reportRepo report_common.ReportRepo
|
||||
configRepo config.ConfigRepo
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
serviceConfig *service_config.ServiceConfig
|
||||
|
||||
data *data.Data
|
||||
}
|
||||
|
||||
func NewDashboardService(
|
||||
questionRepo questioncommon.QuestionRepo,
|
||||
answerRepo answercommon.AnswerRepo,
|
||||
commentRepo comment_common.CommentCommonRepo,
|
||||
voteRepo activity_common.VoteRepo,
|
||||
userRepo usercommon.UserRepo,
|
||||
reportRepo report_common.ReportRepo,
|
||||
configRepo config.ConfigRepo,
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService,
|
||||
serviceConfig *service_config.ServiceConfig,
|
||||
|
||||
data *data.Data,
|
||||
) *DashboardService {
|
||||
return &DashboardService{
|
||||
questionRepo: questionRepo,
|
||||
answerRepo: answerRepo,
|
||||
commentRepo: commentRepo,
|
||||
voteRepo: voteRepo,
|
||||
userRepo: userRepo,
|
||||
reportRepo: reportRepo,
|
||||
configRepo: configRepo,
|
||||
siteInfoService: siteInfoService,
|
||||
serviceConfig: serviceConfig,
|
||||
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.DashboardInfo, error) {
|
||||
dashboardInfo := &schema.DashboardInfo{}
|
||||
infoStr, err := ds.data.Cache.GetString(ctx, schema.DashBoardCachekey)
|
||||
if err != nil {
|
||||
info, statisticalErr := ds.Statistical(ctx)
|
||||
if statisticalErr != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
setCacheErr := ds.SetCache(ctx, info)
|
||||
if setCacheErr != nil {
|
||||
log.Error("ds.SetCache", setCacheErr)
|
||||
}
|
||||
return info, err
|
||||
}
|
||||
err = json.Unmarshal([]byte(infoStr), dashboardInfo)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
|
||||
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
|
||||
return dashboardInfo, nil
|
||||
}
|
||||
|
||||
func (ds *DashboardService) SetCache(ctx context.Context, info *schema.DashboardInfo) error {
|
||||
infoStr, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
err = ds.data.Cache.SetString(ctx, schema.DashBoardCachekey, string(infoStr), schema.DashBoardCacheTime)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Statistical
|
||||
func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) {
|
||||
dashboardInfo := &schema.DashboardInfo{}
|
||||
questionCount, err := ds.questionRepo.GetQuestionCount(ctx)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
answerCount, err := ds.answerRepo.GetAnswerCount(ctx)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
commentCount, err := ds.commentRepo.GetCommentCount(ctx)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
|
||||
typeKeys := []string{
|
||||
"question.vote_up",
|
||||
"question.vote_down",
|
||||
"answer.vote_up",
|
||||
"answer.vote_down",
|
||||
}
|
||||
var activityTypes []int
|
||||
|
||||
for _, typeKey := range typeKeys {
|
||||
var t int
|
||||
t, err = ds.configRepo.GetConfigType(typeKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
activityTypes = append(activityTypes, t)
|
||||
}
|
||||
|
||||
voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
userCount, err := ds.userRepo.GetUserCount(ctx)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
|
||||
reportCount, err := ds.reportRepo.GetReportCount(ctx)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
|
||||
siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
|
||||
dashboardInfo.QuestionCount = questionCount
|
||||
dashboardInfo.AnswerCount = answerCount
|
||||
dashboardInfo.CommentCount = commentCount
|
||||
dashboardInfo.VoteCount = voteCount
|
||||
dashboardInfo.UserCount = userCount
|
||||
dashboardInfo.ReportCount = reportCount
|
||||
|
||||
dashboardInfo.UploadingFiles = true
|
||||
emailconfig, err := ds.GetEmailConfig()
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
if emailconfig.SMTPHost != "" {
|
||||
dashboardInfo.SMTP = true
|
||||
}
|
||||
siteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
siteUrl, err := url.Parse(siteGeneral.SiteUrl)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
if siteUrl.Scheme == "https" {
|
||||
dashboardInfo.HTTPS = true
|
||||
}
|
||||
|
||||
dirSize, err := dir.DirSize(ds.serviceConfig.UploadPath)
|
||||
if err != nil {
|
||||
return dashboardInfo, err
|
||||
}
|
||||
size := dir.FormatFileSize(dirSize)
|
||||
dashboardInfo.OccupyingStorageSpace = size
|
||||
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
|
||||
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
|
||||
dashboardInfo.TimeZone = siteInfoInterface.TimeZone
|
||||
dashboardInfo.VersionInfo.Version = constant.Version
|
||||
dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx)
|
||||
return dashboardInfo, nil
|
||||
}
|
||||
|
||||
func (ds *DashboardService) RemoteVersion(ctx context.Context) string {
|
||||
url := "https://answer.dev/getlatest"
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("User-Agent", "Answer/"+constant.Version)
|
||||
resp, err := (&http.Client{}).Do(req)
|
||||
if err != nil {
|
||||
log.Error("http.Client error", err)
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respByte, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Error("http.Client error", err)
|
||||
return ""
|
||||
}
|
||||
remoteVersion := &schema.RemoteVersion{}
|
||||
err = json.Unmarshal(respByte, remoteVersion)
|
||||
if err != nil {
|
||||
log.Error("json.Unmarshal error", err)
|
||||
return ""
|
||||
}
|
||||
return remoteVersion.Release.Version
|
||||
}
|
||||
|
||||
func (ds *DashboardService) GetEmailConfig() (ec *export.EmailConfig, err error) {
|
||||
emailConf, err := ds.configRepo.GetString("email.config")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ec = &export.EmailConfig{}
|
||||
err = json.Unmarshal([]byte(emailConf), ec)
|
||||
if err != nil {
|
||||
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return ec, nil
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package dashboard
|
|
@ -8,6 +8,7 @@ import (
|
|||
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||
"github.com/answerdev/answer/internal/service/comment"
|
||||
"github.com/answerdev/answer/internal/service/comment_common"
|
||||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/follow"
|
||||
"github.com/answerdev/answer/internal/service/meta"
|
||||
|
@ -21,6 +22,8 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/report_backyard"
|
||||
"github.com/answerdev/answer/internal/service/report_handle_backyard"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/answerdev/answer/internal/service/tag"
|
||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
|
@ -61,8 +64,10 @@ var ProviderSetService = wire.NewSet(
|
|||
report_backyard.NewReportBackyardService,
|
||||
user_backyard.NewUserBackyardService,
|
||||
reason.NewReasonService,
|
||||
NewSiteInfoService,
|
||||
siteinfo_common.NewSiteInfoCommonService,
|
||||
siteinfo.NewSiteInfoService,
|
||||
notficationcommon.NewNotificationCommon,
|
||||
notification.NewNotificationService,
|
||||
activity.NewAnswerActivityService,
|
||||
dashboard.NewDashboardService,
|
||||
)
|
||||
|
|
|
@ -38,6 +38,7 @@ type QuestionRepo interface {
|
|||
UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)
|
||||
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
|
||||
CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error)
|
||||
GetQuestionCount(ctx context.Context) (count int64, err error)
|
||||
}
|
||||
|
||||
// QuestionCommon user service
|
||||
|
|
|
@ -2,9 +2,8 @@ package report_backyard
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
|
@ -180,20 +179,20 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
|
|||
case "question":
|
||||
r.QuestionID = questionId
|
||||
r.Title = question.Title
|
||||
r.Excerpt = rs.cutOutTagParsedText(question.OriginalText)
|
||||
r.Excerpt = htmltext.FetchExcerpt(question.ParsedText, "...", 240)
|
||||
|
||||
case "answer":
|
||||
r.QuestionID = questionId
|
||||
r.AnswerID = answerId
|
||||
r.Title = question.Title
|
||||
r.Excerpt = rs.cutOutTagParsedText(answer.OriginalText)
|
||||
r.Excerpt = htmltext.FetchExcerpt(answer.ParsedText, "...", 240)
|
||||
|
||||
case "comment":
|
||||
r.QuestionID = questionId
|
||||
r.AnswerID = answerId
|
||||
r.CommentID = commentId
|
||||
r.Title = question.Title
|
||||
r.Excerpt = rs.cutOutTagParsedText(cmt.OriginalText)
|
||||
r.Excerpt = htmltext.FetchExcerpt(cmt.ParsedText, "...", 240)
|
||||
}
|
||||
|
||||
// parse reason
|
||||
|
@ -214,12 +213,3 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
|
|||
}
|
||||
resp = &res
|
||||
}
|
||||
|
||||
func (rs *ReportBackyardService) cutOutTagParsedText(parsedText string) string {
|
||||
parsedText = strings.TrimSpace(parsedText)
|
||||
idx := strings.Index(parsedText, "\n")
|
||||
if idx >= 0 {
|
||||
parsedText = parsedText[0:idx]
|
||||
}
|
||||
return parsedText
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package report_common
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
)
|
||||
|
@ -12,4 +13,5 @@ type ReportRepo interface {
|
|||
GetReportListPage(ctx context.Context, query schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error)
|
||||
GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error)
|
||||
UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error)
|
||||
GetReportCount(ctx context.Context) (count int64, err error)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package service_config
|
||||
|
||||
type ServiceConfig struct {
|
||||
SecretKey string `json:"secret_key" mapstructure:"secret_key"`
|
||||
WebHost string `json:"web_host" mapstructure:"web_host"`
|
||||
UploadPath string `json:"upload_path" mapstructure:"upload_path"`
|
||||
SecretKey string `json:"secret_key" mapstructure:"secret_key" yaml:"secret_key"`
|
||||
UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"`
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package service
|
||||
package siteinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
|
@ -25,41 +27,37 @@ func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService
|
|||
}
|
||||
}
|
||||
|
||||
func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) {
|
||||
var (
|
||||
siteType = "general"
|
||||
siteInfo *entity.SiteInfo
|
||||
exist bool
|
||||
)
|
||||
resp = schema.SiteGeneralResp{}
|
||||
|
||||
siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType)
|
||||
// GetSiteGeneral get site info general
|
||||
func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
|
||||
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return
|
||||
return nil, errors.BadRequest(reason.SiteInfoNotFound)
|
||||
}
|
||||
|
||||
_ = json.Unmarshal([]byte(siteInfo.Content), &resp)
|
||||
return
|
||||
resp = &schema.SiteGeneralResp{}
|
||||
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) {
|
||||
var (
|
||||
siteType = "interface"
|
||||
siteInfo *entity.SiteInfo
|
||||
exist bool
|
||||
)
|
||||
resp = schema.SiteInterfaceResp{}
|
||||
|
||||
siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType)
|
||||
if !exist {
|
||||
return
|
||||
// GetSiteInterface get site info interface
|
||||
func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
|
||||
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = json.Unmarshal([]byte(siteInfo.Content), &resp)
|
||||
return
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.SiteInfoNotFound)
|
||||
}
|
||||
resp = &schema.SiteInterfaceResp{}
|
||||
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {
|
||||
req.FormatSiteUrl()
|
||||
var (
|
||||
siteType = "general"
|
||||
content []byte
|
||||
|
@ -77,10 +75,9 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe
|
|||
|
||||
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
|
||||
var (
|
||||
siteType = "interface"
|
||||
themeExist,
|
||||
langExist bool
|
||||
content []byte
|
||||
siteType = "interface"
|
||||
themeExist bool
|
||||
content []byte
|
||||
)
|
||||
|
||||
// check theme
|
||||
|
@ -96,13 +93,7 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site
|
|||
}
|
||||
|
||||
// check language
|
||||
for _, lang := range schema.GetLangOptions {
|
||||
if lang.Value == req.Language {
|
||||
langExist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !langExist {
|
||||
if !translator.CheckLanguageIsValid(req.Language) {
|
||||
err = errors.BadRequest(reason.LangNotFound)
|
||||
return
|
||||
}
|
|
@ -2,6 +2,7 @@ package siteinfo_common
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package siteinfo_common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
type SiteInfoCommonService struct {
|
||||
siteInfoRepo SiteInfoRepo
|
||||
}
|
||||
|
||||
func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService {
|
||||
return &SiteInfoCommonService{
|
||||
siteInfoRepo: siteInfoRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSiteGeneral get site info general
|
||||
func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
|
||||
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.SiteInfoNotFound)
|
||||
}
|
||||
|
||||
resp = &schema.SiteGeneralResp{}
|
||||
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetSiteInterface get site info interface
|
||||
func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
|
||||
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.SiteInfoNotFound)
|
||||
}
|
||||
resp = &schema.SiteInterfaceResp{}
|
||||
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
|
||||
return resp, nil
|
||||
}
|
|
@ -3,9 +3,8 @@ package tag
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
|
@ -344,12 +343,13 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
|
|||
|
||||
resp := make([]*schema.GetTagPageResp, 0)
|
||||
for _, tag := range tags {
|
||||
excerpt := htmltext.FetchExcerpt(tag.ParsedText, "...", 240)
|
||||
resp = append(resp, &schema.GetTagPageResp{
|
||||
TagID: tag.ID,
|
||||
SlugName: tag.SlugName,
|
||||
DisplayName: tag.DisplayName,
|
||||
OriginalText: cutOutTagParsedText(tag.OriginalText),
|
||||
ParsedText: cutOutTagParsedText(tag.ParsedText),
|
||||
OriginalText: excerpt,
|
||||
ParsedText: excerpt,
|
||||
FollowCount: tag.FollowCount,
|
||||
QuestionCount: tag.QuestionCount,
|
||||
IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID),
|
||||
|
@ -371,12 +371,3 @@ func (ts *TagService) checkTagIsFollow(ctx context.Context, userID, tagID string
|
|||
}
|
||||
return followed
|
||||
}
|
||||
|
||||
func cutOutTagParsedText(parsedText string) string {
|
||||
parsedText = strings.TrimSpace(parsedText)
|
||||
idx := strings.Index(parsedText, "\n")
|
||||
if idx >= 0 {
|
||||
parsedText = parsedText[0:idx]
|
||||
}
|
||||
return parsedText
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/service/service_config"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/disintegration/imaging"
|
||||
|
@ -27,11 +28,13 @@ const (
|
|||
|
||||
// UploaderService user service
|
||||
type UploaderService struct {
|
||||
serviceConfig *service_config.ServiceConfig
|
||||
serviceConfig *service_config.ServiceConfig
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewUploaderService new upload service
|
||||
func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderService {
|
||||
func NewUploaderService(serviceConfig *service_config.ServiceConfig,
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService {
|
||||
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -41,7 +44,8 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderSe
|
|||
panic(err)
|
||||
}
|
||||
return &UploaderService{
|
||||
serviceConfig: serviceConfig,
|
||||
serviceConfig: serviceConfig,
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,10 +126,14 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.File
|
|||
|
||||
func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
|
||||
url string, err error) {
|
||||
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath)
|
||||
if err := ctx.SaveUploadedFile(file, filePath); err != nil {
|
||||
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, fileSubPath)
|
||||
url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath)
|
||||
return url, nil
|
||||
}
|
||||
|
|
|
@ -15,12 +15,14 @@ type UserRepo interface {
|
|||
UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error
|
||||
UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error
|
||||
UpdateEmail(ctx context.Context, userID, email string) error
|
||||
UpdateLanguage(ctx context.Context, userID, language string) error
|
||||
UpdatePass(ctx context.Context, userID, pass string) error
|
||||
UpdateInfo(ctx context.Context, userInfo *entity.User) (err error)
|
||||
GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)
|
||||
BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error)
|
||||
GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error)
|
||||
GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error)
|
||||
GetUserCount(ctx context.Context) (count int64, err error)
|
||||
}
|
||||
|
||||
// UserCommon user service
|
||||
|
|
|
@ -11,12 +11,14 @@ import (
|
|||
|
||||
"github.com/Chain-Zhang/pinyin"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/answerdev/answer/internal/service/auth"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/service_config"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/google/uuid"
|
||||
|
@ -29,11 +31,12 @@ import (
|
|||
|
||||
// UserService user service
|
||||
type UserService struct {
|
||||
userRepo usercommon.UserRepo
|
||||
userActivity activity.UserActiveActivityRepo
|
||||
serviceConfig *service_config.ServiceConfig
|
||||
emailService *export.EmailService
|
||||
authService *auth.AuthService
|
||||
userRepo usercommon.UserRepo
|
||||
userActivity activity.UserActiveActivityRepo
|
||||
serviceConfig *service_config.ServiceConfig
|
||||
emailService *export.EmailService
|
||||
authService *auth.AuthService
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
func NewUserService(userRepo usercommon.UserRepo,
|
||||
|
@ -41,13 +44,15 @@ func NewUserService(userRepo usercommon.UserRepo,
|
|||
emailService *export.EmailService,
|
||||
authService *auth.AuthService,
|
||||
serviceConfig *service_config.ServiceConfig,
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService,
|
||||
) *UserService {
|
||||
return &UserService{
|
||||
userRepo: userRepo,
|
||||
userActivity: userActivity,
|
||||
emailService: emailService,
|
||||
serviceConfig: serviceConfig,
|
||||
authService: authService,
|
||||
userRepo: userRepo,
|
||||
userActivity: userActivity,
|
||||
emailService: emailService,
|
||||
serviceConfig: serviceConfig,
|
||||
authService: authService,
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,35 +71,6 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// GetUserStatus get user info by user id
|
||||
func (us *UserService) GetUserStatus(ctx context.Context, userID, token string) (resp *schema.GetUserStatusResp, err error) {
|
||||
resp = &schema.GetUserStatusResp{}
|
||||
if len(userID) == 0 {
|
||||
return resp, nil
|
||||
}
|
||||
userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
|
||||
userCacheInfo := &entity.UserCacheInfo{
|
||||
UserID: userID,
|
||||
UserStatus: userInfo.Status,
|
||||
EmailStatus: userInfo.MailStatus,
|
||||
}
|
||||
err = us.authService.UpdateUserCacheInfo(ctx, token, userCacheInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp = &schema.GetUserStatusResp{
|
||||
Status: schema.UserStatusShow[userInfo.Status],
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) (
|
||||
resp *schema.GetOtherUserInfoResp, err error,
|
||||
) {
|
||||
|
@ -168,7 +144,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
|
|||
UserID: userInfo.ID,
|
||||
}
|
||||
code := uuid.NewString()
|
||||
verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.serviceConfig.WebHost, code)
|
||||
verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code)
|
||||
title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -283,6 +259,18 @@ func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, er
|
|||
return has, nil
|
||||
}
|
||||
|
||||
// UserUpdateInterface update user interface
|
||||
func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) {
|
||||
if !translator.CheckLanguageIsValid(req.Language) {
|
||||
return errors.BadRequest(reason.LangNotFound)
|
||||
}
|
||||
err = us.userRepo.UpdateLanguage(ctx, req.UserId, req.Language)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserRegisterByEmail user register
|
||||
func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) (
|
||||
resp *schema.GetUserResp, err error,
|
||||
|
@ -320,7 +308,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
|
|||
UserID: userInfo.ID,
|
||||
}
|
||||
code := uuid.NewString()
|
||||
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code)
|
||||
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code)
|
||||
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -363,7 +351,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e
|
|||
UserID: userInfo.ID,
|
||||
}
|
||||
code := uuid.NewString()
|
||||
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code)
|
||||
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code)
|
||||
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -489,21 +477,26 @@ func (us *UserService) encryptPassword(ctx context.Context, Pass string) (string
|
|||
}
|
||||
|
||||
// UserChangeEmailSendCode user change email verification
|
||||
func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) error {
|
||||
func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) (
|
||||
resp *schema.UserVerifyEmailErrorResponse, err error) {
|
||||
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.UserNotFound)
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
|
||||
_, exist, err = us.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
return errors.BadRequest(reason.EmailDuplicate)
|
||||
resp = &schema.UserVerifyEmailErrorResponse{
|
||||
Key: "e_mail",
|
||||
Value: reason.EmailDuplicate,
|
||||
}
|
||||
return resp, errors.BadRequest(reason.EmailDuplicate)
|
||||
}
|
||||
|
||||
data := &schema.EmailCodeContent{
|
||||
|
@ -512,19 +505,19 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
|
|||
}
|
||||
code := uuid.NewString()
|
||||
var title, body string
|
||||
verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.serviceConfig.WebHost, code)
|
||||
verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.getSiteUrl(ctx), code)
|
||||
if userInfo.MailStatus == entity.EmailStatusToBeVerified {
|
||||
title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL)
|
||||
} else {
|
||||
title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("send email confirmation %s", verifyEmailURL)
|
||||
|
||||
go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString())
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UserChangeEmailVerify user change email verify code
|
||||
|
@ -560,3 +553,13 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSiteUrl get site url
|
||||
func (us *UserService) getSiteUrl(ctx context.Context) string {
|
||||
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general failed: %s", err)
|
||||
return ""
|
||||
}
|
||||
return siteGeneral.SiteUrl
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package dir
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func CreateDirIfNotExist(path string) error {
|
||||
return os.MkdirAll(path, os.ModePerm)
|
||||
|
@ -15,3 +19,32 @@ func CheckFileExist(path string) bool {
|
|||
f, err := os.Stat(path)
|
||||
return err == nil && !f.IsDir()
|
||||
}
|
||||
|
||||
func DirSize(path string) (int64, error) {
|
||||
var size int64
|
||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() {
|
||||
size += info.Size()
|
||||
}
|
||||
return err
|
||||
})
|
||||
return size, err
|
||||
}
|
||||
|
||||
func FormatFileSize(fileSize int64) (size string) {
|
||||
if fileSize < 1024 {
|
||||
//return strconv.FormatInt(fileSize, 10) + "B"
|
||||
return fmt.Sprintf("%.2f B", float64(fileSize)/float64(1))
|
||||
} else if fileSize < (1024 * 1024) {
|
||||
return fmt.Sprintf("%.2f KB", float64(fileSize)/float64(1024))
|
||||
} else if fileSize < (1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2f MB", float64(fileSize)/float64(1024*1024))
|
||||
} else if fileSize < (1024 * 1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2f GB", float64(fileSize)/float64(1024*1024*1024))
|
||||
} else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2f TB", float64(fileSize)/float64(1024*1024*1024*1024))
|
||||
} else { //if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024)
|
||||
return fmt.Sprintf("%.2f EB", float64(fileSize)/float64(1024*1024*1024*1024*1024))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package htmltext
|
||||
|
||||
import (
|
||||
"github.com/grokify/html-strip-tags-go"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ClearText clear HTML, get the clear text
|
||||
func ClearText(html string) (text string) {
|
||||
if len(html) == 0 {
|
||||
text = html
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
re *regexp.Regexp
|
||||
codeReg = `(?ism)<(pre)>.*<\/pre>`
|
||||
codeRepl = "{code...}"
|
||||
linkReg = `(?ism)<a.*?[^<]>(.*)?<\/a>`
|
||||
linkRepl = " [$1] "
|
||||
spaceReg = ` +`
|
||||
spaceRepl = " "
|
||||
)
|
||||
re = regexp.MustCompile(codeReg)
|
||||
html = re.ReplaceAllString(html, codeRepl)
|
||||
|
||||
re = regexp.MustCompile(linkReg)
|
||||
html = re.ReplaceAllString(html, linkRepl)
|
||||
|
||||
text = strings.NewReplacer(
|
||||
"\n", " ",
|
||||
"\r", " ",
|
||||
"\t", " ",
|
||||
).Replace(strip.StripTags(html))
|
||||
|
||||
// replace multiple spaces to one space
|
||||
re = regexp.MustCompile(spaceReg)
|
||||
text = strings.TrimSpace(re.ReplaceAllString(text, spaceRepl))
|
||||
return
|
||||
}
|
||||
|
||||
// FetchExcerpt return the excerpt from the HTML string
|
||||
func FetchExcerpt(html, trimMarker string, limit int) (text string) {
|
||||
if len(html) == 0 {
|
||||
text = html
|
||||
return
|
||||
}
|
||||
|
||||
text = ClearText(html)
|
||||
runeText := []rune(text)
|
||||
if len(runeText) <= limit {
|
||||
text = string(runeText)
|
||||
} else {
|
||||
text = string(runeText[0:limit])
|
||||
}
|
||||
|
||||
text += trimMarker
|
||||
return
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package htmltext
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClearText(t *testing.T) {
|
||||
var (
|
||||
expected,
|
||||
clearedText string
|
||||
)
|
||||
|
||||
// test code clear text
|
||||
expected = "hello{code...}"
|
||||
clearedText = ClearText("<p>hello<pre>var a = \"good\"</pre></p>")
|
||||
assert.Equal(t, expected, clearedText)
|
||||
|
||||
// test link clear text
|
||||
expected = "hello [example.com]"
|
||||
clearedText = ClearText("<p>hello <a href=\"http://example.com/\">example.com</a></p>")
|
||||
assert.Equal(t, expected, clearedText)
|
||||
clearedText = ClearText("<p>hello<a href=\"https://example.com/\">example.com</a></p>")
|
||||
assert.Equal(t, expected, clearedText)
|
||||
|
||||
expected = "hello world"
|
||||
clearedText = ClearText("<div> hello</div>\n<div>world</div>")
|
||||
assert.Equal(t, expected, clearedText)
|
||||
}
|
||||
|
||||
func TestFetchExcerpt(t *testing.T) {
|
||||
var (
|
||||
expected,
|
||||
text string
|
||||
)
|
||||
|
||||
// test english string
|
||||
expected = "hello..."
|
||||
text = FetchExcerpt("<p>hello world</p>", "...", 5)
|
||||
assert.Equal(t, expected, text)
|
||||
|
||||
// test mixed string
|
||||
expected = "hello你好..."
|
||||
text = FetchExcerpt("<p>hello你好world</p>", "...", 7)
|
||||
assert.Equal(t, expected, text)
|
||||
|
||||
// test mixed string with emoticon
|
||||
expected = "hello你好😂..."
|
||||
text = FetchExcerpt("<p>hello你好😂world</p>", "...", 8)
|
||||
assert.Equal(t, expected, text)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package writer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ReplaceFile remove old file and write new file
|
||||
func ReplaceFile(filePath, content string) error {
|
||||
_ = os.Remove(filePath)
|
||||
return WriteFile(filePath, content)
|
||||
}
|
||||
|
||||
// WriteFile write file to path
|
||||
func WriteFile(filePath, content string) error {
|
||||
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
writer := bufio.NewWriter(file)
|
||||
if _, err := writer.WriteString(content); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
#!/bin/bash
|
||||
/usr/bin/answer init
|
||||
/usr/bin/answer run -c /data/conf/config.yaml
|
||||
/usr/bin/answer upgrade
|
||||
/usr/bin/answer run -C /data/
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
|
@ -19,7 +20,8 @@ module.exports = {
|
|||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
plugins: ['react', '@typescript-eslint'],
|
||||
rules: {
|
||||
|
@ -64,7 +66,7 @@ module.exports = {
|
|||
position: 'before',
|
||||
},
|
||||
{
|
||||
pattern: '@answer/**',
|
||||
pattern: '@/**',
|
||||
group: 'internal',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
extends: ['@commitlint/routes-conventional'],
|
||||
};
|
||||
|
|
|
@ -1,36 +1,51 @@
|
|||
const path = require('path');
|
||||
const {
|
||||
addWebpackModuleRule,
|
||||
addWebpackAlias
|
||||
} = require("customize-cra");
|
||||
|
||||
const path = require("path");
|
||||
const i18nPath = path.resolve(__dirname, "../i18n");
|
||||
|
||||
module.exports = {
|
||||
webpack: function (config, env) {
|
||||
if (env === 'production') {
|
||||
webpack: function(config, env) {
|
||||
if (env === "production") {
|
||||
config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH;
|
||||
}
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'@answer/pages': path.resolve(__dirname, 'src/pages'),
|
||||
'@answer/components': path.resolve(__dirname, 'src/components'),
|
||||
'@answer/stores': path.resolve(__dirname, 'src/stores'),
|
||||
'@answer/hooks': path.resolve(__dirname, 'src/hooks'),
|
||||
'@answer/utils': path.resolve(__dirname, 'src/utils'),
|
||||
'@answer/common': path.resolve(__dirname, 'src/common'),
|
||||
'@answer/api': path.resolve(__dirname, 'src/services/api'),
|
||||
};
|
||||
|
||||
addWebpackAlias({
|
||||
["@"]: path.resolve(__dirname, "src"),
|
||||
"@i18n": i18nPath
|
||||
})(config);
|
||||
|
||||
addWebpackModuleRule({
|
||||
test: /\.ya?ml$/,
|
||||
use: "yaml-loader"
|
||||
})(config);
|
||||
|
||||
// add i18n dir to ModuleScopePlugin allowedPaths
|
||||
const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === "ModuleScopePlugin");
|
||||
if (moduleScopePlugin) {
|
||||
moduleScopePlugin.allowedPaths.push(i18nPath);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
devServer: function (configFunction) {
|
||||
return function (proxy, allowedHost) {
|
||||
devServer: function(configFunction) {
|
||||
return function(proxy, allowedHost) {
|
||||
const config = configFunction(proxy, allowedHost);
|
||||
config.proxy = {
|
||||
'/answer': {
|
||||
target: 'http://10.0.10.98:2060',
|
||||
"/answer": {
|
||||
target: "http://10.0.10.98:2060",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
secure: false
|
||||
},
|
||||
"/installation": {
|
||||
target: "http://10.0.10.98:2060",
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
};
|
||||
return config;
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -10,12 +10,12 @@
|
|||
"build:prod": "env-cmd -f .env.production react-app-rewired build",
|
||||
"build": "env-cmd -f .env react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint . --cache --fix --ext .ts,.tsx",
|
||||
"prepare": "cd .. && husky install",
|
||||
"cz": "cz",
|
||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
||||
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
|
||||
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
||||
"preinstall": "node ./scripts/preinstall.js"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
@ -101,7 +101,8 @@
|
|||
"sass": "^1.54.4",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "*",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"yaml-loader": "^0.8.0"
|
||||
},
|
||||
"packageManager": "pnpm@7.9.5",
|
||||
"engines": {
|
||||
|
|
|
@ -77,6 +77,7 @@ specifiers:
|
|||
tsconfig-paths-webpack-plugin: ^4.0.0
|
||||
typescript: '*'
|
||||
web-vitals: ^2.1.4
|
||||
yaml-loader: ^0.8.0
|
||||
zustand: ^4.1.1
|
||||
|
||||
dependencies:
|
||||
|
@ -159,6 +160,7 @@ devDependencies:
|
|||
tsconfig-paths-webpack-plugin: 4.0.0
|
||||
typescript: 4.8.3
|
||||
web-vitals: 2.1.4
|
||||
yaml-loader: 0.8.0
|
||||
|
||||
packages:
|
||||
|
||||
|
@ -7040,6 +7042,10 @@ packages:
|
|||
filelist: 1.0.4
|
||||
minimatch: 3.1.2
|
||||
|
||||
/javascript-stringify/2.1.0:
|
||||
resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==}
|
||||
dev: true
|
||||
|
||||
/jest-changed-files/27.5.1:
|
||||
resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
|
@ -11682,6 +11688,15 @@ packages:
|
|||
/yallist/4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
/yaml-loader/0.8.0:
|
||||
resolution: {integrity: sha512-LjeKnTzVBKWiQBeE2L9ssl6WprqaUIxCSNs5tle8PaDydgu3wVFXTbMfsvF2MSErpy9TDVa092n4q6adYwJaWg==}
|
||||
engines: {node: '>= 12.13'}
|
||||
dependencies:
|
||||
javascript-stringify: 2.1.0
|
||||
loader-utils: 2.0.2
|
||||
yaml: 2.1.1
|
||||
dev: true
|
||||
|
||||
/yaml/1.10.2:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed.
|
||||
if (!/pnpm/.test(process.env.npm_execpath)) {
|
||||
console.warn(`\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\u001b[39m\n`)
|
||||
process.exit(1)
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import router from '@/router';
|
||||
import './i18n/init';
|
||||
import { routes, createBrowserRouter } from '@/router';
|
||||
|
||||
function App() {
|
||||
const router = createBrowserRouter(routes);
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
export const LOGIN_NEED_BACK = [
|
||||
'/users/login',
|
||||
'/users/register',
|
||||
'/users/account-recovery',
|
||||
'/users/password-reset',
|
||||
];
|
||||
export const DEFAULT_LANG = 'en_US';
|
||||
export const CURRENT_LANG_STORAGE_KEY = '_a_lang_';
|
||||
export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_';
|
||||
export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
|
||||
export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';
|
||||
export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';
|
||||
export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
|
||||
|
||||
export const ADMIN_LIST_STATUS = {
|
||||
// normal;
|
||||
|
@ -56,3 +57,494 @@ export const ADMIN_NAV_MENUS = [
|
|||
child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }],
|
||||
},
|
||||
];
|
||||
|
||||
export const TIMEZONES = [
|
||||
{
|
||||
label: 'Africa',
|
||||
options: [
|
||||
{ value: 'Africa/Abidjan', label: 'Abidjan' },
|
||||
{ value: 'Africa/Accra', label: 'Accra' },
|
||||
{ value: 'Africa/Addis_Ababa', label: 'Addis Ababa' },
|
||||
{ value: 'Africa/Algiers', label: 'Algiers' },
|
||||
{ value: 'Africa/Asmara', label: 'Asmara' },
|
||||
{ value: 'Africa/Bamako', label: 'Bamako' },
|
||||
{ value: 'Africa/Bangui', label: 'Bangui' },
|
||||
{ value: 'Africa/Banjul', label: 'Banjul' },
|
||||
{ value: 'Africa/Bissau', label: 'Bissau' },
|
||||
{ value: 'Africa/Blantyre', label: 'Blantyre' },
|
||||
{ value: 'Africa/Brazzaville', label: 'Brazzaville' },
|
||||
{ value: 'Africa/Bujumbura', label: 'Bujumbura' },
|
||||
{ value: 'Africa/Cairo', label: 'Cairo' },
|
||||
{ value: 'Africa/Casablanca', label: 'Casablanca' },
|
||||
{ value: 'Africa/Ceuta', label: 'Ceuta' },
|
||||
{ value: 'Africa/Conakry', label: 'Conakry' },
|
||||
{ value: 'Africa/Dakar', label: 'Dakar' },
|
||||
{ value: 'Africa/Dar_es_Salaam', label: 'Dar es Salaam' },
|
||||
{ value: 'Africa/Djibouti', label: 'Djibouti' },
|
||||
{ value: 'Africa/Douala', label: 'Douala' },
|
||||
{ value: 'Africa/El_Aaiun', label: 'El Aaiun' },
|
||||
{ value: 'Africa/Freetown', label: 'Freetown' },
|
||||
{ value: 'Africa/Gaborone', label: 'Gaborone' },
|
||||
{ value: 'Africa/Harare', label: 'Harare' },
|
||||
{ value: 'Africa/Johannesburg', label: 'Johannesburg' },
|
||||
{ value: 'Africa/Juba', label: 'Juba' },
|
||||
{ value: 'Africa/Kampala', label: 'Kampala' },
|
||||
{ value: 'Africa/Khartoum', label: 'Khartoum' },
|
||||
{ value: 'Africa/Kigali', label: 'Kigali' },
|
||||
{ value: 'Africa/Kinshasa', label: 'Kinshasa' },
|
||||
{ value: 'Africa/Lagos', label: 'Lagos' },
|
||||
{ value: 'Africa/Libreville', label: 'Libreville' },
|
||||
{ value: 'Africa/Lome', label: 'Lome' },
|
||||
{ value: 'Africa/Luanda', label: 'Luanda' },
|
||||
{ value: 'Africa/Lubumbashi', label: 'Lubumbashi' },
|
||||
{ value: 'Africa/Lusaka', label: 'Lusaka' },
|
||||
{ value: 'Africa/Malabo', label: 'Malabo' },
|
||||
{ value: 'Africa/Maputo', label: 'Maputo' },
|
||||
{ value: 'Africa/Maseru', label: 'Maseru' },
|
||||
{ value: 'Africa/Mbabane', label: 'Mbabane' },
|
||||
{ value: 'Africa/Mogadishu', label: 'Mogadishu' },
|
||||
{ value: 'Africa/Monrovia', label: 'Monrovia' },
|
||||
{ value: 'Africa/Nairobi', label: 'Nairobi' },
|
||||
{ value: 'Africa/Ndjamena', label: 'Ndjamena' },
|
||||
{ value: 'Africa/Niamey', label: 'Niamey' },
|
||||
{ value: 'Africa/Nouakchott', label: 'Nouakchott' },
|
||||
{ value: 'Africa/Ouagadougou', label: 'Ouagadougou' },
|
||||
{ value: 'Africa/Porto-Novo', label: 'Porto-Novo' },
|
||||
{ value: 'Africa/Sao_Tome', label: 'Sao Tome' },
|
||||
{ value: 'Africa/Tripoli', label: 'Tripoli' },
|
||||
{ value: 'Africa/Tunis', label: 'Tunis' },
|
||||
{ value: 'Africa/Windhoek', label: 'Windhoek' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'America',
|
||||
options: [
|
||||
{ value: 'America/Adak', label: 'Adak' },
|
||||
{ value: 'America/Anchorage', label: 'Anchorage' },
|
||||
{ value: 'America/Anguilla', label: 'Anguilla' },
|
||||
{ value: 'America/Antigua', label: 'Antigua' },
|
||||
{ value: 'America/Araguaina', label: 'Araguaina' },
|
||||
{
|
||||
value: 'America/Argentina/Buenos_Aires',
|
||||
label: 'Argentina - Buenos Aires',
|
||||
},
|
||||
{ value: 'America/Argentina/Catamarca', label: 'Argentina - Catamarca' },
|
||||
{ value: 'America/Argentina/Cordoba', label: 'Argentina - Cordoba' },
|
||||
{ value: 'America/Argentina/Jujuy', label: 'Argentina - Jujuy' },
|
||||
{ value: 'America/Argentina/La_Rioja', label: 'Argentina - La Rioja' },
|
||||
{ value: 'America/Argentina/Mendoza', label: 'Argentina - Mendoza' },
|
||||
{
|
||||
value: 'America/Argentina/Rio_Gallegos',
|
||||
label: 'Argentina - Rio Gallegos',
|
||||
},
|
||||
{ value: 'America/Argentina/Salta', label: 'Argentina - Salta' },
|
||||
{ value: 'America/Argentina/San_Juan', label: 'Argentina - San Juan' },
|
||||
{ value: 'America/Argentina/San_Luis', label: 'Argentina - San Luis' },
|
||||
{ value: 'America/Argentina/Tucuman', label: 'Argentina - Tucuman' },
|
||||
{ value: 'America/Argentina/Ushuaia', label: 'Argentina - Ushuaia' },
|
||||
{ value: 'America/Aruba', label: 'Aruba' },
|
||||
{ value: 'America/Asuncion', label: 'Asuncion' },
|
||||
{ value: 'America/Atikokan', label: 'Atikokan' },
|
||||
{ value: 'America/Bahia', label: 'Bahia' },
|
||||
{ value: 'America/Bahia_Banderas', label: 'Bahia Banderas' },
|
||||
{ value: 'America/Barbados', label: 'Barbados' },
|
||||
{ value: 'America/Belem', label: 'Belem' },
|
||||
{ value: 'America/Belize', label: 'Belize' },
|
||||
{ value: 'America/Blanc-Sablon', label: 'Blanc-Sablon' },
|
||||
{ value: 'America/Boa_Vista', label: 'Boa Vista' },
|
||||
{ value: 'America/Bogota', label: 'Bogota' },
|
||||
{ value: 'America/Boise', label: 'Boise' },
|
||||
{ value: 'America/Cambridge_Bay', label: 'Cambridge Bay' },
|
||||
{ value: 'America/Campo_Grande', label: 'Campo Grande' },
|
||||
{ value: 'America/Cancun', label: 'Cancun' },
|
||||
{ value: 'America/Caracas', label: 'Caracas' },
|
||||
{ value: 'America/Cayenne', label: 'Cayenne' },
|
||||
{ value: 'America/Cayman', label: 'Cayman' },
|
||||
{ value: 'America/Chicago', label: 'Chicago' },
|
||||
{ value: 'America/Chihuahua', label: 'Chihuahua' },
|
||||
{ value: 'America/Costa_Rica', label: 'Costa Rica' },
|
||||
{ value: 'America/Creston', label: 'Creston' },
|
||||
{ value: 'America/Cuiaba', label: 'Cuiaba' },
|
||||
{ value: 'America/Curacao', label: 'Curacao' },
|
||||
{ value: 'America/Danmarkshavn', label: 'Danmarkshavn' },
|
||||
{ value: 'America/Dawson', label: 'Dawson' },
|
||||
{ value: 'America/Dawson_Creek', label: 'Dawson Creek' },
|
||||
{ value: 'America/Denver', label: 'Denver' },
|
||||
{ value: 'America/Detroit', label: 'Detroit' },
|
||||
{ value: 'America/Dominica', label: 'Dominica' },
|
||||
{ value: 'America/Edmonton', label: 'Edmonton' },
|
||||
{ value: 'America/Eirunepe', label: 'Eirunepe' },
|
||||
{ value: 'America/El_Salvador', label: 'El Salvador' },
|
||||
{ value: 'America/Fort_Nelson', label: 'Fort Nelson' },
|
||||
{ value: 'America/Fortaleza', label: 'Fortaleza' },
|
||||
{ value: 'America/Glace_Bay', label: 'Glace Bay' },
|
||||
{ value: 'America/Godthab', label: 'Godthab' },
|
||||
{ value: 'America/Goose_Bay', label: 'Goose Bay' },
|
||||
{ value: 'America/Grand_Turk', label: 'Grand Turk' },
|
||||
{ value: 'America/Grenada', label: 'Grenada' },
|
||||
{ value: 'America/Guadeloupe', label: 'Guadeloupe' },
|
||||
{ value: 'America/Guatemala', label: 'Guatemala' },
|
||||
{ value: 'America/Guayaquil', label: 'Guayaquil' },
|
||||
{ value: 'America/Guyana', label: 'Guyana' },
|
||||
{ value: 'America/Halifax', label: 'Halifax' },
|
||||
{ value: 'America/Havana', label: 'Havana' },
|
||||
{ value: 'America/Hermosillo', label: 'Hermosillo' },
|
||||
{
|
||||
value: 'America/Indiana/Indianapolis',
|
||||
label: 'Indiana - Indianapolis',
|
||||
},
|
||||
{ value: 'America/Indiana/Knox', label: 'Indiana - Knox' },
|
||||
{ value: 'America/Indiana/Marengo', label: 'Indiana - Marengo' },
|
||||
{ value: 'America/Indiana/Petersburg', label: 'Indiana - Petersburg' },
|
||||
{ value: 'America/Indiana/Tell_City', label: 'Indiana - Tell City' },
|
||||
{ value: 'America/Indiana/Vevay', label: 'Indiana - Vevay' },
|
||||
{ value: 'America/Indiana/Vincennes', label: 'Indiana - Vincennes' },
|
||||
{ value: 'America/Indiana/Winamac', label: 'Indiana - Winamac' },
|
||||
{ value: 'America/Inuvik', label: 'Inuvik' },
|
||||
{ value: 'America/Iqaluit', label: 'Iqaluit' },
|
||||
{ value: 'America/Jamaica', label: 'Jamaica' },
|
||||
{ value: 'America/Juneau', label: 'Juneau' },
|
||||
{ value: 'America/Kentucky/Louisville', label: 'Kentucky - Louisville' },
|
||||
{ value: 'America/Kentucky/Monticello', label: 'Kentucky - Monticello' },
|
||||
{ value: 'America/Kralendijk', label: 'Kralendijk' },
|
||||
{ value: 'America/La_Paz', label: 'La Paz' },
|
||||
{ value: 'America/Lima', label: 'Lima' },
|
||||
{ value: 'America/Los_Angeles', label: 'Los Angeles' },
|
||||
{ value: 'America/Lower_Princes', label: 'Lower Princes' },
|
||||
{ value: 'America/Maceio', label: 'Maceio' },
|
||||
{ value: 'America/Managua', label: 'Managua' },
|
||||
{ value: 'America/Manaus', label: 'Manaus' },
|
||||
{ value: 'America/Marigot', label: 'Marigot' },
|
||||
{ value: 'America/Martinique', label: 'Martinique' },
|
||||
{ value: 'America/Matamoros', label: 'Matamoros' },
|
||||
{ value: 'America/Mazatlan', label: 'Mazatlan' },
|
||||
{ value: 'America/Miquelon', label: 'Miquelon' },
|
||||
{ value: 'America/Moncton', label: 'Moncton' },
|
||||
{ value: 'America/Monterrey', label: 'Monterrey' },
|
||||
{ value: 'America/Montevideo', label: 'Montevideo' },
|
||||
{ value: 'America/Montserrat', label: 'Montserrat' },
|
||||
{ value: 'America/Nassau', label: 'Nassau' },
|
||||
{ value: 'America/New_York', label: 'New York' },
|
||||
{ value: 'America/Nipigon', label: 'Nipigon' },
|
||||
{ value: 'America/Nome', label: 'Nome' },
|
||||
{ value: 'America/Noronha', label: 'Noronha' },
|
||||
{ value: 'America/North_Dakota/Beulah', label: 'North Dakota - Beulah' },
|
||||
{ value: 'America/North_Dakota/Center', label: 'North Dakota - Center' },
|
||||
{
|
||||
value: 'America/North_Dakota/New_Salem',
|
||||
label: 'North Dakota - New Salem',
|
||||
},
|
||||
{ value: 'America/Ojinaga', label: 'Ojinaga' },
|
||||
{ value: 'America/Panama', label: 'Panama' },
|
||||
{ value: 'America/Pangnirtung', label: 'Pangnirtung' },
|
||||
{ value: 'America/Paramaribo', label: 'Paramaribo' },
|
||||
{ value: 'America/Phoenix', label: 'Phoenix' },
|
||||
{ value: 'America/Port-au-Prince', label: 'Port-au-Prince' },
|
||||
{ value: 'America/Port_of_Spain', label: 'Port of Spain' },
|
||||
{ value: 'America/Porto_Velho', label: 'Porto Velho' },
|
||||
{ value: 'America/Puerto_Rico', label: 'Puerto Rico' },
|
||||
{ value: 'America/Punta_Arenas', label: 'Punta Arenas' },
|
||||
{ value: 'America/Rainy_River', label: 'Rainy River' },
|
||||
{ value: 'America/Rankin_Inlet', label: 'Rankin Inlet' },
|
||||
{ value: 'America/Recife', label: 'Recife' },
|
||||
{ value: 'America/Regina', label: 'Regina' },
|
||||
{ value: 'America/Resolute', label: 'Resolute' },
|
||||
{ value: 'America/Rio_Branco', label: 'Rio Branco' },
|
||||
{ value: 'America/Santarem', label: 'Santarem' },
|
||||
{ value: 'America/Santiago', label: 'Santiago' },
|
||||
{ value: 'America/Santo_Domingo', label: 'Santo Domingo' },
|
||||
{ value: 'America/Sao_Paulo', label: 'Sao Paulo' },
|
||||
{ value: 'America/Scoresbysund', label: 'Scoresbysund' },
|
||||
{ value: 'America/Sitka', label: 'Sitka' },
|
||||
{ value: 'America/St_Barthelemy', label: 'St Barthelemy' },
|
||||
{ value: 'America/St_Johns', label: 'St Johns' },
|
||||
{ value: 'America/St_Kitts', label: 'St Kitts' },
|
||||
{ value: 'America/St_Lucia', label: 'St Lucia' },
|
||||
{ value: 'America/St_Thomas', label: 'St Thomas' },
|
||||
{ value: 'America/St_Vincent', label: 'St Vincent' },
|
||||
{ value: 'America/Swift_Current', label: 'Swift Current' },
|
||||
{ value: 'America/Tegucigalpa', label: 'Tegucigalpa' },
|
||||
{ value: 'America/Thule', label: 'Thule' },
|
||||
{ value: 'America/Thunder_Bay', label: 'Thunder Bay' },
|
||||
{ value: 'America/Tijuana', label: 'Tijuana' },
|
||||
{ value: 'America/Toronto', label: 'Toronto' },
|
||||
{ value: 'America/Tortola', label: 'Tortola' },
|
||||
{ value: 'America/Vancouver', label: 'Vancouver' },
|
||||
{ value: 'America/Whitehorse', label: 'Whitehorse' },
|
||||
{ value: 'America/Winnipeg', label: 'Winnipeg' },
|
||||
{ value: 'America/Yakutat', label: 'Yakutat' },
|
||||
{ value: 'America/Yellowknife', label: 'Yellowknife' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Antarctica',
|
||||
options: [
|
||||
{ value: 'Antarctica/Casey', label: 'Casey' },
|
||||
{ value: 'Antarctica/Davis', label: 'Davis' },
|
||||
{ value: 'Antarctica/DumontDUrville', label: 'DumontDUrville' },
|
||||
{ value: 'Antarctica/Macquarie', label: 'Macquarie' },
|
||||
{ value: 'Antarctica/Mawson', label: 'Mawson' },
|
||||
{ value: 'Antarctica/McMurdo', label: 'McMurdo' },
|
||||
{ value: 'Antarctica/Palmer', label: 'Palmer' },
|
||||
{ value: 'Antarctica/Rothera', label: 'Rothera' },
|
||||
{ value: 'Antarctica/Syowa', label: 'Syowa' },
|
||||
{ value: 'Antarctica/Troll', label: 'Troll' },
|
||||
{ value: 'Antarctica/Vostok', label: 'Vostok' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Arctic',
|
||||
options: [{ value: 'Arctic/Longyearbyen', label: 'Longyearbyen' }],
|
||||
},
|
||||
{
|
||||
label: 'Asia',
|
||||
options: [
|
||||
{ value: 'Asia/Aden', label: 'Aden' },
|
||||
{ value: 'Asia/Almaty', label: 'Almaty' },
|
||||
{ value: 'Asia/Amman', label: 'Amman' },
|
||||
{ value: 'Asia/Anadyr', label: 'Anadyr' },
|
||||
{ value: 'Asia/Aqtau', label: 'Aqtau' },
|
||||
{ value: 'Asia/Aqtobe', label: 'Aqtobe' },
|
||||
{ value: 'Asia/Ashgabat', label: 'Ashgabat' },
|
||||
{ value: 'Asia/Atyrau', label: 'Atyrau' },
|
||||
{ value: 'Asia/Baghdad', label: 'Baghdad' },
|
||||
{ value: 'Asia/Bahrain', label: 'Bahrain' },
|
||||
{ value: 'Asia/Baku', label: 'Baku' },
|
||||
{ value: 'Asia/Bangkok', label: 'Bangkok' },
|
||||
{ value: 'Asia/Barnaul', label: 'Barnaul' },
|
||||
{ value: 'Asia/Beirut', label: 'Beirut' },
|
||||
{ value: 'Asia/Bishkek', label: 'Bishkek' },
|
||||
{ value: 'Asia/Brunei', label: 'Brunei' },
|
||||
{ value: 'Asia/Chita', label: 'Chita' },
|
||||
{ value: 'Asia/Choibalsan', label: 'Choibalsan' },
|
||||
{ value: 'Asia/Colombo', label: 'Colombo' },
|
||||
{ value: 'Asia/Damascus', label: 'Damascus' },
|
||||
{ value: 'Asia/Dhaka', label: 'Dhaka' },
|
||||
{ value: 'Asia/Dili', label: 'Dili' },
|
||||
{ value: 'Asia/Dubai', label: 'Dubai' },
|
||||
{ value: 'Asia/Dushanbe', label: 'Dushanbe' },
|
||||
{ value: 'Asia/Famagusta', label: 'Famagusta' },
|
||||
{ value: 'Asia/Gaza', label: 'Gaza' },
|
||||
{ value: 'Asia/Hebron', label: 'Hebron' },
|
||||
{ value: 'Asia/Ho_Chi_Minh', label: 'Ho Chi Minh' },
|
||||
{ value: 'Asia/Hong_Kong', label: 'Hong Kong' },
|
||||
{ value: 'Asia/Hovd', label: 'Hovd' },
|
||||
{ value: 'Asia/Irkutsk', label: 'Irkutsk' },
|
||||
{ value: 'Asia/Jakarta', label: 'Jakarta' },
|
||||
{ value: 'Asia/Jayapura', label: 'Jayapura' },
|
||||
{ value: 'Asia/Jerusalem', label: 'Jerusalem' },
|
||||
{ value: 'Asia/Kabul', label: 'Kabul' },
|
||||
{ value: 'Asia/Kamchatka', label: 'Kamchatka' },
|
||||
{ value: 'Asia/Karachi', label: 'Karachi' },
|
||||
{ value: 'Asia/Kathmandu', label: 'Kathmandu' },
|
||||
{ value: 'Asia/Khandyga', label: 'Khandyga' },
|
||||
{ value: 'Asia/Kolkata', label: 'Kolkata' },
|
||||
{ value: 'Asia/Krasnoyarsk', label: 'Krasnoyarsk' },
|
||||
{ value: 'Asia/Kuala_Lumpur', label: 'Kuala Lumpur' },
|
||||
{ value: 'Asia/Kuching', label: 'Kuching' },
|
||||
{ value: 'Asia/Kuwait', label: 'Kuwait' },
|
||||
{ value: 'Asia/Macau', label: 'Macau' },
|
||||
{ value: 'Asia/Magadan', label: 'Magadan' },
|
||||
{ value: 'Asia/Makassar', label: 'Makassar' },
|
||||
{ value: 'Asia/Manila', label: 'Manila' },
|
||||
{ value: 'Asia/Muscat', label: 'Muscat' },
|
||||
{ value: 'Asia/Nicosia', label: 'Nicosia' },
|
||||
{ value: 'Asia/Novokuznetsk', label: 'Novokuznetsk' },
|
||||
{ value: 'Asia/Novosibirsk', label: 'Novosibirsk' },
|
||||
{ value: 'Asia/Omsk', label: 'Omsk' },
|
||||
{ value: 'Asia/Oral', label: 'Oral' },
|
||||
{ value: 'Asia/Phnom_Penh', label: 'Phnom Penh' },
|
||||
{ value: 'Asia/Pontianak', label: 'Pontianak' },
|
||||
{ value: 'Asia/Pyongyang', label: 'Pyongyang' },
|
||||
{ value: 'Asia/Qatar', label: 'Qatar' },
|
||||
{ value: 'Asia/Qostanay', label: 'Qostanay' },
|
||||
{ value: 'Asia/Qyzylorda', label: 'Qyzylorda' },
|
||||
{ value: 'Asia/Riyadh', label: 'Riyadh' },
|
||||
{ value: 'Asia/Sakhalin', label: 'Sakhalin' },
|
||||
{ value: 'Asia/Samarkand', label: 'Samarkand' },
|
||||
{ value: 'Asia/Seoul', label: 'Seoul' },
|
||||
{ value: 'Asia/Shanghai', label: 'Shanghai' },
|
||||
{ value: 'Asia/Singapore', label: 'Singapore' },
|
||||
{ value: 'Asia/Srednekolymsk', label: 'Srednekolymsk' },
|
||||
{ value: 'Asia/Taipei', label: 'Taipei' },
|
||||
{ value: 'Asia/Tashkent', label: 'Tashkent' },
|
||||
{ value: 'Asia/Tbilisi', label: 'Tbilisi' },
|
||||
{ value: 'Asia/Tehran', label: 'Tehran' },
|
||||
{ value: 'Asia/Thimphu', label: 'Thimphu' },
|
||||
{ value: 'Asia/Tokyo', label: 'Tokyo' },
|
||||
{ value: 'Asia/Tomsk', label: 'Tomsk' },
|
||||
{ value: 'Asia/Ulaanbaatar', label: 'Ulaanbaatar' },
|
||||
{ value: 'Asia/Urumqi', label: 'Urumqi' },
|
||||
{ value: 'Asia/Ust-Nera', label: 'Ust-Nera' },
|
||||
{ value: 'Asia/Vientiane', label: 'Vientiane' },
|
||||
{ value: 'Asia/Vladivostok', label: 'Vladivostok' },
|
||||
{ value: 'Asia/Yakutsk', label: 'Yakutsk' },
|
||||
{ value: 'Asia/Yangon', label: 'Yangon' },
|
||||
{ value: 'Asia/Yekaterinburg', label: 'Yekaterinburg' },
|
||||
{ value: 'Asia/Yerevan', label: 'Yerevan' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Atlantic',
|
||||
options: [
|
||||
{ value: 'Atlantic/Azores', label: 'Azores' },
|
||||
{ value: 'Atlantic/Bermuda', label: 'Bermuda' },
|
||||
{ value: 'Atlantic/Canary', label: 'Canary' },
|
||||
{ value: 'Atlantic/Cape_Verde', label: 'Cape Verde' },
|
||||
{ value: 'Atlantic/Faroe', label: 'Faroe' },
|
||||
{ value: 'Atlantic/Madeira', label: 'Madeira' },
|
||||
{ value: 'Atlantic/Reykjavik', label: 'Reykjavik' },
|
||||
{ value: 'Atlantic/South_Georgia', label: 'South Georgia' },
|
||||
{ value: 'Atlantic/Stanley', label: 'Stanley' },
|
||||
{ value: 'Atlantic/St_Helena', label: 'St Helena' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Australia',
|
||||
options: [
|
||||
{ value: 'Australia/Adelaide', label: 'Adelaide' },
|
||||
{ value: 'Australia/Brisbane', label: 'Brisbane' },
|
||||
{ value: 'Australia/Broken_Hill', label: 'Broken Hill' },
|
||||
{ value: 'Australia/Currie', label: 'Currie' },
|
||||
{ value: 'Australia/Darwin', label: 'Darwin' },
|
||||
{ value: 'Australia/Eucla', label: 'Eucla' },
|
||||
{ value: 'Australia/Hobart', label: 'Hobart' },
|
||||
{ value: 'Australia/Lindeman', label: 'Lindeman' },
|
||||
{ value: 'Australia/Lord_Howe', label: 'Lord Howe' },
|
||||
{ value: 'Australia/Melbourne', label: 'Melbourne' },
|
||||
{ value: 'Australia/Perth', label: 'Perth' },
|
||||
{ value: 'Australia/Sydney', label: 'Sydney' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Europe',
|
||||
options: [
|
||||
{ value: 'Europe/Amsterdam', label: 'Amsterdam' },
|
||||
{ value: 'Europe/Andorra', label: 'Andorra' },
|
||||
{ value: 'Europe/Astrakhan', label: 'Astrakhan' },
|
||||
{ value: 'Europe/Athens', label: 'Athens' },
|
||||
{ value: 'Europe/Belgrade', label: 'Belgrade' },
|
||||
{ value: 'Europe/Berlin', label: 'Berlin' },
|
||||
{ value: 'Europe/Bratislava', label: 'Bratislava' },
|
||||
{ value: 'Europe/Brussels', label: 'Brussels' },
|
||||
{ value: 'Europe/Bucharest', label: 'Bucharest' },
|
||||
{ value: 'Europe/Budapest', label: 'Budapest' },
|
||||
{ value: 'Europe/Busingen', label: 'Busingen' },
|
||||
{ value: 'Europe/Chisinau', label: 'Chisinau' },
|
||||
{ value: 'Europe/Copenhagen', label: 'Copenhagen' },
|
||||
{ value: 'Europe/Dublin', label: 'Dublin' },
|
||||
{ value: 'Europe/Gibraltar', label: 'Gibraltar' },
|
||||
{ value: 'Europe/Guernsey', label: 'Guernsey' },
|
||||
{ value: 'Europe/Helsinki', label: 'Helsinki' },
|
||||
{ value: 'Europe/Isle_of_Man', label: 'Isle of Man' },
|
||||
{ value: 'Europe/Istanbul', label: 'Istanbul' },
|
||||
{ value: 'Europe/Jersey', label: 'Jersey' },
|
||||
{ value: 'Europe/Kaliningrad', label: 'Kaliningrad' },
|
||||
{ value: 'Europe/Kiev', label: 'Kiev' },
|
||||
{ value: 'Europe/Kirov', label: 'Kirov' },
|
||||
{ value: 'Europe/Lisbon', label: 'Lisbon' },
|
||||
{ value: 'Europe/Ljubljana', label: 'Ljubljana' },
|
||||
{ value: 'Europe/London', label: 'London' },
|
||||
{ value: 'Europe/Luxembourg', label: 'Luxembourg' },
|
||||
{ value: 'Europe/Madrid', label: 'Madrid' },
|
||||
{ value: 'Europe/Malta', label: 'Malta' },
|
||||
{ value: 'Europe/Mariehamn', label: 'Mariehamn' },
|
||||
{ value: 'Europe/Minsk', label: 'Minsk' },
|
||||
{ value: 'Europe/Monaco', label: 'Monaco' },
|
||||
{ value: 'Europe/Moscow', label: 'Moscow' },
|
||||
{ value: 'Europe/Oslo', label: 'Oslo' },
|
||||
{ value: 'Europe/Paris', label: 'Paris' },
|
||||
{ value: 'Europe/Podgorica', label: 'Podgorica' },
|
||||
{ value: 'Europe/Prague', label: 'Prague' },
|
||||
{ value: 'Europe/Riga', label: 'Riga' },
|
||||
{ value: 'Europe/Rome', label: 'Rome' },
|
||||
{ value: 'Europe/Samara', label: 'Samara' },
|
||||
{ value: 'Europe/San_Marino', label: 'San Marino' },
|
||||
{ value: 'Europe/Sarajevo', label: 'Sarajevo' },
|
||||
{ value: 'Europe/Saratov', label: 'Saratov' },
|
||||
{ value: 'Europe/Simferopol', label: 'Simferopol' },
|
||||
{ value: 'Europe/Skopje', label: 'Skopje' },
|
||||
{ value: 'Europe/Sofia', label: 'Sofia' },
|
||||
{ value: 'Europe/Stockholm', label: 'Stockholm' },
|
||||
{ value: 'Europe/Tallinn', label: 'Tallinn' },
|
||||
{ value: 'Europe/Tirane', label: 'Tirane' },
|
||||
{ value: 'Europe/Ulyanovsk', label: 'Ulyanovsk' },
|
||||
{ value: 'Europe/Uzhgorod', label: 'Uzhgorod' },
|
||||
{ value: 'Europe/Vaduz', label: 'Vaduz' },
|
||||
{ value: 'Europe/Vatican', label: 'Vatican' },
|
||||
{ value: 'Europe/Vienna', label: 'Vienna' },
|
||||
{ value: 'Europe/Vilnius', label: 'Vilnius' },
|
||||
{ value: 'Europe/Volgograd', label: 'Volgograd' },
|
||||
{ value: 'Europe/Warsaw', label: 'Warsaw' },
|
||||
{ value: 'Europe/Zagreb', label: 'Zagreb' },
|
||||
{ value: 'Europe/Zaporozhye', label: 'Zaporozhye' },
|
||||
{ value: 'Europe/Zurich', label: 'Zurich' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Indian',
|
||||
options: [
|
||||
{ value: 'Indian/Antananarivo', label: 'Antananarivo' },
|
||||
{ value: 'Indian/Chagos', label: 'Chagos' },
|
||||
{ value: 'Indian/Christmas', label: 'Christmas' },
|
||||
{ value: 'Indian/Cocos', label: 'Cocos' },
|
||||
{ value: 'Indian/Comoro', label: 'Comoro' },
|
||||
{ value: 'Indian/Kerguelen', label: 'Kerguelen' },
|
||||
{ value: 'Indian/Mahe', label: 'Mahe' },
|
||||
{ value: 'Indian/Maldives', label: 'Maldives' },
|
||||
{ value: 'Indian/Mauritius', label: 'Mauritius' },
|
||||
{ value: 'Indian/Mayotte', label: 'Mayotte' },
|
||||
{ value: 'Indian/Reunion', label: 'Reunion' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Pacific',
|
||||
options: [
|
||||
{ value: 'Pacific/Apia', label: 'Apia' },
|
||||
{ value: 'Pacific/Auckland', label: 'Auckland' },
|
||||
{ value: 'Pacific/Bougainville', label: 'Bougainville' },
|
||||
{ value: 'Pacific/Chatham', label: 'Chatham' },
|
||||
{ value: 'Pacific/Chuuk', label: 'Chuuk' },
|
||||
{ value: 'Pacific/Easter', label: 'Easter' },
|
||||
{ value: 'Pacific/Efate', label: 'Efate' },
|
||||
{ value: 'Pacific/Enderbury', label: 'Enderbury' },
|
||||
{ value: 'Pacific/Fakaofo', label: 'Fakaofo' },
|
||||
{ value: 'Pacific/Fiji', label: 'Fiji' },
|
||||
{ value: 'Pacific/Funafuti', label: 'Funafuti' },
|
||||
|
||||
{ value: 'Pacific/Galapagos', label: 'Galapagos' },
|
||||
{ value: 'Pacific/Gambier', label: 'Gambier' },
|
||||
{ value: 'Pacific/Guadalcanal', label: 'Guadalcanal' },
|
||||
{ value: 'Pacific/Guam', label: 'Guam' },
|
||||
{ value: 'Pacific/Honolulu', label: 'Honolulu' },
|
||||
{ value: 'Pacific/Kiritimati', label: 'Kiritimati' },
|
||||
{ value: 'Pacific/Kosrae', label: 'Kosrae' },
|
||||
{ value: 'Pacific/Kwajalein', label: 'Kwajalein' },
|
||||
{ value: 'Pacific/Majuro', label: 'Majuro' },
|
||||
{ value: 'Pacific/Marquesas', label: 'Marquesas' },
|
||||
{ value: 'Pacific/Midway', label: 'Midway' },
|
||||
{ value: 'Pacific/Nauru', label: 'Nauru' },
|
||||
{ value: 'Pacific/Niue', label: 'Niue' },
|
||||
{ value: 'Pacific/Norfolk', label: 'Norfolk' },
|
||||
{ value: 'Pacific/Noumea', label: 'Noumea' },
|
||||
{ value: 'Pacific/Pago_Pago', label: 'Pago Pago' },
|
||||
{ value: 'Pacific/Palau', label: 'Palau' },
|
||||
{ value: 'Pacific/Pitcairn', label: 'Pitcairn' },
|
||||
{ value: 'Pacific/Pohnpei', label: 'Pohnpei' },
|
||||
{ value: 'Pacific/Port_Moresby', label: 'Port Moresby' },
|
||||
{ value: 'Pacific/Rarotonga', label: 'Rarotonga' },
|
||||
{ value: 'Pacific/Saipan', label: 'Saipan' },
|
||||
{ value: 'Pacific/Tahiti', label: 'Tahiti' },
|
||||
{ value: 'Pacific/Tarawa', label: 'Tarawa' },
|
||||
{ value: 'Pacific/Tongatapu', label: 'Tongatapu' },
|
||||
{ value: 'Pacific/Wake', label: 'Wake' },
|
||||
{ value: 'Pacific/Wallis', label: 'Wallis' },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
label: 'UTC',
|
||||
options: [{ value: 'UTC', label: 'UTC' }],
|
||||
},
|
||||
];
|
||||
export const DEFAULT_TIMEZONE = 'UTC+0';
|
||||
|
|
|
@ -109,16 +109,19 @@ export interface UserInfoBase {
|
|||
*/
|
||||
status?: string;
|
||||
/** roles */
|
||||
is_admin?: true;
|
||||
is_admin?: boolean;
|
||||
}
|
||||
|
||||
export interface UserInfoRes extends UserInfoBase {
|
||||
bio: string;
|
||||
bio_html: string;
|
||||
create_time?: string;
|
||||
/** value = 1 active; value = 2 inactivated
|
||||
/**
|
||||
* value = 1 active;
|
||||
* value = 2 inactivated
|
||||
*/
|
||||
mail_status: number;
|
||||
language: string;
|
||||
e_mail?: string;
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
@ -228,6 +231,7 @@ export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted';
|
|||
|
||||
export interface AdminContentsReq extends Paging {
|
||||
status: AdminContentsFilterBy;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -257,12 +261,15 @@ export interface AdminSettingsGeneral {
|
|||
name: string;
|
||||
short_description: string;
|
||||
description: string;
|
||||
site_url: string;
|
||||
contact_email: string;
|
||||
}
|
||||
|
||||
export interface AdminSettingsInterface {
|
||||
logo: string;
|
||||
language: string;
|
||||
theme: string;
|
||||
time_zone?: string;
|
||||
}
|
||||
|
||||
export interface AdminSettingsSmtp {
|
||||
|
@ -321,3 +328,24 @@ export interface SearchResItem {
|
|||
export interface SearchRes extends ListResult<SearchResItem> {
|
||||
extra: any;
|
||||
}
|
||||
|
||||
export interface AdminDashboard {
|
||||
info: {
|
||||
question_count: number;
|
||||
answer_count: number;
|
||||
comment_count: number;
|
||||
vote_count: number;
|
||||
user_count: number;
|
||||
report_count: number;
|
||||
uploading_files: boolean;
|
||||
smtp: boolean;
|
||||
time_zone: string;
|
||||
occupying_storage_space: string;
|
||||
app_start_time: number;
|
||||
https: boolean;
|
||||
version_info: {
|
||||
remote_version: string;
|
||||
version: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom';
|
|||
|
||||
import { useAccordionButton } from 'react-bootstrap/AccordionButton';
|
||||
|
||||
import { Icon } from '@answer/components';
|
||||
import { Icon } from '@/components';
|
||||
|
||||
function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' });
|
||||
|
|
|
@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Icon } from '@answer/components';
|
||||
import { bookmark, postVote } from '@answer/api';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import { userInfoStore } from '@answer/stores';
|
||||
import { useToast } from '@answer/hooks';
|
||||
import { Icon } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { useToast } from '@/hooks';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import { bookmark, postVote } from '@/services';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
@ -32,7 +32,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
state: data?.collected,
|
||||
count: data?.collectCount,
|
||||
});
|
||||
const { username = '' } = userInfoStore((state) => state.user);
|
||||
const { username = '' } = loggedUserInfoStore((state) => state.user);
|
||||
const toast = useToast();
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
|
@ -48,7 +48,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
}, []);
|
||||
|
||||
const handleVote = (type: 'up' | 'down') => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
|||
};
|
||||
|
||||
const handleBookmark = () => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
bookmark({
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { memo, FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Avatar } from '@answer/components';
|
||||
|
||||
import { Avatar } from '@/components';
|
||||
import { formatCount } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Icon, FormatTime } from '@answer/components';
|
||||
import { Icon, FormatTime } from '@/components';
|
||||
|
||||
const ActionBar = ({
|
||||
nickName,
|
||||
|
|
|
@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { TextArea, Mentions } from '@answer/components';
|
||||
import { usePageUsers } from '@answer/hooks';
|
||||
import { TextArea, Mentions } from '@/components';
|
||||
import { usePageUsers } from '@/hooks';
|
||||
|
||||
const Form = ({
|
||||
className = '',
|
||||
|
|
|
@ -2,8 +2,8 @@ import { useState, memo } from 'react';
|
|||
import { Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { TextArea, Mentions } from '@answer/components';
|
||||
import { usePageUsers } from '@answer/hooks';
|
||||
import { TextArea, Mentions } from '@/components';
|
||||
import { usePageUsers } from '@/hooks';
|
||||
|
||||
const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
|
|
@ -7,17 +7,18 @@ import classNames from 'classnames';
|
|||
import { unionBy } from 'lodash';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import * as Types from '@answer/common/interface';
|
||||
import * as Types from '@/common/interface';
|
||||
import { Modal } from '@/components';
|
||||
import { usePageUsers, useReportModal } from '@/hooks';
|
||||
import { matchedUsers, parseUserInfo } from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import {
|
||||
useQueryComments,
|
||||
addComment,
|
||||
deleteComment,
|
||||
updateComment,
|
||||
postVote,
|
||||
} from '@answer/api';
|
||||
import { Modal } from '@answer/components';
|
||||
import { usePageUsers, useReportModal } from '@answer/hooks';
|
||||
import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils';
|
||||
} from '@/services';
|
||||
|
||||
import { Form, ActionBar, Reply } from './components';
|
||||
|
||||
|
@ -163,7 +164,7 @@ const Comment = ({ objectId, mode }) => {
|
|||
};
|
||||
|
||||
const handleVote = (id, is_cancel) => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -189,7 +190,7 @@ const Comment = ({ objectId, mode }) => {
|
|||
};
|
||||
|
||||
const handleAction = ({ action }, item) => {
|
||||
if (!isLogin(true)) {
|
||||
if (!tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (action === 'report') {
|
||||
|
|
|
@ -2,10 +2,10 @@ import { FC, useEffect, useState, memo } from 'react';
|
|||
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Modal as AnswerModal } from '@answer/components';
|
||||
import { uploadImage } from '@answer/api';
|
||||
import { Modal as AnswerModal } from '@/components';
|
||||
import ToolItem from '../toolItem';
|
||||
import { IEditorContext } from '../types';
|
||||
import { uploadImage } from '@/services';
|
||||
|
||||
const Image: FC<IEditorContext> = ({ editor }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
|
||||
|
|
|
@ -3,9 +3,9 @@ import { Card, Button } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { TagSelector, Tag } from '@answer/components';
|
||||
import { isLogin } from '@answer/utils';
|
||||
import { useFollowingTags, followTags } from '@answer/api';
|
||||
import { TagSelector, Tag } from '@/components';
|
||||
import { tryLoggedAndActicevated } from '@/utils/guard';
|
||||
import { useFollowingTags, followTags } from '@/services';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
|
@ -32,7 +32,7 @@ const Index: FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
if (!isLogin()) {
|
||||
if (!tryLoggedAndActicevated().ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,10 +37,10 @@ const Index: FC<Props> = ({ time, preFix, className }) => {
|
|||
between < 3600 * 24 * 366 &&
|
||||
dayjs.unix(from).format('YYYY') === dayjs.unix(now).format('YYYY')
|
||||
) {
|
||||
return dayjs.unix(from).format(t('dates.long_date'));
|
||||
return dayjs.unix(from).tz().format(t('dates.long_date'));
|
||||
}
|
||||
|
||||
return dayjs.unix(from).format(t('dates.long_date_with_year'));
|
||||
return dayjs.unix(from).tz().format(t('dates.long_date_with_year'));
|
||||
};
|
||||
|
||||
if (!time) {
|
||||
|
@ -50,8 +50,8 @@ const Index: FC<Props> = ({ time, preFix, className }) => {
|
|||
return (
|
||||
<time
|
||||
className={classNames('', className)}
|
||||
dateTime={dayjs.unix(time).toISOString()}
|
||||
title={dayjs.unix(time).format(t('dates.long_date_with_time'))}>
|
||||
dateTime={dayjs.unix(time).tz().toISOString()}
|
||||
title={dayjs.unix(time).tz().format(t('dates.long_date_with_time'))}>
|
||||
{preFix ? `${preFix} ` : ''}
|
||||
{formatTime(time)}
|
||||
</time>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue