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
|
/.fleet
|
||||||
/.vscode/*.log
|
/.vscode/*.log
|
||||||
/cmd/answer/*.sh
|
/cmd/answer/*.sh
|
||||||
/cmd/answer/upfiles/*
|
/cmd/answer/uploads/*
|
||||||
/cmd/logs
|
/cmd/logs
|
||||||
/configs/config-dev.yaml
|
/configs/config-dev.yaml
|
||||||
/go.work*
|
/go.work*
|
||||||
|
|
|
@ -29,7 +29,7 @@ RUN apk --no-cache add build-base git \
|
||||||
&& make clean build \
|
&& make clean build \
|
||||||
&& cp answer /usr/bin/answer
|
&& 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
|
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
|
||||||
|
|
||||||
# stage3 copy the binary and resource files into fresh container
|
# stage3 copy the binary and resource files into fresh container
|
||||||
|
|
|
@ -91,7 +91,7 @@ swaggerui:
|
||||||
service_config:
|
service_config:
|
||||||
secret_key: "answer" #encryption key
|
secret_key: "answer" #encryption key
|
||||||
web_host: "http://127.0.0.1" #Page access using domain name address
|
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
|
## 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 .
|
docker build -t answer:v1.0.0 .
|
||||||
```
|
```
|
||||||
## common problem
|
## 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 安装指引
|
||||||
|
|
||||||
安装 Answer 之前,您需要先安装基本环境。
|
## 使用 docker 安装
|
||||||
- 数据库
|
### 步骤 1: 使用 docker 命令启动项目
|
||||||
- [MySQL](http://dev.mysql.com):版本 >= 5.7
|
|
||||||
|
|
||||||
然后,您可以通过以下几种方式来安装 Answer:
|
|
||||||
|
|
||||||
- 采用 Docker 部署
|
|
||||||
- 二进制安装
|
|
||||||
- 源码安装
|
|
||||||
|
|
||||||
## 使用 Docker-compose 安装 Answer
|
|
||||||
```bash
|
```bash
|
||||||
$ mkdir answer && cd answer
|
docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
|
||||||
$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml
|
```
|
||||||
$ docker-compose up
|
### 步骤 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
|
### 步骤 3:安装完成后访问项目路径开始使用
|
||||||
可以从 Docker Hub 或者 GitHub Container registry 下载最新的 tags 镜像
|
[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)
|
||||||
|
请下载您当下系统所需要的对应版本
|
||||||
|
|
||||||
```
|
### 步骤 2: 使用命令行安装
|
||||||
# 将镜像从 docker hub 拉到本地
|
> 以下命令中 -C 指定的是 answer 所需的数据目录,您可以根据实际需要进行修改
|
||||||
$ 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 参数说明
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./answer init -C ./answer-data/
|
||||||
```
|
```
|
||||||
server:
|
server:
|
||||||
http:
|
http:
|
||||||
|
|
|
@ -4,14 +4,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/answerdev/answer/internal/base/conf"
|
||||||
"github.com/answerdev/answer/internal/cli"
|
"github.com/answerdev/answer/internal/cli"
|
||||||
|
"github.com/answerdev/answer/internal/install"
|
||||||
"github.com/answerdev/answer/internal/migrations"
|
"github.com/answerdev/answer/internal/migrations"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// configFilePath is the config file path
|
|
||||||
configFilePath string
|
|
||||||
// dataDirPath save all answer application data in this directory. like config file, upload file...
|
// dataDirPath save all answer application data in this directory. like config file, upload file...
|
||||||
dataDirPath string
|
dataDirPath string
|
||||||
// dumpDataPath dump data path
|
// dumpDataPath dump data path
|
||||||
|
@ -21,9 +21,7 @@ var (
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time)
|
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(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/")
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&configFilePath, "config", "c", "", "config path, eg: -c config.yaml")
|
|
||||||
|
|
||||||
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/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",
|
Short: "Run the application",
|
||||||
Long: `Run the application`,
|
Long: `Run the application`,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
cli.FormatAllPath(dataDirPath)
|
||||||
|
fmt.Println("config file path: ", cli.GetConfigFilePath())
|
||||||
|
fmt.Println("Answer is string..........................")
|
||||||
runApp()
|
runApp()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -59,18 +60,27 @@ To run answer, use:
|
||||||
Short: "init answer application",
|
Short: "init answer application",
|
||||||
Long: `init answer application`,
|
Long: `init answer application`,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
// check config file and database. if config file exists and database is already created, init done
|
||||||
cli.InstallAllInitialEnvironment(dataDirPath)
|
cli.InstallAllInitialEnvironment(dataDirPath)
|
||||||
c, err := readConfig()
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
fmt.Println("read config failed: ", err.Error())
|
fmt.Println("read config failed: ", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("read config successfully")
|
|
||||||
if err := migrations.InitDB(c.Data.Database); err != nil {
|
fmt.Println("config file read successfully, try to connect database...")
|
||||||
fmt.Println("init database error: ", err.Error())
|
if cli.CheckDBTableExist(c.Data.Database) {
|
||||||
|
fmt.Println("connect to database successfully and table already exists, do nothing.")
|
||||||
return
|
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",
|
Short: "upgrade Answer version",
|
||||||
Long: `upgrade Answer version`,
|
Long: `upgrade Answer version`,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
c, err := readConfig()
|
cli.FormatAllPath(dataDirPath)
|
||||||
|
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("read config failed: ", err.Error())
|
fmt.Println("read config failed: ", err.Error())
|
||||||
return
|
return
|
||||||
|
@ -100,7 +111,8 @@ To run answer, use:
|
||||||
Long: `back up data`,
|
Long: `back up data`,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
fmt.Println("Answer is backing up data")
|
fmt.Println("Answer is backing up data")
|
||||||
c, err := readConfig()
|
cli.FormatAllPath(dataDirPath)
|
||||||
|
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("read config failed: ", err.Error())
|
fmt.Println("read config failed: ", err.Error())
|
||||||
return
|
return
|
||||||
|
@ -120,8 +132,9 @@ To run answer, use:
|
||||||
Short: "checking the required environment",
|
Short: "checking the required environment",
|
||||||
Long: `Check if the current environment meets the startup requirements`,
|
Long: `Check if the current environment meets the startup requirements`,
|
||||||
Run: func(_ *cobra.Command, _ []string) {
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
cli.FormatAllPath(dataDirPath)
|
||||||
fmt.Println("Start checking the required environment...")
|
fmt.Println("Start checking the required environment...")
|
||||||
if cli.CheckConfigFile(configFilePath) {
|
if cli.CheckConfigFile(cli.GetConfigFilePath()) {
|
||||||
fmt.Println("config file exists [✔]")
|
fmt.Println("config file exists [✔]")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("config file not exists [x]")
|
fmt.Println("config file not exists [x]")
|
||||||
|
@ -133,13 +146,13 @@ To run answer, use:
|
||||||
fmt.Println("upload directory not exists [x]")
|
fmt.Println("upload directory not exists [x]")
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := readConfig()
|
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("read config failed: ", err.Error())
|
fmt.Println("read config failed: ", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cli.CheckDB(c.Data.Database) {
|
if cli.CheckDBConnection(c.Data.Database) {
|
||||||
fmt.Println("db connection successfully [✔]")
|
fmt.Println("db connection successfully [✔]")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("db connection failed [x]")
|
fmt.Println("db connection failed [x]")
|
||||||
|
|
|
@ -2,13 +2,14 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"time"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/base/conf"
|
"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/cli"
|
||||||
|
"github.com/answerdev/answer/internal/schema"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/segmentfault/pacman"
|
"github.com/segmentfault/pacman"
|
||||||
"github.com/segmentfault/pacman/contrib/conf/viper"
|
|
||||||
"github.com/segmentfault/pacman/contrib/log/zap"
|
"github.com/segmentfault/pacman/contrib/log/zap"
|
||||||
"github.com/segmentfault/pacman/contrib/server/http"
|
"github.com/segmentfault/pacman/contrib/server/http"
|
||||||
"github.com/segmentfault/pacman/log"
|
"github.com/segmentfault/pacman/log"
|
||||||
|
@ -40,8 +41,7 @@ func main() {
|
||||||
func runApp() {
|
func runApp() {
|
||||||
log.SetLogger(zap.NewLogger(
|
log.SetLogger(zap.NewLogger(
|
||||||
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
|
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
|
||||||
|
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||||
c, err := readConfig()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -50,27 +50,15 @@ func runApp() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
constant.Version = Version
|
||||||
|
schema.AppStartTime = time.Now()
|
||||||
|
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
if err := app.Run(); err != nil {
|
if err := app.Run(); err != nil {
|
||||||
panic(err)
|
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 {
|
func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application {
|
||||||
return pacman.NewApp(
|
return pacman.NewApp(
|
||||||
pacman.WithName(Name),
|
pacman.WithName(Name),
|
||||||
|
|
|
@ -44,6 +44,7 @@ import (
|
||||||
auth2 "github.com/answerdev/answer/internal/service/auth"
|
auth2 "github.com/answerdev/answer/internal/service/auth"
|
||||||
"github.com/answerdev/answer/internal/service/collection_common"
|
"github.com/answerdev/answer/internal/service/collection_common"
|
||||||
comment2 "github.com/answerdev/answer/internal/service/comment"
|
comment2 "github.com/answerdev/answer/internal/service/comment"
|
||||||
|
"github.com/answerdev/answer/internal/service/dashboard"
|
||||||
export2 "github.com/answerdev/answer/internal/service/export"
|
export2 "github.com/answerdev/answer/internal/service/export"
|
||||||
"github.com/answerdev/answer/internal/service/follow"
|
"github.com/answerdev/answer/internal/service/follow"
|
||||||
meta2 "github.com/answerdev/answer/internal/service/meta"
|
meta2 "github.com/answerdev/answer/internal/service/meta"
|
||||||
|
@ -58,6 +59,8 @@ import (
|
||||||
"github.com/answerdev/answer/internal/service/report_handle_backyard"
|
"github.com/answerdev/answer/internal/service/report_handle_backyard"
|
||||||
"github.com/answerdev/answer/internal/service/revision_common"
|
"github.com/answerdev/answer/internal/service/revision_common"
|
||||||
"github.com/answerdev/answer/internal/service/service_config"
|
"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"
|
tag2 "github.com/answerdev/answer/internal/service/tag"
|
||||||
"github.com/answerdev/answer/internal/service/tag_common"
|
"github.com/answerdev/answer/internal/service/tag_common"
|
||||||
"github.com/answerdev/answer/internal/service/uploader"
|
"github.com/answerdev/answer/internal/service/uploader"
|
||||||
|
@ -76,7 +79,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
langController := controller.NewLangController(i18nTranslator)
|
|
||||||
engine, err := data.NewDB(debug, dbConf)
|
engine, err := data.NewDB(debug, dbConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
@ -90,6 +92,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||||
cleanup()
|
cleanup()
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
siteInfoRepo := site_info.NewSiteInfo(dataData)
|
||||||
|
siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo)
|
||||||
|
langController := controller.NewLangController(i18nTranslator, siteInfoCommonService)
|
||||||
authRepo := auth.NewAuthRepo(dataData)
|
authRepo := auth.NewAuthRepo(dataData)
|
||||||
authService := auth2.NewAuthService(authRepo)
|
authService := auth2.NewAuthService(authRepo)
|
||||||
configRepo := config.NewConfigRepo(dataData)
|
configRepo := config.NewConfigRepo(dataData)
|
||||||
|
@ -99,12 +104,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||||
userRankRepo := rank.NewUserRankRepo(dataData, configRepo)
|
userRankRepo := rank.NewUserRankRepo(dataData, configRepo)
|
||||||
userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo)
|
userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo)
|
||||||
emailRepo := export.NewEmailRepo(dataData)
|
emailRepo := export.NewEmailRepo(dataData)
|
||||||
siteInfoRepo := site_info.NewSiteInfo(dataData)
|
|
||||||
emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo)
|
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)
|
captchaRepo := captcha.NewCaptchaRepo(dataData)
|
||||||
captchaService := action.NewCaptchaService(captchaRepo)
|
captchaService := action.NewCaptchaService(captchaRepo)
|
||||||
uploaderService := uploader.NewUploaderService(serviceConf)
|
uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
|
||||||
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService)
|
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService)
|
||||||
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
|
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
|
||||||
commentCommonRepo := comment.NewCommentCommonRepo(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)
|
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService)
|
||||||
questionController := controller.NewQuestionController(questionService, rankService)
|
questionController := controller.NewQuestionController(questionService, rankService)
|
||||||
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
|
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)
|
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
|
||||||
searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo)
|
searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo)
|
||||||
searchController := controller.NewSearchController(searchService)
|
searchController := controller.NewSearchController(searchService)
|
||||||
|
@ -166,14 +171,15 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||||
reasonService := reason2.NewReasonService(reasonRepo)
|
reasonService := reason2.NewReasonService(reasonRepo)
|
||||||
reasonController := controller.NewReasonController(reasonService)
|
reasonController := controller.NewReasonController(reasonService)
|
||||||
themeController := controller_backyard.NewThemeController()
|
themeController := controller_backyard.NewThemeController()
|
||||||
siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService)
|
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService)
|
||||||
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
|
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
|
||||||
siteinfoController := controller.NewSiteinfoController(siteInfoService)
|
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
|
||||||
notificationRepo := notification.NewNotificationRepo(dataData)
|
notificationRepo := notification.NewNotificationRepo(dataData)
|
||||||
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService)
|
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService)
|
||||||
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon)
|
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon)
|
||||||
notificationController := controller.NewNotificationController(notificationService)
|
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)
|
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
|
||||||
uiRouter := router.NewUIRouter()
|
uiRouter := router.NewUIRouter()
|
||||||
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)
|
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)
|
||||||
|
|
|
@ -17,4 +17,4 @@ swaggerui:
|
||||||
service_config:
|
service_config:
|
||||||
secret_key: "answer"
|
secret_key: "answer"
|
||||||
web_host: "http://127.0.0.1:9080"
|
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",
|
"description": "answer id or question title",
|
||||||
"name": "query",
|
"name": "query",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "question id",
|
|
||||||
"name": "question_id",
|
|
||||||
"in": "query"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"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": {
|
"/answer/admin/api/language/options": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -487,14 +509,14 @@ const docTemplate = `{
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Get siteinfo general",
|
"description": "get site general information",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo general",
|
"summary": "get site general information",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
|
@ -522,14 +544,14 @@ const docTemplate = `{
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Get siteinfo interface",
|
"description": "update site general information",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo interface",
|
"summary": "update site general information",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "general",
|
"description": "general",
|
||||||
|
@ -558,25 +580,14 @@ const docTemplate = `{
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Get siteinfo interface",
|
"description": "get site interface",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo interface",
|
"summary": "get site interface",
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "general",
|
|
||||||
"name": "data",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/schema.AddCommentReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
|
@ -604,14 +615,14 @@ const docTemplate = `{
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Get siteinfo interface",
|
"description": "update site info interface",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo interface",
|
"summary": "update site info interface",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "general",
|
"description": "general",
|
||||||
|
@ -2710,14 +2721,14 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
"/answer/api/v1/siteinfo": {
|
"/answer/api/v1/siteinfo": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get siteinfo",
|
"description": "get site info",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"site"
|
"site"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo",
|
"summary": "get site info",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
|
@ -5281,11 +5292,17 @@ const docTemplate = `{
|
||||||
"schema.SiteGeneralReq": {
|
"schema.SiteGeneralReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"contact_email",
|
||||||
"description",
|
"description",
|
||||||
"name",
|
"name",
|
||||||
"short_description"
|
"short_description",
|
||||||
|
"site_url"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"contact_email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 512
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 2000
|
"maxLength": 2000
|
||||||
|
@ -5297,17 +5314,27 @@ const docTemplate = `{
|
||||||
"short_description": {
|
"short_description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"site_url": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 512
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema.SiteGeneralResp": {
|
"schema.SiteGeneralResp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"contact_email",
|
||||||
"description",
|
"description",
|
||||||
"name",
|
"name",
|
||||||
"short_description"
|
"short_description",
|
||||||
|
"site_url"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"contact_email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 512
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 2000
|
"maxLength": 2000
|
||||||
|
@ -5319,6 +5346,10 @@ const docTemplate = `{
|
||||||
"short_description": {
|
"short_description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"site_url": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 512
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5326,7 +5357,8 @@ const docTemplate = `{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"language",
|
"language",
|
||||||
"theme"
|
"theme",
|
||||||
|
"time_zone"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"language": {
|
"language": {
|
||||||
|
@ -5340,6 +5372,10 @@ const docTemplate = `{
|
||||||
"theme": {
|
"theme": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 128
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"time_zone": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 128
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5347,7 +5383,8 @@ const docTemplate = `{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"language",
|
"language",
|
||||||
"theme"
|
"theme",
|
||||||
|
"time_zone"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"language": {
|
"language": {
|
||||||
|
@ -5361,6 +5398,10 @@ const docTemplate = `{
|
||||||
"theme": {
|
"theme": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 128
|
"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",
|
"description": "answer id or question title",
|
||||||
"name": "query",
|
"name": "query",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "question id",
|
|
||||||
"name": "question_id",
|
|
||||||
"in": "query"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"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": {
|
"/answer/admin/api/language/options": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -475,14 +497,14 @@
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Get siteinfo general",
|
"description": "get site general information",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo general",
|
"summary": "get site general information",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
|
@ -510,14 +532,14 @@
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Get siteinfo interface",
|
"description": "update site general information",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo interface",
|
"summary": "update site general information",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "general",
|
"description": "general",
|
||||||
|
@ -546,25 +568,14 @@
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Get siteinfo interface",
|
"description": "get site interface",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo interface",
|
"summary": "get site interface",
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "general",
|
|
||||||
"name": "data",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/schema.AddCommentReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
|
@ -592,14 +603,14 @@
|
||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Get siteinfo interface",
|
"description": "update site info interface",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"admin"
|
"admin"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo interface",
|
"summary": "update site info interface",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "general",
|
"description": "general",
|
||||||
|
@ -2698,14 +2709,14 @@
|
||||||
},
|
},
|
||||||
"/answer/api/v1/siteinfo": {
|
"/answer/api/v1/siteinfo": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get siteinfo",
|
"description": "get site info",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"site"
|
"site"
|
||||||
],
|
],
|
||||||
"summary": "Get siteinfo",
|
"summary": "get site info",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
|
@ -5269,11 +5280,17 @@
|
||||||
"schema.SiteGeneralReq": {
|
"schema.SiteGeneralReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"contact_email",
|
||||||
"description",
|
"description",
|
||||||
"name",
|
"name",
|
||||||
"short_description"
|
"short_description",
|
||||||
|
"site_url"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"contact_email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 512
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 2000
|
"maxLength": 2000
|
||||||
|
@ -5285,17 +5302,27 @@
|
||||||
"short_description": {
|
"short_description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"site_url": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 512
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema.SiteGeneralResp": {
|
"schema.SiteGeneralResp": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
"contact_email",
|
||||||
"description",
|
"description",
|
||||||
"name",
|
"name",
|
||||||
"short_description"
|
"short_description",
|
||||||
|
"site_url"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"contact_email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 512
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 2000
|
"maxLength": 2000
|
||||||
|
@ -5307,6 +5334,10 @@
|
||||||
"short_description": {
|
"short_description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 255
|
"maxLength": 255
|
||||||
|
},
|
||||||
|
"site_url": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 512
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5314,7 +5345,8 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"language",
|
"language",
|
||||||
"theme"
|
"theme",
|
||||||
|
"time_zone"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"language": {
|
"language": {
|
||||||
|
@ -5328,6 +5360,10 @@
|
||||||
"theme": {
|
"theme": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 128
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"time_zone": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 128
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -5335,7 +5371,8 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"language",
|
"language",
|
||||||
"theme"
|
"theme",
|
||||||
|
"time_zone"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"language": {
|
"language": {
|
||||||
|
@ -5349,6 +5386,10 @@
|
||||||
"theme": {
|
"theme": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 128
|
"maxLength": 128
|
||||||
|
},
|
||||||
|
"time_zone": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 128
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -983,6 +983,9 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
schema.SiteGeneralReq:
|
schema.SiteGeneralReq:
|
||||||
properties:
|
properties:
|
||||||
|
contact_email:
|
||||||
|
maxLength: 512
|
||||||
|
type: string
|
||||||
description:
|
description:
|
||||||
maxLength: 2000
|
maxLength: 2000
|
||||||
type: string
|
type: string
|
||||||
|
@ -992,13 +995,21 @@ definitions:
|
||||||
short_description:
|
short_description:
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
type: string
|
type: string
|
||||||
|
site_url:
|
||||||
|
maxLength: 512
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
|
- contact_email
|
||||||
- description
|
- description
|
||||||
- name
|
- name
|
||||||
- short_description
|
- short_description
|
||||||
|
- site_url
|
||||||
type: object
|
type: object
|
||||||
schema.SiteGeneralResp:
|
schema.SiteGeneralResp:
|
||||||
properties:
|
properties:
|
||||||
|
contact_email:
|
||||||
|
maxLength: 512
|
||||||
|
type: string
|
||||||
description:
|
description:
|
||||||
maxLength: 2000
|
maxLength: 2000
|
||||||
type: string
|
type: string
|
||||||
|
@ -1008,10 +1019,15 @@ definitions:
|
||||||
short_description:
|
short_description:
|
||||||
maxLength: 255
|
maxLength: 255
|
||||||
type: string
|
type: string
|
||||||
|
site_url:
|
||||||
|
maxLength: 512
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
|
- contact_email
|
||||||
- description
|
- description
|
||||||
- name
|
- name
|
||||||
- short_description
|
- short_description
|
||||||
|
- site_url
|
||||||
type: object
|
type: object
|
||||||
schema.SiteInterfaceReq:
|
schema.SiteInterfaceReq:
|
||||||
properties:
|
properties:
|
||||||
|
@ -1024,9 +1040,13 @@ definitions:
|
||||||
theme:
|
theme:
|
||||||
maxLength: 128
|
maxLength: 128
|
||||||
type: string
|
type: string
|
||||||
|
time_zone:
|
||||||
|
maxLength: 128
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- language
|
- language
|
||||||
- theme
|
- theme
|
||||||
|
- time_zone
|
||||||
type: object
|
type: object
|
||||||
schema.SiteInterfaceResp:
|
schema.SiteInterfaceResp:
|
||||||
properties:
|
properties:
|
||||||
|
@ -1039,9 +1059,13 @@ definitions:
|
||||||
theme:
|
theme:
|
||||||
maxLength: 128
|
maxLength: 128
|
||||||
type: string
|
type: string
|
||||||
|
time_zone:
|
||||||
|
maxLength: 128
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- language
|
- language
|
||||||
- theme
|
- theme
|
||||||
|
- time_zone
|
||||||
type: object
|
type: object
|
||||||
schema.TagItem:
|
schema.TagItem:
|
||||||
properties:
|
properties:
|
||||||
|
@ -1394,10 +1418,6 @@ paths:
|
||||||
in: query
|
in: query
|
||||||
name: query
|
name: query
|
||||||
type: string
|
type: string
|
||||||
- description: question id
|
|
||||||
in: query
|
|
||||||
name: question_id
|
|
||||||
type: string
|
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
@ -1434,6 +1454,23 @@ paths:
|
||||||
summary: AdminSetAnswerStatus
|
summary: AdminSetAnswerStatus
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- 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:
|
/answer/admin/api/language/options:
|
||||||
get:
|
get:
|
||||||
description: Get language options
|
description: Get language options
|
||||||
|
@ -1662,7 +1699,7 @@ paths:
|
||||||
- admin
|
- admin
|
||||||
/answer/admin/api/siteinfo/general:
|
/answer/admin/api/siteinfo/general:
|
||||||
get:
|
get:
|
||||||
description: Get siteinfo general
|
description: get site general information
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
@ -1677,11 +1714,11 @@ paths:
|
||||||
type: object
|
type: object
|
||||||
security:
|
security:
|
||||||
- ApiKeyAuth: []
|
- ApiKeyAuth: []
|
||||||
summary: Get siteinfo general
|
summary: get site general information
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
put:
|
put:
|
||||||
description: Get siteinfo interface
|
description: update site general information
|
||||||
parameters:
|
parameters:
|
||||||
- description: general
|
- description: general
|
||||||
in: body
|
in: body
|
||||||
|
@ -1698,19 +1735,12 @@ paths:
|
||||||
$ref: '#/definitions/handler.RespBody'
|
$ref: '#/definitions/handler.RespBody'
|
||||||
security:
|
security:
|
||||||
- ApiKeyAuth: []
|
- ApiKeyAuth: []
|
||||||
summary: Get siteinfo interface
|
summary: update site general information
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
/answer/admin/api/siteinfo/interface:
|
/answer/admin/api/siteinfo/interface:
|
||||||
get:
|
get:
|
||||||
description: Get siteinfo interface
|
description: get site interface
|
||||||
parameters:
|
|
||||||
- description: general
|
|
||||||
in: body
|
|
||||||
name: data
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/schema.AddCommentReq'
|
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
@ -1725,11 +1755,11 @@ paths:
|
||||||
type: object
|
type: object
|
||||||
security:
|
security:
|
||||||
- ApiKeyAuth: []
|
- ApiKeyAuth: []
|
||||||
summary: Get siteinfo interface
|
summary: get site interface
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
put:
|
put:
|
||||||
description: Get siteinfo interface
|
description: update site info interface
|
||||||
parameters:
|
parameters:
|
||||||
- description: general
|
- description: general
|
||||||
in: body
|
in: body
|
||||||
|
@ -1746,7 +1776,7 @@ paths:
|
||||||
$ref: '#/definitions/handler.RespBody'
|
$ref: '#/definitions/handler.RespBody'
|
||||||
security:
|
security:
|
||||||
- ApiKeyAuth: []
|
- ApiKeyAuth: []
|
||||||
summary: Get siteinfo interface
|
summary: update site info interface
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
/answer/admin/api/theme/options:
|
/answer/admin/api/theme/options:
|
||||||
|
@ -3014,7 +3044,7 @@ paths:
|
||||||
- Search
|
- Search
|
||||||
/answer/api/v1/siteinfo:
|
/answer/api/v1/siteinfo:
|
||||||
get:
|
get:
|
||||||
description: Get siteinfo
|
description: get site info
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
@ -3027,7 +3057,7 @@ paths:
|
||||||
data:
|
data:
|
||||||
$ref: '#/definitions/schema.SiteGeneralResp'
|
$ref: '#/definitions/schema.SiteGeneralResp'
|
||||||
type: object
|
type: object
|
||||||
summary: Get siteinfo
|
summary: get site info
|
||||||
tags:
|
tags:
|
||||||
- site
|
- site
|
||||||
/answer/api/v1/tag:
|
/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/goccy/go-json v0.9.11
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/google/wire v0.5.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/copier v0.3.5
|
||||||
github.com/jinzhu/now v1.1.5
|
github.com/jinzhu/now v1.1.5
|
||||||
github.com/lib/pq v1.10.7
|
github.com/lib/pq v1.10.7
|
||||||
|
@ -24,7 +25,7 @@ require (
|
||||||
github.com/segmentfault/pacman v1.0.1
|
github.com/segmentfault/pacman v1.0.1
|
||||||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05
|
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/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/log/zap v0.0.0-20221018072427-a15dd1434e05
|
||||||
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05
|
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05
|
||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.6.1
|
||||||
|
@ -35,6 +36,7 @@ require (
|
||||||
golang.org/x/crypto v0.1.0
|
golang.org/x/crypto v0.1.0
|
||||||
golang.org/x/net v0.1.0
|
golang.org/x/net v0.1.0
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
xorm.io/builder v0.3.12
|
xorm.io/builder v0.3.12
|
||||||
xorm.io/core v0.7.3
|
xorm.io/core v0.7.3
|
||||||
xorm.io/xorm v1.3.2
|
xorm.io/xorm v1.3.2
|
||||||
|
@ -110,6 +112,5 @@ require (
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.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
|
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.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/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/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-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/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=
|
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/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 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/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-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0=
|
||||||
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/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 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc=
|
||||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk=
|
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=
|
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A=
|
||||||
|
|
956
i18n/en_US.yaml
956
i18n/en_US.yaml
|
@ -1,3 +1,5 @@
|
||||||
|
# The following fields are used for back-end
|
||||||
|
backend:
|
||||||
base:
|
base:
|
||||||
success:
|
success:
|
||||||
other: "Success."
|
other: "Success."
|
||||||
|
@ -89,6 +91,17 @@ error:
|
||||||
set_avatar:
|
set_avatar:
|
||||||
other: "Avatar set failed."
|
other: "Avatar set failed."
|
||||||
|
|
||||||
|
config:
|
||||||
|
read_config_failed:
|
||||||
|
other: "Read config failed"
|
||||||
|
database:
|
||||||
|
connection_failed:
|
||||||
|
other: "Database connection failed"
|
||||||
|
create_table_failed:
|
||||||
|
other: "Create table failed"
|
||||||
|
install:
|
||||||
|
create_config_failed:
|
||||||
|
other: "Can’t create the config.yaml file."
|
||||||
report:
|
report:
|
||||||
spam:
|
spam:
|
||||||
name:
|
name:
|
||||||
|
@ -170,3 +183,946 @@ notification:
|
||||||
other: "Your answer has been deleted"
|
other: "Your answer has been deleted"
|
||||||
your_comment_was_deleted:
|
your_comment_was_deleted:
|
||||||
other: "Your comment has been deleted"
|
other: "Your comment has been deleted"
|
||||||
|
# The following fields are used for interface presentation(Front-end)
|
||||||
|
ui:
|
||||||
|
how_to_format:
|
||||||
|
title: How to Format
|
||||||
|
description: >-
|
||||||
|
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre
|
||||||
|
class="mb-2"><code><https://url.com><br/><br/>[Title](https://url.com)</code></pre></li><li><p
|
||||||
|
class="mb-2">put returns between paragraphs</p></li><li><p
|
||||||
|
class="mb-2"><em>_italic_</em> or **<strong>bold</strong>**</p></li><li><p
|
||||||
|
class="mb-2">indent code by 4 spaces</p></li><li><p class="mb-2">quote by
|
||||||
|
placing <code>></code> at start of line</p></li><li><p
|
||||||
|
class="mb-2">backtick escapes <code>`like _this_`</code></p></li><li><p
|
||||||
|
class="mb-2">create code fences with backticks <code>`</code></p><pre
|
||||||
|
class="mb-0"><code>```<br/>code here<br/>```</code></pre></li></ul>
|
||||||
|
pagination:
|
||||||
|
prev: Prev
|
||||||
|
next: Next
|
||||||
|
page_title:
|
||||||
|
question: Question
|
||||||
|
questions: Questions
|
||||||
|
tag: Tag
|
||||||
|
tags: Tags
|
||||||
|
tag_wiki: tag wiki
|
||||||
|
edit_tag: Edit Tag
|
||||||
|
ask_a_question: Add Question
|
||||||
|
edit_question: Edit Question
|
||||||
|
edit_answer: Edit Answer
|
||||||
|
search: Search
|
||||||
|
posts_containing: Posts containing
|
||||||
|
settings: Settings
|
||||||
|
notifications: Notifications
|
||||||
|
login: Log In
|
||||||
|
sign_up: Sign Up
|
||||||
|
account_recovery: Account Recovery
|
||||||
|
account_activation: Account Activation
|
||||||
|
confirm_email: Confirm Email
|
||||||
|
account_suspended: Account Suspended
|
||||||
|
admin: Admin
|
||||||
|
change_email: Modify Email
|
||||||
|
install: Answer Installation
|
||||||
|
upgrade: Answer Upgrade
|
||||||
|
maintenance: Website Maintenance
|
||||||
|
notifications:
|
||||||
|
title: Notifications
|
||||||
|
inbox: Inbox
|
||||||
|
achievement: Achievements
|
||||||
|
all_read: Mark all as read
|
||||||
|
show_more: Show more
|
||||||
|
suspended:
|
||||||
|
title: Your Account has been Suspended
|
||||||
|
until_time: 'Your account was suspended until {{ time }}.'
|
||||||
|
forever: This user was suspended forever.
|
||||||
|
end: You don't meet a community guideline.
|
||||||
|
editor:
|
||||||
|
blockquote:
|
||||||
|
text: Blockquote
|
||||||
|
bold:
|
||||||
|
text: Strong
|
||||||
|
chart:
|
||||||
|
text: Chart
|
||||||
|
flow_chart: Flow chart
|
||||||
|
sequence_diagram: Sequence diagram
|
||||||
|
class_diagram: Class diagram
|
||||||
|
state_diagram: State diagram
|
||||||
|
entity_relationship_diagram: Entity relationship diagram
|
||||||
|
user_defined_diagram: User defined diagram
|
||||||
|
gantt_chart: Gantt chart
|
||||||
|
pie_chart: Pie chart
|
||||||
|
code:
|
||||||
|
text: Code Sample
|
||||||
|
add_code: Add code sample
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
code:
|
||||||
|
label: Code
|
||||||
|
msg:
|
||||||
|
empty: Code cannot be empty.
|
||||||
|
language:
|
||||||
|
label: Language (optional)
|
||||||
|
placeholder: Automatic detection
|
||||||
|
btn_cancel: Cancel
|
||||||
|
btn_confirm: Add
|
||||||
|
formula:
|
||||||
|
text: Formula
|
||||||
|
options:
|
||||||
|
inline: Inline formula
|
||||||
|
block: Block formula
|
||||||
|
heading:
|
||||||
|
text: Heading
|
||||||
|
options:
|
||||||
|
h1: Heading 1
|
||||||
|
h2: Heading 2
|
||||||
|
h3: Heading 3
|
||||||
|
h4: Heading 4
|
||||||
|
h5: Heading 5
|
||||||
|
h6: Heading 6
|
||||||
|
help:
|
||||||
|
text: Help
|
||||||
|
hr:
|
||||||
|
text: Horizontal Rule
|
||||||
|
image:
|
||||||
|
text: Image
|
||||||
|
add_image: Add image
|
||||||
|
tab_image: Upload image
|
||||||
|
form_image:
|
||||||
|
fields:
|
||||||
|
file:
|
||||||
|
label: Image File
|
||||||
|
btn: Select image
|
||||||
|
msg:
|
||||||
|
empty: File cannot be empty.
|
||||||
|
only_image: Only image files are allowed.
|
||||||
|
max_size: File size cannot exceed 4MB.
|
||||||
|
description:
|
||||||
|
label: Description (optional)
|
||||||
|
tab_url: Image URL
|
||||||
|
form_url:
|
||||||
|
fields:
|
||||||
|
url:
|
||||||
|
label: Image URL
|
||||||
|
msg:
|
||||||
|
empty: Image URL cannot be empty.
|
||||||
|
name:
|
||||||
|
label: Description (optional)
|
||||||
|
btn_cancel: Cancel
|
||||||
|
btn_confirm: Add
|
||||||
|
uploading: Uploading
|
||||||
|
indent:
|
||||||
|
text: Indent
|
||||||
|
outdent:
|
||||||
|
text: Outdent
|
||||||
|
italic:
|
||||||
|
text: Emphasis
|
||||||
|
link:
|
||||||
|
text: Hyperlink
|
||||||
|
add_link: Add hyperlink
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
url:
|
||||||
|
label: URL
|
||||||
|
msg:
|
||||||
|
empty: URL cannot be empty.
|
||||||
|
name:
|
||||||
|
label: Description (optional)
|
||||||
|
btn_cancel: Cancel
|
||||||
|
btn_confirm: Add
|
||||||
|
ordered_list:
|
||||||
|
text: Numbered List
|
||||||
|
unordered_list:
|
||||||
|
text: Bulleted List
|
||||||
|
table:
|
||||||
|
text: Table
|
||||||
|
heading: Heading
|
||||||
|
cell: Cell
|
||||||
|
close_modal:
|
||||||
|
title: I am closing this post as...
|
||||||
|
btn_cancel: Cancel
|
||||||
|
btn_submit: Submit
|
||||||
|
remark:
|
||||||
|
empty: Cannot be empty.
|
||||||
|
msg:
|
||||||
|
empty: Please select a reason.
|
||||||
|
report_modal:
|
||||||
|
flag_title: I am flagging to report this post as...
|
||||||
|
close_title: I am closing this post as...
|
||||||
|
review_question_title: Review question
|
||||||
|
review_answer_title: Review answer
|
||||||
|
review_comment_title: Review comment
|
||||||
|
btn_cancel: Cancel
|
||||||
|
btn_submit: Submit
|
||||||
|
remark:
|
||||||
|
empty: Cannot be empty.
|
||||||
|
msg:
|
||||||
|
empty: Please select a reason.
|
||||||
|
tag_modal:
|
||||||
|
title: Create new tag
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
display_name:
|
||||||
|
label: Display Name
|
||||||
|
msg:
|
||||||
|
empty: Display name cannot be empty.
|
||||||
|
range: Display name up to 35 characters.
|
||||||
|
slug_name:
|
||||||
|
label: URL Slug
|
||||||
|
description: 'Must use the character set "a-z", "0-9", "+ # - ."'
|
||||||
|
msg:
|
||||||
|
empty: URL slug cannot be empty.
|
||||||
|
range: URL slug up to 35 characters.
|
||||||
|
character: URL slug contains unallowed character set.
|
||||||
|
description:
|
||||||
|
label: Description (optional)
|
||||||
|
btn_cancel: Cancel
|
||||||
|
btn_submit: Submit
|
||||||
|
tag_info:
|
||||||
|
created_at: Created
|
||||||
|
edited_at: Edited
|
||||||
|
synonyms:
|
||||||
|
title: Synonyms
|
||||||
|
text: The following tags will be remapped to
|
||||||
|
empty: No synonyms found.
|
||||||
|
btn_add: Add a synonym
|
||||||
|
btn_edit: Edit
|
||||||
|
btn_save: Save
|
||||||
|
synonyms_text: The following tags will be remapped to
|
||||||
|
delete:
|
||||||
|
title: Delete this tag
|
||||||
|
content: >-
|
||||||
|
<p>We do not allowed deleting tag with posts.</p><p>Please remove this tag
|
||||||
|
from the posts first.</p>
|
||||||
|
content2: Are you sure you wish to delete?
|
||||||
|
close: Close
|
||||||
|
edit_tag:
|
||||||
|
title: Edit Tag
|
||||||
|
default_reason: Edit tag
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
revision:
|
||||||
|
label: Revision
|
||||||
|
display_name:
|
||||||
|
label: Display Name
|
||||||
|
slug_name:
|
||||||
|
label: URL Slug
|
||||||
|
info: 'Must use the character set "a-z", "0-9", "+ # - ."'
|
||||||
|
description:
|
||||||
|
label: Description
|
||||||
|
edit_summary:
|
||||||
|
label: Edit Summary
|
||||||
|
placeholder: >-
|
||||||
|
Briefly explain your changes (corrected spelling, fixed grammar,
|
||||||
|
improved formatting)
|
||||||
|
btn_save_edits: Save edits
|
||||||
|
btn_cancel: Cancel
|
||||||
|
dates:
|
||||||
|
long_date: MMM D
|
||||||
|
long_date_with_year: 'MMM D, YYYY'
|
||||||
|
long_date_with_time: 'MMM D, YYYY [at] HH:mm'
|
||||||
|
now: now
|
||||||
|
x_seconds_ago: '{{count}}s ago'
|
||||||
|
x_minutes_ago: '{{count}}m ago'
|
||||||
|
x_hours_ago: '{{count}}h ago'
|
||||||
|
hour: hour
|
||||||
|
day: day
|
||||||
|
comment:
|
||||||
|
btn_add_comment: Add comment
|
||||||
|
reply_to: Reply to
|
||||||
|
btn_reply: Reply
|
||||||
|
btn_edit: Edit
|
||||||
|
btn_delete: Delete
|
||||||
|
btn_flag: Flag
|
||||||
|
btn_save_edits: Save edits
|
||||||
|
btn_cancel: Cancel
|
||||||
|
show_more: Show more comment
|
||||||
|
tip_question: >-
|
||||||
|
Use comments to ask for more information or suggest improvements. Avoid
|
||||||
|
answering questions in comments.
|
||||||
|
tip_answer: >-
|
||||||
|
Use comments to reply to other users or notify them of changes. If you are
|
||||||
|
adding new information, edit your post instead of commenting.
|
||||||
|
edit_answer:
|
||||||
|
title: Edit Answer
|
||||||
|
default_reason: Edit answer
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
revision:
|
||||||
|
label: Revision
|
||||||
|
answer:
|
||||||
|
label: Answer
|
||||||
|
edit_summary:
|
||||||
|
label: Edit Summary
|
||||||
|
placeholder: >-
|
||||||
|
Briefly explain your changes (corrected spelling, fixed grammar,
|
||||||
|
improved formatting)
|
||||||
|
btn_save_edits: Save edits
|
||||||
|
btn_cancel: Cancel
|
||||||
|
tags:
|
||||||
|
title: Tags
|
||||||
|
sort_buttons:
|
||||||
|
popular: Popular
|
||||||
|
name: Name
|
||||||
|
newest: newest
|
||||||
|
button_follow: Follow
|
||||||
|
button_following: Following
|
||||||
|
tag_label: questions
|
||||||
|
search_placeholder: Filter by tag name
|
||||||
|
no_description: The tag has no description.
|
||||||
|
more: More
|
||||||
|
ask:
|
||||||
|
title: Add Question
|
||||||
|
edit_title: Edit Question
|
||||||
|
default_reason: Edit question
|
||||||
|
similar_questions: Similar questions
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
revision:
|
||||||
|
label: Revision
|
||||||
|
title:
|
||||||
|
label: Title
|
||||||
|
placeholder: Be specific and imagine you're asking a question to another person
|
||||||
|
msg:
|
||||||
|
empty: Title cannot be empty.
|
||||||
|
range: Title up to 150 characters
|
||||||
|
body:
|
||||||
|
label: Body
|
||||||
|
msg:
|
||||||
|
empty: Body cannot be empty.
|
||||||
|
tags:
|
||||||
|
label: Tags
|
||||||
|
msg:
|
||||||
|
empty: Tags cannot be empty.
|
||||||
|
answer:
|
||||||
|
label: Answer
|
||||||
|
msg:
|
||||||
|
empty: Answer cannot be empty.
|
||||||
|
btn_post_question: Post your question
|
||||||
|
btn_save_edits: Save edits
|
||||||
|
answer_question: Answer your own question
|
||||||
|
post_question&answer: Post your question and answer
|
||||||
|
tag_selector:
|
||||||
|
add_btn: Add tag
|
||||||
|
create_btn: Create new tag
|
||||||
|
search_tag: Search tag
|
||||||
|
hint: 'Describe what your question is about, at least one tag is required.'
|
||||||
|
no_result: No tags matched
|
||||||
|
header:
|
||||||
|
nav:
|
||||||
|
question: Questions
|
||||||
|
tag: Tags
|
||||||
|
user: Users
|
||||||
|
profile: Profile
|
||||||
|
setting: Settings
|
||||||
|
logout: Log out
|
||||||
|
admin: Admin
|
||||||
|
search:
|
||||||
|
placeholder: Search
|
||||||
|
footer:
|
||||||
|
build_on: >-
|
||||||
|
Built on <1> Answer </1>- the open-source software that power Q&A
|
||||||
|
communities<br />Made with love © 2022 Answer
|
||||||
|
upload_img:
|
||||||
|
name: Change
|
||||||
|
loading: loading...
|
||||||
|
pic_auth_code:
|
||||||
|
title: Captcha
|
||||||
|
placeholder: Type the text above
|
||||||
|
msg:
|
||||||
|
empty: Captcha cannot be empty.
|
||||||
|
inactive:
|
||||||
|
first: >-
|
||||||
|
You're almost done! We sent an activation mail to <bold>{{mail}}</bold>.
|
||||||
|
Please follow the instructions in the mail to activate your account.
|
||||||
|
info: 'If it doesn''t arrive, check your spam folder.'
|
||||||
|
another: >-
|
||||||
|
We sent another activation email to you at <bold>{{mail}}</bold>. It might
|
||||||
|
take a few minutes for it to arrive; be sure to check your spam folder.
|
||||||
|
btn_name: Resend activation email
|
||||||
|
change_btn_name: Change email
|
||||||
|
msg:
|
||||||
|
empty: Cannot be empty.
|
||||||
|
login:
|
||||||
|
page_title: Welcome to Answer
|
||||||
|
info_sign: Don't have an account? <1>Sign up</1>
|
||||||
|
info_login: Already have an account? <1>Log in</1>
|
||||||
|
forgot_pass: Forgot password?
|
||||||
|
name:
|
||||||
|
label: Name
|
||||||
|
msg:
|
||||||
|
empty: Name cannot be empty.
|
||||||
|
range: Name up to 30 characters.
|
||||||
|
email:
|
||||||
|
label: Email
|
||||||
|
msg:
|
||||||
|
empty: Email cannot be empty.
|
||||||
|
password:
|
||||||
|
label: Password
|
||||||
|
msg:
|
||||||
|
empty: Password cannot be empty.
|
||||||
|
different: The passwords entered on both sides are inconsistent
|
||||||
|
account_forgot:
|
||||||
|
page_title: Forgot Your Password
|
||||||
|
btn_name: Send me recovery email
|
||||||
|
send_success: >-
|
||||||
|
If an account matches <strong>{{mail}}</strong>, you should receive an email
|
||||||
|
with instructions on how to reset your password shortly.
|
||||||
|
email:
|
||||||
|
label: Email
|
||||||
|
msg:
|
||||||
|
empty: Email cannot be empty.
|
||||||
|
change_email:
|
||||||
|
page_title: Welcome to Answer
|
||||||
|
btn_cancel: Cancel
|
||||||
|
btn_update: Update email address
|
||||||
|
send_success: >-
|
||||||
|
If an account matches <strong>{{mail}}</strong>, you should receive an email
|
||||||
|
with instructions on how to reset your password shortly.
|
||||||
|
email:
|
||||||
|
label: New Email
|
||||||
|
msg:
|
||||||
|
empty: Email cannot be empty.
|
||||||
|
password_reset:
|
||||||
|
page_title: Password Reset
|
||||||
|
btn_name: Reset my password
|
||||||
|
reset_success: >-
|
||||||
|
You successfully changed your password; you will be redirected to the log in
|
||||||
|
page.
|
||||||
|
link_invalid: >-
|
||||||
|
Sorry, this password reset link is no longer valid. Perhaps your password is
|
||||||
|
already reset?
|
||||||
|
to_login: Continue to log in page
|
||||||
|
password:
|
||||||
|
label: Password
|
||||||
|
msg:
|
||||||
|
empty: Password cannot be empty.
|
||||||
|
length: The length needs to be between 8 and 32
|
||||||
|
different: The passwords entered on both sides are inconsistent
|
||||||
|
password_confirm:
|
||||||
|
label: Confirm New Password
|
||||||
|
settings:
|
||||||
|
page_title: Settings
|
||||||
|
nav:
|
||||||
|
profile: Profile
|
||||||
|
notification: Notifications
|
||||||
|
account: Account
|
||||||
|
interface: Interface
|
||||||
|
profile:
|
||||||
|
btn_name: Update profile
|
||||||
|
display_name:
|
||||||
|
label: Display Name
|
||||||
|
msg: Display name cannot be empty.
|
||||||
|
msg_range: Display name up to 30 characters
|
||||||
|
username:
|
||||||
|
label: Username
|
||||||
|
caption: People can mention you as "@username".
|
||||||
|
msg: Username cannot be empty.
|
||||||
|
msg_range: Username up to 30 characters
|
||||||
|
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||||
|
avatar:
|
||||||
|
label: Profile Image
|
||||||
|
gravatar: Gravatar
|
||||||
|
gravatar_text: You can change image on <1>gravatar.com</1>
|
||||||
|
custom: Custom
|
||||||
|
btn_refresh: Refresh
|
||||||
|
custom_text: You can upload your image.
|
||||||
|
default: Default
|
||||||
|
msg: Please upload an avatar
|
||||||
|
bio:
|
||||||
|
label: About Me (optional)
|
||||||
|
website:
|
||||||
|
label: Website (optional)
|
||||||
|
placeholder: 'https://example.com'
|
||||||
|
msg: Website incorrect format
|
||||||
|
location:
|
||||||
|
label: Location (optional)
|
||||||
|
placeholder: 'City, Country'
|
||||||
|
notification:
|
||||||
|
email:
|
||||||
|
label: Email Notifications
|
||||||
|
radio: 'Answers to your questions, comments, and more'
|
||||||
|
account:
|
||||||
|
change_email_btn: Change email
|
||||||
|
change_pass_btn: Change password
|
||||||
|
change_email_info: >-
|
||||||
|
We've sent an email to that address. Please follow the confirmation
|
||||||
|
instructions.
|
||||||
|
email:
|
||||||
|
label: Email
|
||||||
|
msg: Email cannot be empty.
|
||||||
|
password_title: Password
|
||||||
|
current_pass:
|
||||||
|
label: Current Password
|
||||||
|
msg:
|
||||||
|
empty: Current Password cannot be empty.
|
||||||
|
length: The length needs to be between 8 and 32.
|
||||||
|
different: The two entered passwords do not match.
|
||||||
|
new_pass:
|
||||||
|
label: New Password
|
||||||
|
pass_confirm:
|
||||||
|
label: Confirm New Password
|
||||||
|
interface:
|
||||||
|
lang:
|
||||||
|
label: Interface Language
|
||||||
|
text: User interface language. It will change when you refresh the page.
|
||||||
|
toast:
|
||||||
|
update: update success
|
||||||
|
update_password: Password changed successfully.
|
||||||
|
flag_success: Thanks for flagging.
|
||||||
|
related_question:
|
||||||
|
title: Related Questions
|
||||||
|
btn: Add question
|
||||||
|
answers: answers
|
||||||
|
question_detail:
|
||||||
|
Asked: Asked
|
||||||
|
asked: asked
|
||||||
|
update: Modified
|
||||||
|
edit: edited
|
||||||
|
Views: Viewed
|
||||||
|
Follow: Follow
|
||||||
|
Following: Following
|
||||||
|
answered: answered
|
||||||
|
closed_in: Closed in
|
||||||
|
show_exist: Show existing question.
|
||||||
|
answers:
|
||||||
|
title: Answers
|
||||||
|
score: Score
|
||||||
|
newest: Newest
|
||||||
|
btn_accept: Accept
|
||||||
|
btn_accepted: Accepted
|
||||||
|
write_answer:
|
||||||
|
title: Your Answer
|
||||||
|
btn_name: Post your answer
|
||||||
|
confirm_title: Continue to answer
|
||||||
|
continue: Continue
|
||||||
|
confirm_info: >-
|
||||||
|
<p>Are you sure you want to add another answer?</p><p>You could use the
|
||||||
|
edit link to refine and improve your existing answer, instead.</p>
|
||||||
|
empty: Answer cannot be empty.
|
||||||
|
delete:
|
||||||
|
title: Delete this post
|
||||||
|
question: >-
|
||||||
|
We do not recommend <strong>deleting questions with answers</strong> because
|
||||||
|
doing so deprives future readers of this knowledge.</p><p>Repeated deletion
|
||||||
|
of answered questions can result in your account being blocked from asking.
|
||||||
|
Are you sure you wish to delete?
|
||||||
|
answer_accepted: >-
|
||||||
|
<p>We do not recommend <strong>deleting accepted answer</strong> because
|
||||||
|
doing so deprives future readers of this knowledge. </p> Repeated deletion
|
||||||
|
of accepted answers can result in your account being blocked from answering.
|
||||||
|
Are you sure you wish to delete?
|
||||||
|
other: Are you sure you wish to delete?
|
||||||
|
tip_question_deleted: This post has been deleted
|
||||||
|
tip_answer_deleted: This answer has been deleted
|
||||||
|
btns:
|
||||||
|
confirm: Confirm
|
||||||
|
cancel: Cancel
|
||||||
|
save: Save
|
||||||
|
delete: Delete
|
||||||
|
login: Log in
|
||||||
|
signup: Sign up
|
||||||
|
logout: Log out
|
||||||
|
verify: Verify
|
||||||
|
add_question: Add question
|
||||||
|
search:
|
||||||
|
title: Search Results
|
||||||
|
keywords: Keywords
|
||||||
|
options: Options
|
||||||
|
follow: Follow
|
||||||
|
following: Following
|
||||||
|
counts: '{{count}} Results'
|
||||||
|
more: More
|
||||||
|
sort_btns:
|
||||||
|
relevance: Relevance
|
||||||
|
newest: Newest
|
||||||
|
active: Active
|
||||||
|
score: Score
|
||||||
|
more: More
|
||||||
|
tips:
|
||||||
|
title: Advanced Search Tips
|
||||||
|
tag: '<1>[tag]</1> search withing a tag'
|
||||||
|
user: '<1>user:username</1> search by author'
|
||||||
|
answer: '<1>answers:0</1> unanswered questions'
|
||||||
|
score: '<1>score:3</1> posts with a 3+ score'
|
||||||
|
question: '<1>is:question</1> search questions'
|
||||||
|
is_answer: '<1>is:answer</1> search answers'
|
||||||
|
empty: We couldn't find anything. <br /> Try different or less specific keywords.
|
||||||
|
share:
|
||||||
|
name: Share
|
||||||
|
copy: Copy link
|
||||||
|
via: Share post via...
|
||||||
|
copied: Copied
|
||||||
|
facebook: Share to Facebook
|
||||||
|
twitter: Share to Twitter
|
||||||
|
cannot_vote_for_self: You can't vote for your own post
|
||||||
|
modal_confirm:
|
||||||
|
title: Error...
|
||||||
|
account_result:
|
||||||
|
page_title: Welcome to Answer
|
||||||
|
success: Your new account is confirmed; you will be redirected to the home page.
|
||||||
|
link: Continue to homepage
|
||||||
|
invalid: >-
|
||||||
|
Sorry, this account confirmation link is no longer valid. Perhaps your
|
||||||
|
account is already active?
|
||||||
|
confirm_new_email: Your email has been updated.
|
||||||
|
confirm_new_email_invalid: >-
|
||||||
|
Sorry, this confirmation link is no longer valid. Perhaps your email was
|
||||||
|
already changed?
|
||||||
|
question:
|
||||||
|
following_tags: Following Tags
|
||||||
|
edit: Edit
|
||||||
|
save: Save
|
||||||
|
follow_tag_tip: Follow tags to curate your list of questions.
|
||||||
|
hot_questions: Hot Questions
|
||||||
|
all_questions: All Questions
|
||||||
|
x_questions: '{{ count }} Questions'
|
||||||
|
x_answers: '{{ count }} answers'
|
||||||
|
questions: Questions
|
||||||
|
answers: Answers
|
||||||
|
newest: Newest
|
||||||
|
active: Active
|
||||||
|
frequent: Frequent
|
||||||
|
score: Score
|
||||||
|
unanswered: Unanswered
|
||||||
|
modified: modified
|
||||||
|
answered: answered
|
||||||
|
asked: asked
|
||||||
|
closed: closed
|
||||||
|
follow_a_tag: Follow a tag
|
||||||
|
more: More
|
||||||
|
personal:
|
||||||
|
overview: Overview
|
||||||
|
answers: Answers
|
||||||
|
answer: answer
|
||||||
|
questions: Questions
|
||||||
|
question: question
|
||||||
|
bookmarks: Bookmarks
|
||||||
|
reputation: Reputation
|
||||||
|
comments: Comments
|
||||||
|
votes: Votes
|
||||||
|
newest: Newest
|
||||||
|
score: Score
|
||||||
|
edit_profile: Edit Profile
|
||||||
|
visited_x_days: 'Visited {{ count }} days'
|
||||||
|
viewed: Viewed
|
||||||
|
joined: Joined
|
||||||
|
last_login: Seen
|
||||||
|
about_me: About Me
|
||||||
|
about_me_empty: '// Hello, World !'
|
||||||
|
top_answers: Top Answers
|
||||||
|
top_questions: Top Questions
|
||||||
|
stats: Stats
|
||||||
|
list_empty: No posts found.<br />Perhaps you'd like to select a different tab?
|
||||||
|
accepted: Accepted
|
||||||
|
answered: answered
|
||||||
|
asked: asked
|
||||||
|
upvote: upvote
|
||||||
|
downvote: downvote
|
||||||
|
mod_short: Mod
|
||||||
|
mod_long: Moderators
|
||||||
|
x_reputation: reputation
|
||||||
|
x_votes: votes received
|
||||||
|
x_answers: answers
|
||||||
|
x_questions: questions
|
||||||
|
install:
|
||||||
|
title: Answer
|
||||||
|
next: Next
|
||||||
|
done: Done
|
||||||
|
config_yaml_error: Can’t create the config.yaml file.
|
||||||
|
lang:
|
||||||
|
label: Please Choose a Language
|
||||||
|
db_type:
|
||||||
|
label: Database Engine
|
||||||
|
db_username:
|
||||||
|
label: Username
|
||||||
|
placeholder: root
|
||||||
|
msg: Username cannot be empty.
|
||||||
|
db_password:
|
||||||
|
label: Password
|
||||||
|
placeholder: root
|
||||||
|
msg: Password cannot be empty.
|
||||||
|
db_host:
|
||||||
|
label: Database Host
|
||||||
|
placeholder: 'db:3306'
|
||||||
|
msg: Database Host cannot be empty.
|
||||||
|
db_name:
|
||||||
|
label: Database Name
|
||||||
|
placeholder: answer
|
||||||
|
msg: Database Name cannot be empty.
|
||||||
|
db_file:
|
||||||
|
label: Database File
|
||||||
|
placeholder: /data/answer.db
|
||||||
|
msg: Database File cannot be empty.
|
||||||
|
config_yaml:
|
||||||
|
title: Create config.yaml
|
||||||
|
label: The config.yaml file created.
|
||||||
|
description: >-
|
||||||
|
You can create the <1>config.yaml</1> file manually in the
|
||||||
|
<1>/var/wwww/xxx/</1> directory and paste the following text into it.
|
||||||
|
info: 'After you’ve done that, click “Next” button.'
|
||||||
|
site_information: Site Information
|
||||||
|
admin_account: Admin Account
|
||||||
|
site_name:
|
||||||
|
label: Site Name
|
||||||
|
msg: Site Name cannot be empty.
|
||||||
|
site_url:
|
||||||
|
label: Site URL
|
||||||
|
text: The address of your site.
|
||||||
|
msg:
|
||||||
|
empty: Site URL cannot be empty.
|
||||||
|
incorrect: Site URL incorrect format.
|
||||||
|
contact_email:
|
||||||
|
label: Contact Email
|
||||||
|
text: Email address of key contact responsible for this site.
|
||||||
|
msg:
|
||||||
|
empty: Contact Email cannot be empty.
|
||||||
|
incorrect: Contact Email incorrect format.
|
||||||
|
admin_name:
|
||||||
|
label: Name
|
||||||
|
msg: Name cannot be empty.
|
||||||
|
admin_password:
|
||||||
|
label: Password
|
||||||
|
text: >-
|
||||||
|
You will need this password to log in. Please store it in a secure
|
||||||
|
location.
|
||||||
|
msg: Password cannot be empty.
|
||||||
|
admin_email:
|
||||||
|
label: Email
|
||||||
|
text: You will need this email to log in.
|
||||||
|
msg:
|
||||||
|
empty: Email cannot be empty.
|
||||||
|
incorrect: Email incorrect format.
|
||||||
|
ready_title: Your Answer is Ready!
|
||||||
|
ready_description: >-
|
||||||
|
If you ever feel like changing more settings, visit <1>admin section</1>;
|
||||||
|
find it in the site menu.
|
||||||
|
good_luck: 'Have fun, and good luck!'
|
||||||
|
warn_title: Warning
|
||||||
|
warn_description: >-
|
||||||
|
The file <1>config.yaml</1> already exists. If you need to reset any of the
|
||||||
|
configuration items in this file, please delete it first.
|
||||||
|
install_now: You may try <1>installing now</1>.
|
||||||
|
installed: Already installed
|
||||||
|
installed_description: >-
|
||||||
|
You appear to have already installed. To reinstall please clear your old
|
||||||
|
database tables first.
|
||||||
|
db_failed: Database connection failed
|
||||||
|
db_failed_description: >-
|
||||||
|
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down.
|
||||||
|
|
||||||
|
page_404:
|
||||||
|
description: 'Unfortunately, this page doesn''t exist.'
|
||||||
|
back_home: Back to homepage
|
||||||
|
page_50X:
|
||||||
|
description: The server encountered an error and could not complete your request.
|
||||||
|
back_home: Back to homepage
|
||||||
|
page_maintenance:
|
||||||
|
description: 'We are under maintenance, we’ll be back soon.'
|
||||||
|
admin:
|
||||||
|
admin_header:
|
||||||
|
title: Admin
|
||||||
|
nav_menus:
|
||||||
|
dashboard: Dashboard
|
||||||
|
contents: Contents
|
||||||
|
questions: Questions
|
||||||
|
answers: Answers
|
||||||
|
users: Users
|
||||||
|
flags: Flags
|
||||||
|
settings: Settings
|
||||||
|
general: General
|
||||||
|
interface: Interface
|
||||||
|
smtp: SMTP
|
||||||
|
dashboard:
|
||||||
|
title: Dashboard
|
||||||
|
welcome: Welcome to Answer Admin!
|
||||||
|
site_statistics: Site Statistics
|
||||||
|
questions: 'Questions:'
|
||||||
|
answers: 'Answers:'
|
||||||
|
comments: 'Comments:'
|
||||||
|
votes: 'Votes:'
|
||||||
|
active_users: 'Active users:'
|
||||||
|
flags: 'Flags:'
|
||||||
|
site_health_status: Site Health Status
|
||||||
|
version: 'Version:'
|
||||||
|
https: 'HTTPS:'
|
||||||
|
uploading_files: 'Uploading files:'
|
||||||
|
smtp: 'SMTP:'
|
||||||
|
timezone: 'Timezone:'
|
||||||
|
system_info: System Info
|
||||||
|
storage_used: 'Storage used:'
|
||||||
|
uptime: 'Uptime:'
|
||||||
|
answer_links: Answer Links
|
||||||
|
documents: Documents
|
||||||
|
feedback: Feedback
|
||||||
|
review: Review
|
||||||
|
config: Config
|
||||||
|
update_to: Update to
|
||||||
|
latest: Latest
|
||||||
|
check_failed: Check failed
|
||||||
|
'yes': 'Yes'
|
||||||
|
'no': 'No'
|
||||||
|
not_allowed: Not allowed
|
||||||
|
allowed: Allowed
|
||||||
|
enabled: Enabled
|
||||||
|
disabled: Disabled
|
||||||
|
flags:
|
||||||
|
title: Flags
|
||||||
|
pending: Pending
|
||||||
|
completed: Completed
|
||||||
|
flagged: Flagged
|
||||||
|
created: Created
|
||||||
|
action: Action
|
||||||
|
review: Review
|
||||||
|
change_modal:
|
||||||
|
title: Change user status to...
|
||||||
|
btn_cancel: Cancel
|
||||||
|
btn_submit: Submit
|
||||||
|
normal_name: normal
|
||||||
|
normal_description: A normal user can ask and answer questions.
|
||||||
|
suspended_name: suspended
|
||||||
|
suspended_description: A suspended user can't log in.
|
||||||
|
deleted_name: deleted
|
||||||
|
deleted_description: 'Delete profile, authentication associations.'
|
||||||
|
inactive_name: inactive
|
||||||
|
inactive_description: An inactive user must re-validate their email.
|
||||||
|
confirm_title: Delete this user
|
||||||
|
confirm_content: Are you sure you want to delete this user? This is permanent!
|
||||||
|
confirm_btn: Delete
|
||||||
|
msg:
|
||||||
|
empty: Please select a reason.
|
||||||
|
status_modal:
|
||||||
|
title: 'Change {{ type }} status to...'
|
||||||
|
normal_name: normal
|
||||||
|
normal_description: A normal post available to everyone.
|
||||||
|
closed_name: closed
|
||||||
|
closed_description: 'A closed question can''t answer, but still can edit, vote and comment.'
|
||||||
|
deleted_name: deleted
|
||||||
|
deleted_description: All reputation gained and lost will be restored.
|
||||||
|
btn_cancel: Cancel
|
||||||
|
btn_submit: Submit
|
||||||
|
btn_next: Next
|
||||||
|
users:
|
||||||
|
title: Users
|
||||||
|
name: Name
|
||||||
|
email: Email
|
||||||
|
reputation: Reputation
|
||||||
|
created_at: Created Time
|
||||||
|
delete_at: Deleted Time
|
||||||
|
suspend_at: Suspended Time
|
||||||
|
status: Status
|
||||||
|
action: Action
|
||||||
|
change: Change
|
||||||
|
all: All
|
||||||
|
inactive: Inactive
|
||||||
|
suspended: Suspended
|
||||||
|
deleted: Deleted
|
||||||
|
normal: Normal
|
||||||
|
filter:
|
||||||
|
placeholder: 'Filter by name, user:id'
|
||||||
|
questions:
|
||||||
|
page_title: Questions
|
||||||
|
normal: Normal
|
||||||
|
closed: Closed
|
||||||
|
deleted: Deleted
|
||||||
|
post: Post
|
||||||
|
votes: Votes
|
||||||
|
answers: Answers
|
||||||
|
created: Created
|
||||||
|
status: Status
|
||||||
|
action: Action
|
||||||
|
change: Change
|
||||||
|
filter:
|
||||||
|
placeholder: 'Filter by title, question:id'
|
||||||
|
answers:
|
||||||
|
page_title: Answers
|
||||||
|
normal: Normal
|
||||||
|
deleted: Deleted
|
||||||
|
post: Post
|
||||||
|
votes: Votes
|
||||||
|
created: Created
|
||||||
|
status: Status
|
||||||
|
action: Action
|
||||||
|
change: Change
|
||||||
|
filter:
|
||||||
|
placeholder: 'Filter by title, answer:id'
|
||||||
|
general:
|
||||||
|
page_title: General
|
||||||
|
name:
|
||||||
|
label: Site Name
|
||||||
|
msg: Site name cannot be empty.
|
||||||
|
text: 'The name of this site, as used in the title tag.'
|
||||||
|
site_url:
|
||||||
|
label: Site URL
|
||||||
|
msg: Site url cannot be empty.
|
||||||
|
validate: Please enter a valid URL.
|
||||||
|
text: The address of your site.
|
||||||
|
short_description:
|
||||||
|
label: Short Site Description (optional)
|
||||||
|
msg: Short site description cannot be empty.
|
||||||
|
text: 'Short description, as used in the title tag on homepage.'
|
||||||
|
description:
|
||||||
|
label: Site Description (optional)
|
||||||
|
msg: Site description cannot be empty.
|
||||||
|
text: 'Describe this site in one sentence, as used in the meta description tag.'
|
||||||
|
contact_email:
|
||||||
|
label: Contact Email
|
||||||
|
msg: Contact email cannot be empty.
|
||||||
|
validate: Contact email is not valid.
|
||||||
|
text: Email address of key contact responsible for this site.
|
||||||
|
interface:
|
||||||
|
page_title: Interface
|
||||||
|
logo:
|
||||||
|
label: Logo (optional)
|
||||||
|
msg: Site logo cannot be empty.
|
||||||
|
text: You can upload your image or <1>reset</1> it to the site title text.
|
||||||
|
theme:
|
||||||
|
label: Theme
|
||||||
|
msg: Theme cannot be empty.
|
||||||
|
text: Select an existing theme.
|
||||||
|
language:
|
||||||
|
label: Interface Language
|
||||||
|
msg: Interface language cannot be empty.
|
||||||
|
text: User interface language. It will change when you refresh the page.
|
||||||
|
time_zone:
|
||||||
|
label: Timezone
|
||||||
|
msg: Timezone cannot be empty.
|
||||||
|
text: Choose a city in the same timezone as you.
|
||||||
|
smtp:
|
||||||
|
page_title: SMTP
|
||||||
|
from_email:
|
||||||
|
label: From Email
|
||||||
|
msg: From email cannot be empty.
|
||||||
|
text: The email address which emails are sent from.
|
||||||
|
from_name:
|
||||||
|
label: From Name
|
||||||
|
msg: From name cannot be empty.
|
||||||
|
text: The name which emails are sent from.
|
||||||
|
smtp_host:
|
||||||
|
label: SMTP Host
|
||||||
|
msg: SMTP host cannot be empty.
|
||||||
|
text: Your mail server.
|
||||||
|
encryption:
|
||||||
|
label: Encryption
|
||||||
|
msg: Encryption cannot be empty.
|
||||||
|
text: For most servers SSL is the recommended option.
|
||||||
|
ssl: SSL
|
||||||
|
none: None
|
||||||
|
smtp_port:
|
||||||
|
label: SMTP Port
|
||||||
|
msg: SMTP port must be number 1 ~ 65535.
|
||||||
|
text: The port to your mail server.
|
||||||
|
smtp_username:
|
||||||
|
label: SMTP Username
|
||||||
|
msg: SMTP username cannot be empty.
|
||||||
|
smtp_password:
|
||||||
|
label: SMTP Password
|
||||||
|
msg: SMTP password cannot be empty.
|
||||||
|
test_email_recipient:
|
||||||
|
label: Test Email Recipients
|
||||||
|
text: Provide email address that will receive test sends.
|
||||||
|
msg: Test email recipients is invalid
|
||||||
|
smtp_authentication:
|
||||||
|
label: SMTP Authentication
|
||||||
|
msg: SMTP authentication cannot be empty.
|
||||||
|
'yes': 'Yes'
|
||||||
|
'no': 'No'
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
# all support language
|
||||||
|
language_options:
|
||||||
|
- label: "简体中文(CN)"
|
||||||
|
value: "zh_CN"
|
||||||
|
- label: "English(US)"
|
||||||
|
value: "en_US"
|
|
@ -1,3 +1,5 @@
|
||||||
|
# The following fields are used for back-end
|
||||||
|
backend:
|
||||||
base:
|
base:
|
||||||
success:
|
success:
|
||||||
other: "Successo"
|
other: "Successo"
|
||||||
|
|
749
i18n/zh_CN.yaml
749
i18n/zh_CN.yaml
|
@ -1,3 +1,4 @@
|
||||||
|
backend:
|
||||||
base:
|
base:
|
||||||
success:
|
success:
|
||||||
other: "成功"
|
other: "成功"
|
||||||
|
@ -170,3 +171,751 @@ notification:
|
||||||
other: "你的答案已被删除"
|
other: "你的答案已被删除"
|
||||||
your_comment_was_deleted:
|
your_comment_was_deleted:
|
||||||
other: "你的评论已被删除"
|
other: "你的评论已被删除"
|
||||||
|
# The following fields are used for interface presentation(Front-end)
|
||||||
|
ui:
|
||||||
|
how_to_format:
|
||||||
|
title: 如何设定文本格式
|
||||||
|
description: >-
|
||||||
|
<ul class="mb-0"><li><p class="mb-2">添加链接:</p><pre
|
||||||
|
class="mb-2"><code><https://url.com><br/><br/>[标题](https://url.com)</code></pre></li><li><p
|
||||||
|
class="mb-2">段落之间使用空行分隔</p></li><li><p class="mb-2"><em>_斜体_</em> 或者
|
||||||
|
**<strong>粗体</strong>**</p></li><li><p class="mb-2">使用 4
|
||||||
|
个空格缩进代码</p></li><li><p
|
||||||
|
class="mb-2">在行首添加<code>></code>表示引用</p></li><li><p class="mb-2">反引号进行转义
|
||||||
|
<code>`像 _这样_`</code></p></li><li><p
|
||||||
|
class="mb-2">使用<code>```</code>创建代码块</p><pre class="mb-0"><code>```<br/>//
|
||||||
|
这是代码<br/>```</code></pre></li></ul>
|
||||||
|
pagination:
|
||||||
|
prev: 上一页
|
||||||
|
next: 下一页
|
||||||
|
page_title:
|
||||||
|
question: 问题
|
||||||
|
questions: 问题
|
||||||
|
tag: 标签
|
||||||
|
tags: 标签
|
||||||
|
tag_wiki: 标签 wiki
|
||||||
|
edit_tag: 编辑标签
|
||||||
|
ask_a_question: 提问题
|
||||||
|
edit_question: 编辑问题
|
||||||
|
edit_answer: 编辑回答
|
||||||
|
search: 搜索
|
||||||
|
posts_containing: 包含
|
||||||
|
settings: 设定
|
||||||
|
notifications: 通知
|
||||||
|
login: 登录
|
||||||
|
sign_up: 注册
|
||||||
|
account_recovery: 账号恢复
|
||||||
|
account_activation: 账号激活
|
||||||
|
confirm_email: 确认电子邮件
|
||||||
|
account_suspended: 账号已封禁
|
||||||
|
admin: 后台管理
|
||||||
|
notifications:
|
||||||
|
title: 通知
|
||||||
|
inbox: 收件箱
|
||||||
|
achievement: 成就
|
||||||
|
all_read: 全部标记为已读
|
||||||
|
show_more: 显示更多
|
||||||
|
suspended:
|
||||||
|
title: 账号已封禁
|
||||||
|
until_time: '你的账号被封禁至{{ time }}。'
|
||||||
|
forever: 你的账号已被永久封禁。
|
||||||
|
end: 违反了我们的社区准则。
|
||||||
|
editor:
|
||||||
|
blockquote:
|
||||||
|
text: 引用
|
||||||
|
bold:
|
||||||
|
text: 粗体
|
||||||
|
chart:
|
||||||
|
text: 图表
|
||||||
|
flow_chart: 流程图
|
||||||
|
sequence_diagram: 时序图
|
||||||
|
class_diagram: 类图
|
||||||
|
state_diagram: 状态图
|
||||||
|
entity_relationship_diagram: ER 图
|
||||||
|
user_defined_diagram: User defined diagram
|
||||||
|
gantt_chart: 甘特图
|
||||||
|
pie_chart: 饼图
|
||||||
|
code:
|
||||||
|
text: 代码块
|
||||||
|
add_code: 添加代码块
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
code:
|
||||||
|
label: 代码块
|
||||||
|
msg:
|
||||||
|
empty: 代码块不能为空
|
||||||
|
language:
|
||||||
|
label: 语言 (可选)
|
||||||
|
placeholder: 自动识别
|
||||||
|
btn_cancel: 取消
|
||||||
|
btn_confirm: 添加
|
||||||
|
formula:
|
||||||
|
text: 公式
|
||||||
|
options:
|
||||||
|
inline: 行内公式
|
||||||
|
block: 公式块
|
||||||
|
heading:
|
||||||
|
text: 标题
|
||||||
|
options:
|
||||||
|
h1: 标题 1
|
||||||
|
h2: 标题 2
|
||||||
|
h3: 标题 3
|
||||||
|
h4: 标题 4
|
||||||
|
h5: 标题 5
|
||||||
|
h6: 标题 6
|
||||||
|
help:
|
||||||
|
text: 帮助
|
||||||
|
hr:
|
||||||
|
text: 水平分割线
|
||||||
|
image:
|
||||||
|
text: 图片
|
||||||
|
add_image: 添加图片
|
||||||
|
tab_image: 上传图片
|
||||||
|
form_image:
|
||||||
|
fields:
|
||||||
|
file:
|
||||||
|
label: 图片文件
|
||||||
|
btn: 选择图片
|
||||||
|
msg:
|
||||||
|
empty: 请选择图片文件。
|
||||||
|
only_image: 只能上传图片文件。
|
||||||
|
max_size: 图片文件大小不能超过 4 MB。
|
||||||
|
description:
|
||||||
|
label: 图片描述(可选)
|
||||||
|
tab_url: 网络图片
|
||||||
|
form_url:
|
||||||
|
fields:
|
||||||
|
url:
|
||||||
|
label: 图片地址
|
||||||
|
msg:
|
||||||
|
empty: 图片地址不能为空
|
||||||
|
name:
|
||||||
|
label: 图片描述(可选)
|
||||||
|
btn_cancel: 取消
|
||||||
|
btn_confirm: 添加
|
||||||
|
uploading: 上传中...
|
||||||
|
indent:
|
||||||
|
text: 添加缩进
|
||||||
|
outdent:
|
||||||
|
text: 减少缩进
|
||||||
|
italic:
|
||||||
|
text: 斜体
|
||||||
|
link:
|
||||||
|
text: 超链接
|
||||||
|
add_link: 添加超链接
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
url:
|
||||||
|
label: 链接
|
||||||
|
msg:
|
||||||
|
empty: 链接不能为空。
|
||||||
|
name:
|
||||||
|
label: 链接描述(可选)
|
||||||
|
btn_cancel: 取消
|
||||||
|
btn_confirm: 添加
|
||||||
|
ordered_list:
|
||||||
|
text: 有编号列表
|
||||||
|
unordered_list:
|
||||||
|
text: 无编号列表
|
||||||
|
table:
|
||||||
|
text: 表格
|
||||||
|
heading: 表头
|
||||||
|
cell: 单元格
|
||||||
|
close_modal:
|
||||||
|
title: 关闭原因是...
|
||||||
|
btn_cancel: 取消
|
||||||
|
btn_submit: 提交
|
||||||
|
remark:
|
||||||
|
empty: 不能为空。
|
||||||
|
msg:
|
||||||
|
empty: 请选择一个原因。
|
||||||
|
report_modal:
|
||||||
|
flag_title: 举报原因是...
|
||||||
|
close_title: 关闭原因是...
|
||||||
|
review_question_title: 审查问题
|
||||||
|
review_answer_title: 审查回答
|
||||||
|
review_comment_title: 审查评论
|
||||||
|
btn_cancel: 取消
|
||||||
|
btn_submit: 提交
|
||||||
|
remark:
|
||||||
|
empty: 不能为空
|
||||||
|
msg:
|
||||||
|
empty: 请选择一个原因。
|
||||||
|
tag_modal:
|
||||||
|
title: 创建新标签
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
display_name:
|
||||||
|
label: 显示名称(别名)
|
||||||
|
msg:
|
||||||
|
empty: 不能为空
|
||||||
|
range: 不能超过 35 个字符
|
||||||
|
slug_name:
|
||||||
|
label: URL 固定链接
|
||||||
|
description: '必须由 "a-z", "0-9", "+ # - ." 组成'
|
||||||
|
msg:
|
||||||
|
empty: 不能为空
|
||||||
|
range: 不能超过 35 个字符
|
||||||
|
character: 包含非法字符
|
||||||
|
description:
|
||||||
|
label: 标签描述(可选)
|
||||||
|
btn_cancel: 取消
|
||||||
|
btn_submit: 提交
|
||||||
|
tag_info:
|
||||||
|
created_at: 创建于
|
||||||
|
edited_at: 编辑于
|
||||||
|
synonyms:
|
||||||
|
title: 同义词
|
||||||
|
text: 以下标签等同于
|
||||||
|
empty: 此标签目前没有同义词。
|
||||||
|
btn_add: 添加同义词
|
||||||
|
btn_edit: 编辑
|
||||||
|
btn_save: 保存
|
||||||
|
synonyms_text: 以下标签等同于
|
||||||
|
delete:
|
||||||
|
title: 删除标签
|
||||||
|
content: <p>不允许删除有关联问题的标签。</p><p>请先从关联的问题中删除此标签的引用。</p>
|
||||||
|
content2: 确定要删除吗?
|
||||||
|
close: 关闭
|
||||||
|
edit_tag:
|
||||||
|
title: 编辑标签
|
||||||
|
default_reason: 编辑标签
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
revision:
|
||||||
|
label: 编辑历史
|
||||||
|
display_name:
|
||||||
|
label: 名称
|
||||||
|
slug_name:
|
||||||
|
label: URL 固定链接
|
||||||
|
info: '必须由 "a-z", "0-9", "+ # - ." 组成'
|
||||||
|
description:
|
||||||
|
label: 描述
|
||||||
|
edit_summary:
|
||||||
|
label: 编辑概要
|
||||||
|
placeholder: 简单描述更改原因 (错别字、文字表达、格式等等)
|
||||||
|
btn_save_edits: 保存更改
|
||||||
|
btn_cancel: 取消
|
||||||
|
dates:
|
||||||
|
long_date: MM月DD日
|
||||||
|
long_date_with_year: YYYY年MM月DD日
|
||||||
|
long_date_with_time: 'YYYY年MM月DD日 HH:mm'
|
||||||
|
now: 刚刚
|
||||||
|
x_seconds_ago: '{{count}} 秒前'
|
||||||
|
x_minutes_ago: '{{count}} 分钟前'
|
||||||
|
x_hours_ago: '{{count}} 小时前'
|
||||||
|
comment:
|
||||||
|
btn_add_comment: 添加评论
|
||||||
|
reply_to: 回复
|
||||||
|
btn_reply: 回复
|
||||||
|
btn_edit: 编辑
|
||||||
|
btn_delete: 删除
|
||||||
|
btn_flag: 举报
|
||||||
|
btn_save_edits: 保存
|
||||||
|
btn_cancel: 取消
|
||||||
|
show_more: 显示更多评论
|
||||||
|
tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。
|
||||||
|
tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。
|
||||||
|
edit_answer:
|
||||||
|
title: 编辑回答
|
||||||
|
default_reason: 编辑回答
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
revision:
|
||||||
|
label: 编辑历史
|
||||||
|
answer:
|
||||||
|
label: 回答内容
|
||||||
|
edit_summary:
|
||||||
|
label: 编辑概要
|
||||||
|
placeholder: 简单描述更改原因 (错别字、文字表达、格式等等)
|
||||||
|
btn_save_edits: 保存更改
|
||||||
|
btn_cancel: 取消
|
||||||
|
tags:
|
||||||
|
title: 标签
|
||||||
|
sort_buttons:
|
||||||
|
popular: 热门
|
||||||
|
name: 名称
|
||||||
|
newest: 最新
|
||||||
|
button_follow: 关注
|
||||||
|
button_following: 已关注
|
||||||
|
tag_label: 个问题
|
||||||
|
search_placeholder: 通过标签名过滤
|
||||||
|
no_description: 此标签无描述。
|
||||||
|
more: 更多
|
||||||
|
ask:
|
||||||
|
title: 提交新的问题
|
||||||
|
edit_title: 编辑问题
|
||||||
|
default_reason: 编辑问题
|
||||||
|
similar_questions: 相似的问题
|
||||||
|
form:
|
||||||
|
fields:
|
||||||
|
revision:
|
||||||
|
label: 编辑历史
|
||||||
|
title:
|
||||||
|
label: 标题
|
||||||
|
placeholder: 请详细描述你的问题
|
||||||
|
msg:
|
||||||
|
empty: 标题不能为空
|
||||||
|
range: 标题最多 150 个字符
|
||||||
|
body:
|
||||||
|
label: 内容
|
||||||
|
msg:
|
||||||
|
empty: 内容不能为空
|
||||||
|
tags:
|
||||||
|
label: 标签
|
||||||
|
msg:
|
||||||
|
empty: 必须选择一个标签
|
||||||
|
answer:
|
||||||
|
label: 回答内容
|
||||||
|
msg:
|
||||||
|
empty: 回答内容不能为空
|
||||||
|
btn_post_question: 提交问题
|
||||||
|
btn_save_edits: 保存更改
|
||||||
|
answer_question: 直接发表回答
|
||||||
|
post_question&answer: 提交问题和回答
|
||||||
|
tag_selector:
|
||||||
|
add_btn: 添加标签
|
||||||
|
create_btn: 创建新标签
|
||||||
|
search_tag: 搜索标签
|
||||||
|
hint: 选择至少一个与问题相关的标签。
|
||||||
|
no_result: 没有匹配的标签
|
||||||
|
header:
|
||||||
|
nav:
|
||||||
|
question: 问题
|
||||||
|
tag: 标签
|
||||||
|
user: 用户
|
||||||
|
profile: 用户主页
|
||||||
|
setting: 账号设置
|
||||||
|
logout: 退出登录
|
||||||
|
admin: 后台管理
|
||||||
|
search:
|
||||||
|
placeholder: 搜索
|
||||||
|
footer:
|
||||||
|
build_on: >-
|
||||||
|
Built on <1> Answer </1>- the open-source software that power Q&A
|
||||||
|
communities<br />Made with love © 2022 Answer
|
||||||
|
upload_img:
|
||||||
|
name: 更改图片
|
||||||
|
loading: 加载中...
|
||||||
|
pic_auth_code:
|
||||||
|
title: 验证码
|
||||||
|
placeholder: 输入图片中的文字
|
||||||
|
msg:
|
||||||
|
empty: 不能为空
|
||||||
|
inactive:
|
||||||
|
first: '马上就好了!我们发送了一封激活邮件到 <bold>{{mail}}</bold>。请按照邮件中的说明激活您的帐户。'
|
||||||
|
info: 如果没有收到,请检查您的垃圾邮件文件夹。
|
||||||
|
another: '我们向您发送了另一封激活电子邮件,地址为 <bold>{{mail}}</bold>。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。'
|
||||||
|
btn_name: 重新发送激活邮件
|
||||||
|
msg:
|
||||||
|
empty: 不能为空
|
||||||
|
login:
|
||||||
|
page_title: 欢迎来到 Answer
|
||||||
|
info_sign: 没有帐户?<1>注册</1>
|
||||||
|
info_login: 已经有一个帐户?<1>登录</1>
|
||||||
|
forgot_pass: 忘记密码?
|
||||||
|
name:
|
||||||
|
label: 昵称
|
||||||
|
msg:
|
||||||
|
empty: 昵称不能为空
|
||||||
|
range: 昵称最多 30 个字符
|
||||||
|
email:
|
||||||
|
label: 邮箱
|
||||||
|
msg:
|
||||||
|
empty: 邮箱不能为空
|
||||||
|
password:
|
||||||
|
label: 密码
|
||||||
|
msg:
|
||||||
|
empty: 密码不能为空
|
||||||
|
different: 两次输入密码不一致
|
||||||
|
account_forgot:
|
||||||
|
page_title: 忘记密码
|
||||||
|
btn_name: 发送恢复邮件
|
||||||
|
send_success: '如无意外,你的邮箱 <strong>{{mail}}</strong> 将会收到一封重置密码的邮件,请根据指引重置你的密码。'
|
||||||
|
email:
|
||||||
|
label: 邮箱
|
||||||
|
msg:
|
||||||
|
empty: 邮箱不能为空
|
||||||
|
password_reset:
|
||||||
|
page_title: 密码重置
|
||||||
|
btn_name: 重置我的密码
|
||||||
|
reset_success: 你已经成功更改密码,将返回登录页面
|
||||||
|
link_invalid: 抱歉,此密码重置链接已失效。也许是你已经重置过密码了?
|
||||||
|
to_login: 前往登录页面
|
||||||
|
password:
|
||||||
|
label: 密码
|
||||||
|
msg:
|
||||||
|
empty: 密码不能为空
|
||||||
|
length: 密码长度在8-32个字符之间
|
||||||
|
different: 两次输入密码不一致
|
||||||
|
password_confirm:
|
||||||
|
label: 确认新密码
|
||||||
|
settings:
|
||||||
|
page_title: 设置
|
||||||
|
nav:
|
||||||
|
profile: 我的资料
|
||||||
|
notification: 通知
|
||||||
|
account: 账号
|
||||||
|
interface: 界面
|
||||||
|
profile:
|
||||||
|
btn_name: 保存更改
|
||||||
|
display_name:
|
||||||
|
label: 昵称
|
||||||
|
msg: 昵称不能为空
|
||||||
|
msg_range: 昵称不能超过 30 个字符
|
||||||
|
username:
|
||||||
|
label: 用户名
|
||||||
|
caption: 用户之间可以通过 "@用户名" 进行交互。
|
||||||
|
msg: 用户名不能为空
|
||||||
|
msg_range: 用户名不能超过 30 个字符
|
||||||
|
character: '用户名只能由 "a-z", "0-9", " - . _" 组成'
|
||||||
|
avatar:
|
||||||
|
label: 头像
|
||||||
|
text: 您可以上传图片作为头像,也可以 <1>重置</1> 为
|
||||||
|
bio:
|
||||||
|
label: 关于我 (可选)
|
||||||
|
website:
|
||||||
|
label: 网站 (可选)
|
||||||
|
placeholder: 'https://example.com'
|
||||||
|
msg: 格式不正确
|
||||||
|
location:
|
||||||
|
label: 位置 (可选)
|
||||||
|
placeholder: '城市, 国家'
|
||||||
|
notification:
|
||||||
|
email:
|
||||||
|
label: 邮件通知
|
||||||
|
radio: 你的提问有新的回答,评论,和其他
|
||||||
|
account:
|
||||||
|
change_email_btn: 更改邮箱
|
||||||
|
change_pass_btn: 更改密码
|
||||||
|
change_email_info: 邮件已发送。请根据指引完成验证。
|
||||||
|
email:
|
||||||
|
label: 邮箱
|
||||||
|
msg: 邮箱不能为空
|
||||||
|
password_title: 密码
|
||||||
|
current_pass:
|
||||||
|
label: 当前密码
|
||||||
|
msg:
|
||||||
|
empty: 当前密码不能为空
|
||||||
|
length: 密码长度必须在 8 至 32 之间
|
||||||
|
different: 两次输入的密码不匹配
|
||||||
|
new_pass:
|
||||||
|
label: 新密码
|
||||||
|
pass_confirm:
|
||||||
|
label: 确认新密码
|
||||||
|
interface:
|
||||||
|
lang:
|
||||||
|
label: 界面语言
|
||||||
|
text: 设置用户界面语言,在刷新页面后生效。
|
||||||
|
toast:
|
||||||
|
update: 更新成功
|
||||||
|
update_password: 更改密码成功。
|
||||||
|
flag_success: 感谢您的标记,我们会尽快处理。
|
||||||
|
related_question:
|
||||||
|
title: 相关问题
|
||||||
|
btn: 我要提问
|
||||||
|
answers: 个回答
|
||||||
|
question_detail:
|
||||||
|
Asked: 提问于
|
||||||
|
asked: 提问于
|
||||||
|
update: 修改于
|
||||||
|
edit: 最后编辑于
|
||||||
|
Views: 阅读次数
|
||||||
|
Follow: 关注此问题
|
||||||
|
Following: 已关注
|
||||||
|
answered: 回答于
|
||||||
|
closed_in: 关闭于
|
||||||
|
show_exist: 查看相关问题。
|
||||||
|
answers:
|
||||||
|
title: 个回答
|
||||||
|
score: 评分
|
||||||
|
newest: 最新
|
||||||
|
btn_accept: 采纳
|
||||||
|
btn_accepted: 已被采纳
|
||||||
|
write_answer:
|
||||||
|
title: 你的回答
|
||||||
|
btn_name: 提交你的回答
|
||||||
|
confirm_title: 继续回答
|
||||||
|
continue: 继续
|
||||||
|
confirm_info: <p>您确定要提交一个新的回答吗?</p><p>您可以直接编辑和改善您之前的回答的。</p>
|
||||||
|
empty: 回答内容不能为空。
|
||||||
|
delete:
|
||||||
|
title: 删除
|
||||||
|
question: >-
|
||||||
|
我们不建议<strong>删除有回答的帖子</strong>。因为这样做会使得后来的读者无法从该问题中获得帮助。</p><p>如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗?
|
||||||
|
answer_accepted: >-
|
||||||
|
<p>我们不建议<strong>删除被采纳的回答</strong>。因为这样做会使得后来的读者无法从该回答中获得帮助。</p>如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗?
|
||||||
|
other: 你确定要删除?
|
||||||
|
tip_question_deleted: 此问题已被删除
|
||||||
|
tip_answer_deleted: 此回答已被删除
|
||||||
|
btns:
|
||||||
|
confirm: 确认
|
||||||
|
cancel: 取消
|
||||||
|
save: 保存
|
||||||
|
delete: 删除
|
||||||
|
login: 登录
|
||||||
|
signup: 注册
|
||||||
|
logout: 退出登录
|
||||||
|
verify: 验证
|
||||||
|
add_question: 我要提问
|
||||||
|
search:
|
||||||
|
title: 搜索结果
|
||||||
|
keywords: 关键词
|
||||||
|
options: 选项
|
||||||
|
follow: 关注
|
||||||
|
following: 已关注
|
||||||
|
counts: '{{count}} 个结果'
|
||||||
|
more: 更多
|
||||||
|
sort_btns:
|
||||||
|
relevance: 相关性
|
||||||
|
newest: 最新的
|
||||||
|
active: 活跃的
|
||||||
|
score: 评分
|
||||||
|
tips:
|
||||||
|
title: 高级搜索提示
|
||||||
|
tag: '<1>[tag]</1> 在指定标签中搜索'
|
||||||
|
user: '<1>user:username</1> 根据作者搜索'
|
||||||
|
answer: '<1>answers:0</1> 搜索未回答的问题'
|
||||||
|
score: '<1>score:3</1> 评分 3 分或以上'
|
||||||
|
question: '<1>is:question</1> 只搜索问题'
|
||||||
|
is_answer: '<1>is:answer</1> 只搜索回答'
|
||||||
|
empty: 找不到任何相关的内容。<br /> 请尝试其他关键字,或者减少查找内容的长度。
|
||||||
|
share:
|
||||||
|
name: 分享
|
||||||
|
copy: 复制链接
|
||||||
|
via: 分享在...
|
||||||
|
copied: 已复制
|
||||||
|
facebook: 分享到 Facebook
|
||||||
|
twitter: 分享到 Twitter
|
||||||
|
cannot_vote_for_self: 不能给自己投票
|
||||||
|
modal_confirm:
|
||||||
|
title: 发生错误...
|
||||||
|
account_result:
|
||||||
|
page_title: 欢迎来到 Answer
|
||||||
|
success: 你的账号已通过验证,即将返回首页。
|
||||||
|
link: 返回首页
|
||||||
|
invalid: 抱歉,此验证链接已失效。也许是你的账号已经通过验证了?
|
||||||
|
confirm_new_email: 你的电子邮箱已更新
|
||||||
|
confirm_new_email_invalid: 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了?
|
||||||
|
question:
|
||||||
|
following_tags: 已关注的标签
|
||||||
|
edit: 编辑
|
||||||
|
save: 保存
|
||||||
|
follow_tag_tip: 按照标签整理您的问题列表。
|
||||||
|
hot_questions: 热点问题
|
||||||
|
all_questions: 全部问题
|
||||||
|
x_questions: '{{ count }} 个问题'
|
||||||
|
x_answers: '{{ count }} 个回答'
|
||||||
|
questions: 个问题
|
||||||
|
answers: 回答
|
||||||
|
newest: 最新
|
||||||
|
active: 活跃
|
||||||
|
frequent: 浏览量
|
||||||
|
score: 评分
|
||||||
|
unanswered: 未回答
|
||||||
|
modified: 修改于
|
||||||
|
answered: 回答于
|
||||||
|
asked: 提问于
|
||||||
|
closed: 已关闭
|
||||||
|
follow_a_tag: 关注一个标签
|
||||||
|
more: 更多
|
||||||
|
personal:
|
||||||
|
overview: 概览
|
||||||
|
answers: 回答
|
||||||
|
answer: 回答
|
||||||
|
questions: 问题
|
||||||
|
question: 问题
|
||||||
|
bookmarks: 收藏
|
||||||
|
reputation: 声望
|
||||||
|
comments: 评论
|
||||||
|
votes: 得票
|
||||||
|
newest: 最新
|
||||||
|
score: 评分
|
||||||
|
edit_profile: 编辑我的资料
|
||||||
|
visited_x_days: 'Visited {{ count }} days'
|
||||||
|
viewed: Viewed
|
||||||
|
joined: 加入于
|
||||||
|
last_login: 上次登录
|
||||||
|
about_me: 关于我
|
||||||
|
about_me_empty: '// Hello, World !'
|
||||||
|
top_answers: 热门回答
|
||||||
|
top_questions: 热门问题
|
||||||
|
stats: 状态
|
||||||
|
list_empty: 没有找到相关的内容。<br />试试看其他标签?
|
||||||
|
accepted: 已采纳
|
||||||
|
answered: 回答于
|
||||||
|
asked: 提问于
|
||||||
|
upvote: 赞同
|
||||||
|
downvote: 反对
|
||||||
|
mod_short: 管理员
|
||||||
|
mod_long: 管理员
|
||||||
|
x_reputation: 声望
|
||||||
|
x_votes: 得票
|
||||||
|
x_answers: 个回答
|
||||||
|
x_questions: 个问题
|
||||||
|
page_404:
|
||||||
|
description: 页面不存在
|
||||||
|
back_home: 回到主页
|
||||||
|
page_50X:
|
||||||
|
description: 服务器遇到了一个错误,无法完成你的请求。
|
||||||
|
back_home: 回到主页
|
||||||
|
admin:
|
||||||
|
admin_header:
|
||||||
|
title: 后台管理
|
||||||
|
nav_menus:
|
||||||
|
dashboard: 后台管理
|
||||||
|
contents: 内容管理
|
||||||
|
questions: 问题
|
||||||
|
answers: 回答
|
||||||
|
users: 用户管理
|
||||||
|
flags: 举报管理
|
||||||
|
settings: 站点设置
|
||||||
|
general: 一般
|
||||||
|
interface: 界面
|
||||||
|
smtp: SMTP
|
||||||
|
dashboard:
|
||||||
|
title: 后台管理
|
||||||
|
welcome: 欢迎来到 Answer 后台管理!
|
||||||
|
version: 版本
|
||||||
|
flags:
|
||||||
|
title: 举报
|
||||||
|
pending: 等待处理
|
||||||
|
completed: 已完成
|
||||||
|
flagged: 被举报内容
|
||||||
|
created: 创建于
|
||||||
|
action: 操作
|
||||||
|
review: 审查
|
||||||
|
change_modal:
|
||||||
|
title: 更改用户状态为...
|
||||||
|
btn_cancel: 取消
|
||||||
|
btn_submit: 提交
|
||||||
|
normal_name: 正常
|
||||||
|
normal_description: 正常状态的用户可以提问和回答。
|
||||||
|
suspended_name: 封禁
|
||||||
|
suspended_description: 被封禁的用户将无法登录。
|
||||||
|
deleted_name: 删除
|
||||||
|
deleted_description: 删除用户的个人信息,认证等等。
|
||||||
|
inactive_name: 不活跃
|
||||||
|
inactive_description: 不活跃的用户必须重新验证邮箱。
|
||||||
|
confirm_title: 删除此用户
|
||||||
|
confirm_content: 确定要删除此用户?此操作无法撤销!
|
||||||
|
confirm_btn: 删除
|
||||||
|
msg:
|
||||||
|
empty: 请选择一个原因
|
||||||
|
status_modal:
|
||||||
|
title: '更改 {{ type }} 状态为...'
|
||||||
|
normal_name: 正常
|
||||||
|
normal_description: 所有用户都可以访问
|
||||||
|
closed_name: 关闭
|
||||||
|
closed_description: 不能回答,但仍然可以编辑、投票和评论。
|
||||||
|
deleted_name: 删除
|
||||||
|
deleted_description: 所有获得/损失的声望将会恢复。
|
||||||
|
btn_cancel: 取消
|
||||||
|
btn_submit: 提交
|
||||||
|
btn_next: 下一步
|
||||||
|
users:
|
||||||
|
title: 用户
|
||||||
|
name: 名称
|
||||||
|
email: 邮箱
|
||||||
|
reputation: 声望
|
||||||
|
created_at: 创建时间
|
||||||
|
delete_at: 删除时间
|
||||||
|
suspend_at: 封禁时间
|
||||||
|
status: 状态
|
||||||
|
action: 操作
|
||||||
|
change: 更改
|
||||||
|
all: 全部
|
||||||
|
inactive: 不活跃
|
||||||
|
suspended: 已封禁
|
||||||
|
deleted: 已删除
|
||||||
|
normal: 正常
|
||||||
|
questions:
|
||||||
|
page_title: 问题
|
||||||
|
normal: 正常
|
||||||
|
closed: 已关闭
|
||||||
|
deleted: 已删除
|
||||||
|
post: 标题
|
||||||
|
votes: 得票数
|
||||||
|
answers: 回答数
|
||||||
|
created: 创建于
|
||||||
|
status: 状态
|
||||||
|
action: 操作
|
||||||
|
change: 更改
|
||||||
|
answers:
|
||||||
|
page_title: 回答
|
||||||
|
normal: 正常
|
||||||
|
deleted: 已删除
|
||||||
|
post: 标题
|
||||||
|
votes: 得票数
|
||||||
|
created: 创建于
|
||||||
|
status: 状态
|
||||||
|
action: 操作
|
||||||
|
change: 更改
|
||||||
|
general:
|
||||||
|
page_title: 一般
|
||||||
|
name:
|
||||||
|
label: 站点名称
|
||||||
|
msg: 不能为空
|
||||||
|
text: 站点的名称,作为站点的标题(HTML 的 title 标签)。
|
||||||
|
short_description:
|
||||||
|
label: 简短的站点标语 (可选)
|
||||||
|
msg: 不能为空
|
||||||
|
text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。
|
||||||
|
description:
|
||||||
|
label: 网站描述 (可选)
|
||||||
|
msg: 不能为空
|
||||||
|
text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。
|
||||||
|
interface:
|
||||||
|
page_title: 界面
|
||||||
|
logo:
|
||||||
|
label: Logo (可选)
|
||||||
|
msg: 不能为空
|
||||||
|
text: 可以上传图片,或者<1>重置</1>为站点标题。
|
||||||
|
theme:
|
||||||
|
label: 主题
|
||||||
|
msg: 不能为空
|
||||||
|
text: 选择一个主题
|
||||||
|
language:
|
||||||
|
label: 界面语言
|
||||||
|
msg: 不能为空
|
||||||
|
text: 设置用户界面语言,在刷新页面后生效。
|
||||||
|
smtp:
|
||||||
|
page_title: SMTP
|
||||||
|
from_email:
|
||||||
|
label: 发件人地址
|
||||||
|
msg: 不能为空
|
||||||
|
text: 用于发送邮件的地址。
|
||||||
|
from_name:
|
||||||
|
label: 发件人名称
|
||||||
|
msg: 不能为空
|
||||||
|
text: 发件人的名称
|
||||||
|
smtp_host:
|
||||||
|
label: SMTP 主机
|
||||||
|
msg: 不能为空
|
||||||
|
text: 邮件服务器
|
||||||
|
encryption:
|
||||||
|
label: 加密
|
||||||
|
msg: 不能为空
|
||||||
|
text: 对于大多数服务器而言,SSL 是推荐开启的。
|
||||||
|
ssl: SSL
|
||||||
|
none: 无加密
|
||||||
|
smtp_port:
|
||||||
|
label: SMTP 端口
|
||||||
|
msg: SMTP 端口必须在 1 ~ 65535 之间。
|
||||||
|
text: 邮件服务器的端口号。
|
||||||
|
smtp_username:
|
||||||
|
label: SMTP 用户名
|
||||||
|
msg: 不能为空
|
||||||
|
smtp_password:
|
||||||
|
label: SMTP 密码
|
||||||
|
msg: 不能为空
|
||||||
|
test_email_recipient:
|
||||||
|
label: 测试邮件收件人
|
||||||
|
text: 提供用于接收测试邮件的邮箱地址。
|
||||||
|
msg: 地址无效
|
||||||
|
smtp_authentication:
|
||||||
|
label: SMTP 认证
|
||||||
|
msg: 不能为空
|
||||||
|
'yes': 是
|
||||||
|
'no': 否
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,64 @@
|
||||||
package conf
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/base/data"
|
"github.com/answerdev/answer/internal/base/data"
|
||||||
"github.com/answerdev/answer/internal/base/server"
|
"github.com/answerdev/answer/internal/base/server"
|
||||||
"github.com/answerdev/answer/internal/base/translator"
|
"github.com/answerdev/answer/internal/base/translator"
|
||||||
|
"github.com/answerdev/answer/internal/cli"
|
||||||
"github.com/answerdev/answer/internal/router"
|
"github.com/answerdev/answer/internal/router"
|
||||||
"github.com/answerdev/answer/internal/service/service_config"
|
"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
|
// AllConfig all config
|
||||||
type AllConfig struct {
|
type AllConfig struct {
|
||||||
Debug bool `json:"debug" mapstructure:"debug"`
|
Debug bool `json:"debug" mapstructure:"debug" yaml:"debug"`
|
||||||
Data *Data `json:"data" mapstructure:"data"`
|
Server *Server `json:"server" mapstructure:"server" yaml:"server"`
|
||||||
Server *Server `json:"server" mapstructure:"server"`
|
Data *Data `json:"data" mapstructure:"data" yaml:"data"`
|
||||||
I18n *translator.I18n `json:"i18n" mapstructure:"i18n"`
|
I18n *translator.I18n `json:"i18n" mapstructure:"i18n" yaml:"i18n"`
|
||||||
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui"`
|
ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config" yaml:"service_config"`
|
||||||
ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config"`
|
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui" yaml:"swaggerui"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server server config
|
// Server server config
|
||||||
type Server struct {
|
type Server struct {
|
||||||
HTTP *server.HTTP `json:"http" mapstructure:"http"`
|
HTTP *server.HTTP `json:"http" mapstructure:"http" yaml:"http"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data data config
|
// Data data config
|
||||||
type Data struct {
|
type Data struct {
|
||||||
Database *data.Database `json:"database" mapstructure:"database"`
|
Database *data.Database `json:"database" mapstructure:"database" yaml:"database"`
|
||||||
Cache *data.CacheConf `json:"cache" mapstructure:"cache"`
|
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
|
// object TagID AnswerList
|
||||||
// key equal database's table name
|
// key equal database's table name
|
||||||
var (
|
var (
|
||||||
|
Version string = ""
|
||||||
|
|
||||||
ObjectTypeStrMapping = map[string]int{
|
ObjectTypeStrMapping = map[string]int{
|
||||||
QuestionObjectType: 1,
|
QuestionObjectType: 1,
|
||||||
AnswerObjectType: 2,
|
AnswerObjectType: 2,
|
||||||
|
@ -47,3 +49,8 @@ var (
|
||||||
8: ReportObjectType,
|
8: ReportObjectType,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SiteTypeGeneral = "general"
|
||||||
|
SiteTypeInterface = "interface"
|
||||||
|
)
|
||||||
|
|
|
@ -2,14 +2,14 @@ package data
|
||||||
|
|
||||||
// Database database config
|
// Database database config
|
||||||
type Database struct {
|
type Database struct {
|
||||||
Driver string `json:"driver" mapstructure:"driver"`
|
Driver string `json:"driver" mapstructure:"driver" yaml:"driver"`
|
||||||
Connection string `json:"connection" mapstructure:"connection"`
|
Connection string `json:"connection" mapstructure:"connection" yaml:"connection"`
|
||||||
ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time"`
|
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"`
|
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"`
|
MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn" yaml:"max_idle_conn,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheConf cache
|
// CacheConf cache
|
||||||
type CacheConf struct {
|
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"
|
LangNotFound = "error.lang.not_found"
|
||||||
ReportHandleFailed = "error.report.handle_failed"
|
ReportHandleFailed = "error.report.handle_failed"
|
||||||
ReportNotFound = "error.report.not_found"
|
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
|
// I18n i18n config
|
||||||
type I18n struct {
|
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
|
package translator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
myTran "github.com/segmentfault/pacman/contrib/i18n"
|
myTran "github.com/segmentfault/pacman/contrib/i18n"
|
||||||
"github.com/segmentfault/pacman/i18n"
|
"github.com/segmentfault/pacman/i18n"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProviderSet is providers.
|
// ProviderSet is providers.
|
||||||
var ProviderSet = wire.NewSet(NewTranslator)
|
var ProviderSet = wire.NewSet(NewTranslator)
|
||||||
var GlobalTrans i18n.Translator
|
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
|
// NewTranslator new a translator
|
||||||
func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
|
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
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/base/reason"
|
"github.com/answerdev/answer/internal/base/reason"
|
||||||
"github.com/answerdev/answer/internal/base/translator"
|
"github.com/answerdev/answer/internal/base/translator"
|
||||||
|
@ -97,9 +98,19 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
|
||||||
|
|
||||||
for _, fieldError := range valErrors {
|
for _, fieldError := range valErrors {
|
||||||
errField = &ErrorField{
|
errField = &ErrorField{
|
||||||
Key: translator.GlobalTrans.Tr(m.Lang, fieldError.Field()),
|
Key: fieldError.Field(),
|
||||||
Value: fieldError.Translate(m.Tran),
|
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))
|
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 {
|
type Checker interface {
|
||||||
Check() (errField *ErrorField, err error)
|
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
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/answerdev/answer/configs"
|
"github.com/answerdev/answer/configs"
|
||||||
"github.com/answerdev/answer/i18n"
|
"github.com/answerdev/answer/i18n"
|
||||||
"github.com/answerdev/answer/pkg/dir"
|
"github.com/answerdev/answer/pkg/dir"
|
||||||
|
"github.com/answerdev/answer/pkg/writer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultConfigFileName = "config.yaml"
|
DefaultConfigFileName = "config.yaml"
|
||||||
|
DefaultCacheFileName = "cache.db"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ConfigFilePath = "/conf/"
|
ConfigFileDir = "/conf/"
|
||||||
UploadFilePath = "/upfiles/"
|
UploadFilePath = "/uploads/"
|
||||||
I18nPath = "/i18n/"
|
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
|
// InstallAllInitialEnvironment install all initial environment
|
||||||
func InstallAllInitialEnvironment(dataDirPath string) {
|
func InstallAllInitialEnvironment(dataDirPath string) {
|
||||||
ConfigFilePath = filepath.Join(dataDirPath, ConfigFilePath)
|
FormatAllPath(dataDirPath)
|
||||||
UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
|
|
||||||
I18nPath = filepath.Join(dataDirPath, I18nPath)
|
|
||||||
|
|
||||||
installConfigFile()
|
|
||||||
installUploadDir()
|
installUploadDir()
|
||||||
installI18nBundle()
|
installI18nBundle()
|
||||||
fmt.Println("install all initial environment done")
|
fmt.Println("install all initial environment done")
|
||||||
}
|
}
|
||||||
|
|
||||||
func installConfigFile() {
|
func InstallConfigFile(configFilePath string) error {
|
||||||
fmt.Println("[config-file] try to install...")
|
if len(configFilePath) == 0 {
|
||||||
defaultConfigFile := filepath.Join(ConfigFilePath, DefaultConfigFileName)
|
configFilePath = filepath.Join(ConfigFileDir, DefaultConfigFileName)
|
||||||
|
}
|
||||||
|
fmt.Println("[config-file] try to create at ", configFilePath)
|
||||||
|
|
||||||
// if config file already exists do nothing.
|
// if config file already exists do nothing.
|
||||||
if CheckConfigFile(defaultConfigFile) {
|
if CheckConfigFile(configFilePath) {
|
||||||
fmt.Printf("[config-file] %s already exists\n", defaultConfigFile)
|
fmt.Printf("[config-file] %s already exists\n", configFilePath)
|
||||||
return
|
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())
|
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())
|
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")
|
fmt.Printf("[config-file] install success\n")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func installUploadDir() {
|
func installUploadDir() {
|
||||||
|
@ -85,7 +97,7 @@ func installI18nBundle() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Printf("[i18n] install %s bundle...\n", item.Name())
|
fmt.Printf("[i18n] install %s bundle...\n", item.Name())
|
||||||
err = writerFile(path, string(content))
|
err = writer.WriteFile(path, string(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error())
|
fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error())
|
||||||
} else {
|
} 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
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/base/data"
|
"github.com/answerdev/answer/internal/base/data"
|
||||||
|
"github.com/answerdev/answer/internal/entity"
|
||||||
"github.com/answerdev/answer/pkg/dir"
|
"github.com/answerdev/answer/pkg/dir"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,12 +16,40 @@ func CheckUploadDir() bool {
|
||||||
return dir.CheckDirExist(UploadFilePath)
|
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)
|
db, err := data.NewDB(false, dataConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("connection database failed: %s\n", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if err = db.Ping(); err != nil {
|
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 false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/answerdev/answer/internal/entity"
|
"github.com/answerdev/answer/internal/entity"
|
||||||
"github.com/answerdev/answer/internal/schema"
|
"github.com/answerdev/answer/internal/schema"
|
||||||
"github.com/answerdev/answer/internal/service"
|
"github.com/answerdev/answer/internal/service"
|
||||||
|
"github.com/answerdev/answer/internal/service/dashboard"
|
||||||
"github.com/answerdev/answer/internal/service/rank"
|
"github.com/answerdev/answer/internal/service/rank"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/segmentfault/pacman/errors"
|
"github.com/segmentfault/pacman/errors"
|
||||||
|
@ -18,11 +19,19 @@ import (
|
||||||
type AnswerController struct {
|
type AnswerController struct {
|
||||||
answerService *service.AnswerService
|
answerService *service.AnswerService
|
||||||
rankService *rank.RankService
|
rankService *rank.RankService
|
||||||
|
dashboardService *dashboard.DashboardService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAnswerController new controller
|
// NewAnswerController new controller
|
||||||
func NewAnswerController(answerService *service.AnswerService, rankService *rank.RankService) *AnswerController {
|
func NewAnswerController(answerService *service.AnswerService,
|
||||||
return &AnswerController{answerService: answerService, rankService: rankService}
|
rankService *rank.RankService,
|
||||||
|
dashboardService *dashboard.DashboardService,
|
||||||
|
) *AnswerController {
|
||||||
|
return &AnswerController{
|
||||||
|
answerService: answerService,
|
||||||
|
rankService: rankService,
|
||||||
|
dashboardService: dashboardService,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveAnswer delete answer
|
// RemoveAnswer delete answer
|
||||||
|
|
|
@ -20,4 +20,5 @@ var ProviderSetController = wire.NewSet(
|
||||||
NewReasonController,
|
NewReasonController,
|
||||||
NewNotificationController,
|
NewNotificationController,
|
||||||
NewSiteinfoController,
|
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"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/base/handler"
|
"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/gin-gonic/gin"
|
||||||
"github.com/segmentfault/pacman/i18n"
|
"github.com/segmentfault/pacman/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LangController struct {
|
type LangController struct {
|
||||||
translator i18n.Translator
|
translator i18n.Translator
|
||||||
|
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLangController new language controller.
|
// NewLangController new language controller.
|
||||||
func NewLangController(tr i18n.Translator) *LangController {
|
func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController {
|
||||||
return &LangController{translator: tr}
|
return &LangController{translator: tr, siteInfoService: siteInfoService}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLangMapping get language config mapping
|
// GetLangMapping get language config mapping
|
||||||
|
@ -33,15 +35,38 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) {
|
||||||
handler.HandleResponse(ctx, nil, resp)
|
handler.HandleResponse(ctx, nil, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLangOptions Get language options
|
// GetAdminLangOptions Get language options
|
||||||
// @Summary Get language options
|
// @Summary Get language options
|
||||||
// @Description Get language options
|
// @Description Get language options
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Tags Lang
|
// @Tags Lang
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} handler.RespBody{}
|
// @Success 200 {object} handler.RespBody{}
|
||||||
// @Router /answer/api/v1/language/options [get]
|
// @Router /answer/api/v1/language/options [get]
|
||||||
// @Router /answer/admin/api/language/options [get]
|
// @Router /answer/admin/api/language/options [get]
|
||||||
func (u *LangController) GetLangOptions(ctx *gin.Context) {
|
func (u *LangController) GetAdminLangOptions(ctx *gin.Context) {
|
||||||
handler.HandleResponse(ctx, nil, schema.GetLangOptions)
|
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 (
|
import (
|
||||||
"github.com/answerdev/answer/internal/base/handler"
|
"github.com/answerdev/answer/internal/base/handler"
|
||||||
"github.com/answerdev/answer/internal/schema"
|
"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"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SiteinfoController struct {
|
type SiteinfoController struct {
|
||||||
siteInfoService *service.SiteInfoService
|
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSiteinfoController new siteinfo controller.
|
// NewSiteinfoController new siteinfo controller.
|
||||||
func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController {
|
func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController {
|
||||||
return &SiteinfoController{
|
return &SiteinfoController{
|
||||||
siteInfoService: siteInfoService,
|
siteInfoService: siteInfoService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInfo godoc
|
// GetSiteInfo get site info
|
||||||
// @Summary Get siteinfo
|
// @Summary get site info
|
||||||
// @Description Get siteinfo
|
// @Description get site info
|
||||||
// @Tags site
|
// @Tags site
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp}
|
// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp}
|
||||||
// @Router /answer/api/v1/siteinfo [get]
|
// @Router /answer/api/v1/siteinfo [get]
|
||||||
func (sc *SiteinfoController) GetInfo(ctx *gin.Context) {
|
func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
|
||||||
var (
|
var err error
|
||||||
resp = &schema.SiteInfoResp{}
|
resp := &schema.SiteInfoResp{}
|
||||||
general schema.SiteGeneralResp
|
resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)
|
||||||
face schema.SiteInterfaceResp
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
general, err = sc.siteInfoService.GetSiteGeneral(ctx)
|
|
||||||
resp.General = &general
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handler.HandleResponse(ctx, err, resp)
|
handler.HandleResponse(ctx, err, resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx)
|
||||||
face, err = sc.siteInfoService.GetSiteInterface(ctx)
|
|
||||||
resp.Face = &face
|
|
||||||
|
|
||||||
handler.HandleResponse(ctx, err, resp)
|
handler.HandleResponse(ctx, err, resp)
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,22 +89,6 @@ func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) {
|
||||||
handler.HandleResponse(ctx, err, resp)
|
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
|
// UserEmailLogin godoc
|
||||||
// @Summary UserEmailLogin
|
// @Summary UserEmailLogin
|
||||||
// @Description UserEmailLogin
|
// @Description UserEmailLogin
|
||||||
|
@ -373,6 +357,27 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
|
||||||
handler.HandleResponse(ctx, err, nil)
|
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
|
// UploadUserAvatar godoc
|
||||||
// @Summary UserUpdateInfo
|
// @Summary UserUpdateInfo
|
||||||
// @Description UserUpdateInfo
|
// @Description UserUpdateInfo
|
||||||
|
@ -490,6 +495,10 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
|
||||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||||
// If the user is not logged in, the api cannot be used.
|
// 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 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)
|
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||||
if !captchaPass {
|
if !captchaPass {
|
||||||
|
@ -501,13 +510,15 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
|
||||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
|
||||||
if len(req.UserID) == 0 {
|
resp, err := uc.userService.UserChangeEmailSendCode(ctx, req)
|
||||||
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
|
if err != nil {
|
||||||
|
if resp != nil {
|
||||||
|
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
|
||||||
|
}
|
||||||
|
handler.HandleResponse(ctx, err, resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
|
|
||||||
err := uc.userService.UserChangeEmailSendCode(ctx, req)
|
|
||||||
handler.HandleResponse(ctx, err, nil)
|
handler.HandleResponse(ctx, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,24 +3,24 @@ package controller_backyard
|
||||||
import (
|
import (
|
||||||
"github.com/answerdev/answer/internal/base/handler"
|
"github.com/answerdev/answer/internal/base/handler"
|
||||||
"github.com/answerdev/answer/internal/schema"
|
"github.com/answerdev/answer/internal/schema"
|
||||||
"github.com/answerdev/answer/internal/service"
|
"github.com/answerdev/answer/internal/service/siteinfo"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SiteInfoController struct {
|
type SiteInfoController struct {
|
||||||
siteInfoService *service.SiteInfoService
|
siteInfoService *siteinfo.SiteInfoService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSiteInfoController new siteinfo controller.
|
// NewSiteInfoController new siteinfo controller.
|
||||||
func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController {
|
func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController {
|
||||||
return &SiteInfoController{
|
return &SiteInfoController{
|
||||||
siteInfoService: siteInfoService,
|
siteInfoService: siteInfoService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGeneral godoc
|
// GetGeneral get site general information
|
||||||
// @Summary Get siteinfo general
|
// @Summary get site general information
|
||||||
// @Description Get siteinfo general
|
// @Description get site general information
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Tags admin
|
// @Tags admin
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
@ -31,23 +31,22 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) {
|
||||||
handler.HandleResponse(ctx, err, resp)
|
handler.HandleResponse(ctx, err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInterface godoc
|
// GetInterface get site interface
|
||||||
// @Summary Get siteinfo interface
|
// @Summary get site interface
|
||||||
// @Description Get siteinfo interface
|
// @Description get site interface
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Tags admin
|
// @Tags admin
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp}
|
// @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp}
|
||||||
// @Router /answer/admin/api/siteinfo/interface [get]
|
// @Router /answer/admin/api/siteinfo/interface [get]
|
||||||
// @Param data body schema.AddCommentReq true "general"
|
|
||||||
func (sc *SiteInfoController) GetInterface(ctx *gin.Context) {
|
func (sc *SiteInfoController) GetInterface(ctx *gin.Context) {
|
||||||
resp, err := sc.siteInfoService.GetSiteInterface(ctx)
|
resp, err := sc.siteInfoService.GetSiteInterface(ctx)
|
||||||
handler.HandleResponse(ctx, err, resp)
|
handler.HandleResponse(ctx, err, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateGeneral godoc
|
// UpdateGeneral update site general information
|
||||||
// @Summary Get siteinfo interface
|
// @Summary update site general information
|
||||||
// @Description Get siteinfo interface
|
// @Description update site general information
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Tags admin
|
// @Tags admin
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
@ -63,9 +62,9 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) {
|
||||||
handler.HandleResponse(ctx, err, nil)
|
handler.HandleResponse(ctx, err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateInterface godoc
|
// UpdateInterface update site interface
|
||||||
// @Summary Get siteinfo interface
|
// @Summary update site info interface
|
||||||
// @Description Get siteinfo interface
|
// @Description update site info interface
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Tags admin
|
// @Tags admin
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
|
|
@ -45,6 +45,7 @@ type User struct {
|
||||||
Location string `xorm:"not null default '' VARCHAR(100) location"`
|
Location string `xorm:"not null default '' VARCHAR(100) location"`
|
||||||
IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"`
|
IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"`
|
||||||
IsAdmin bool `xorm:"not null default false BOOL is_admin"`
|
IsAdmin bool `xorm:"not null default false BOOL is_admin"`
|
||||||
|
Language string `xorm:"not null default '' VARCHAR(100) language"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName user table name
|
// 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
|
package migrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/base/data"
|
"github.com/answerdev/answer/internal/base/data"
|
||||||
"github.com/answerdev/answer/internal/entity"
|
"github.com/answerdev/answer/internal/entity"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -55,11 +57,6 @@ func InitDB(dataConf *data.Database) (err error) {
|
||||||
return fmt.Errorf("init admin user failed: %s", err)
|
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)
|
err = initConfigTable(engine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("init config table: %s", err)
|
return fmt.Errorf("init config table: %s", err)
|
||||||
|
@ -82,12 +79,79 @@ func initAdminUser(engine *xorm.Engine) error {
|
||||||
return err
|
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{
|
_, err := engine.InsertOne(&entity.SiteInfo{
|
||||||
Type: "interface",
|
Type: "interface",
|
||||||
Content: `{"logo":"","theme":"black","language":"en_US"}`,
|
Content: string(interfaceDataBytes),
|
||||||
Status: 1,
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +189,7 @@ func initConfigTable(engine *xorm.Engine) error {
|
||||||
{ID: 30, Key: "answer.vote_up", Value: `0`},
|
{ID: 30, Key: "answer.vote_up", Value: `0`},
|
||||||
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
|
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
|
||||||
{ID: 32, Key: "question.follow", 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: 35, Key: "tag.follow", Value: `0`},
|
||||||
{ID: 36, Key: "rank.question.add", Value: `0`},
|
{ID: 36, Key: "rank.question.add", Value: `0`},
|
||||||
{ID: 37, Key: "rank.question.edit", 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/base/data"
|
||||||
"github.com/answerdev/answer/internal/entity"
|
"github.com/answerdev/answer/internal/entity"
|
||||||
"github.com/segmentfault/pacman/log"
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,6 +42,7 @@ var noopMigration = func(_ *xorm.Engine) error { return nil }
|
||||||
var migrations = []Migration{
|
var migrations = []Migration{
|
||||||
// 0->1
|
// 0->1
|
||||||
NewMigration("this is first version, no operation", noopMigration),
|
NewMigration("this is first version, no operation", noopMigration),
|
||||||
|
NewMigration("add user language", addUserLanguage),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current db version
|
// GetCurrentDBVersion returns the current db version
|
||||||
|
@ -86,17 +86,17 @@ func Migrate(dataConf *data.Database) error {
|
||||||
expectedVersion := ExpectedVersion()
|
expectedVersion := ExpectedVersion()
|
||||||
|
|
||||||
for currentDBVersion < 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)
|
currentDBVersion, currentDBVersion+1, expectedVersion)
|
||||||
migrationFunc := migrations[currentDBVersion]
|
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 {
|
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
|
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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
currentDBVersion++
|
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"
|
"context"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/base/data"
|
"github.com/answerdev/answer/internal/base/data"
|
||||||
|
"github.com/answerdev/answer/internal/base/reason"
|
||||||
"github.com/answerdev/answer/internal/entity"
|
"github.com/answerdev/answer/internal/entity"
|
||||||
"github.com/answerdev/answer/internal/service/activity_common"
|
"github.com/answerdev/answer/internal/service/activity_common"
|
||||||
|
"github.com/segmentfault/pacman/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VoteRepo activity repository
|
// VoteRepo activity repository
|
||||||
|
@ -39,3 +41,12 @@ func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string)
|
||||||
}
|
}
|
||||||
return ""
|
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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/base/constant"
|
"github.com/answerdev/answer/internal/base/constant"
|
||||||
|
@ -102,6 +103,16 @@ func (ar *answerRepo) GetAnswer(ctx context.Context, id string) (
|
||||||
return
|
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
|
// GetAnswerList get answer list all
|
||||||
func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) {
|
func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) {
|
||||||
answerList = make([]*entity.Answer, 0)
|
answerList = make([]*entity.Answer, 0)
|
||||||
|
|
|
@ -79,6 +79,15 @@ func (cr *commentRepo) GetComment(ctx context.Context, commentID string) (
|
||||||
return
|
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
|
// GetCommentPage get comment page
|
||||||
func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) (
|
func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) (
|
||||||
commentList []*entity.Comment, total int64, err error,
|
commentList []*entity.Comment, total int64, err error,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/base/constant"
|
"github.com/answerdev/answer/internal/base/constant"
|
||||||
|
@ -162,6 +163,16 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu
|
||||||
return
|
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
|
// GetQuestionPage get question page
|
||||||
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) {
|
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)
|
questionList = make([]*entity.Question, 0)
|
||||||
|
|
|
@ -94,3 +94,12 @@ func (ar *reportRepo) UpdateByID(
|
||||||
}
|
}
|
||||||
return
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/answerdev/answer/pkg/htmltext"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ var (
|
||||||
"`question`.`id`",
|
"`question`.`id`",
|
||||||
"`question`.`id` as `question_id`",
|
"`question`.`id` as `question_id`",
|
||||||
"`title`",
|
"`title`",
|
||||||
"`original_text`",
|
"`parsed_text`",
|
||||||
"`question`.`created_at`",
|
"`question`.`created_at`",
|
||||||
"`user_id`",
|
"`user_id`",
|
||||||
"`vote_count`",
|
"`vote_count`",
|
||||||
|
@ -38,7 +39,7 @@ var (
|
||||||
"`answer`.`id` as `id`",
|
"`answer`.`id` as `id`",
|
||||||
"`question_id`",
|
"`question_id`",
|
||||||
"`question`.`title` as `title`",
|
"`question`.`title` as `title`",
|
||||||
"`answer`.`original_text` as `original_text`",
|
"`answer`.`parsed_text` as `parsed_text`",
|
||||||
"`answer`.`created_at`",
|
"`answer`.`created_at`",
|
||||||
"`answer`.`user_id` as `user_id`",
|
"`answer`.`user_id` as `user_id`",
|
||||||
"`answer`.`vote_count` as `vote_count`",
|
"`answer`.`vote_count` as `vote_count`",
|
||||||
|
@ -142,13 +143,22 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
|
||||||
argsA = append(argsA, votes)
|
argsA = append(argsA, votes)
|
||||||
}
|
}
|
||||||
|
|
||||||
b = b.Union("all", ub)
|
//b = b.Union("all", ub)
|
||||||
|
ubSQL, _, err := ub.ToSQL()
|
||||||
querySQL, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -412,7 +422,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
|
||||||
object = schema.SearchObject{
|
object = schema.SearchObject{
|
||||||
ID: string(r["id"]),
|
ID: string(r["id"]),
|
||||||
Title: string(r["title"]),
|
Title: string(r["title"]),
|
||||||
Excerpt: cutOutParsedText(string(r["original_text"])),
|
Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240),
|
||||||
CreatedAtParsed: tp.Unix(),
|
CreatedAtParsed: tp.Unix(),
|
||||||
UserInfo: userInfo,
|
UserInfo: userInfo,
|
||||||
Tags: tags,
|
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{}) {
|
func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) {
|
||||||
relevanceRes := []string{}
|
relevanceRes := []string{}
|
||||||
args = []interface{}{}
|
args = []interface{}{}
|
||||||
|
|
|
@ -101,6 +101,14 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err
|
||||||
return
|
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
|
// UpdateInfo update user info
|
||||||
func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {
|
func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {
|
||||||
_, err = ur.data.DB.Where("id = ?", userInfo.ID).
|
_, err = ur.data.DB.Where("id = ?", userInfo.ID).
|
||||||
|
@ -149,3 +157,12 @@ func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *ent
|
||||||
}
|
}
|
||||||
return
|
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_backyard.SiteInfoController
|
||||||
siteinfoController *controller.SiteinfoController
|
siteinfoController *controller.SiteinfoController
|
||||||
notificationController *controller.NotificationController
|
notificationController *controller.NotificationController
|
||||||
|
dashboardController *controller.DashboardController
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAnswerAPIRouter(
|
func NewAnswerAPIRouter(
|
||||||
|
@ -50,6 +51,7 @@ func NewAnswerAPIRouter(
|
||||||
siteInfoController *controller_backyard.SiteInfoController,
|
siteInfoController *controller_backyard.SiteInfoController,
|
||||||
siteinfoController *controller.SiteinfoController,
|
siteinfoController *controller.SiteinfoController,
|
||||||
notificationController *controller.NotificationController,
|
notificationController *controller.NotificationController,
|
||||||
|
dashboardController *controller.DashboardController,
|
||||||
|
|
||||||
) *AnswerAPIRouter {
|
) *AnswerAPIRouter {
|
||||||
return &AnswerAPIRouter{
|
return &AnswerAPIRouter{
|
||||||
|
@ -73,13 +75,14 @@ func NewAnswerAPIRouter(
|
||||||
siteInfoController: siteInfoController,
|
siteInfoController: siteInfoController,
|
||||||
notificationController: notificationController,
|
notificationController: notificationController,
|
||||||
siteinfoController: siteinfoController,
|
siteinfoController: siteinfoController,
|
||||||
|
dashboardController: dashboardController,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
||||||
// i18n
|
// i18n
|
||||||
r.GET("/language/config", a.langController.GetLangMapping)
|
r.GET("/language/config", a.langController.GetLangMapping)
|
||||||
r.GET("/language/options", a.langController.GetLangOptions)
|
r.GET("/language/options", a.langController.GetUserLangOptions)
|
||||||
|
|
||||||
// comment
|
// comment
|
||||||
r.GET("/comment/page", a.commentController.GetCommentWithPage)
|
r.GET("/comment/page", a.commentController.GetCommentWithPage)
|
||||||
|
@ -88,7 +91,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
||||||
|
|
||||||
// user
|
// user
|
||||||
r.GET("/user/info", a.userController.GetUserInfoByUserID)
|
r.GET("/user/info", a.userController.GetUserInfoByUserID)
|
||||||
r.GET("/user/status", a.userController.GetUserStatus)
|
|
||||||
r.GET("/user/action/record", a.userController.ActionRecord)
|
r.GET("/user/action/record", a.userController.ActionRecord)
|
||||||
r.POST("/user/login/email", a.userController.UserEmailLogin)
|
r.POST("/user/login/email", a.userController.UserEmailLogin)
|
||||||
r.POST("/user/register/email", a.userController.UserRegisterByEmail)
|
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)
|
r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage)
|
||||||
|
|
||||||
//siteinfo
|
//siteinfo
|
||||||
r.GET("/siteinfo", a.siteinfoController.GetInfo)
|
r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
||||||
|
@ -177,6 +179,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
||||||
// user
|
// user
|
||||||
r.PUT("/user/password", a.userController.UserModifyPassWord)
|
r.PUT("/user/password", a.userController.UserModifyPassWord)
|
||||||
r.PUT("/user/info", a.userController.UserUpdateInfo)
|
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/avatar/upload", a.userController.UploadUserAvatar)
|
||||||
r.POST("/user/post/file", a.userController.UploadUserPostFile)
|
r.POST("/user/post/file", a.userController.UploadUserPostFile)
|
||||||
r.POST("/user/notice/set", a.userController.UserNoticeSet)
|
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)
|
r.GET("/reasons", a.reasonController.Reasons)
|
||||||
|
|
||||||
// language
|
// language
|
||||||
r.GET("/language/options", a.langController.GetLangOptions)
|
r.GET("/language/options", a.langController.GetAdminLangOptions)
|
||||||
|
|
||||||
// theme
|
// theme
|
||||||
r.GET("/theme/options", a.themeController.GetThemeOptions)
|
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.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
|
||||||
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
|
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
|
||||||
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
|
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
|
// SwaggerConfig struct describes configure for the Swagger API endpoint
|
||||||
type SwaggerConfig struct {
|
type SwaggerConfig struct {
|
||||||
Show bool `json:"show"`
|
Show bool `json:"show" mapstructure:"show" yaml:"show"`
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol" mapstructure:"protocol" yaml:"protocol"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host" mapstructure:"host" yaml:"host"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address" mapstructure:"address" yaml:"address"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,9 @@ func (a *UIRouter) Register(r *gin.Engine) {
|
||||||
filePath = UIRootFilePath + name
|
filePath = UIRootFilePath + name
|
||||||
case "/manifest.json":
|
case "/manifest.json":
|
||||||
filePath = UIRootFilePath + name
|
filePath = UIRootFilePath + name
|
||||||
|
case "/install":
|
||||||
|
c.Redirect(http.StatusFound, "/")
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
filePath = UIIndexFilePath
|
filePath = UIIndexFilePath
|
||||||
c.Header("content-type", "text/html;charset=utf-8")
|
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 ErrTypeModal = ErrTypeData{ErrType: "modal"}
|
||||||
|
|
||||||
var ErrTypeToast = ErrTypeData{ErrType: "toast"}
|
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
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
// SiteGeneralReq site general request
|
// SiteGeneralReq site general request
|
||||||
type SiteGeneralReq struct {
|
type SiteGeneralReq struct {
|
||||||
Name string `validate:"required,gt=1,lte=128" comment:"site name" form:"name" json:"name"`
|
Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"`
|
||||||
ShortDescription string `validate:"required,gt=3,lte=255" comment:"short site description" form:"short_description" json:"short_description"`
|
ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"`
|
||||||
Description string `validate:"required,gt=3,lte=2000" comment:"site description" form:"description" json:"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
|
// SiteInterfaceReq site interface request
|
||||||
type SiteInterfaceReq struct {
|
type SiteInterfaceReq struct {
|
||||||
Logo string `validate:"omitempty,gt=0,lte=256" comment:"logo" form:"logo" json:"logo"`
|
Logo string `validate:"omitempty,gt=0,lte=256" form:"logo" json:"logo"`
|
||||||
Theme string `validate:"required,gt=1,lte=128" comment:"theme" form:"theme" json:"theme"`
|
Theme string `validate:"required,gt=1,lte=128" form:"theme" json:"theme"`
|
||||||
Language string `validate:"required,gt=1,lte=128" comment:"interface language" form:"language" json:"language"`
|
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
|
// SiteGeneralResp site general response
|
||||||
|
|
|
@ -62,6 +62,8 @@ type GetUserResp struct {
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
// ip info
|
// ip info
|
||||||
IPInfo string `json:"ip_info"`
|
IPInfo string `json:"ip_info"`
|
||||||
|
// language
|
||||||
|
Language string `json:"language"`
|
||||||
// access token
|
// access token
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
// is admin
|
// is admin
|
||||||
|
@ -305,6 +307,14 @@ func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error)
|
||||||
return nil, nil
|
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 {
|
type UserRetrievePassWordRequest struct {
|
||||||
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail
|
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail
|
||||||
CaptchaID string `json:"captcha_id" ` // captcha_id
|
CaptchaID string `json:"captcha_id" ` // captcha_id
|
||||||
|
|
|
@ -7,4 +7,5 @@ import (
|
||||||
// VoteRepo activity repository
|
// VoteRepo activity repository
|
||||||
type VoteRepo interface {
|
type VoteRepo interface {
|
||||||
GetVoteStatus(ctx context.Context, objectId, userId string) (status string)
|
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/entity"
|
||||||
"github.com/answerdev/answer/internal/schema"
|
"github.com/answerdev/answer/internal/schema"
|
||||||
|
"github.com/answerdev/answer/pkg/htmltext"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AnswerRepo interface {
|
type AnswerRepo interface {
|
||||||
|
@ -20,6 +21,7 @@ type AnswerRepo interface {
|
||||||
SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error)
|
SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error)
|
||||||
CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error)
|
CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error)
|
||||||
UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error)
|
UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error)
|
||||||
|
GetAnswerCount(ctx context.Context) (count int64, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnswerCommon user service
|
// AnswerCommon user service
|
||||||
|
@ -74,11 +76,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer
|
||||||
info := schema.AdminAnswerInfo{}
|
info := schema.AdminAnswerInfo{}
|
||||||
info.ID = data.ID
|
info.ID = data.ID
|
||||||
info.QuestionID = data.QuestionID
|
info.QuestionID = data.QuestionID
|
||||||
info.Description = data.ParsedText
|
|
||||||
info.Adopted = data.Adopted
|
info.Adopted = data.Adopted
|
||||||
info.VoteCount = data.VoteCount
|
info.VoteCount = data.VoteCount
|
||||||
info.CreateTime = data.CreatedAt.Unix()
|
info.CreateTime = data.CreatedAt.Unix()
|
||||||
info.UpdateTime = data.UpdatedAt.Unix()
|
info.UpdateTime = data.UpdatedAt.Unix()
|
||||||
info.UserID = data.UserID
|
info.UserID = data.UserID
|
||||||
|
info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240)
|
||||||
return &info
|
return &info
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
// CommentCommonRepo comment repository
|
// CommentCommonRepo comment repository
|
||||||
type CommentCommonRepo interface {
|
type CommentCommonRepo interface {
|
||||||
GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)
|
GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)
|
||||||
|
GetCommentCount(ctx context.Context) (count int64, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentCommonService user service
|
// 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"
|
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||||
"github.com/answerdev/answer/internal/service/comment"
|
"github.com/answerdev/answer/internal/service/comment"
|
||||||
"github.com/answerdev/answer/internal/service/comment_common"
|
"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/export"
|
||||||
"github.com/answerdev/answer/internal/service/follow"
|
"github.com/answerdev/answer/internal/service/follow"
|
||||||
"github.com/answerdev/answer/internal/service/meta"
|
"github.com/answerdev/answer/internal/service/meta"
|
||||||
|
@ -21,6 +22,8 @@ import (
|
||||||
"github.com/answerdev/answer/internal/service/report_backyard"
|
"github.com/answerdev/answer/internal/service/report_backyard"
|
||||||
"github.com/answerdev/answer/internal/service/report_handle_backyard"
|
"github.com/answerdev/answer/internal/service/report_handle_backyard"
|
||||||
"github.com/answerdev/answer/internal/service/revision_common"
|
"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"
|
"github.com/answerdev/answer/internal/service/tag"
|
||||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||||
"github.com/answerdev/answer/internal/service/uploader"
|
"github.com/answerdev/answer/internal/service/uploader"
|
||||||
|
@ -61,8 +64,10 @@ var ProviderSetService = wire.NewSet(
|
||||||
report_backyard.NewReportBackyardService,
|
report_backyard.NewReportBackyardService,
|
||||||
user_backyard.NewUserBackyardService,
|
user_backyard.NewUserBackyardService,
|
||||||
reason.NewReasonService,
|
reason.NewReasonService,
|
||||||
NewSiteInfoService,
|
siteinfo_common.NewSiteInfoCommonService,
|
||||||
|
siteinfo.NewSiteInfoService,
|
||||||
notficationcommon.NewNotificationCommon,
|
notficationcommon.NewNotificationCommon,
|
||||||
notification.NewNotificationService,
|
notification.NewNotificationService,
|
||||||
activity.NewAnswerActivityService,
|
activity.NewAnswerActivityService,
|
||||||
|
dashboard.NewDashboardService,
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,6 +38,7 @@ type QuestionRepo interface {
|
||||||
UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)
|
UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)
|
||||||
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
|
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
|
||||||
CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error)
|
CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error)
|
||||||
|
GetQuestionCount(ctx context.Context) (count int64, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// QuestionCommon user service
|
// QuestionCommon user service
|
||||||
|
|
|
@ -2,9 +2,8 @@ package report_backyard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/service/config"
|
"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/pager"
|
||||||
"github.com/answerdev/answer/internal/base/reason"
|
"github.com/answerdev/answer/internal/base/reason"
|
||||||
|
@ -180,20 +179,20 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
|
||||||
case "question":
|
case "question":
|
||||||
r.QuestionID = questionId
|
r.QuestionID = questionId
|
||||||
r.Title = question.Title
|
r.Title = question.Title
|
||||||
r.Excerpt = rs.cutOutTagParsedText(question.OriginalText)
|
r.Excerpt = htmltext.FetchExcerpt(question.ParsedText, "...", 240)
|
||||||
|
|
||||||
case "answer":
|
case "answer":
|
||||||
r.QuestionID = questionId
|
r.QuestionID = questionId
|
||||||
r.AnswerID = answerId
|
r.AnswerID = answerId
|
||||||
r.Title = question.Title
|
r.Title = question.Title
|
||||||
r.Excerpt = rs.cutOutTagParsedText(answer.OriginalText)
|
r.Excerpt = htmltext.FetchExcerpt(answer.ParsedText, "...", 240)
|
||||||
|
|
||||||
case "comment":
|
case "comment":
|
||||||
r.QuestionID = questionId
|
r.QuestionID = questionId
|
||||||
r.AnswerID = answerId
|
r.AnswerID = answerId
|
||||||
r.CommentID = commentId
|
r.CommentID = commentId
|
||||||
r.Title = question.Title
|
r.Title = question.Title
|
||||||
r.Excerpt = rs.cutOutTagParsedText(cmt.OriginalText)
|
r.Excerpt = htmltext.FetchExcerpt(cmt.ParsedText, "...", 240)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse reason
|
// parse reason
|
||||||
|
@ -214,12 +213,3 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
|
||||||
}
|
}
|
||||||
resp = &res
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/entity"
|
"github.com/answerdev/answer/internal/entity"
|
||||||
"github.com/answerdev/answer/internal/schema"
|
"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)
|
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)
|
GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error)
|
||||||
UpdateByID(ctx context.Context, id string, handleData entity.Report) (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
|
package service_config
|
||||||
|
|
||||||
type ServiceConfig struct {
|
type ServiceConfig struct {
|
||||||
SecretKey string `json:"secret_key" mapstructure:"secret_key"`
|
SecretKey string `json:"secret_key" mapstructure:"secret_key" yaml:"secret_key"`
|
||||||
WebHost string `json:"web_host" mapstructure:"web_host"`
|
UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"`
|
||||||
UploadPath string `json:"upload_path" mapstructure:"upload_path"`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package service
|
package siteinfo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/answerdev/answer/internal/base/constant"
|
||||||
"github.com/answerdev/answer/internal/base/reason"
|
"github.com/answerdev/answer/internal/base/reason"
|
||||||
|
"github.com/answerdev/answer/internal/base/translator"
|
||||||
"github.com/answerdev/answer/internal/entity"
|
"github.com/answerdev/answer/internal/entity"
|
||||||
"github.com/answerdev/answer/internal/schema"
|
"github.com/answerdev/answer/internal/schema"
|
||||||
"github.com/answerdev/answer/internal/service/export"
|
"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) {
|
// GetSiteGeneral get site info general
|
||||||
var (
|
func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
|
||||||
siteType = "general"
|
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
|
||||||
siteInfo *entity.SiteInfo
|
if err != nil {
|
||||||
exist bool
|
return nil, err
|
||||||
)
|
}
|
||||||
resp = schema.SiteGeneralResp{}
|
|
||||||
|
|
||||||
siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType)
|
|
||||||
if !exist {
|
if !exist {
|
||||||
return
|
return nil, errors.BadRequest(reason.SiteInfoNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = json.Unmarshal([]byte(siteInfo.Content), &resp)
|
resp = &schema.SiteGeneralResp{}
|
||||||
return
|
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) {
|
// GetSiteInterface get site info interface
|
||||||
var (
|
func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
|
||||||
siteType = "interface"
|
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
|
||||||
siteInfo *entity.SiteInfo
|
if err != nil {
|
||||||
exist bool
|
return nil, err
|
||||||
)
|
}
|
||||||
resp = schema.SiteInterfaceResp{}
|
|
||||||
|
|
||||||
siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType)
|
|
||||||
if !exist {
|
if !exist {
|
||||||
return
|
return nil, errors.BadRequest(reason.SiteInfoNotFound)
|
||||||
}
|
}
|
||||||
|
resp = &schema.SiteInterfaceResp{}
|
||||||
_ = json.Unmarshal([]byte(siteInfo.Content), &resp)
|
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
|
||||||
return
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {
|
func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {
|
||||||
|
req.FormatSiteUrl()
|
||||||
var (
|
var (
|
||||||
siteType = "general"
|
siteType = "general"
|
||||||
content []byte
|
content []byte
|
||||||
|
@ -78,8 +76,7 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe
|
||||||
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
|
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
|
||||||
var (
|
var (
|
||||||
siteType = "interface"
|
siteType = "interface"
|
||||||
themeExist,
|
themeExist bool
|
||||||
langExist bool
|
|
||||||
content []byte
|
content []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -96,13 +93,7 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site
|
||||||
}
|
}
|
||||||
|
|
||||||
// check language
|
// check language
|
||||||
for _, lang := range schema.GetLangOptions {
|
if !translator.CheckLanguageIsValid(req.Language) {
|
||||||
if lang.Value == req.Language {
|
|
||||||
langExist = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !langExist {
|
|
||||||
err = errors.BadRequest(reason.LangNotFound)
|
err = errors.BadRequest(reason.LangNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ package siteinfo_common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/entity"
|
"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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/answerdev/answer/internal/service/revision_common"
|
"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/pager"
|
||||||
"github.com/answerdev/answer/internal/base/reason"
|
"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)
|
resp := make([]*schema.GetTagPageResp, 0)
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
|
excerpt := htmltext.FetchExcerpt(tag.ParsedText, "...", 240)
|
||||||
resp = append(resp, &schema.GetTagPageResp{
|
resp = append(resp, &schema.GetTagPageResp{
|
||||||
TagID: tag.ID,
|
TagID: tag.ID,
|
||||||
SlugName: tag.SlugName,
|
SlugName: tag.SlugName,
|
||||||
DisplayName: tag.DisplayName,
|
DisplayName: tag.DisplayName,
|
||||||
OriginalText: cutOutTagParsedText(tag.OriginalText),
|
OriginalText: excerpt,
|
||||||
ParsedText: cutOutTagParsedText(tag.ParsedText),
|
ParsedText: excerpt,
|
||||||
FollowCount: tag.FollowCount,
|
FollowCount: tag.FollowCount,
|
||||||
QuestionCount: tag.QuestionCount,
|
QuestionCount: tag.QuestionCount,
|
||||||
IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID),
|
IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID),
|
||||||
|
@ -371,12 +371,3 @@ func (ts *TagService) checkTagIsFollow(ctx context.Context, userID, tagID string
|
||||||
}
|
}
|
||||||
return followed
|
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/base/reason"
|
||||||
"github.com/answerdev/answer/internal/service/service_config"
|
"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/dir"
|
||||||
"github.com/answerdev/answer/pkg/uid"
|
"github.com/answerdev/answer/pkg/uid"
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
|
@ -28,10 +29,12 @@ const (
|
||||||
// UploaderService user service
|
// UploaderService user service
|
||||||
type UploaderService struct {
|
type UploaderService struct {
|
||||||
serviceConfig *service_config.ServiceConfig
|
serviceConfig *service_config.ServiceConfig
|
||||||
|
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUploaderService new upload service
|
// 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))
|
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -42,6 +45,7 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderSe
|
||||||
}
|
}
|
||||||
return &UploaderService{
|
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) (
|
func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
|
||||||
url string, err error) {
|
url string, err error) {
|
||||||
|
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath)
|
filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath)
|
||||||
if err := ctx.SaveUploadedFile(file, filePath); err != nil {
|
if err := ctx.SaveUploadedFile(file, filePath); err != nil {
|
||||||
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
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
|
return url, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,14 @@ type UserRepo interface {
|
||||||
UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error
|
UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error
|
||||||
UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error
|
UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error
|
||||||
UpdateEmail(ctx context.Context, userID, email string) 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
|
UpdatePass(ctx context.Context, userID, pass string) error
|
||||||
UpdateInfo(ctx context.Context, userInfo *entity.User) (err error)
|
UpdateInfo(ctx context.Context, userInfo *entity.User) (err error)
|
||||||
GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)
|
GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)
|
||||||
BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error)
|
BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error)
|
||||||
GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err 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)
|
GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error)
|
||||||
|
GetUserCount(ctx context.Context) (count int64, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserCommon user service
|
// UserCommon user service
|
||||||
|
|
|
@ -11,12 +11,14 @@ import (
|
||||||
|
|
||||||
"github.com/Chain-Zhang/pinyin"
|
"github.com/Chain-Zhang/pinyin"
|
||||||
"github.com/answerdev/answer/internal/base/reason"
|
"github.com/answerdev/answer/internal/base/reason"
|
||||||
|
"github.com/answerdev/answer/internal/base/translator"
|
||||||
"github.com/answerdev/answer/internal/entity"
|
"github.com/answerdev/answer/internal/entity"
|
||||||
"github.com/answerdev/answer/internal/schema"
|
"github.com/answerdev/answer/internal/schema"
|
||||||
"github.com/answerdev/answer/internal/service/activity"
|
"github.com/answerdev/answer/internal/service/activity"
|
||||||
"github.com/answerdev/answer/internal/service/auth"
|
"github.com/answerdev/answer/internal/service/auth"
|
||||||
"github.com/answerdev/answer/internal/service/export"
|
"github.com/answerdev/answer/internal/service/export"
|
||||||
"github.com/answerdev/answer/internal/service/service_config"
|
"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"
|
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||||
"github.com/answerdev/answer/pkg/checker"
|
"github.com/answerdev/answer/pkg/checker"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -34,6 +36,7 @@ type UserService struct {
|
||||||
serviceConfig *service_config.ServiceConfig
|
serviceConfig *service_config.ServiceConfig
|
||||||
emailService *export.EmailService
|
emailService *export.EmailService
|
||||||
authService *auth.AuthService
|
authService *auth.AuthService
|
||||||
|
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserService(userRepo usercommon.UserRepo,
|
func NewUserService(userRepo usercommon.UserRepo,
|
||||||
|
@ -41,6 +44,7 @@ func NewUserService(userRepo usercommon.UserRepo,
|
||||||
emailService *export.EmailService,
|
emailService *export.EmailService,
|
||||||
authService *auth.AuthService,
|
authService *auth.AuthService,
|
||||||
serviceConfig *service_config.ServiceConfig,
|
serviceConfig *service_config.ServiceConfig,
|
||||||
|
siteInfoService *siteinfo_common.SiteInfoCommonService,
|
||||||
) *UserService {
|
) *UserService {
|
||||||
return &UserService{
|
return &UserService{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
|
@ -48,6 +52,7 @@ func NewUserService(userRepo usercommon.UserRepo,
|
||||||
emailService: emailService,
|
emailService: emailService,
|
||||||
serviceConfig: serviceConfig,
|
serviceConfig: serviceConfig,
|
||||||
authService: authService,
|
authService: authService,
|
||||||
|
siteInfoService: siteInfoService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,35 +71,6 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
|
||||||
return resp, nil
|
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) (
|
func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) (
|
||||||
resp *schema.GetOtherUserInfoResp, err error,
|
resp *schema.GetOtherUserInfoResp, err error,
|
||||||
) {
|
) {
|
||||||
|
@ -168,7 +144,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
|
||||||
UserID: userInfo.ID,
|
UserID: userInfo.ID,
|
||||||
}
|
}
|
||||||
code := uuid.NewString()
|
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)
|
title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -283,6 +259,18 @@ func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, er
|
||||||
return has, nil
|
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
|
// UserRegisterByEmail user register
|
||||||
func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) (
|
func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) (
|
||||||
resp *schema.GetUserResp, err error,
|
resp *schema.GetUserResp, err error,
|
||||||
|
@ -320,7 +308,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
|
||||||
UserID: userInfo.ID,
|
UserID: userInfo.ID,
|
||||||
}
|
}
|
||||||
code := uuid.NewString()
|
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)
|
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -363,7 +351,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e
|
||||||
UserID: userInfo.ID,
|
UserID: userInfo.ID,
|
||||||
}
|
}
|
||||||
code := uuid.NewString()
|
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)
|
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -489,21 +477,26 @@ func (us *UserService) encryptPassword(ctx context.Context, Pass string) (string
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserChangeEmailSendCode user change email verification
|
// 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)
|
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !exist {
|
if !exist {
|
||||||
return errors.BadRequest(reason.UserNotFound)
|
return nil, errors.BadRequest(reason.UserNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, exist, err = us.userRepo.GetByEmail(ctx, req.Email)
|
_, exist, err = us.userRepo.GetByEmail(ctx, req.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if exist {
|
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{
|
data := &schema.EmailCodeContent{
|
||||||
|
@ -512,19 +505,19 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
|
||||||
}
|
}
|
||||||
code := uuid.NewString()
|
code := uuid.NewString()
|
||||||
var title, body string
|
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 {
|
if userInfo.MailStatus == entity.EmailStatusToBeVerified {
|
||||||
title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL)
|
title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL)
|
||||||
} else {
|
} else {
|
||||||
title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL)
|
title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Infof("send email confirmation %s", verifyEmailURL)
|
log.Infof("send email confirmation %s", verifyEmailURL)
|
||||||
|
|
||||||
go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString())
|
go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString())
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserChangeEmailVerify user change email verify code
|
// UserChangeEmailVerify user change email verify code
|
||||||
|
@ -560,3 +553,13 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string
|
||||||
}
|
}
|
||||||
return nil
|
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
|
package dir
|
||||||
|
|
||||||
import "os"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
func CreateDirIfNotExist(path string) error {
|
func CreateDirIfNotExist(path string) error {
|
||||||
return os.MkdirAll(path, os.ModePerm)
|
return os.MkdirAll(path, os.ModePerm)
|
||||||
|
@ -15,3 +19,32 @@ func CheckFileExist(path string) bool {
|
||||||
f, err := os.Stat(path)
|
f, err := os.Stat(path)
|
||||||
return err == nil && !f.IsDir()
|
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
|
#!/bin/bash
|
||||||
/usr/bin/answer init
|
/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 = {
|
module.exports = {
|
||||||
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
es2021: true,
|
es2021: true,
|
||||||
|
@ -19,7 +20,8 @@ module.exports = {
|
||||||
},
|
},
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: 'latest',
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
project: './tsconfig.json',
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
},
|
},
|
||||||
plugins: ['react', '@typescript-eslint'],
|
plugins: ['react', '@typescript-eslint'],
|
||||||
rules: {
|
rules: {
|
||||||
|
@ -64,7 +66,7 @@ module.exports = {
|
||||||
position: 'before',
|
position: 'before',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: '@answer/**',
|
pattern: '@/**',
|
||||||
group: 'internal',
|
group: 'internal',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
module.exports = {
|
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 = {
|
module.exports = {
|
||||||
webpack: function(config, env) {
|
webpack: function(config, env) {
|
||||||
if (env === 'production') {
|
if (env === "production") {
|
||||||
config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH;
|
config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH;
|
||||||
}
|
}
|
||||||
config.resolve.alias = {
|
|
||||||
...config.resolve.alias,
|
addWebpackAlias({
|
||||||
'@': path.resolve(__dirname, 'src'),
|
["@"]: path.resolve(__dirname, "src"),
|
||||||
'@answer/pages': path.resolve(__dirname, 'src/pages'),
|
"@i18n": i18nPath
|
||||||
'@answer/components': path.resolve(__dirname, 'src/components'),
|
})(config);
|
||||||
'@answer/stores': path.resolve(__dirname, 'src/stores'),
|
|
||||||
'@answer/hooks': path.resolve(__dirname, 'src/hooks'),
|
addWebpackModuleRule({
|
||||||
'@answer/utils': path.resolve(__dirname, 'src/utils'),
|
test: /\.ya?ml$/,
|
||||||
'@answer/common': path.resolve(__dirname, 'src/common'),
|
use: "yaml-loader"
|
||||||
'@answer/api': path.resolve(__dirname, 'src/services/api'),
|
})(config);
|
||||||
};
|
|
||||||
|
// add i18n dir to ModuleScopePlugin allowedPaths
|
||||||
|
const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === "ModuleScopePlugin");
|
||||||
|
if (moduleScopePlugin) {
|
||||||
|
moduleScopePlugin.allowedPaths.push(i18nPath);
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
|
||||||
devServer: function(configFunction) {
|
devServer: function(configFunction) {
|
||||||
return function(proxy, allowedHost) {
|
return function(proxy, allowedHost) {
|
||||||
const config = configFunction(proxy, allowedHost);
|
const config = configFunction(proxy, allowedHost);
|
||||||
config.proxy = {
|
config.proxy = {
|
||||||
'/answer': {
|
"/answer": {
|
||||||
target: 'http://10.0.10.98:2060',
|
target: "http://10.0.10.98:2060",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false
|
||||||
},
|
},
|
||||||
|
"/installation": {
|
||||||
|
target: "http://10.0.10.98:2060",
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,12 +10,12 @@
|
||||||
"build:prod": "env-cmd -f .env.production react-app-rewired build",
|
"build:prod": "env-cmd -f .env.production react-app-rewired build",
|
||||||
"build": "env-cmd -f .env react-app-rewired build",
|
"build": "env-cmd -f .env react-app-rewired build",
|
||||||
"test": "react-app-rewired test",
|
"test": "react-app-rewired test",
|
||||||
"eject": "react-scripts eject",
|
|
||||||
"lint": "eslint . --cache --fix --ext .ts,.tsx",
|
"lint": "eslint . --cache --fix --ext .ts,.tsx",
|
||||||
"prepare": "cd .. && husky install",
|
"prepare": "cd .. && husky install",
|
||||||
"cz": "cz",
|
"cz": "cz",
|
||||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
"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": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
|
@ -101,7 +101,8 @@
|
||||||
"sass": "^1.54.4",
|
"sass": "^1.54.4",
|
||||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||||
"typescript": "*",
|
"typescript": "*",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4",
|
||||||
|
"yaml-loader": "^0.8.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.9.5",
|
"packageManager": "pnpm@7.9.5",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -77,6 +77,7 @@ specifiers:
|
||||||
tsconfig-paths-webpack-plugin: ^4.0.0
|
tsconfig-paths-webpack-plugin: ^4.0.0
|
||||||
typescript: '*'
|
typescript: '*'
|
||||||
web-vitals: ^2.1.4
|
web-vitals: ^2.1.4
|
||||||
|
yaml-loader: ^0.8.0
|
||||||
zustand: ^4.1.1
|
zustand: ^4.1.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -159,6 +160,7 @@ devDependencies:
|
||||||
tsconfig-paths-webpack-plugin: 4.0.0
|
tsconfig-paths-webpack-plugin: 4.0.0
|
||||||
typescript: 4.8.3
|
typescript: 4.8.3
|
||||||
web-vitals: 2.1.4
|
web-vitals: 2.1.4
|
||||||
|
yaml-loader: 0.8.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
@ -7040,6 +7042,10 @@ packages:
|
||||||
filelist: 1.0.4
|
filelist: 1.0.4
|
||||||
minimatch: 3.1.2
|
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:
|
/jest-changed-files/27.5.1:
|
||||||
resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==}
|
resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==}
|
||||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||||
|
@ -11682,6 +11688,15 @@ packages:
|
||||||
/yallist/4.0.0:
|
/yallist/4.0.0:
|
||||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
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:
|
/yaml/1.10.2:
|
||||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||||
engines: {node: '>= 6'}
|
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 { RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
import router from '@/router';
|
import './i18n/init';
|
||||||
|
import { routes, createBrowserRouter } from '@/router';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const router = createBrowserRouter(routes);
|
||||||
return <RouterProvider router={router} />;
|
return <RouterProvider router={router} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
export const LOGIN_NEED_BACK = [
|
export const DEFAULT_LANG = 'en_US';
|
||||||
'/users/login',
|
export const CURRENT_LANG_STORAGE_KEY = '_a_lang_';
|
||||||
'/users/register',
|
export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_';
|
||||||
'/users/account-recovery',
|
export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
|
||||||
'/users/password-reset',
|
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 = {
|
export const ADMIN_LIST_STATUS = {
|
||||||
// normal;
|
// normal;
|
||||||
|
@ -56,3 +57,494 @@ export const ADMIN_NAV_MENUS = [
|
||||||
child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }],
|
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;
|
status?: string;
|
||||||
/** roles */
|
/** roles */
|
||||||
is_admin?: true;
|
is_admin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserInfoRes extends UserInfoBase {
|
export interface UserInfoRes extends UserInfoBase {
|
||||||
bio: string;
|
bio: string;
|
||||||
bio_html: string;
|
bio_html: string;
|
||||||
create_time?: string;
|
create_time?: string;
|
||||||
/** value = 1 active; value = 2 inactivated
|
/**
|
||||||
|
* value = 1 active;
|
||||||
|
* value = 2 inactivated
|
||||||
*/
|
*/
|
||||||
mail_status: number;
|
mail_status: number;
|
||||||
|
language: string;
|
||||||
e_mail?: string;
|
e_mail?: string;
|
||||||
[prop: string]: any;
|
[prop: string]: any;
|
||||||
}
|
}
|
||||||
|
@ -228,6 +231,7 @@ export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted';
|
||||||
|
|
||||||
export interface AdminContentsReq extends Paging {
|
export interface AdminContentsReq extends Paging {
|
||||||
status: AdminContentsFilterBy;
|
status: AdminContentsFilterBy;
|
||||||
|
query?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -257,12 +261,15 @@ export interface AdminSettingsGeneral {
|
||||||
name: string;
|
name: string;
|
||||||
short_description: string;
|
short_description: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
site_url: string;
|
||||||
|
contact_email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminSettingsInterface {
|
export interface AdminSettingsInterface {
|
||||||
logo: string;
|
logo: string;
|
||||||
language: string;
|
language: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
|
time_zone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminSettingsSmtp {
|
export interface AdminSettingsSmtp {
|
||||||
|
@ -321,3 +328,24 @@ export interface SearchResItem {
|
||||||
export interface SearchRes extends ListResult<SearchResItem> {
|
export interface SearchRes extends ListResult<SearchResItem> {
|
||||||
extra: any;
|
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 { useAccordionButton } from 'react-bootstrap/AccordionButton';
|
||||||
|
|
||||||
import { Icon } from '@answer/components';
|
import { Icon } from '@/components';
|
||||||
|
|
||||||
function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' });
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' });
|
||||||
|
|
|
@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Icon } from '@answer/components';
|
import { Icon } from '@/components';
|
||||||
import { bookmark, postVote } from '@answer/api';
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
import { isLogin } from '@answer/utils';
|
import { useToast } from '@/hooks';
|
||||||
import { userInfoStore } from '@answer/stores';
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
import { useToast } from '@answer/hooks';
|
import { bookmark, postVote } from '@/services';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -32,7 +32,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
||||||
state: data?.collected,
|
state: data?.collected,
|
||||||
count: data?.collectCount,
|
count: data?.collectCount,
|
||||||
});
|
});
|
||||||
const { username = '' } = userInfoStore((state) => state.user);
|
const { username = '' } = loggedUserInfoStore((state) => state.user);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -48,7 +48,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleVote = (type: 'up' | 'down') => {
|
const handleVote = (type: 'up' | 'down') => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ const Index: FC<Props> = ({ className, data }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBookmark = () => {
|
const handleBookmark = () => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bookmark({
|
bookmark({
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { memo, FC } from 'react';
|
import { memo, FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Avatar } from '@answer/components';
|
import { Avatar } from '@/components';
|
||||||
|
|
||||||
import { formatCount } from '@/utils';
|
import { formatCount } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Icon, FormatTime } from '@answer/components';
|
import { Icon, FormatTime } from '@/components';
|
||||||
|
|
||||||
const ActionBar = ({
|
const ActionBar = ({
|
||||||
nickName,
|
nickName,
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { TextArea, Mentions } from '@answer/components';
|
import { TextArea, Mentions } from '@/components';
|
||||||
import { usePageUsers } from '@answer/hooks';
|
import { usePageUsers } from '@/hooks';
|
||||||
|
|
||||||
const Form = ({
|
const Form = ({
|
||||||
className = '',
|
className = '',
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { useState, memo } from 'react';
|
||||||
import { Button } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { TextArea, Mentions } from '@answer/components';
|
import { TextArea, Mentions } from '@/components';
|
||||||
import { usePageUsers } from '@answer/hooks';
|
import { usePageUsers } from '@/hooks';
|
||||||
|
|
||||||
const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
|
|
@ -7,17 +7,18 @@ import classNames from 'classnames';
|
||||||
import { unionBy } from 'lodash';
|
import { unionBy } from 'lodash';
|
||||||
import { marked } from 'marked';
|
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 {
|
import {
|
||||||
useQueryComments,
|
useQueryComments,
|
||||||
addComment,
|
addComment,
|
||||||
deleteComment,
|
deleteComment,
|
||||||
updateComment,
|
updateComment,
|
||||||
postVote,
|
postVote,
|
||||||
} from '@answer/api';
|
} from '@/services';
|
||||||
import { Modal } from '@answer/components';
|
|
||||||
import { usePageUsers, useReportModal } from '@answer/hooks';
|
|
||||||
import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils';
|
|
||||||
|
|
||||||
import { Form, ActionBar, Reply } from './components';
|
import { Form, ActionBar, Reply } from './components';
|
||||||
|
|
||||||
|
@ -163,7 +164,7 @@ const Comment = ({ objectId, mode }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVote = (id, is_cancel) => {
|
const handleVote = (id, is_cancel) => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +190,7 @@ const Comment = ({ objectId, mode }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAction = ({ action }, item) => {
|
const handleAction = ({ action }, item) => {
|
||||||
if (!isLogin(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (action === 'report') {
|
if (action === 'report') {
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { FC, useEffect, useState, memo } from 'react';
|
||||||
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
|
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Modal as AnswerModal } from '@answer/components';
|
import { Modal as AnswerModal } from '@/components';
|
||||||
import { uploadImage } from '@answer/api';
|
|
||||||
import ToolItem from '../toolItem';
|
import ToolItem from '../toolItem';
|
||||||
import { IEditorContext } from '../types';
|
import { IEditorContext } from '../types';
|
||||||
|
import { uploadImage } from '@/services';
|
||||||
|
|
||||||
const Image: FC<IEditorContext> = ({ editor }) => {
|
const Image: FC<IEditorContext> = ({ editor }) => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
|
const { t } = useTranslation('translation', { keyPrefix: 'editor' });
|
||||||
|
|
|
@ -3,9 +3,9 @@ import { Card, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { TagSelector, Tag } from '@answer/components';
|
import { TagSelector, Tag } from '@/components';
|
||||||
import { isLogin } from '@answer/utils';
|
import { tryLoggedAndActicevated } from '@/utils/guard';
|
||||||
import { useFollowingTags, followTags } from '@answer/api';
|
import { useFollowingTags, followTags } from '@/services';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||||
|
@ -32,7 +32,7 @@ const Index: FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isLogin()) {
|
if (!tryLoggedAndActicevated().ok) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,10 +37,10 @@ const Index: FC<Props> = ({ time, preFix, className }) => {
|
||||||
between < 3600 * 24 * 366 &&
|
between < 3600 * 24 * 366 &&
|
||||||
dayjs.unix(from).format('YYYY') === dayjs.unix(now).format('YYYY')
|
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) {
|
if (!time) {
|
||||||
|
@ -50,8 +50,8 @@ const Index: FC<Props> = ({ time, preFix, className }) => {
|
||||||
return (
|
return (
|
||||||
<time
|
<time
|
||||||
className={classNames('', className)}
|
className={classNames('', className)}
|
||||||
dateTime={dayjs.unix(time).toISOString()}
|
dateTime={dayjs.unix(time).tz().toISOString()}
|
||||||
title={dayjs.unix(time).format(t('dates.long_date_with_time'))}>
|
title={dayjs.unix(time).tz().format(t('dates.long_date_with_time'))}>
|
||||||
{preFix ? `${preFix} ` : ''}
|
{preFix ? `${preFix} ` : ''}
|
||||||
{formatTime(time)}
|
{formatTime(time)}
|
||||||
</time>
|
</time>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue