mirror of https://gitee.com/answerdev/answer.git
Merge branch 'main' into ai_0.3_fix_del
This commit is contained in:
commit
761944440c
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
|
@ -0,0 +1,7 @@
|
|||
Fixes #
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
-
|
||||
-
|
||||
-
|
|
@ -4,9 +4,9 @@ on:
|
|||
push:
|
||||
branches: [ "main" ]
|
||||
tags:
|
||||
- 2.*
|
||||
- 1.*
|
||||
- 0.*
|
||||
- v2.*
|
||||
- v1.*
|
||||
- v0.*
|
||||
# pull_request:
|
||||
# branches: [ "main" ]
|
||||
|
||||
|
@ -17,6 +17,13 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
|
@ -34,7 +41,8 @@ jobs:
|
|||
# branch event
|
||||
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
|
||||
# tag event
|
||||
type=ref,enable=true,priority=600,prefix=,suffix=,event=tag
|
||||
#type=ref,enable=true,priority=600,prefix=,suffix=,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
|
||||
|
||||
|
@ -42,6 +50,8 @@ jobs:
|
|||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
/.fleet
|
||||
/.vscode/*.log
|
||||
/cmd/answer/*.sh
|
||||
/cmd/answer/upfiles/*
|
||||
/cmd/answer/uploads/*
|
||||
/cmd/logs
|
||||
/configs/config-dev.yaml
|
||||
/go.work*
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:16 AS node-builder
|
||||
FROM amd64/node AS node-builder
|
||||
|
||||
LABEL maintainer="mingcheng<mc@sf.com>"
|
||||
|
||||
|
@ -29,7 +29,7 @@ RUN apk --no-cache add build-base git \
|
|||
&& make clean build \
|
||||
&& cp answer /usr/bin/answer
|
||||
|
||||
RUN mkdir -p /data/upfiles && chmod 777 /data/upfiles \
|
||||
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
|
||||
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
|
||||
|
||||
# stage3 copy the binary and resource files into fresh container
|
||||
|
|
130
INSTALL.md
130
INSTALL.md
|
@ -1,103 +1,69 @@
|
|||
# How to build and install
|
||||
# Answer installation guide
|
||||
## Environment Preparation
|
||||
- Memory >= 512M
|
||||
- If using MySQL version >= 5.7
|
||||
|
||||
Before installing Answer, you need to install the base environment first.
|
||||
- database
|
||||
- [MySQL](http://dev.mysql.com) Version >= 5.7
|
||||
|
||||
You can then install Answer in several ways:
|
||||
|
||||
- [Deploy with Docker](#Docker-compose-for-Answer)
|
||||
- [Binary installation](#Install-Answer-using-binary)
|
||||
- [Source installation](#Compile-the-image)
|
||||
|
||||
## Docker-compose for Answer
|
||||
## Installing with docker
|
||||
### Step 1: Start the project with the docker command
|
||||
```bash
|
||||
$ mkdir answer && cd answer
|
||||
$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml
|
||||
$ docker-compose up
|
||||
docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
|
||||
```
|
||||
|
||||
In browser, open URL [http://127.0.0.1:9080/](http://127.0.0.1:9080/).
|
||||
### Step 2: Visit the installation url
|
||||
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
|
||||
|
||||
You can log in with the default administrator username (**`admin@admin.com`**) and password (**`admin`**).
|
||||
After selecting the language click next to select the appropriate database, if you just want to experience it currently, it is recommended to select sqlite as the database directly, as shown below
|
||||
|
||||
## Docker for Answer
|
||||
Visit [Docker Hub](https://hub.docker.com/r/answerdev/answer) or GitHub Container registry to see all available images and tags.
|
||||

|
||||
|
||||
### Usage
|
||||
To persist data beyond the life of a Docker container, use a volume (/var/data -> /data). You can modify this based on your situation.
|
||||
Then click next to create the configuration file, click next to enter the basic website information and administrator information, as shown below
|
||||
|
||||
```
|
||||
# Pull image from Docker Hub.
|
||||
$ docker pull answerdev/answer:latest
|
||||

|
||||
|
||||
# Create local directory for volume.
|
||||
$ mkdir -p /var/data
|
||||
Click Next to complete the installation
|
||||
|
||||
# Run the image first
|
||||
$ docker run --name=answer -p 9080:80 -v /var/data:/data answerdev/answer
|
||||
### Step 3: After installation, visit the project path to start using
|
||||
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
|
||||
|
||||
# After successful first startup, a configuration file will be generated in the /var/data directory
|
||||
# /var/data/conf/config.yaml
|
||||
# Need to modify the Mysql database address in the configuration file
|
||||
vim /var/data/conf/config.yaml
|
||||
Login with the administrator username and password you just created.
|
||||
|
||||
# Modify database connection
|
||||
# connection: [username]:[password]@tcp([host]:[port])/[DbName]
|
||||
...
|
||||
|
||||
# After configuring the configuration file, you can start the container again to start the service
|
||||
$ docker start answer
|
||||
## Installing with docker-compose
|
||||
### Step 1: Start the project with the docker-compose command
|
||||
```bash
|
||||
mkdir answer && cd answer
|
||||
wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
## Install Answer using binary
|
||||
### Step 2: Visit the installation url
|
||||
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
|
||||
|
||||
1. Unzip the compressed package
|
||||
2. Use the command `cd` to enter the directory you just created
|
||||
3. Execute the command `./answer init`
|
||||
4. Answer will generate a `./data` directory in the current directory
|
||||
5. Enter the `data` directory and modify the `config.yaml` file
|
||||
6. Modify the database connection identify your database connection information
|
||||
`connection: [username]:[password]@tcp([host]:[port])/[DbName]`
|
||||
7. Use `cd ..` to return the directory from step 2, and execute `./answer run -c ./data/conf/config.yaml`
|
||||
The exact configuration is the same as for docker use
|
||||
|
||||
## Available Commands
|
||||
Usage: `answer [command]`
|
||||
### Step 3: After installation, visit home page
|
||||
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
|
||||
|
||||
- `help`: Help about any command
|
||||
- `init`: Init answer application
|
||||
- `run`: Run answer application
|
||||
- `check`: Check answer required environment
|
||||
- `dump`: Backup answer data
|
||||
## Install with binary
|
||||
### Step 1: Download the binaries
|
||||
[https://github.com/answerdev/answer/releases](https://github.com/answerdev/answer/releases)
|
||||
Download the version you need for your current system
|
||||
|
||||
## config.yaml Description
|
||||
Here is a sample/default config.yaml file, as would be created from `answer init`.
|
||||
```
|
||||
server:
|
||||
http:
|
||||
addr: 0.0.0.0:80 #Project access port number
|
||||
data:
|
||||
database:
|
||||
connection: root:root@tcp(127.0.0.1:3306)/answer #MySQL database connection address
|
||||
cache:
|
||||
file_path: "/tmp/cache/cache.db" #Cache file storage path
|
||||
i18n:
|
||||
bundle_dir: "/data/i18n" #Internationalized file storage directory
|
||||
swaggerui:
|
||||
show: true #Whether to display the swaggerapi documentation, address /swagger/index.html
|
||||
protocol: http #swagger protocol header
|
||||
host: 127.0.0.1 #An accessible IP address or domain name
|
||||
address: ':80' #accessible port number
|
||||
service_config:
|
||||
secret_key: "answer" #encryption key
|
||||
web_host: "http://127.0.0.1" #Page access using domain name address
|
||||
upload_path: "./upfiles" #upload directory
|
||||
### Step 2: Install using command line
|
||||
> The following command -C specifies the data directory required for answer, you can modify it as you see fit
|
||||
|
||||
```bash
|
||||
. /answer init -C . /answer-data/
|
||||
```
|
||||
|
||||
## Compile the image
|
||||
If you have modified the source files and want to repackage the image, you can use the following to repackage the image
|
||||
Then visit: [http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) to install, the configuration is the same as using docker installation
|
||||
|
||||
### Step 3: Start with command line
|
||||
After the installation is complete, the program will exit, so use the command to start the project formally
|
||||
```bash
|
||||
. /answer run -C . /answer-data/
|
||||
```
|
||||
docker build -t answer:v1.0.0 .
|
||||
```
|
||||
## common problem
|
||||
1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`upfiles`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and upfiles directories.
|
||||
|
||||
After normal startup you can access [http://127.0.0.1:9080/](http://127.0.0.1:9080/) to log in using the administrator username password specified during installation
|
||||
|
||||
## Installation FAQ
|
||||
- Having trouble reinstalling using docker? The default command we give is to use `answer-data` to name the volume, so if you don't need the original data again, please delete it voluntarily `docker volume rm answer-data`
|
||||
|
|
144
INSTALL_CN.md
144
INSTALL_CN.md
|
@ -1,106 +1,68 @@
|
|||
# Answer 安装指引
|
||||
## 环境准备
|
||||
- 内存 >= 512M
|
||||
- 如果使用 MySQL 版本 >= 5.7
|
||||
|
||||
安装 Answer 之前,您需要先安装基本环境。
|
||||
- 数据库
|
||||
- [MySQL](http://dev.mysql.com):版本 >= 5.7
|
||||
|
||||
然后,您可以通过以下几种方式来安装 Answer:
|
||||
|
||||
- 采用 Docker 部署
|
||||
- 二进制安装
|
||||
- 源码安装
|
||||
|
||||
## 使用 Docker-compose 安装 Answer
|
||||
## 使用 docker 安装
|
||||
### 步骤 1: 使用 docker 命令启动项目
|
||||
```bash
|
||||
$ mkdir answer && cd answer
|
||||
$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml
|
||||
$ docker-compose up
|
||||
docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
|
||||
```
|
||||
### 步骤 2: 访问安装路径进行项目安装
|
||||
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
|
||||
|
||||
选择语言后点击下一步选择合适的数据库,如果当前只是想体验,建议直接选择 sqlite 作为数据库,如下图所示
|
||||
|
||||

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

|
||||
|
||||
点击下一步即可安装完成
|
||||
|
||||
### 步骤 3:安装完成后访问项目路径开始使用
|
||||
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
|
||||
|
||||
使用刚才创建的管理员用户名密码即可登录。
|
||||
|
||||
## 使用 docker-compose 安装
|
||||
### 步骤 1: 使用 docker-compose 命令启动项目
|
||||
```bash
|
||||
mkdir answer && cd answer
|
||||
wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
启动完成后使用浏览器访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/).
|
||||
### 步骤 2: 访问安装路径进行项目安装
|
||||
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
|
||||
|
||||
你可以使用默认的用户名:( **`admin@admin.com`** ) 和密码:( **`admin`** ) 进行登录.
|
||||
具体配置与 docker 使用时相同
|
||||
|
||||
## 使用Docker 安装 Answer
|
||||
可以从 Docker Hub 或者 GitHub Container registry 下载最新的 tags 镜像
|
||||
### 步骤 3:安装完成后访问项目路径开始使用
|
||||
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
|
||||
|
||||
### 用法
|
||||
将配置和存储目录挂在到镜像之外 volume (/var/data -> /data),你可以修改外部挂载地址
|
||||
## 使用 二进制 安装
|
||||
### 步骤 1: 下载二进制文件
|
||||
[https://github.com/answerdev/answer/releases](https://github.com/answerdev/answer/releases)
|
||||
请下载您当下系统所需要的对应版本
|
||||
|
||||
```
|
||||
# 将镜像从 docker hub 拉到本地
|
||||
$ docker pull answerdev/answer:latest
|
||||
### 步骤 2: 使用命令行安装
|
||||
> 以下命令中 -C 指定的是 answer 所需的数据目录,您可以根据实际需要进行修改
|
||||
|
||||
# 创建一个挂载目录
|
||||
$ 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
|
||||
```bash
|
||||
./answer init -C ./answer-data/
|
||||
```
|
||||
|
||||
## 使用二进制 安装 Answer
|
||||
可以使用编译完成的各个平台的二进制文件运行 Answer 项目
|
||||
### 用法
|
||||
从 GitHub 最新版本的tag中下载对应平台的二进制文件压缩包
|
||||
然后访问:[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) 进行安装,具体配置与使用 docker 安装相同
|
||||
|
||||
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 参数说明
|
||||
|
||||
```
|
||||
server:
|
||||
http:
|
||||
addr: 0.0.0.0:80 #项目访问端口号
|
||||
data:
|
||||
database:
|
||||
connection: root:root@tcp(127.0.0.1:3306)/answer #mysql数据库连接地址
|
||||
cache:
|
||||
file_path: "/tmp/cache/cache.db" #缓存文件存放路径
|
||||
i18n:
|
||||
bundle_dir: "/data/i18n" #国际化文件存放目录
|
||||
swaggerui:
|
||||
show: true #是否显示swaggerapi文档,地址 /swagger/index.html
|
||||
protocol: http #swagger 协议头
|
||||
host: 127.0.0.1 #可被访问的ip地址或域名
|
||||
address: ':80' #可被访问的端口号
|
||||
service_config:
|
||||
secret_key: "answer" #加密key
|
||||
web_host: "http://127.0.0.1" #页面访问使用域名地址
|
||||
upload_path: "./upfiles" #上传目录
|
||||
### 步骤 3: 使用命令行启动
|
||||
安装完成之后程序会退出,请使用命令正式启动项目
|
||||
```bash
|
||||
./answer run -C ./answer-data/
|
||||
```
|
||||
|
||||
## 编译镜像
|
||||
如果修改了源文件并且要重新打包镜像可以使用以下语句重新打包镜像
|
||||
```
|
||||
docker build -t answer:v1.0.0 .
|
||||
```
|
||||
## 常见问题
|
||||
1. 项目无法启动,answer 主程序启动依赖配置文件 config.yaml 、国际化翻译目录 /i18n 、上传文件存放目录 /upfiles,需要确保项目启动时加载了配置文件 answer run -c config.yaml 以及在 config.yaml 正确的指定 i18n 和 upfiles 目录的配置项
|
||||
正常启动后可以访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/) 使用安装时指定的管理员用户名密码进行登录
|
||||
|
||||
## 安装常见问题
|
||||
- 使用 docker 重新安装遇到问题?默认我们给出的命令是使用 `answer-data` 命名卷,所以如果重新不需要原来的数据,请主动进行删除 `docker volume rm answer-data`
|
||||
|
|
7
Makefile
7
Makefile
|
@ -1,6 +1,6 @@
|
|||
.PHONY: build clean ui
|
||||
|
||||
VERSION=0.0.1
|
||||
VERSION=0.4.0
|
||||
BIN=answer
|
||||
DIR_SRC=./cmd/answer
|
||||
DOCKER_CMD=docker
|
||||
|
@ -26,11 +26,10 @@ generate:
|
|||
go mod tidy
|
||||
|
||||
test:
|
||||
@$(GO) test ./...
|
||||
@$(GO) test ./internal/repo/repo_test
|
||||
|
||||
# clean all build result
|
||||
clean:
|
||||
|
||||
@$(GO) clean ./...
|
||||
@rm -f $(BIN)
|
||||
|
||||
|
@ -40,6 +39,6 @@ install-ui-packages:
|
|||
|
||||
ui:
|
||||
@npm config set registry https://repo.huaweicloud.com/repository/npm/
|
||||
@cd ui && echo "REACT_APP_VERSION=$(VERSION)" >> .env && pnpm install && pnpm build && cd -
|
||||
@cd ui && pnpm install && pnpm build && cd -
|
||||
|
||||
all: clean build
|
||||
|
|
|
@ -8,10 +8,11 @@ An open-source knowledge-based community software. You can use it to quickly bui
|
|||
|
||||
To learn more about the project, visit [answer.dev](https://answer.dev).
|
||||
|
||||
[](https://github.com/answerdev/answer/blob/main/LICENSE)
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://github.com/answerdev/answer/blob/main/LICENSE)
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://goreportcard.com/report/github.com/answerdev/answer)
|
||||
[](https://discord.gg/Jm7Y4cbUej)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
@ -22,7 +23,7 @@ To learn more about the project, visit [answer.dev](https://answer.dev).
|
|||
### Running with docker
|
||||
|
||||
```bash
|
||||
docker run -d -p 9080:80 -v $PWD/answer-data:/data --name answer answerdev/answer:latest
|
||||
docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
|
||||
```
|
||||
|
||||
For more information, see [INSTALL.md](./INSTALL.md)
|
||||
|
|
11
README_CN.md
11
README_CN.md
|
@ -4,14 +4,15 @@
|
|||
|
||||
# Answer - 构建问答社区
|
||||
|
||||
一款极简的、问答形式的知识社区开源软件,用来快速构建产品你的产品问答支持社区、用户问答社区、粉丝社区等。
|
||||
一款问答形式的知识社区开源软件,用来快速构建产品你的产品技术社区、客户支持社区、用户社区等。
|
||||
|
||||
了解更多关于该项目的内容,请访问 [answer.dev](https://answer.dev).
|
||||
|
||||
[](https://github.com/answerdev/answer/blob/main/LICENSE)
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://github.com/answerdev/answer/blob/main/LICENSE)
|
||||
[](https://golang.org/)
|
||||
[](https://reactjs.org/)
|
||||
[](https://goreportcard.com/report/github.com/answerdev/answer)
|
||||
[](https://discord.gg/Jm7Y4cbUej)
|
||||
|
||||
## 截图
|
||||
|
||||
|
@ -22,7 +23,7 @@
|
|||
### 使用 docker 快速搭建
|
||||
|
||||
```bash
|
||||
docker run -d -p 9080:80 -v $PWD/answer-data:/data --name answer answerdev/answer:latest
|
||||
docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
|
||||
```
|
||||
|
||||
其他安装配置细节请参考 [INSTALL.md](./INSTALL.md)
|
||||
|
|
|
@ -4,14 +4,14 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/install"
|
||||
"github.com/answerdev/answer/internal/migrations"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// configFilePath is the config file path
|
||||
configFilePath string
|
||||
// dataDirPath save all answer application data in this directory. like config file, upload file...
|
||||
dataDirPath string
|
||||
// dumpDataPath dump data path
|
||||
|
@ -21,9 +21,7 @@ var (
|
|||
func init() {
|
||||
rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time)
|
||||
|
||||
initCmd.Flags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/")
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&configFilePath, "config", "c", "", "config path, eg: -c config.yaml")
|
||||
rootCmd.PersistentFlags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/")
|
||||
|
||||
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
|
||||
|
||||
|
@ -49,6 +47,9 @@ To run answer, use:
|
|||
Short: "Run the application",
|
||||
Long: `Run the application`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
fmt.Println("config file path: ", cli.GetConfigFilePath())
|
||||
fmt.Println("Answer is string..........................")
|
||||
runApp()
|
||||
},
|
||||
}
|
||||
|
@ -59,18 +60,27 @@ To run answer, use:
|
|||
Short: "init answer application",
|
||||
Long: `init answer application`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
// check config file and database. if config file exists and database is already created, init done
|
||||
cli.InstallAllInitialEnvironment(dataDirPath)
|
||||
c, err := readConfig()
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
|
||||
configFileExist := cli.CheckConfigFile(cli.GetConfigFilePath())
|
||||
if configFileExist {
|
||||
fmt.Println("config file exists, try to read the config...")
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("config file read successfully, try to connect database...")
|
||||
if cli.CheckDBTableExist(c.Data.Database) {
|
||||
fmt.Println("connect to database successfully and table already exists, do nothing.")
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Println("read config successfully")
|
||||
if err := migrations.InitDB(c.Data.Database); err != nil {
|
||||
fmt.Println("init database error: ", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println("init database successfully")
|
||||
|
||||
// start installation server to install
|
||||
install.Run(cli.GetConfigFilePath())
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -80,7 +90,8 @@ To run answer, use:
|
|||
Short: "upgrade Answer version",
|
||||
Long: `upgrade Answer version`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
c, err := readConfig()
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
|
@ -100,7 +111,8 @@ To run answer, use:
|
|||
Long: `back up data`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
fmt.Println("Answer is backing up data")
|
||||
c, err := readConfig()
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
|
@ -120,8 +132,9 @@ To run answer, use:
|
|||
Short: "checking the required environment",
|
||||
Long: `Check if the current environment meets the startup requirements`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
fmt.Println("Start checking the required environment...")
|
||||
if cli.CheckConfigFile(configFilePath) {
|
||||
if cli.CheckConfigFile(cli.GetConfigFilePath()) {
|
||||
fmt.Println("config file exists [✔]")
|
||||
} else {
|
||||
fmt.Println("config file not exists [x]")
|
||||
|
@ -133,13 +146,13 @@ To run answer, use:
|
|||
fmt.Println("upload directory not exists [x]")
|
||||
}
|
||||
|
||||
c, err := readConfig()
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if cli.CheckDB(c.Data.Database) {
|
||||
if cli.CheckDBConnection(c.Data.Database) {
|
||||
fmt.Println("db connection successfully [✔]")
|
||||
} else {
|
||||
fmt.Println("db connection failed [x]")
|
||||
|
|
|
@ -2,13 +2,14 @@ package main
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman"
|
||||
"github.com/segmentfault/pacman/contrib/conf/viper"
|
||||
"github.com/segmentfault/pacman/contrib/log/zap"
|
||||
"github.com/segmentfault/pacman/contrib/server/http"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -19,7 +20,7 @@ var (
|
|||
// Name is the name of the project
|
||||
Name = "answer"
|
||||
// Version is the version of the project
|
||||
Version = "development"
|
||||
Version = "0.0.0"
|
||||
// Revision is the git short commit revision number
|
||||
Revision = ""
|
||||
// Time is the build time of the project
|
||||
|
@ -40,8 +41,7 @@ func main() {
|
|||
func runApp() {
|
||||
log.SetLogger(zap.NewLogger(
|
||||
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
|
||||
|
||||
c, err := readConfig()
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -50,27 +50,15 @@ func runApp() {
|
|||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
constant.Version = Version
|
||||
schema.AppStartTime = time.Now()
|
||||
|
||||
defer cleanup()
|
||||
if err := app.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func readConfig() (c *conf.AllConfig, err error) {
|
||||
if len(configFilePath) == 0 {
|
||||
configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName)
|
||||
}
|
||||
c = &conf.AllConfig{}
|
||||
config, err := viper.NewWithPath(configFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = config.Parse(&c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application {
|
||||
return pacman.NewApp(
|
||||
pacman.WithName(Name),
|
||||
|
|
|
@ -34,6 +34,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/repo/search_common"
|
||||
"github.com/answerdev/answer/internal/repo/site_info"
|
||||
"github.com/answerdev/answer/internal/repo/tag"
|
||||
"github.com/answerdev/answer/internal/repo/tag_common"
|
||||
"github.com/answerdev/answer/internal/repo/unique"
|
||||
"github.com/answerdev/answer/internal/repo/user"
|
||||
"github.com/answerdev/answer/internal/router"
|
||||
|
@ -44,6 +45,7 @@ import (
|
|||
auth2 "github.com/answerdev/answer/internal/service/auth"
|
||||
"github.com/answerdev/answer/internal/service/collection_common"
|
||||
comment2 "github.com/answerdev/answer/internal/service/comment"
|
||||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
export2 "github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/follow"
|
||||
meta2 "github.com/answerdev/answer/internal/service/meta"
|
||||
|
@ -57,9 +59,12 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/report_backyard"
|
||||
"github.com/answerdev/answer/internal/service/report_handle_backyard"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/internal/service/search_parser"
|
||||
"github.com/answerdev/answer/internal/service/service_config"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
tag2 "github.com/answerdev/answer/internal/service/tag"
|
||||
"github.com/answerdev/answer/internal/service/tag_common"
|
||||
tag_common2 "github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
"github.com/answerdev/answer/internal/service/user_backyard"
|
||||
"github.com/answerdev/answer/internal/service/user_common"
|
||||
|
@ -76,7 +81,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
langController := controller.NewLangController(i18nTranslator)
|
||||
engine, err := data.NewDB(debug, dbConf)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -90,6 +94,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
siteInfoRepo := site_info.NewSiteInfo(dataData)
|
||||
siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo)
|
||||
langController := controller.NewLangController(i18nTranslator, siteInfoCommonService)
|
||||
authRepo := auth.NewAuthRepo(dataData)
|
||||
authService := auth2.NewAuthService(authRepo)
|
||||
configRepo := config.NewConfigRepo(dataData)
|
||||
|
@ -99,20 +106,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
userRankRepo := rank.NewUserRankRepo(dataData, configRepo)
|
||||
userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo)
|
||||
emailRepo := export.NewEmailRepo(dataData)
|
||||
siteInfoRepo := site_info.NewSiteInfo(dataData)
|
||||
emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo)
|
||||
userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf)
|
||||
userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService)
|
||||
captchaRepo := captcha.NewCaptchaRepo(dataData)
|
||||
captchaService := action.NewCaptchaService(captchaRepo)
|
||||
uploaderService := uploader.NewUploaderService(serviceConf)
|
||||
uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
|
||||
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService)
|
||||
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
|
||||
commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo)
|
||||
userCommon := usercommon.NewUserCommon(userRepo)
|
||||
answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
|
||||
questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo)
|
||||
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
|
||||
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagRepo)
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo)
|
||||
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo)
|
||||
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
|
||||
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo)
|
||||
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, configRepo)
|
||||
|
@ -123,18 +129,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configRepo, activityRepo, userRankRepo, voteRepo)
|
||||
voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configRepo, questionRepo, answerRepo, commentCommonRepo, objService)
|
||||
voteController := controller.NewVoteController(voteService)
|
||||
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
|
||||
tagRelRepo := tag.NewTagRelRepo(dataData)
|
||||
revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo)
|
||||
revisionService := revision_common.NewRevisionService(revisionRepo, userRepo)
|
||||
tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, revisionService, siteInfoCommonService)
|
||||
followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
|
||||
tagService := tag2.NewTagService(tagRepo, revisionService, followRepo)
|
||||
tagController := controller.NewTagController(tagService, rankService)
|
||||
tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService)
|
||||
tagController := controller.NewTagController(tagService, tagCommonService, rankService)
|
||||
followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
|
||||
followService := follow.NewFollowService(followFollowRepo, followRepo, tagRepo)
|
||||
followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo)
|
||||
followController := controller.NewFollowController(followService)
|
||||
collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo)
|
||||
collectionGroupRepo := collection.NewCollectionGroupRepo(dataData)
|
||||
tagRelRepo := tag.NewTagRelRepo(dataData)
|
||||
tagCommonService := tagcommon.NewTagCommonService(tagRepo, tagRelRepo, revisionService)
|
||||
collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo)
|
||||
answerCommon := answercommon.NewAnswerCommon(answerRepo)
|
||||
metaRepo := meta.NewMetaRepo(dataData)
|
||||
|
@ -148,9 +155,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService)
|
||||
questionController := controller.NewQuestionController(questionService, rankService)
|
||||
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
|
||||
answerController := controller.NewAnswerController(answerService, rankService)
|
||||
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
|
||||
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
|
||||
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
|
||||
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
|
||||
searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo)
|
||||
searchService := service.NewSearchService(searchParser, searchRepo)
|
||||
searchController := controller.NewSearchController(searchService)
|
||||
serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService)
|
||||
revisionController := controller.NewRevisionController(serviceRevisionService)
|
||||
|
@ -166,14 +175,16 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
reasonService := reason2.NewReasonService(reasonRepo)
|
||||
reasonController := controller.NewReasonController(reasonService)
|
||||
themeController := controller_backyard.NewThemeController()
|
||||
siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService)
|
||||
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService, tagCommonService)
|
||||
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
|
||||
siteinfoController := controller.NewSiteinfoController(siteInfoService)
|
||||
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
|
||||
notificationRepo := notification.NewNotificationRepo(dataData)
|
||||
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService)
|
||||
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon)
|
||||
notificationController := controller.NewNotificationController(notificationService)
|
||||
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController)
|
||||
dashboardController := controller.NewDashboardController(dashboardService)
|
||||
uploadController := controller.NewUploadController(uploaderService)
|
||||
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, uploadController)
|
||||
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
|
||||
uiRouter := router.NewUIRouter()
|
||||
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)
|
||||
|
|
|
@ -17,4 +17,4 @@ swaggerui:
|
|||
service_config:
|
||||
secret_key: "answer"
|
||||
web_host: "http://127.0.0.1:9080"
|
||||
upload_path: "/data/upfiles"
|
||||
upload_path: "/data/uploads"
|
||||
|
|
|
@ -1,29 +1,12 @@
|
|||
version: "3.9"
|
||||
version: "3"
|
||||
services:
|
||||
answer:
|
||||
image: answerdev/answer:latest
|
||||
image: answerdev/answer
|
||||
ports:
|
||||
- '9080:80'
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
links:
|
||||
- db
|
||||
volumes:
|
||||
- ./answer-data/data:/data
|
||||
db:
|
||||
image: mariadb:10.4.7
|
||||
ports:
|
||||
- '13306:3306'
|
||||
restart: on-failure
|
||||
environment:
|
||||
MYSQL_DATABASE: answer
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
healthcheck:
|
||||
test: [ "CMD", "mysqladmin" ,"ping", "-uroot", "-proot"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci', '--skip-character-set-client-handshake']
|
||||
volumes:
|
||||
- ./answer-data/mysql:/var/lib/mysql
|
||||
- answer-data:/data
|
||||
|
||||
volumes:
|
||||
answer-data:
|
||||
|
|
1084
docs/docs.go
1084
docs/docs.go
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
1084
docs/swagger.json
1084
docs/swagger.json
File diff suppressed because it is too large
Load Diff
|
@ -20,6 +20,69 @@ definitions:
|
|||
description: reason key
|
||||
type: string
|
||||
type: object
|
||||
install.CheckConfigFileResp:
|
||||
properties:
|
||||
config_file_exist:
|
||||
type: boolean
|
||||
db_connection_success:
|
||||
type: boolean
|
||||
db_table_exist:
|
||||
type: boolean
|
||||
type: object
|
||||
install.CheckDatabaseReq:
|
||||
properties:
|
||||
db_file:
|
||||
type: string
|
||||
db_host:
|
||||
type: string
|
||||
db_name:
|
||||
type: string
|
||||
db_password:
|
||||
type: string
|
||||
db_type:
|
||||
enum:
|
||||
- postgres
|
||||
- sqlite3
|
||||
- mysql
|
||||
type: string
|
||||
db_username:
|
||||
type: string
|
||||
required:
|
||||
- db_type
|
||||
type: object
|
||||
install.InitBaseInfoReq:
|
||||
properties:
|
||||
contact_email:
|
||||
maxLength: 500
|
||||
type: string
|
||||
email:
|
||||
maxLength: 500
|
||||
type: string
|
||||
lang:
|
||||
maxLength: 30
|
||||
type: string
|
||||
name:
|
||||
maxLength: 30
|
||||
type: string
|
||||
password:
|
||||
maxLength: 32
|
||||
minLength: 8
|
||||
type: string
|
||||
site_name:
|
||||
maxLength: 30
|
||||
type: string
|
||||
site_url:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- contact_email
|
||||
- email
|
||||
- lang
|
||||
- name
|
||||
- password
|
||||
- site_name
|
||||
- site_url
|
||||
type: object
|
||||
pager.PageModel:
|
||||
properties:
|
||||
count:
|
||||
|
@ -305,6 +368,10 @@ definitions:
|
|||
description: if main tag slug name is not empty, this tag is synonymous with
|
||||
the main tag
|
||||
type: string
|
||||
recommend:
|
||||
type: boolean
|
||||
reserved:
|
||||
type: boolean
|
||||
slug_name:
|
||||
description: slug name
|
||||
type: string
|
||||
|
@ -481,6 +548,17 @@ definitions:
|
|||
smtp_username:
|
||||
type: string
|
||||
type: object
|
||||
schema.GetSiteLegalInfoResp:
|
||||
properties:
|
||||
privacy_policy_original_text:
|
||||
type: string
|
||||
privacy_policy_parsed_text:
|
||||
type: string
|
||||
terms_of_service_original_text:
|
||||
type: string
|
||||
terms_of_service_parsed_text:
|
||||
type: string
|
||||
type: object
|
||||
schema.GetTagPageResp:
|
||||
properties:
|
||||
created_at:
|
||||
|
@ -507,6 +585,10 @@ definitions:
|
|||
question_count:
|
||||
description: question amount
|
||||
type: integer
|
||||
recommend:
|
||||
type: boolean
|
||||
reserved:
|
||||
type: boolean
|
||||
slug_name:
|
||||
description: slug_name
|
||||
type: string
|
||||
|
@ -552,6 +634,10 @@ definitions:
|
|||
question_count:
|
||||
description: question amount
|
||||
type: integer
|
||||
recommend:
|
||||
type: boolean
|
||||
reserved:
|
||||
type: boolean
|
||||
slug_name:
|
||||
description: slug name
|
||||
type: string
|
||||
|
@ -652,6 +738,9 @@ definitions:
|
|||
is_admin:
|
||||
description: is admin
|
||||
type: boolean
|
||||
language:
|
||||
description: language
|
||||
type: string
|
||||
last_login_date:
|
||||
description: last login date
|
||||
type: integer
|
||||
|
@ -723,6 +812,9 @@ definitions:
|
|||
is_admin:
|
||||
description: is admin
|
||||
type: boolean
|
||||
language:
|
||||
description: language
|
||||
type: string
|
||||
last_login_date:
|
||||
description: last login date
|
||||
type: integer
|
||||
|
@ -956,6 +1048,8 @@ definitions:
|
|||
type: string
|
||||
id:
|
||||
type: string
|
||||
question_id:
|
||||
type: string
|
||||
status:
|
||||
description: Status
|
||||
type: string
|
||||
|
@ -981,8 +1075,47 @@ definitions:
|
|||
description: object_type
|
||||
type: string
|
||||
type: object
|
||||
schema.SiteBrandingReq:
|
||||
properties:
|
||||
favicon:
|
||||
maxLength: 512
|
||||
type: string
|
||||
logo:
|
||||
maxLength: 512
|
||||
type: string
|
||||
mobile_logo:
|
||||
maxLength: 512
|
||||
type: string
|
||||
square_icon:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- logo
|
||||
- square_icon
|
||||
type: object
|
||||
schema.SiteBrandingResp:
|
||||
properties:
|
||||
favicon:
|
||||
maxLength: 512
|
||||
type: string
|
||||
logo:
|
||||
maxLength: 512
|
||||
type: string
|
||||
mobile_logo:
|
||||
maxLength: 512
|
||||
type: string
|
||||
square_icon:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- logo
|
||||
- square_icon
|
||||
type: object
|
||||
schema.SiteGeneralReq:
|
||||
properties:
|
||||
contact_email:
|
||||
maxLength: 512
|
||||
type: string
|
||||
description:
|
||||
maxLength: 2000
|
||||
type: string
|
||||
|
@ -992,13 +1125,21 @@ definitions:
|
|||
short_description:
|
||||
maxLength: 255
|
||||
type: string
|
||||
site_url:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- contact_email
|
||||
- description
|
||||
- name
|
||||
- short_description
|
||||
- site_url
|
||||
type: object
|
||||
schema.SiteGeneralResp:
|
||||
properties:
|
||||
contact_email:
|
||||
maxLength: 512
|
||||
type: string
|
||||
description:
|
||||
maxLength: 2000
|
||||
type: string
|
||||
|
@ -1008,40 +1149,95 @@ definitions:
|
|||
short_description:
|
||||
maxLength: 255
|
||||
type: string
|
||||
site_url:
|
||||
maxLength: 512
|
||||
type: string
|
||||
required:
|
||||
- contact_email
|
||||
- description
|
||||
- name
|
||||
- short_description
|
||||
- site_url
|
||||
type: object
|
||||
schema.SiteInterfaceReq:
|
||||
properties:
|
||||
language:
|
||||
maxLength: 128
|
||||
type: string
|
||||
logo:
|
||||
maxLength: 256
|
||||
type: string
|
||||
theme:
|
||||
maxLength: 128
|
||||
type: string
|
||||
time_zone:
|
||||
maxLength: 128
|
||||
type: string
|
||||
required:
|
||||
- language
|
||||
- theme
|
||||
- time_zone
|
||||
type: object
|
||||
schema.SiteInterfaceResp:
|
||||
properties:
|
||||
language:
|
||||
maxLength: 128
|
||||
type: string
|
||||
logo:
|
||||
maxLength: 256
|
||||
type: string
|
||||
theme:
|
||||
maxLength: 128
|
||||
type: string
|
||||
time_zone:
|
||||
maxLength: 128
|
||||
type: string
|
||||
required:
|
||||
- language
|
||||
- theme
|
||||
- time_zone
|
||||
type: object
|
||||
schema.SiteLegalReq:
|
||||
properties:
|
||||
privacy_policy_original_text:
|
||||
type: string
|
||||
privacy_policy_parsed_text:
|
||||
type: string
|
||||
terms_of_service_original_text:
|
||||
type: string
|
||||
terms_of_service_parsed_text:
|
||||
type: string
|
||||
type: object
|
||||
schema.SiteLegalResp:
|
||||
properties:
|
||||
privacy_policy_original_text:
|
||||
type: string
|
||||
privacy_policy_parsed_text:
|
||||
type: string
|
||||
terms_of_service_original_text:
|
||||
type: string
|
||||
terms_of_service_parsed_text:
|
||||
type: string
|
||||
type: object
|
||||
schema.SiteWriteReq:
|
||||
properties:
|
||||
recommend_tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required_tag:
|
||||
type: boolean
|
||||
reserved_tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
schema.SiteWriteResp:
|
||||
properties:
|
||||
recommend_tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required_tag:
|
||||
type: boolean
|
||||
reserved_tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
schema.TagItem:
|
||||
properties:
|
||||
|
@ -1068,6 +1264,10 @@ definitions:
|
|||
description: if main tag slug name is not empty, this tag is synonymous with
|
||||
the main tag
|
||||
type: string
|
||||
recommend:
|
||||
type: boolean
|
||||
reserved:
|
||||
type: boolean
|
||||
slug_name:
|
||||
type: string
|
||||
type: object
|
||||
|
@ -1195,6 +1395,15 @@ definitions:
|
|||
- synonym_tag_list
|
||||
- tag_id
|
||||
type: object
|
||||
schema.UpdateUserInterfaceRequest:
|
||||
properties:
|
||||
language:
|
||||
description: language
|
||||
maxLength: 100
|
||||
type: string
|
||||
required:
|
||||
- language
|
||||
type: object
|
||||
schema.UpdateUserStatusReq:
|
||||
properties:
|
||||
status:
|
||||
|
@ -1271,10 +1480,16 @@ definitions:
|
|||
type: string
|
||||
e_mail:
|
||||
description: e_mail
|
||||
maxLength: 500
|
||||
type: string
|
||||
pass:
|
||||
description: password
|
||||
maxLength: 32
|
||||
minLength: 8
|
||||
type: string
|
||||
required:
|
||||
- e_mail
|
||||
- pass
|
||||
type: object
|
||||
schema.UserModifyPassWordRequest:
|
||||
properties:
|
||||
|
@ -1366,6 +1581,13 @@ definitions:
|
|||
votes:
|
||||
type: integer
|
||||
type: object
|
||||
translator.LangOption:
|
||||
properties:
|
||||
label:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
info:
|
||||
contact: {}
|
||||
paths:
|
||||
|
@ -1434,6 +1656,23 @@ paths:
|
|||
summary: AdminSetAnswerStatus
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/dashboard:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: DashboardInfo
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: DashboardInfo
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/language/options:
|
||||
get:
|
||||
description: Get language options
|
||||
|
@ -1444,8 +1683,6 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get language options
|
||||
tags:
|
||||
- Lang
|
||||
|
@ -1660,9 +1897,50 @@ paths:
|
|||
summary: update smtp config
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/siteinfo/branding:
|
||||
get:
|
||||
description: get site interface
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.SiteBrandingResp'
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: get site interface
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
description: update site info branding
|
||||
parameters:
|
||||
- description: branding info
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.SiteBrandingReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: update site info branding
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/siteinfo/general:
|
||||
get:
|
||||
description: Get siteinfo general
|
||||
description: get site general information
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -1677,11 +1955,11 @@ paths:
|
|||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get siteinfo general
|
||||
summary: get site general information
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
description: Get siteinfo interface
|
||||
description: update site general information
|
||||
parameters:
|
||||
- description: general
|
||||
in: body
|
||||
|
@ -1698,19 +1976,12 @@ paths:
|
|||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get siteinfo interface
|
||||
summary: update site general information
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/siteinfo/interface:
|
||||
get:
|
||||
description: Get siteinfo interface
|
||||
parameters:
|
||||
- description: general
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.AddCommentReq'
|
||||
description: get site interface
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -1725,11 +1996,11 @@ paths:
|
|||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get siteinfo interface
|
||||
summary: get site interface
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
description: Get siteinfo interface
|
||||
description: update site info interface
|
||||
parameters:
|
||||
- description: general
|
||||
in: body
|
||||
|
@ -1746,7 +2017,89 @@ paths:
|
|||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get siteinfo interface
|
||||
summary: update site info interface
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/siteinfo/legal:
|
||||
get:
|
||||
description: Set the legal information for the site
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.SiteLegalResp'
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Set the legal information for the site
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
description: update site legal info
|
||||
parameters:
|
||||
- description: write info
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.SiteLegalReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: update site legal info
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/siteinfo/write:
|
||||
get:
|
||||
description: get site interface
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.SiteWriteResp'
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: get site interface
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
description: update site write info
|
||||
parameters:
|
||||
- description: write info
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.SiteWriteReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: update site write info
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/theme/options:
|
||||
|
@ -2155,6 +2508,41 @@ paths:
|
|||
summary: get comment page
|
||||
tags:
|
||||
- Comment
|
||||
/answer/api/v1/file:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
description: upload file
|
||||
parameters:
|
||||
- description: identify the source of the file upload
|
||||
enum:
|
||||
- post
|
||||
- avatar
|
||||
- branding
|
||||
in: formData
|
||||
name: source
|
||||
required: true
|
||||
type: string
|
||||
- description: file
|
||||
in: formData
|
||||
name: file
|
||||
required: true
|
||||
type: file
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: upload file
|
||||
tags:
|
||||
- Upload
|
||||
/answer/api/v1/follow:
|
||||
post:
|
||||
consumes:
|
||||
|
@ -2237,8 +2625,6 @@ paths:
|
|||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Get language options
|
||||
tags:
|
||||
- Lang
|
||||
|
@ -3014,7 +3400,7 @@ paths:
|
|||
- Search
|
||||
/answer/api/v1/siteinfo:
|
||||
get:
|
||||
description: Get siteinfo
|
||||
description: get site info
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -3027,7 +3413,34 @@ paths:
|
|||
data:
|
||||
$ref: '#/definitions/schema.SiteGeneralResp'
|
||||
type: object
|
||||
summary: Get siteinfo
|
||||
summary: get site info
|
||||
tags:
|
||||
- site
|
||||
/answer/api/v1/siteinfo/legal:
|
||||
get:
|
||||
description: get site legal info
|
||||
parameters:
|
||||
- description: legal information type
|
||||
enum:
|
||||
- tos
|
||||
- privacy
|
||||
in: query
|
||||
name: info_type
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.GetSiteLegalInfoResp'
|
||||
type: object
|
||||
summary: get site legal info
|
||||
tags:
|
||||
- site
|
||||
/answer/api/v1/tag:
|
||||
|
@ -3247,32 +3660,6 @@ paths:
|
|||
summary: ActionRecord
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/avatar/upload:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
description: UserUpdateInfo
|
||||
parameters:
|
||||
- description: file
|
||||
in: formData
|
||||
name: file
|
||||
required: true
|
||||
type: file
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: UserUpdateInfo
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/email:
|
||||
put:
|
||||
consumes:
|
||||
|
@ -3425,6 +3812,35 @@ paths:
|
|||
summary: UserUpdateInfo update user info
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/interface:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: UserUpdateInterface update user interface config
|
||||
parameters:
|
||||
- description: access-token
|
||||
in: header
|
||||
name: Authorization
|
||||
required: true
|
||||
type: string
|
||||
- description: UpdateInfoRequest
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.UpdateUserInterfaceRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: UserUpdateInterface update user interface config
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/login/email:
|
||||
post:
|
||||
consumes:
|
||||
|
@ -3564,32 +3980,6 @@ paths:
|
|||
summary: RetrievePassWord
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/post/file:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
description: upload user post file
|
||||
parameters:
|
||||
- description: file
|
||||
in: formData
|
||||
name: file
|
||||
required: true
|
||||
type: file
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: upload user post file
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/register/email:
|
||||
post:
|
||||
consumes:
|
||||
|
@ -3617,28 +4007,6 @@ paths:
|
|||
summary: UserRegisterByEmail
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/user/status:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: get user status info
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.GetUserResp'
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: get user status info
|
||||
tags:
|
||||
- User
|
||||
/answer/api/v1/vote/down:
|
||||
post:
|
||||
consumes:
|
||||
|
@ -3697,6 +4065,117 @@ paths:
|
|||
summary: vote up
|
||||
tags:
|
||||
- Activity
|
||||
/installation/base-info:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: init base info
|
||||
parameters:
|
||||
- description: InitBaseInfoReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/install.InitBaseInfoReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
summary: init base info
|
||||
tags:
|
||||
- installation
|
||||
/installation/config-file/check:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: check config file if exist when installation
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/install.CheckConfigFileResp'
|
||||
type: object
|
||||
summary: check config file if exist when installation
|
||||
tags:
|
||||
- installation
|
||||
/installation/db/check:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: check database if exist when installation
|
||||
parameters:
|
||||
- description: CheckDatabaseReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/install.CheckDatabaseReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/install.CheckConfigFileResp'
|
||||
type: object
|
||||
summary: check database if exist when installation
|
||||
tags:
|
||||
- installation
|
||||
/installation/init:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: init environment
|
||||
parameters:
|
||||
- description: CheckDatabaseReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/install.CheckDatabaseReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
summary: init environment
|
||||
tags:
|
||||
- installation
|
||||
/installation/language/options:
|
||||
get:
|
||||
description: get installation language options
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/translator.LangOption'
|
||||
type: array
|
||||
type: object
|
||||
summary: get installation language options
|
||||
tags:
|
||||
- Lang
|
||||
/personal/question/page:
|
||||
get:
|
||||
consumes:
|
||||
|
|
5
go.mod
5
go.mod
|
@ -15,6 +15,7 @@ require (
|
|||
github.com/goccy/go-json v0.9.11
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/wire v0.5.0
|
||||
github.com/grokify/html-strip-tags-go v0.0.1
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/jinzhu/now v1.1.5
|
||||
github.com/lib/pq v1.10.7
|
||||
|
@ -24,7 +25,7 @@ require (
|
|||
github.com/segmentfault/pacman v1.0.1
|
||||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632
|
||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05
|
||||
github.com/spf13/cobra v1.6.1
|
||||
|
@ -35,6 +36,7 @@ require (
|
|||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/net v0.1.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
xorm.io/builder v0.3.12
|
||||
xorm.io/core v0.7.3
|
||||
xorm.io/xorm v1.3.2
|
||||
|
@ -110,6 +112,5 @@ require (
|
|||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
|
6
go.sum
6
go.sum
|
@ -299,6 +299,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
|
|||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0=
|
||||
github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
|
@ -594,8 +596,8 @@ github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1
|
|||
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk=
|
||||
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0=
|
||||
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8=
|
||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc=
|
||||
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk=
|
||||
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A=
|
||||
|
|
1174
i18n/en_US.yaml
1174
i18n/en_US.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
|||
# all support language
|
||||
language_options:
|
||||
- label: "简体中文(CN)"
|
||||
value: "zh_CN"
|
||||
- label: "English(US)"
|
||||
value: "en_US"
|
312
i18n/it_IT.yaml
312
i18n/it_IT.yaml
|
@ -1,170 +1,172 @@
|
|||
base:
|
||||
success:
|
||||
other: "Successo"
|
||||
unknown:
|
||||
other: "Errore sconosciuto"
|
||||
request_format_error:
|
||||
other: "Il formato della richiesta non è valido"
|
||||
unauthorized_error:
|
||||
other: "Non autorizzato"
|
||||
database_error:
|
||||
other: "Errore server dati"
|
||||
# The following fields are used for back-end
|
||||
backend:
|
||||
base:
|
||||
success:
|
||||
other: "Successo"
|
||||
unknown:
|
||||
other: "Errore sconosciuto"
|
||||
request_format_error:
|
||||
other: "Il formato della richiesta non è valido"
|
||||
unauthorized_error:
|
||||
other: "Non autorizzato"
|
||||
database_error:
|
||||
other: "Errore server dati"
|
||||
|
||||
email:
|
||||
other: "email"
|
||||
password:
|
||||
other: "password"
|
||||
|
||||
email_or_password_wrong_error: &email_or_password_wrong
|
||||
other: "Email o password errati"
|
||||
|
||||
error:
|
||||
admin:
|
||||
email_or_password_wrong: *email_or_password_wrong
|
||||
answer:
|
||||
not_found:
|
||||
other: "Risposta non trovata"
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: "Non si hanno di privilegi sufficienti per modificare il commento"
|
||||
not_found:
|
||||
other: "Commento non trovato"
|
||||
email:
|
||||
duplicate:
|
||||
other: "email già esistente"
|
||||
need_to_be_verified:
|
||||
other: "email deve essere verificata"
|
||||
verify_url_expired:
|
||||
other: "l'url di verifica email è scaduto, si prega di reinviare la email"
|
||||
lang:
|
||||
not_found:
|
||||
other: "lingua non trovata"
|
||||
object:
|
||||
captcha_verification_failed:
|
||||
other: "captcha errato"
|
||||
disallow_follow:
|
||||
other: "Non sei autorizzato a seguire"
|
||||
disallow_vote:
|
||||
other: "non sei autorizzato a votare"
|
||||
disallow_vote_your_self:
|
||||
other: "Non puoi votare un tuo post!"
|
||||
not_found:
|
||||
other: "oggetto non trovato"
|
||||
verification_failed:
|
||||
other: "verifica fallita"
|
||||
email_or_password_incorrect:
|
||||
other: "email o password incorretti"
|
||||
old_password_verification_failed:
|
||||
other: "la verifica della vecchia password è fallita"
|
||||
new_password_same_as_previous_setting:
|
||||
other: "La nuova password è identica alla precedente"
|
||||
question:
|
||||
not_found:
|
||||
other: "domanda non trovata"
|
||||
rank:
|
||||
fail_to_meet_the_condition:
|
||||
other: "Condizioni non valide per il grado"
|
||||
other: "email"
|
||||
password:
|
||||
other: "password"
|
||||
|
||||
email_or_password_wrong_error: &email_or_password_wrong
|
||||
other: "Email o password errati"
|
||||
|
||||
error:
|
||||
admin:
|
||||
email_or_password_wrong: *email_or_password_wrong
|
||||
answer:
|
||||
not_found:
|
||||
other: "Risposta non trovata"
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: "Non si hanno di privilegi sufficienti per modificare il commento"
|
||||
not_found:
|
||||
other: "Commento non trovato"
|
||||
email:
|
||||
duplicate:
|
||||
other: "email già esistente"
|
||||
need_to_be_verified:
|
||||
other: "email deve essere verificata"
|
||||
verify_url_expired:
|
||||
other: "l'url di verifica email è scaduto, si prega di reinviare la email"
|
||||
lang:
|
||||
not_found:
|
||||
other: "lingua non trovata"
|
||||
object:
|
||||
captcha_verification_failed:
|
||||
other: "captcha errato"
|
||||
disallow_follow:
|
||||
other: "Non sei autorizzato a seguire"
|
||||
disallow_vote:
|
||||
other: "non sei autorizzato a votare"
|
||||
disallow_vote_your_self:
|
||||
other: "Non puoi votare un tuo post!"
|
||||
not_found:
|
||||
other: "oggetto non trovato"
|
||||
verification_failed:
|
||||
other: "verifica fallita"
|
||||
email_or_password_incorrect:
|
||||
other: "email o password incorretti"
|
||||
old_password_verification_failed:
|
||||
other: "la verifica della vecchia password è fallita"
|
||||
new_password_same_as_previous_setting:
|
||||
other: "La nuova password è identica alla precedente"
|
||||
question:
|
||||
not_found:
|
||||
other: "domanda non trovata"
|
||||
rank:
|
||||
fail_to_meet_the_condition:
|
||||
other: "Condizioni non valide per il grado"
|
||||
report:
|
||||
handle_failed:
|
||||
other: "Gestione del report fallita"
|
||||
not_found:
|
||||
other: "Report non trovato"
|
||||
tag:
|
||||
not_found:
|
||||
other: "Etichetta non trovata"
|
||||
theme:
|
||||
not_found:
|
||||
other: "tema non trovato"
|
||||
user:
|
||||
email_or_password_wrong:
|
||||
other: *email_or_password_wrong
|
||||
not_found:
|
||||
other: "utente non trovato"
|
||||
suspended:
|
||||
other: "utente sospeso"
|
||||
username_invalid:
|
||||
other: "utente non valido"
|
||||
username_duplicate:
|
||||
other: "utente già in uso"
|
||||
|
||||
report:
|
||||
handle_failed:
|
||||
other: "Gestione del report fallita"
|
||||
not_found:
|
||||
other: "Report non trovato"
|
||||
tag:
|
||||
not_found:
|
||||
other: "Etichetta non trovata"
|
||||
theme:
|
||||
not_found:
|
||||
other: "tema non trovato"
|
||||
user:
|
||||
email_or_password_wrong:
|
||||
other: *email_or_password_wrong
|
||||
not_found:
|
||||
other: "utente non trovato"
|
||||
suspended:
|
||||
other: "utente sospeso"
|
||||
username_invalid:
|
||||
other: "utente non valido"
|
||||
username_duplicate:
|
||||
other: "utente già in uso"
|
||||
|
||||
report:
|
||||
spam:
|
||||
name:
|
||||
other: "spam"
|
||||
description:
|
||||
other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente"
|
||||
rude:
|
||||
name:
|
||||
other: "scortese o violento"
|
||||
description:
|
||||
other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso"
|
||||
duplicate:
|
||||
name:
|
||||
other: "duplicato"
|
||||
description:
|
||||
other: "Questa domanda è già stata posta e ha già una risposta."
|
||||
not_answer:
|
||||
name:
|
||||
other: "non è una risposta"
|
||||
description:
|
||||
other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto."
|
||||
not_need:
|
||||
name:
|
||||
other: "non più necessario"
|
||||
description:
|
||||
other: "Questo commento è datato, conversazionale o non rilevante a questo articolo."
|
||||
other:
|
||||
name:
|
||||
other: "altro"
|
||||
description:
|
||||
other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra."
|
||||
|
||||
question:
|
||||
close:
|
||||
duplicate:
|
||||
spam:
|
||||
name:
|
||||
other: "spam"
|
||||
description:
|
||||
other: "Questa domanda è già stata chiesta o ha già una risposta."
|
||||
guideline:
|
||||
other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente"
|
||||
rude:
|
||||
name:
|
||||
other: "motivo legato alla community"
|
||||
other: "scortese o violento"
|
||||
description:
|
||||
other: "Questa domanda non soddisfa le linee guida della comunità."
|
||||
multiple:
|
||||
other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso"
|
||||
duplicate:
|
||||
name:
|
||||
other: "richiede maggiori dettagli o chiarezza"
|
||||
other: "duplicato"
|
||||
description:
|
||||
other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema."
|
||||
other: "Questa domanda è già stata posta e ha già una risposta."
|
||||
not_answer:
|
||||
name:
|
||||
other: "non è una risposta"
|
||||
description:
|
||||
other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto."
|
||||
not_need:
|
||||
name:
|
||||
other: "non più necessario"
|
||||
description:
|
||||
other: "Questo commento è datato, conversazionale o non rilevante a questo articolo."
|
||||
other:
|
||||
name:
|
||||
other: "altro"
|
||||
description:
|
||||
other: "Questo articolo richiede un'altro motivo non listato sopra."
|
||||
other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra."
|
||||
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
other: "domanda aggiornata"
|
||||
answer_the_question:
|
||||
other: "domanda risposta"
|
||||
update_answer:
|
||||
other: "risposta aggiornata"
|
||||
adopt_answer:
|
||||
other: "risposta accettata"
|
||||
comment_question:
|
||||
other: "domanda commentata"
|
||||
comment_answer:
|
||||
other: "risposta commentata"
|
||||
reply_to_you:
|
||||
other: "hai ricevuto risposta"
|
||||
mention_you:
|
||||
other: "sei stato menzionato"
|
||||
your_question_is_closed:
|
||||
other: "la tua domanda è stata chiusa"
|
||||
your_question_was_deleted:
|
||||
other: "la tua domanda è stata rimossa"
|
||||
your_answer_was_deleted:
|
||||
other: "la tua risposta è stata rimossa"
|
||||
your_comment_was_deleted:
|
||||
other: "il tuo commento è stato rimosso"
|
||||
question:
|
||||
close:
|
||||
duplicate:
|
||||
name:
|
||||
other: "spam"
|
||||
description:
|
||||
other: "Questa domanda è già stata chiesta o ha già una risposta."
|
||||
guideline:
|
||||
name:
|
||||
other: "motivo legato alla community"
|
||||
description:
|
||||
other: "Questa domanda non soddisfa le linee guida della comunità."
|
||||
multiple:
|
||||
name:
|
||||
other: "richiede maggiori dettagli o chiarezza"
|
||||
description:
|
||||
other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema."
|
||||
other:
|
||||
name:
|
||||
other: "altro"
|
||||
description:
|
||||
other: "Questo articolo richiede un'altro motivo non listato sopra."
|
||||
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
other: "domanda aggiornata"
|
||||
answer_the_question:
|
||||
other: "domanda risposta"
|
||||
update_answer:
|
||||
other: "risposta aggiornata"
|
||||
adopt_answer:
|
||||
other: "risposta accettata"
|
||||
comment_question:
|
||||
other: "domanda commentata"
|
||||
comment_answer:
|
||||
other: "risposta commentata"
|
||||
reply_to_you:
|
||||
other: "hai ricevuto risposta"
|
||||
mention_you:
|
||||
other: "sei stato menzionato"
|
||||
your_question_is_closed:
|
||||
other: "la tua domanda è stata chiusa"
|
||||
your_question_was_deleted:
|
||||
other: "la tua domanda è stata rimossa"
|
||||
your_answer_was_deleted:
|
||||
other: "la tua risposta è stata rimossa"
|
||||
your_comment_was_deleted:
|
||||
other: "il tuo commento è stato rimosso"
|
||||
|
|
1061
i18n/zh_CN.yaml
1061
i18n/zh_CN.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,30 +1,64 @@
|
|||
package conf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/server"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/router"
|
||||
"github.com/answerdev/answer/internal/service/service_config"
|
||||
"github.com/answerdev/answer/pkg/writer"
|
||||
"github.com/segmentfault/pacman/contrib/conf/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// AllConfig all config
|
||||
type AllConfig struct {
|
||||
Debug bool `json:"debug" mapstructure:"debug"`
|
||||
Data *Data `json:"data" mapstructure:"data"`
|
||||
Server *Server `json:"server" mapstructure:"server"`
|
||||
I18n *translator.I18n `json:"i18n" mapstructure:"i18n"`
|
||||
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui"`
|
||||
ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config"`
|
||||
Debug bool `json:"debug" mapstructure:"debug" yaml:"debug"`
|
||||
Server *Server `json:"server" mapstructure:"server" yaml:"server"`
|
||||
Data *Data `json:"data" mapstructure:"data" yaml:"data"`
|
||||
I18n *translator.I18n `json:"i18n" mapstructure:"i18n" yaml:"i18n"`
|
||||
ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config" yaml:"service_config"`
|
||||
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui" yaml:"swaggerui"`
|
||||
}
|
||||
|
||||
// Server server config
|
||||
type Server struct {
|
||||
HTTP *server.HTTP `json:"http" mapstructure:"http"`
|
||||
HTTP *server.HTTP `json:"http" mapstructure:"http" yaml:"http"`
|
||||
}
|
||||
|
||||
// Data data config
|
||||
type Data struct {
|
||||
Database *data.Database `json:"database" mapstructure:"database"`
|
||||
Cache *data.CacheConf `json:"cache" mapstructure:"cache"`
|
||||
Database *data.Database `json:"database" mapstructure:"database" yaml:"database"`
|
||||
Cache *data.CacheConf `json:"cache" mapstructure:"cache" yaml:"cache"`
|
||||
}
|
||||
|
||||
// ReadConfig read config
|
||||
func ReadConfig(configFilePath string) (c *AllConfig, err error) {
|
||||
if len(configFilePath) == 0 {
|
||||
configFilePath = filepath.Join(cli.ConfigFileDir, cli.DefaultConfigFileName)
|
||||
}
|
||||
c = &AllConfig{}
|
||||
config, err := viper.NewWithPath(configFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = config.Parse(&c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// RewriteConfig rewrite config file path
|
||||
func RewriteConfig(configFilePath string, allConfig *AllConfig) error {
|
||||
buf := bytes.Buffer{}
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
if err := enc.Encode(allConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
return writer.ReplaceFile(configFilePath, buf.String())
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ const (
|
|||
// object TagID AnswerList
|
||||
// key equal database's table name
|
||||
var (
|
||||
Version string = ""
|
||||
|
||||
ObjectTypeStrMapping = map[string]int{
|
||||
QuestionObjectType: 1,
|
||||
AnswerObjectType: 2,
|
||||
|
@ -47,3 +49,12 @@ var (
|
|||
8: ReportObjectType,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
SiteTypeGeneral = "general"
|
||||
SiteTypeInterface = "interface"
|
||||
SiteTypeBranding = "branding"
|
||||
SiteTypeWrite = "write"
|
||||
SiteTypeLegal = "legal"
|
||||
)
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@ package data
|
|||
|
||||
// Database database config
|
||||
type Database struct {
|
||||
Driver string `json:"driver" mapstructure:"driver"`
|
||||
Connection string `json:"connection" mapstructure:"connection"`
|
||||
ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time"`
|
||||
MaxOpenConn int `json:"max_open_conn" mapstructure:"max_open_conn"`
|
||||
MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn"`
|
||||
Driver string `json:"driver" mapstructure:"driver" yaml:"driver"`
|
||||
Connection string `json:"connection" mapstructure:"connection" yaml:"connection"`
|
||||
ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time" yaml:"conn_max_life_time,omitempty"`
|
||||
MaxOpenConn int `json:"max_open_conn" mapstructure:"max_open_conn" yaml:"max_open_conn,omitempty"`
|
||||
MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn" yaml:"max_idle_conn,omitempty"`
|
||||
}
|
||||
|
||||
// CacheConf cache
|
||||
type CacheConf struct {
|
||||
FilePath string `json:"file_path" mapstructure:"file_path"`
|
||||
FilePath string `json:"file_path" mapstructure:"file_path" yaml:"file_path"`
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
EmailOrPasswordWrong = "error.user.email_or_password_wrong"
|
||||
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
|
||||
CommentNotFound = "error.comment.not_found"
|
||||
QuestionNotFound = "error.question.not_found"
|
||||
QuestionCannotDeleted = "error.question.cannot_deleted"
|
||||
|
@ -25,6 +25,8 @@ const (
|
|||
DisallowFollow = "error.object.disallow_follow"
|
||||
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
|
||||
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
|
||||
OldPasswordVerificationFailed = "error.object.old_password_verification_failed"
|
||||
NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting"
|
||||
UserNotFound = "error.user.not_found"
|
||||
UsernameInvalid = "error.user.username_invalid"
|
||||
UsernameDuplicate = "error.user.username_duplicate"
|
||||
|
@ -35,9 +37,17 @@ const (
|
|||
UserSuspended = "error.user.suspended"
|
||||
ObjectNotFound = "error.object.not_found"
|
||||
TagNotFound = "error.tag.not_found"
|
||||
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
|
||||
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
|
||||
ThemeNotFound = "error.theme.not_found"
|
||||
LangNotFound = "error.lang.not_found"
|
||||
ReportHandleFailed = "error.report.handle_failed"
|
||||
ReportNotFound = "error.report.not_found"
|
||||
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"
|
||||
UploadFileSourceUnsupported = "error.upload.source_unsupported"
|
||||
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
|
||||
)
|
||||
|
|
|
@ -2,5 +2,5 @@ package translator
|
|||
|
||||
// I18n i18n config
|
||||
type I18n struct {
|
||||
BundleDir string `json:"bundle_dir" mapstructure:"bundle_dir"`
|
||||
BundleDir string `json:"bundle_dir" mapstructure:"bundle_dir" yaml:"bundle_dir"`
|
||||
}
|
||||
|
|
|
@ -1,17 +1,100 @@
|
|||
package translator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/wire"
|
||||
myTran "github.com/segmentfault/pacman/contrib/i18n"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ProviderSet is providers.
|
||||
var ProviderSet = wire.NewSet(NewTranslator)
|
||||
var GlobalTrans i18n.Translator
|
||||
|
||||
// LangOption language option
|
||||
type LangOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// DefaultLangOption default language option. If user config the language is default, the language option is admin choose.
|
||||
const DefaultLangOption = "Default"
|
||||
|
||||
var (
|
||||
// LanguageOptions language
|
||||
LanguageOptions []*LangOption
|
||||
)
|
||||
|
||||
// NewTranslator new a translator
|
||||
func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
|
||||
GlobalTrans, err = myTran.NewTranslator(c.BundleDir)
|
||||
entries, err := os.ReadDir(c.BundleDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read the Bundle resources file from entries
|
||||
for _, file := range entries {
|
||||
// ignore directory
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
// ignore non-YAML file
|
||||
if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" {
|
||||
continue
|
||||
}
|
||||
buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err)
|
||||
}
|
||||
|
||||
// only parse the backend translation
|
||||
translation := struct {
|
||||
Content map[string]interface{} `yaml:"backend"`
|
||||
}{}
|
||||
if err = yaml.Unmarshal(buf, &translation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := yaml.Marshal(translation.Content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err)
|
||||
}
|
||||
|
||||
// add translator use backend translation
|
||||
if err = myTran.AddTranslator(content, file.Name()); err != nil {
|
||||
return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err)
|
||||
}
|
||||
}
|
||||
GlobalTrans = myTran.GlobalTrans
|
||||
|
||||
i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read i18n file failed: %s", err)
|
||||
}
|
||||
|
||||
s := struct {
|
||||
LangOption []*LangOption `yaml:"language_options"`
|
||||
}{}
|
||||
err = yaml.Unmarshal(i18nFile, &s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("i18n file parsing failed: %s", err)
|
||||
}
|
||||
LanguageOptions = s.LangOption
|
||||
return GlobalTrans, err
|
||||
}
|
||||
|
||||
// CheckLanguageIsValid check user input language is valid
|
||||
func CheckLanguageIsValid(lang string) bool {
|
||||
if lang == DefaultLangOption {
|
||||
return true
|
||||
}
|
||||
for _, option := range LanguageOptions {
|
||||
if option.Value == lang {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package validator
|
|||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
|
@ -25,10 +26,10 @@ type MyValidator struct {
|
|||
Lang i18n.Language
|
||||
}
|
||||
|
||||
// ErrorField error field
|
||||
type ErrorField struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
// FormErrorField indicates the current form error content. which field is error and error message.
|
||||
type FormErrorField struct {
|
||||
ErrorField string `json:"error_field"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
|
||||
// GlobalValidatorMapping is a mapping from validator to translator used
|
||||
|
@ -86,7 +87,7 @@ func GetValidatorByLang(la string) *MyValidator {
|
|||
}
|
||||
|
||||
// Check /
|
||||
func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error) {
|
||||
func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err error) {
|
||||
err = m.Validate.Struct(value)
|
||||
if err != nil {
|
||||
var valErrors validator.ValidationErrors
|
||||
|
@ -96,18 +97,35 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
|
|||
}
|
||||
|
||||
for _, fieldError := range valErrors {
|
||||
errField = &ErrorField{
|
||||
Key: translator.GlobalTrans.Tr(m.Lang, fieldError.Field()),
|
||||
Value: fieldError.Translate(m.Tran),
|
||||
errField := &FormErrorField{
|
||||
ErrorField: fieldError.Field(),
|
||||
ErrorMsg: fieldError.Translate(m.Tran),
|
||||
}
|
||||
return errField, myErrors.BadRequest(reason.RequestFormatError).WithMsg(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.ErrorField = originalTag
|
||||
}
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
}
|
||||
if len(errFields) > 0 {
|
||||
errMsg := ""
|
||||
if len(errFields) == 1 {
|
||||
errMsg = errFields[0].ErrorMsg
|
||||
}
|
||||
return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := value.(Checker); ok {
|
||||
errField, err = v.Check()
|
||||
errFields, err = v.Check()
|
||||
if err != nil {
|
||||
return errField, err
|
||||
return errFields, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
|
@ -115,5 +133,26 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
|
|||
|
||||
// Checker .
|
||||
type Checker interface {
|
||||
Check() (errField *ErrorField, err error)
|
||||
Check() (errField []*FormErrorField, err error)
|
||||
}
|
||||
|
||||
func getObjectTagByFieldName(obj interface{}, fieldName string) (tag string) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
objT := reflect.TypeOf(obj)
|
||||
objT = objT.Elem()
|
||||
|
||||
structField, exists := objT.FieldByName(fieldName)
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
tag = structField.Tag.Get("json")
|
||||
if len(tag) == 0 {
|
||||
return structField.Tag.Get("form")
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
|
|
@ -1,59 +1,71 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/answerdev/answer/configs"
|
||||
"github.com/answerdev/answer/i18n"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
"github.com/answerdev/answer/pkg/writer"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultConfigFileName = "config.yaml"
|
||||
DefaultCacheFileName = "cache.db"
|
||||
)
|
||||
|
||||
var (
|
||||
ConfigFilePath = "/conf/"
|
||||
UploadFilePath = "/upfiles/"
|
||||
ConfigFileDir = "/conf/"
|
||||
UploadFilePath = "/uploads/"
|
||||
I18nPath = "/i18n/"
|
||||
CacheDir = "/cache/"
|
||||
)
|
||||
|
||||
// GetConfigFilePath get config file path
|
||||
func GetConfigFilePath() string {
|
||||
return filepath.Join(ConfigFileDir, DefaultConfigFileName)
|
||||
}
|
||||
|
||||
func FormatAllPath(dataDirPath string) {
|
||||
ConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir)
|
||||
UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
|
||||
I18nPath = filepath.Join(dataDirPath, I18nPath)
|
||||
CacheDir = filepath.Join(dataDirPath, CacheDir)
|
||||
}
|
||||
|
||||
// InstallAllInitialEnvironment install all initial environment
|
||||
func InstallAllInitialEnvironment(dataDirPath string) {
|
||||
ConfigFilePath = filepath.Join(dataDirPath, ConfigFilePath)
|
||||
UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
|
||||
I18nPath = filepath.Join(dataDirPath, I18nPath)
|
||||
|
||||
installConfigFile()
|
||||
FormatAllPath(dataDirPath)
|
||||
installUploadDir()
|
||||
installI18nBundle()
|
||||
fmt.Println("install all initial environment done")
|
||||
}
|
||||
|
||||
func installConfigFile() {
|
||||
fmt.Println("[config-file] try to install...")
|
||||
defaultConfigFile := filepath.Join(ConfigFilePath, DefaultConfigFileName)
|
||||
func InstallConfigFile(configFilePath string) error {
|
||||
if len(configFilePath) == 0 {
|
||||
configFilePath = filepath.Join(ConfigFileDir, DefaultConfigFileName)
|
||||
}
|
||||
fmt.Println("[config-file] try to create at ", configFilePath)
|
||||
|
||||
// if config file already exists do nothing.
|
||||
if CheckConfigFile(defaultConfigFile) {
|
||||
fmt.Printf("[config-file] %s already exists\n", defaultConfigFile)
|
||||
return
|
||||
if CheckConfigFile(configFilePath) {
|
||||
fmt.Printf("[config-file] %s already exists\n", configFilePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := dir.CreateDirIfNotExist(ConfigFilePath); err != nil {
|
||||
if err := dir.CreateDirIfNotExist(ConfigFileDir); err != nil {
|
||||
fmt.Printf("[config-file] create directory fail %s\n", err.Error())
|
||||
return
|
||||
return fmt.Errorf("create directory fail %s", err.Error())
|
||||
}
|
||||
fmt.Printf("[config-file] create directory success, config file is %s\n", defaultConfigFile)
|
||||
fmt.Printf("[config-file] create directory success, config file is %s\n", configFilePath)
|
||||
|
||||
if err := writerFile(defaultConfigFile, string(configs.Config)); err != nil {
|
||||
if err := writer.WriteFile(configFilePath, string(configs.Config)); err != nil {
|
||||
fmt.Printf("[config-file] install fail %s\n", err.Error())
|
||||
return
|
||||
return fmt.Errorf("write file failed %s", err)
|
||||
}
|
||||
fmt.Printf("[config-file] install success\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func installUploadDir() {
|
||||
|
@ -85,7 +97,7 @@ func installI18nBundle() {
|
|||
continue
|
||||
}
|
||||
fmt.Printf("[i18n] install %s bundle...\n", item.Name())
|
||||
err = writerFile(path, string(content))
|
||||
err = writer.WriteFile(path, string(content))
|
||||
if err != nil {
|
||||
fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error())
|
||||
} else {
|
||||
|
@ -93,21 +105,3 @@ func installI18nBundle() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writerFile(filePath, content string) error {
|
||||
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
writer := bufio.NewWriter(file)
|
||||
if _, err := writer.WriteString(content); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writer.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
)
|
||||
|
||||
|
@ -13,12 +16,40 @@ func CheckUploadDir() bool {
|
|||
return dir.CheckDirExist(UploadFilePath)
|
||||
}
|
||||
|
||||
func CheckDB(dataConf *data.Database) bool {
|
||||
// CheckDBConnection check database whether the connection is normal
|
||||
func CheckDBConnection(dataConf *data.Database) bool {
|
||||
db, err := data.NewDB(false, dataConf)
|
||||
if err != nil {
|
||||
fmt.Printf("connection database failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
fmt.Printf("connection ping database failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// CheckDBTableExist check database whether the table is already exists
|
||||
func CheckDBTableExist(dataConf *data.Database) bool {
|
||||
db, err := data.NewDB(false, dataConf)
|
||||
if err != nil {
|
||||
fmt.Printf("connection database failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
fmt.Printf("connection ping database failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
|
||||
exist, err := db.IsTableExist(&entity.Version{})
|
||||
if err != nil {
|
||||
fmt.Printf("check table exist failed: %s\n", err)
|
||||
return false
|
||||
}
|
||||
if !exist {
|
||||
fmt.Printf("check table not exist\n")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
@ -16,13 +17,21 @@ import (
|
|||
|
||||
// AnswerController answer controller
|
||||
type AnswerController struct {
|
||||
answerService *service.AnswerService
|
||||
rankService *rank.RankService
|
||||
answerService *service.AnswerService
|
||||
rankService *rank.RankService
|
||||
dashboardService *dashboard.DashboardService
|
||||
}
|
||||
|
||||
// NewAnswerController new controller
|
||||
func NewAnswerController(answerService *service.AnswerService, rankService *rank.RankService) *AnswerController {
|
||||
return &AnswerController{answerService: answerService, rankService: rankService}
|
||||
func NewAnswerController(answerService *service.AnswerService,
|
||||
rankService *rank.RankService,
|
||||
dashboardService *dashboard.DashboardService,
|
||||
) *AnswerController {
|
||||
return &AnswerController{
|
||||
answerService: answerService,
|
||||
rankService: rankService,
|
||||
dashboardService: dashboardService,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAnswer delete answer
|
||||
|
|
|
@ -20,4 +20,6 @@ var ProviderSetController = wire.NewSet(
|
|||
NewReasonController,
|
||||
NewNotificationController,
|
||||
NewSiteinfoController,
|
||||
NewDashboardController,
|
||||
NewUploadController,
|
||||
)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
package controller
|
|
@ -0,0 +1,36 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type DashboardController struct {
|
||||
dashboardService *dashboard.DashboardService
|
||||
}
|
||||
|
||||
// NewDashboardController new controller
|
||||
func NewDashboardController(
|
||||
dashboardService *dashboard.DashboardService,
|
||||
) *DashboardController {
|
||||
return &DashboardController{
|
||||
dashboardService: dashboardService,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardInfo godoc
|
||||
// @Summary DashboardInfo
|
||||
// @Description DashboardInfo
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /answer/admin/api/dashboard [get]
|
||||
// @Success 200 {object} handler.RespBody
|
||||
func (ac *DashboardController) DashboardInfo(ctx *gin.Context) {
|
||||
info, err := ac.dashboardService.StatisticalByCache(ctx)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"info": info,
|
||||
})
|
||||
}
|
|
@ -4,18 +4,20 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
)
|
||||
|
||||
type LangController struct {
|
||||
translator i18n.Translator
|
||||
translator i18n.Translator
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewLangController new language controller.
|
||||
func NewLangController(tr i18n.Translator) *LangController {
|
||||
return &LangController{translator: tr}
|
||||
func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController {
|
||||
return &LangController{translator: tr, siteInfoService: siteInfoService}
|
||||
}
|
||||
|
||||
// GetLangMapping get language config mapping
|
||||
|
@ -33,15 +35,38 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// GetLangOptions Get language options
|
||||
// GetAdminLangOptions Get language options
|
||||
// @Summary Get language options
|
||||
// @Description Get language options
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/language/options [get]
|
||||
// @Router /answer/admin/api/language/options [get]
|
||||
func (u *LangController) GetLangOptions(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, nil, schema.GetLangOptions)
|
||||
func (u *LangController) GetAdminLangOptions(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
|
||||
}
|
||||
|
||||
// GetUserLangOptions Get language options
|
||||
// @Summary Get language options
|
||||
// @Description Get language options
|
||||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/language/options [get]
|
||||
func (u *LangController) GetUserLangOptions(ctx *gin.Context) {
|
||||
siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
options := translator.LanguageOptions
|
||||
if len(siteInterfaceResp.Language) > 0 {
|
||||
defaultOption := []*translator.LangOption{
|
||||
{Label: translator.DefaultLangOption, Value: translator.DefaultLangOption},
|
||||
}
|
||||
options = append(defaultOption, options...)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, options)
|
||||
}
|
||||
|
|
|
@ -3,45 +3,73 @@ package controller
|
|||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
type SiteinfoController struct {
|
||||
siteInfoService *service.SiteInfoService
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewSiteinfoController new siteinfo controller.
|
||||
func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController {
|
||||
func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController {
|
||||
return &SiteinfoController{
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo godoc
|
||||
// @Summary Get siteinfo
|
||||
// @Description Get siteinfo
|
||||
// GetSiteInfo get site info
|
||||
// @Summary get site info
|
||||
// @Description get site info
|
||||
// @Tags site
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp}
|
||||
// @Router /answer/api/v1/siteinfo [get]
|
||||
func (sc *SiteinfoController) GetInfo(ctx *gin.Context) {
|
||||
var (
|
||||
resp = &schema.SiteInfoResp{}
|
||||
general schema.SiteGeneralResp
|
||||
face schema.SiteInterfaceResp
|
||||
err error
|
||||
)
|
||||
|
||||
general, err = sc.siteInfoService.GetSiteGeneral(ctx)
|
||||
resp.General = &general
|
||||
func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
|
||||
var err error
|
||||
resp := &schema.SiteInfoResp{}
|
||||
resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
return
|
||||
log.Error(err)
|
||||
}
|
||||
resp.Interface, err = sc.siteInfoService.GetSiteInterface(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
face, err = sc.siteInfoService.GetSiteInterface(ctx)
|
||||
resp.Face = &face
|
||||
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
resp.Branding, err = sc.siteInfoService.GetSiteBranding(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// GetSiteLegalInfo get site legal info
|
||||
// @Summary get site legal info
|
||||
// @Description get site legal info
|
||||
// @Tags site
|
||||
// @Param info_type query string true "legal information type" Enums(tos, privacy)
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetSiteLegalInfoResp}
|
||||
// @Router /answer/api/v1/siteinfo/legal [get]
|
||||
func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) {
|
||||
req := &schema.GetSiteLegalInfoReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
siteLegal, err := sc.siteInfoService.GetSiteLegal(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
resp := &schema.GetSiteLegalInfoResp{}
|
||||
if req.IsTOS() {
|
||||
resp.TermsOfServiceOriginalText = siteLegal.TermsOfServiceOriginalText
|
||||
resp.TermsOfServiceParsedText = siteLegal.TermsOfServiceParsedText
|
||||
} else if req.IsPrivacy() {
|
||||
resp.PrivacyPolicyOriginalText = siteLegal.PrivacyPolicyOriginalText
|
||||
resp.PrivacyPolicyParsedText = siteLegal.PrivacyPolicyParsedText
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
|
|
@ -7,19 +7,25 @@ import (
|
|||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/internal/service/tag"
|
||||
"github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// TagController tag controller
|
||||
type TagController struct {
|
||||
tagService *tag.TagService
|
||||
rankService *rank.RankService
|
||||
tagService *tag.TagService
|
||||
tagCommonService *tag_common.TagCommonService
|
||||
rankService *rank.RankService
|
||||
}
|
||||
|
||||
// NewTagController new controller
|
||||
func NewTagController(tagService *tag.TagService, rankService *rank.RankService) *TagController {
|
||||
return &TagController{tagService: tagService, rankService: rankService}
|
||||
func NewTagController(
|
||||
tagService *tag.TagService,
|
||||
tagCommonService *tag_common.TagCommonService,
|
||||
rankService *rank.RankService,
|
||||
) *TagController {
|
||||
return &TagController{tagService: tagService, tagCommonService: tagCommonService, rankService: rankService}
|
||||
}
|
||||
|
||||
// SearchTagLike get tag list
|
||||
|
@ -36,8 +42,9 @@ func (tc *TagController) SearchTagLike(ctx *gin.Context) {
|
|||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := tc.tagService.SearchTagLike(ctx, req)
|
||||
userinfo := middleware.GetUserInfoFromContext(ctx)
|
||||
req.IsAdmin = userinfo.IsAdmin
|
||||
resp, err := tc.tagCommonService.SearchTagLike(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// file is uploaded by markdown(or something else) editor
|
||||
fileFromPost = "post"
|
||||
// file is used to change the user's avatar
|
||||
fileFromAvatar = "avatar"
|
||||
// file is logo/icon images
|
||||
fileFromBranding = "branding"
|
||||
)
|
||||
|
||||
// UploadController upload controller
|
||||
type UploadController struct {
|
||||
uploaderService *uploader.UploaderService
|
||||
}
|
||||
|
||||
// NewUploadController new controller
|
||||
func NewUploadController(uploaderService *uploader.UploaderService) *UploadController {
|
||||
return &UploadController{
|
||||
uploaderService: uploaderService,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFile upload file
|
||||
// @Summary upload file
|
||||
// @Description upload file
|
||||
// @Tags Upload
|
||||
// @Accept multipart/form-data
|
||||
// @Security ApiKeyAuth
|
||||
// @Param source formData string true "identify the source of the file upload" Enums(post, avatar, branding)
|
||||
// @Param file formData file true "file"
|
||||
// @Success 200 {object} handler.RespBody{data=string}
|
||||
// @Router /answer/api/v1/file [post]
|
||||
func (uc *UploadController) UploadFile(ctx *gin.Context) {
|
||||
var (
|
||||
url string
|
||||
err error
|
||||
)
|
||||
|
||||
source := ctx.PostForm("source")
|
||||
switch source {
|
||||
case fileFromAvatar:
|
||||
url, err = uc.uploaderService.UploadAvatarFile(ctx)
|
||||
case fileFromPost:
|
||||
url, err = uc.uploaderService.UploadPostFile(ctx)
|
||||
case fileFromBranding:
|
||||
url, err = uc.uploaderService.UploadBrandingFile(ctx)
|
||||
default:
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.UploadFileSourceUnsupported), nil)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, err, url)
|
||||
}
|
|
@ -1,14 +1,11 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/action"
|
||||
|
@ -17,7 +14,6 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// UserController user controller
|
||||
|
@ -89,22 +85,6 @@ func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetUserStatus get user status info
|
||||
// @Summary get user status info
|
||||
// @Description get user status info
|
||||
// @Tags User
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
|
||||
// @Router /answer/api/v1/user/status [get]
|
||||
func (uc *UserController) GetUserStatus(ctx *gin.Context) {
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
token := middleware.ExtractToken(ctx)
|
||||
resp, err := uc.userService.GetUserStatus(ctx, userID, token)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserEmailLogin godoc
|
||||
// @Summary UserEmailLogin
|
||||
// @Description UserEmailLogin
|
||||
|
@ -122,24 +102,22 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
|
|||
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
resp := schema.UserVerifyEmailErrorResponse{
|
||||
Key: "captcha_code",
|
||||
Value: "error.object.verification_failed",
|
||||
}
|
||||
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userService.EmailLogin(ctx, req)
|
||||
if err != nil {
|
||||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
|
||||
resp := schema.UserVerifyEmailErrorResponse{
|
||||
Key: "e_mail",
|
||||
Value: "error.object.email_or_password_incorrect",
|
||||
}
|
||||
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "e_mail",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields)
|
||||
return
|
||||
}
|
||||
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
|
||||
|
@ -162,12 +140,11 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
|
|||
}
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
resp := schema.UserVerifyEmailErrorResponse{
|
||||
Key: "captcha_code",
|
||||
Value: "error.object.verification_failed",
|
||||
}
|
||||
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
|
||||
|
@ -293,12 +270,11 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
|
|||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(),
|
||||
req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
resp := schema.UserVerifyEmailErrorResponse{
|
||||
Key: "captcha_code",
|
||||
Value: "error.object.verification_failed",
|
||||
}
|
||||
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -330,22 +306,19 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
if !oldPassVerification {
|
||||
resp := schema.UserVerifyEmailErrorResponse{
|
||||
Key: "old_pass",
|
||||
Value: "error.object.old_password_verification_failed",
|
||||
}
|
||||
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "old_pass",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
if req.OldPass == req.Pass {
|
||||
|
||||
resp := schema.UserVerifyEmailErrorResponse{
|
||||
Key: "pass",
|
||||
Value: "error.object.new_password_same_as_previous_setting",
|
||||
}
|
||||
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields)
|
||||
return
|
||||
}
|
||||
err = uc.userService.UserModifyPassword(ctx, req)
|
||||
|
@ -373,64 +346,25 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UploadUserAvatar godoc
|
||||
// @Summary UserUpdateInfo
|
||||
// @Description UserUpdateInfo
|
||||
// UserUpdateInterface update user interface config
|
||||
// @Summary UserUpdateInterface update user interface config
|
||||
// @Description UserUpdateInterface update user interface config
|
||||
// @Tags User
|
||||
// @Accept multipart/form-data
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param file formData file true "file"
|
||||
// @Success 200 {object} handler.RespBody{data=string}
|
||||
// @Router /answer/api/v1/user/avatar/upload [post]
|
||||
func (uc *UserController) UploadUserAvatar(ctx *gin.Context) {
|
||||
// max size
|
||||
var filesMax int64 = 5 << 20
|
||||
var valuesMax int64 = 5
|
||||
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, filesMax+valuesMax)
|
||||
_, header, err := ctx.Request.FormFile("file")
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
|
||||
// @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
|
||||
}
|
||||
fileExt := strings.ToLower(path.Ext(header.Filename))
|
||||
if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" {
|
||||
log.Errorf("upload file format is not supported: %s", fileExt)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
|
||||
url, err := uc.uploaderService.UploadAvatarFile(ctx, header, fileExt)
|
||||
handler.HandleResponse(ctx, err, url)
|
||||
}
|
||||
|
||||
// UploadUserPostFile godoc
|
||||
// @Summary upload user post file
|
||||
// @Description upload user post file
|
||||
// @Tags User
|
||||
// @Accept multipart/form-data
|
||||
// @Security ApiKeyAuth
|
||||
// @Param file formData file true "file"
|
||||
// @Success 200 {object} handler.RespBody{data=string}
|
||||
// @Router /answer/api/v1/user/post/file [post]
|
||||
func (uc *UserController) UploadUserPostFile(ctx *gin.Context) {
|
||||
// max size
|
||||
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
|
||||
_, header, err := ctx.Request.FormFile("file")
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
fileExt := strings.ToLower(path.Ext(header.Filename))
|
||||
if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" {
|
||||
log.Errorf("upload file format is not supported: %s", fileExt)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
|
||||
url, err := uc.uploaderService.UploadPostFile(ctx, header, fileExt)
|
||||
handler.HandleResponse(ctx, err, url)
|
||||
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
|
||||
err := uc.userService.UserUpdateInterface(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// ActionRecord godoc
|
||||
|
@ -490,24 +424,29 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
|
|||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
// If the user is not logged in, the api cannot be used.
|
||||
// If the user email is not verified, that also can use this api to modify the email.
|
||||
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
resp := schema.UserVerifyEmailErrorResponse{
|
||||
Key: "captcha_code",
|
||||
Value: "error.object.verification_failed",
|
||||
}
|
||||
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.UserID) == 0 {
|
||||
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
|
||||
return
|
||||
}
|
||||
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
|
||||
ErrorField: "captcha_code",
|
||||
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
|
||||
})
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
|
||||
return
|
||||
}
|
||||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
|
||||
err := uc.userService.UserChangeEmailSendCode(ctx, req)
|
||||
resp, err := uc.userService.UserChangeEmailSendCode(ctx, req)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
resp.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.ErrorMsg)
|
||||
}
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,25 +2,27 @@ package controller_backyard
|
|||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SiteInfoController site info controller
|
||||
type SiteInfoController struct {
|
||||
siteInfoService *service.SiteInfoService
|
||||
siteInfoService *siteinfo.SiteInfoService
|
||||
}
|
||||
|
||||
// NewSiteInfoController new siteinfo controller.
|
||||
func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController {
|
||||
// NewSiteInfoController new site info controller
|
||||
func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController {
|
||||
return &SiteInfoController{
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetGeneral godoc
|
||||
// @Summary Get siteinfo general
|
||||
// @Description Get siteinfo general
|
||||
// GetGeneral get site general information
|
||||
// @Summary get site general information
|
||||
// @Description get site general information
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
|
@ -31,23 +33,61 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetInterface godoc
|
||||
// @Summary Get siteinfo interface
|
||||
// @Description Get siteinfo interface
|
||||
// GetInterface get site interface
|
||||
// @Summary get site interface
|
||||
// @Description get site interface
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp}
|
||||
// @Router /answer/admin/api/siteinfo/interface [get]
|
||||
// @Param data body schema.AddCommentReq true "general"
|
||||
func (sc *SiteInfoController) GetInterface(ctx *gin.Context) {
|
||||
resp, err := sc.siteInfoService.GetSiteInterface(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdateGeneral godoc
|
||||
// @Summary Get siteinfo interface
|
||||
// @Description Get siteinfo interface
|
||||
// GetSiteBranding get site interface
|
||||
// @Summary get site interface
|
||||
// @Description get site interface
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.SiteBrandingResp}
|
||||
// @Router /answer/admin/api/siteinfo/branding [get]
|
||||
func (sc *SiteInfoController) GetSiteBranding(ctx *gin.Context) {
|
||||
resp, err := sc.siteInfoService.GetSiteBranding(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetSiteWrite get site interface
|
||||
// @Summary get site interface
|
||||
// @Description get site interface
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.SiteWriteResp}
|
||||
// @Router /answer/admin/api/siteinfo/write [get]
|
||||
func (sc *SiteInfoController) GetSiteWrite(ctx *gin.Context) {
|
||||
resp, err := sc.siteInfoService.GetSiteWrite(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetSiteLegal Set the legal information for the site
|
||||
// @Summary Set the legal information for the site
|
||||
// @Description Set the legal information for the site
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.SiteLegalResp}
|
||||
// @Router /answer/admin/api/siteinfo/legal [get]
|
||||
func (sc *SiteInfoController) GetSiteLegal(ctx *gin.Context) {
|
||||
resp, err := sc.siteInfoService.GetSiteLegal(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdateGeneral update site general information
|
||||
// @Summary update site general information
|
||||
// @Description update site general information
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
|
@ -63,9 +103,9 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UpdateInterface godoc
|
||||
// @Summary Get siteinfo interface
|
||||
// @Description Get siteinfo interface
|
||||
// UpdateInterface update site interface
|
||||
// @Summary update site info interface
|
||||
// @Description update site info interface
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
|
@ -81,6 +121,62 @@ func (sc *SiteInfoController) UpdateInterface(ctx *gin.Context) {
|
|||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UpdateBranding update site branding
|
||||
// @Summary update site info branding
|
||||
// @Description update site info branding
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Param data body schema.SiteBrandingReq true "branding info"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/admin/api/siteinfo/branding [put]
|
||||
func (sc *SiteInfoController) UpdateBranding(ctx *gin.Context) {
|
||||
req := &schema.SiteBrandingReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
err := sc.siteInfoService.SaveSiteBranding(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UpdateSiteWrite update site write info
|
||||
// @Summary update site write info
|
||||
// @Description update site write info
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Param data body schema.SiteWriteReq true "write info"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/admin/api/siteinfo/write [put]
|
||||
func (sc *SiteInfoController) UpdateSiteWrite(ctx *gin.Context) {
|
||||
req := &schema.SiteWriteReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := sc.siteInfoService.SaveSiteWrite(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdateSiteLegal update site legal info
|
||||
// @Summary update site legal info
|
||||
// @Description update site legal info
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Param data body schema.SiteLegalReq true "write info"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/admin/api/siteinfo/legal [put]
|
||||
func (sc *SiteInfoController) UpdateSiteLegal(ctx *gin.Context) {
|
||||
req := &schema.SiteLegalReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
err := sc.siteInfoService.SaveSiteLegal(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetSMTPConfig get smtp config
|
||||
// @Summary GetSMTPConfig get smtp config
|
||||
// @Description GetSMTPConfig get smtp config
|
||||
|
|
|
@ -5,4 +5,5 @@ type UserCacheInfo struct {
|
|||
UserID string `json:"user_id"`
|
||||
UserStatus int `json:"user_status"`
|
||||
EmailStatus int `json:"email_status"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ type Tag struct {
|
|||
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
|
||||
QuestionCount int `xorm:"not null default 0 INT(11) question_count"`
|
||||
Status int `xorm:"not null default 1 INT(11) status"`
|
||||
Recommend bool `xorm:"not null default false BOOL recommend"`
|
||||
Reserved bool `xorm:"not null default false BOOL reserved"`
|
||||
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
|
||||
}
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ type User struct {
|
|||
Location string `xorm:"not null default '' VARCHAR(100) location"`
|
||||
IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"`
|
||||
IsAdmin bool `xorm:"not null default false BOOL is_admin"`
|
||||
Language string `xorm:"not null default '' VARCHAR(100) language"`
|
||||
}
|
||||
|
||||
// TableName user table name
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/configs"
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/migrations"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// LangOptions get installation language options
|
||||
// @Summary get installation language options
|
||||
// @Description get installation language options
|
||||
// @Tags Lang
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]translator.LangOption}
|
||||
// @Router /installation/language/options [get]
|
||||
func LangOptions(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
|
||||
}
|
||||
|
||||
// CheckConfigFile check config file if exist when installation
|
||||
// @Summary check config file if exist when installation
|
||||
// @Description check config file if exist when installation
|
||||
// @Tags installation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
|
||||
// @Router /installation/config-file/check [post]
|
||||
func CheckConfigFile(ctx *gin.Context) {
|
||||
resp := &CheckConfigFileResp{}
|
||||
resp.ConfigFileExist = cli.CheckConfigFile(confPath)
|
||||
if !resp.ConfigFileExist {
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
return
|
||||
}
|
||||
allConfig, err := conf.ReadConfig(confPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
err = errors.BadRequest(reason.ReadConfigFailed)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
resp.DBConnectionSuccess = cli.CheckDBConnection(allConfig.Data.Database)
|
||||
if resp.DBConnectionSuccess {
|
||||
resp.DbTableExist = cli.CheckDBTableExist(allConfig.Data.Database)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// CheckDatabase check database if exist when installation
|
||||
// @Summary check database if exist when installation
|
||||
// @Description check database if exist when installation
|
||||
// @Tags installation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq"
|
||||
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
|
||||
// @Router /installation/db/check [post]
|
||||
func CheckDatabase(ctx *gin.Context) {
|
||||
req := &CheckDatabaseReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := &CheckDatabaseResp{}
|
||||
dataConf := &data.Database{
|
||||
Driver: req.DbType,
|
||||
Connection: req.GetConnection(),
|
||||
}
|
||||
resp.ConnectionSuccess = cli.CheckDBConnection(dataConf)
|
||||
if !resp.ConnectionSuccess {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// InitEnvironment init environment
|
||||
// @Summary init environment
|
||||
// @Description init environment
|
||||
// @Tags installation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @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{}
|
||||
// @Router /installation/base-info [post]
|
||||
func InitBaseInfo(ctx *gin.Context) {
|
||||
req := &InitBaseInfoReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.FormatSiteUrl()
|
||||
|
||||
c, err := conf.ReadConfig(confPath)
|
||||
if err != nil {
|
||||
log.Errorf("read config failed %s", err)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if cli.CheckDBTableExist(c.Data.Database) {
|
||||
log.Warnf("database is already initialized")
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := migrations.InitDB(c.Data.Database); err != nil {
|
||||
log.Error("init database error: ", err.Error())
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), schema.ErrTypeAlert)
|
||||
return
|
||||
}
|
||||
|
||||
err = migrations.UpdateInstallInfo(c.Data.Database, req.Language, req.SiteName, req.SiteURL, req.ContactEmail,
|
||||
req.AdminName, req.AdminPassword, req.AdminEmail)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), nil)
|
||||
return
|
||||
}
|
||||
|
||||
handler.HandleResponse(ctx, nil, nil)
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
os.Exit(0)
|
||||
}()
|
||||
return
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
port = os.Getenv("INSTALL_PORT")
|
||||
confPath = ""
|
||||
)
|
||||
|
||||
func Run(configPath string) {
|
||||
confPath = configPath
|
||||
// initialize translator for return internationalization error when installing.
|
||||
_, err := translator.NewTranslator(&translator.I18n{BundleDir: cli.I18nPath})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
installServer := NewInstallHTTPServer()
|
||||
if len(port) == 0 {
|
||||
port = "80"
|
||||
}
|
||||
fmt.Printf("[SUCCESS] answer installation service will run at: http://localhost:%s/install/ \n", port)
|
||||
if err = installServer.Run(":" + port); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package install
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// CheckConfigFileResp check config file if exist or not response
|
||||
type CheckConfigFileResp struct {
|
||||
ConfigFileExist bool `json:"config_file_exist"`
|
||||
DBConnectionSuccess bool `json:"db_connection_success"`
|
||||
DbTableExist bool `json:"db_table_exist"`
|
||||
}
|
||||
|
||||
// CheckDatabaseReq check database
|
||||
type CheckDatabaseReq struct {
|
||||
DbType string `validate:"required,oneof=postgres sqlite3 mysql" json:"db_type"`
|
||||
DbUsername string `json:"db_username"`
|
||||
DbPassword string `json:"db_password"`
|
||||
DbHost string `json:"db_host"`
|
||||
DbName string `json:"db_name"`
|
||||
DbFile string `json:"db_file"`
|
||||
}
|
||||
|
||||
// GetConnection get connection string
|
||||
func (r *CheckDatabaseReq) GetConnection() string {
|
||||
if r.DbType == string(schemas.SQLITE) {
|
||||
return r.DbFile
|
||||
}
|
||||
if r.DbType == string(schemas.MYSQL) {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s)/%s",
|
||||
r.DbUsername, r.DbPassword, r.DbHost, r.DbName)
|
||||
}
|
||||
if r.DbType == string(schemas.POSTGRES) {
|
||||
host, port := parsePgSQLHostPort(r.DbHost)
|
||||
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||
host, port, r.DbUsername, r.DbPassword, r.DbName)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parsePgSQLHostPort(dbHost string) (host string, port string) {
|
||||
if strings.Contains(dbHost, ":") {
|
||||
idx := strings.LastIndex(dbHost, ":")
|
||||
host, port = dbHost[:idx], dbHost[idx+1:]
|
||||
} else if len(dbHost) > 0 {
|
||||
host = dbHost
|
||||
}
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
if port == "" {
|
||||
port = "5432"
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
// CheckDatabaseResp check database response
|
||||
type CheckDatabaseResp struct {
|
||||
ConnectionSuccess bool `json:"connection_success"`
|
||||
}
|
||||
|
||||
// InitEnvironmentResp init environment response
|
||||
type InitEnvironmentResp struct {
|
||||
Success bool `json:"success"`
|
||||
CreateConfigFailed bool `json:"create_config_failed"`
|
||||
DefaultConfig string `json:"default_config"`
|
||||
ErrType string `json:"err_type"`
|
||||
}
|
||||
|
||||
// InitBaseInfoReq init base info request
|
||||
type InitBaseInfoReq struct {
|
||||
Language string `validate:"required,gt=0,lte=30" json:"lang"`
|
||||
SiteName string `validate:"required,gt=0,lte=30" json:"site_name"`
|
||||
SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"`
|
||||
ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"`
|
||||
AdminName string `validate:"required,gt=4,lte=30" json:"name"`
|
||||
AdminPassword string `validate:"required,gte=8,lte=32" json:"password"`
|
||||
AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"`
|
||||
}
|
||||
|
||||
func (r *InitBaseInfoReq) FormatSiteUrl() {
|
||||
parsedUrl, err := url.Parse(r.SiteURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r.SiteURL = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package install
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/answerdev/answer/ui"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
const UIStaticPath = "build/static"
|
||||
|
||||
type _resource struct {
|
||||
fs embed.FS
|
||||
}
|
||||
|
||||
// Open to implement the interface by http.FS required
|
||||
func (r *_resource) Open(name string) (fs.File, error) {
|
||||
name = fmt.Sprintf(UIStaticPath+"/%s", name)
|
||||
log.Debugf("open static path %s", name)
|
||||
return r.fs.Open(name)
|
||||
}
|
||||
|
||||
// NewInstallHTTPServer new install http server.
|
||||
func NewInstallHTTPServer() *gin.Engine {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
|
||||
r.StaticFS("/static", http.FS(&_resource{
|
||||
fs: ui.Build,
|
||||
}))
|
||||
|
||||
installApi := r.Group("")
|
||||
installApi.GET("/install", WebPage)
|
||||
installApi.GET("/50x", WebPage)
|
||||
installApi.GET("/installation/language/options", LangOptions)
|
||||
installApi.POST("/installation/db/check", CheckDatabase)
|
||||
installApi.POST("/installation/config-file/check", CheckConfigFile)
|
||||
installApi.POST("/installation/init", InitEnvironment)
|
||||
installApi.POST("/installation/base-info", InitBaseInfo)
|
||||
|
||||
r.NoRoute(func(ctx *gin.Context) {
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func WebPage(c *gin.Context) {
|
||||
filePath := ""
|
||||
var file []byte
|
||||
var err error
|
||||
filePath = "build/index.html"
|
||||
c.Header("content-type", "text/html;charset=utf-8")
|
||||
file, err = ui.Build.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(file))
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
|
@ -55,11 +57,6 @@ func InitDB(dataConf *data.Database) (err error) {
|
|||
return fmt.Errorf("init admin user failed: %s", err)
|
||||
}
|
||||
|
||||
err = initSiteInfo(engine)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init site info failed: %s", err)
|
||||
}
|
||||
|
||||
err = initConfigTable(engine)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init config table: %s", err)
|
||||
|
@ -82,12 +79,79 @@ func initAdminUser(engine *xorm.Engine) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func initSiteInfo(engine *xorm.Engine) error {
|
||||
func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail string) error {
|
||||
interfaceData := map[string]string{
|
||||
"logo": "",
|
||||
"theme": "black",
|
||||
"language": language,
|
||||
}
|
||||
interfaceDataBytes, _ := json.Marshal(interfaceData)
|
||||
_, err := engine.InsertOne(&entity.SiteInfo{
|
||||
Type: "interface",
|
||||
Content: `{"logo":"","theme":"black","language":"en_US"}`,
|
||||
Content: string(interfaceDataBytes),
|
||||
Status: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
generalData := map[string]string{
|
||||
"name": siteName,
|
||||
"site_url": siteURL,
|
||||
"contact_email": contactEmail,
|
||||
}
|
||||
generalDataBytes, _ := json.Marshal(generalData)
|
||||
_, err = engine.InsertOne(&entity.SiteInfo{
|
||||
Type: "general",
|
||||
Content: string(generalDataBytes),
|
||||
Status: 1,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func updateAdminInfo(engine *xorm.Engine, adminName, adminPassword, adminEmail string) error {
|
||||
generateFromPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("")
|
||||
}
|
||||
adminPassword = string(generateFromPassword)
|
||||
|
||||
// update admin info
|
||||
_, err = engine.ID("1").Update(&entity.User{
|
||||
Username: adminName,
|
||||
Pass: adminPassword,
|
||||
EMail: adminEmail,
|
||||
DisplayName: adminName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update admin user info failed: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateInstallInfo update some init data about the admin interface and admin password
|
||||
func UpdateInstallInfo(dataConf *data.Database, language string,
|
||||
siteName string,
|
||||
siteURL string,
|
||||
contactEmail string,
|
||||
adminName string,
|
||||
adminPassword string,
|
||||
adminEmail string) error {
|
||||
|
||||
engine, err := data.NewDB(false, dataConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database connection error: %s", err)
|
||||
}
|
||||
|
||||
err = updateAdminInfo(engine, adminName, adminPassword, adminEmail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update admin info failed: %s", err)
|
||||
}
|
||||
|
||||
err = initSiteInfo(engine, language, siteName, siteURL, contactEmail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init site info failed: %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -125,7 +189,7 @@ func initConfigTable(engine *xorm.Engine) error {
|
|||
{ID: 30, Key: "answer.vote_up", Value: `0`},
|
||||
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
|
||||
{ID: 32, Key: "question.follow", Value: `0`},
|
||||
{ID: 33, Key: "email.config", Value: `{"from_name":"answer","from_email":"answer@answer.com","smtp_host":"smtp.answer.org","smtp_port":465,"smtp_password":"answer","smtp_username":"answer@answer.com","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`},
|
||||
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`},
|
||||
{ID: 35, Key: "tag.follow", Value: `0`},
|
||||
{ID: 36, Key: "rank.question.add", Value: `0`},
|
||||
{ID: 37, Key: "rank.question.edit", Value: `0`},
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
|
@ -43,6 +42,8 @@ var noopMigration = func(_ *xorm.Engine) error { return nil }
|
|||
var migrations = []Migration{
|
||||
// 0->1
|
||||
NewMigration("this is first version, no operation", noopMigration),
|
||||
NewMigration("add user language", addUserLanguage),
|
||||
NewMigration("add recommend and reserved tag fields", addTagRecommendedAndReserved),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
@ -86,17 +87,17 @@ func Migrate(dataConf *data.Database) error {
|
|||
expectedVersion := ExpectedVersion()
|
||||
|
||||
for currentDBVersion < expectedVersion {
|
||||
log.Infof("[migrate] current db version is %d, try to migrate version %d, latest version is %d",
|
||||
fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n",
|
||||
currentDBVersion, currentDBVersion+1, expectedVersion)
|
||||
migrationFunc := migrations[currentDBVersion]
|
||||
log.Infof("[migrate] try to migrate db version %d, description: %s", currentDBVersion+1, migrationFunc.Description())
|
||||
fmt.Printf("[migrate] try to migrate db version %d, description: %s\n", currentDBVersion+1, migrationFunc.Description())
|
||||
if err := migrationFunc.Migrate(engine); err != nil {
|
||||
log.Errorf("[migrate] migrate to db version %d failed: ", currentDBVersion+1, err.Error())
|
||||
fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error())
|
||||
return err
|
||||
}
|
||||
log.Infof("[migrate] migrate to db version %d success", currentDBVersion+1)
|
||||
fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1)
|
||||
if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil {
|
||||
log.Errorf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
|
||||
fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
|
||||
return err
|
||||
}
|
||||
currentDBVersion++
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addUserLanguage(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
Language string `xorm:"not null default '' VARCHAR(100) language"`
|
||||
}
|
||||
return x.Sync(new(User))
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addTagRecommendedAndReserved(x *xorm.Engine) error {
|
||||
type Tag struct {
|
||||
Recommend bool `xorm:"not null default false BOOL recommend"`
|
||||
Reserved bool `xorm:"not null default false BOOL reserved"`
|
||||
}
|
||||
return x.Sync(new(Tag))
|
||||
}
|
|
@ -4,8 +4,10 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// VoteRepo activity repository
|
||||
|
@ -39,3 +41,12 @@ func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string)
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (vr *VoteRepo) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) {
|
||||
list := make([]*entity.Activity, 0)
|
||||
count, err = vr.data.DB.Where("cancelled =0").In("activity_type", activityTypes).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
|
@ -102,6 +103,16 @@ func (ar *answerRepo) GetAnswer(ctx context.Context, id string) (
|
|||
return
|
||||
}
|
||||
|
||||
// GetQuestionCount
|
||||
func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) {
|
||||
list := make([]*entity.Answer, 0)
|
||||
count, err = ar.data.DB.Where("status = ?", entity.AnswerStatusAvailable).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAnswerList get answer list all
|
||||
func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) {
|
||||
answerList = make([]*entity.Answer, 0)
|
||||
|
@ -193,7 +204,7 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
|
|||
case entity.AnswerSearchOrderByVote:
|
||||
session = session.OrderBy("vote_count desc")
|
||||
default:
|
||||
session = session.OrderBy("adopted desc,vote_count desc")
|
||||
session = session.OrderBy("adopted desc,vote_count desc,created_at asc")
|
||||
}
|
||||
session = session.And("status = ?", entity.AnswerStatusAvailable)
|
||||
|
||||
|
|
|
@ -79,6 +79,15 @@ func (cr *commentRepo) GetComment(ctx context.Context, commentID string) (
|
|||
return
|
||||
}
|
||||
|
||||
func (cr *commentRepo) GetCommentCount(ctx context.Context) (count int64, err error) {
|
||||
list := make([]*entity.Comment, 0)
|
||||
count, err = cr.data.DB.Where("status = ?", entity.CommentStatusAvailable).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetCommentPage get comment page
|
||||
func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) (
|
||||
commentList []*entity.Comment, total int64, err error,
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/repo/search_common"
|
||||
"github.com/answerdev/answer/internal/repo/site_info"
|
||||
"github.com/answerdev/answer/internal/repo/tag"
|
||||
"github.com/answerdev/answer/internal/repo/tag_common"
|
||||
"github.com/answerdev/answer/internal/repo/unique"
|
||||
"github.com/answerdev/answer/internal/repo/user"
|
||||
"github.com/google/wire"
|
||||
|
@ -53,6 +54,7 @@ var ProviderSetRepo = wire.NewSet(
|
|||
activity.NewQuestionActivityRepo,
|
||||
activity.NewUserActiveActivityRepo,
|
||||
tag.NewTagRepo,
|
||||
tag_common.NewTagCommonRepo,
|
||||
tag.NewTagRelRepo,
|
||||
collection.NewCollectionRepo,
|
||||
collection.NewCollectionGroupRepo,
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
|
@ -162,6 +163,16 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu
|
|||
return
|
||||
}
|
||||
|
||||
func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) {
|
||||
questionList := make([]*entity.Question, 0)
|
||||
|
||||
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}).FindAndCount(&questionList)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetQuestionPage get question page
|
||||
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) {
|
||||
questionList = make([]*entity.Question, 0)
|
||||
|
|
|
@ -81,7 +81,7 @@ func Test_notificationRepo_GetNotificationPage(t *testing.T) {
|
|||
err := notificationRepo.AddNotification(context.TODO(), ent)
|
||||
assert.NoError(t, err)
|
||||
|
||||
notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: userID})
|
||||
notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: ent.UserID})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, total > 0)
|
||||
assert.Equal(t, notificationPage[0].UserID, ent.UserID)
|
||||
|
|
|
@ -52,7 +52,8 @@ var (
|
|||
func TestMain(t *testing.M) {
|
||||
dbSetting, ok := dbSettingMapping[os.Getenv("TEST_DB_DRIVER")]
|
||||
if !ok {
|
||||
dbSetting = dbSettingMapping[string(schemas.MYSQL)]
|
||||
// Use sqlite3 to test.
|
||||
dbSetting = dbSettingMapping[string(schemas.SQLITE)]
|
||||
}
|
||||
defer func() {
|
||||
if tearDown != nil {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/repo/tag"
|
||||
"github.com/answerdev/answer/internal/repo/tag_common"
|
||||
"github.com/answerdev/answer/internal/repo/unique"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -42,8 +43,8 @@ var (
|
|||
|
||||
func addTagList() {
|
||||
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
|
||||
tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)
|
||||
err := tagRepo.AddTagList(context.TODO(), testTagList)
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo)
|
||||
err := tagCommonRepo.AddTagList(context.TODO(), testTagList)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -51,9 +52,9 @@ func addTagList() {
|
|||
|
||||
func Test_tagRepo_GetTagByID(t *testing.T) {
|
||||
tagOnce.Do(addTagList)
|
||||
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID)
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
|
||||
|
@ -61,9 +62,9 @@ func Test_tagRepo_GetTagByID(t *testing.T) {
|
|||
|
||||
func Test_tagRepo_GetTagBySlugName(t *testing.T) {
|
||||
tagOnce.Do(addTagList)
|
||||
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTag, exist, err := tagRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName)
|
||||
gotTag, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
|
||||
|
@ -80,36 +81,36 @@ func Test_tagRepo_GetTagList(t *testing.T) {
|
|||
|
||||
func Test_tagRepo_GetTagListByIDs(t *testing.T) {
|
||||
tagOnce.Do(addTagList)
|
||||
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTags, err := tagRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID})
|
||||
gotTags, err := tagCommonRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
|
||||
}
|
||||
|
||||
func Test_tagRepo_GetTagListByName(t *testing.T) {
|
||||
tagOnce.Do(addTagList)
|
||||
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTags, err := tagRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, 1)
|
||||
gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, 1, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
|
||||
}
|
||||
|
||||
func Test_tagRepo_GetTagListByNames(t *testing.T) {
|
||||
tagOnce.Do(addTagList)
|
||||
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTags, err := tagRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName})
|
||||
gotTags, err := tagCommonRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
|
||||
}
|
||||
|
||||
func Test_tagRepo_GetTagPage(t *testing.T) {
|
||||
tagOnce.Do(addTagList)
|
||||
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTags, _, err := tagRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "")
|
||||
gotTags, _, err := tagCommonRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
|
||||
}
|
||||
|
@ -121,7 +122,9 @@ func Test_tagRepo_RemoveTag(t *testing.T) {
|
|||
err := tagRepo.RemoveTag(context.TODO(), testTagList[1].ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, exist, err := tagRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName)
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
_, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exist)
|
||||
}
|
||||
|
@ -134,21 +137,22 @@ func Test_tagRepo_UpdateTag(t *testing.T) {
|
|||
err := tagRepo.UpdateTag(context.TODO(), testTagList[0])
|
||||
assert.NoError(t, err)
|
||||
|
||||
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID)
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, testTagList[0].DisplayName, gotTag.DisplayName)
|
||||
}
|
||||
|
||||
func Test_tagRepo_UpdateTagQuestionCount(t *testing.T) {
|
||||
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
|
||||
tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
testTagList[0].DisplayName = "golang"
|
||||
err := tagRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)
|
||||
err := tagCommonRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)
|
||||
assert.NoError(t, err)
|
||||
|
||||
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID)
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, 100, gotTag.QuestionCount)
|
||||
|
@ -166,7 +170,9 @@ func Test_tagRepo_UpdateTagSynonym(t *testing.T) {
|
|||
converter.StringToInt64(testTagList[0].ID), testTagList[0].SlugName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[2].ID)
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, testTagList[0].ID, fmt.Sprintf("%d", gotTag.MainTagID))
|
||||
|
|
|
@ -94,3 +94,12 @@ func (ar *reportRepo) UpdateByID(
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (vr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) {
|
||||
list := make([]*entity.Report, 0)
|
||||
count, err = vr.data.DB.Where("status =?", entity.ReportStatusPending).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
|
@ -25,7 +27,7 @@ var (
|
|||
"`question`.`id`",
|
||||
"`question`.`id` as `question_id`",
|
||||
"`title`",
|
||||
"`original_text`",
|
||||
"`parsed_text`",
|
||||
"`question`.`created_at`",
|
||||
"`user_id`",
|
||||
"`vote_count`",
|
||||
|
@ -38,7 +40,7 @@ var (
|
|||
"`answer`.`id` as `id`",
|
||||
"`question_id`",
|
||||
"`question`.`title` as `title`",
|
||||
"`answer`.`original_text` as `original_text`",
|
||||
"`answer`.`parsed_text` as `parsed_text`",
|
||||
"`answer`.`created_at`",
|
||||
"`answer`.`user_id` as `user_id`",
|
||||
"`answer`.`vote_count` as `vote_count`",
|
||||
|
@ -66,7 +68,7 @@ func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon
|
|||
}
|
||||
|
||||
// SearchContents search question and answer data
|
||||
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
|
||||
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
|
||||
if words = filterWords(words); len(words) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -115,10 +117,12 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
|
|||
}
|
||||
|
||||
// check tag
|
||||
if tagID != "" {
|
||||
if len(tagIDs) > 0 {
|
||||
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
|
||||
Where(builder.Eq{"tag_rel.tag_id": tagID})
|
||||
argsQ = append(argsQ, tagID)
|
||||
Where(builder.In("tag_rel.tag_id", tagIDs))
|
||||
for _, tagID := range tagIDs {
|
||||
argsQ = append(argsQ, tagID)
|
||||
}
|
||||
}
|
||||
|
||||
// check user
|
||||
|
@ -142,13 +146,22 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
|
|||
argsA = append(argsA, votes)
|
||||
}
|
||||
|
||||
b = b.Union("all", ub)
|
||||
|
||||
querySQL, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
|
||||
//b = b.Union("all", ub)
|
||||
ubSQL, _, err := ub.ToSQL()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
|
||||
bSQL, _, err := b.ToSQL()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sql := fmt.Sprintf("(%s UNION ALL %s)", ubSQL, bSQL)
|
||||
|
||||
querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -183,7 +196,7 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
|
|||
}
|
||||
|
||||
// SearchQuestions search question data
|
||||
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
|
||||
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, notAccepted bool, views, answers int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
|
||||
if words = filterWords(words); len(words) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -213,11 +226,26 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
|
|||
}
|
||||
|
||||
// check need filter has not accepted
|
||||
if limitNoAccepted {
|
||||
if notAccepted {
|
||||
b.And(builder.Eq{"accepted_answer_id": 0})
|
||||
args = append(args, 0)
|
||||
}
|
||||
|
||||
// check views
|
||||
if views > -1 {
|
||||
b.And(builder.Gte{"view_count": views})
|
||||
args = append(args, views)
|
||||
}
|
||||
|
||||
// check answers
|
||||
if answers == 0 {
|
||||
b.And(builder.Eq{"answer_count": answers})
|
||||
args = append(args, answers)
|
||||
} else if answers > 0 {
|
||||
b.And(builder.Gte{"answer_count": answers})
|
||||
args = append(args, answers)
|
||||
}
|
||||
|
||||
if answers == 0 {
|
||||
b.And(builder.Eq{"answer_count": 0})
|
||||
args = append(args, 0)
|
||||
|
@ -264,7 +292,7 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
|
|||
}
|
||||
|
||||
// SearchAnswers search answer data
|
||||
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
|
||||
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
|
||||
if words = filterWords(words); len(words) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -293,11 +321,23 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAc
|
|||
}
|
||||
}
|
||||
|
||||
if limitAccepted {
|
||||
// check tags
|
||||
// check tag
|
||||
if len(tagIDs) > 0 {
|
||||
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
|
||||
Where(builder.In("tag_rel.tag_id", tagIDs))
|
||||
for _, tagID := range tagIDs {
|
||||
args = append(args, tagID)
|
||||
}
|
||||
}
|
||||
|
||||
// check limit accepted
|
||||
if accepted {
|
||||
b.Where(builder.Eq{"adopted": schema.AnswerAdoptedEnable})
|
||||
args = append(args, schema.AnswerAdoptedEnable)
|
||||
}
|
||||
|
||||
// check question id
|
||||
if questionID != "" {
|
||||
b.Where(builder.Eq{"question_id": questionID})
|
||||
args = append(args, questionID)
|
||||
|
@ -380,11 +420,12 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
|
|||
|
||||
// get tags
|
||||
err = sr.data.DB.
|
||||
Select("`display_name`,`slug_name`,`main_tag_slug_name`").
|
||||
Select("`display_name`,`slug_name`,`main_tag_slug_name`,`recommend`,`reserved`").
|
||||
Table("tag").
|
||||
Join("INNER", "tag_rel", "tag.id = tag_rel.tag_id").
|
||||
Where(builder.Eq{"tag_rel.object_id": r["question_id"]}).
|
||||
And(builder.Eq{"tag_rel.status": entity.TagRelStatusAvailable}).
|
||||
UseBool("recommend", "reserved").
|
||||
Find(&tagsEntity)
|
||||
|
||||
if err != nil {
|
||||
|
@ -411,8 +452,9 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
|
|||
|
||||
object = schema.SearchObject{
|
||||
ID: string(r["id"]),
|
||||
QuestionID: string(r["question_id"]),
|
||||
Title: string(r["title"]),
|
||||
Excerpt: cutOutParsedText(string(r["original_text"])),
|
||||
Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240),
|
||||
CreatedAtParsed: tp.Unix(),
|
||||
UserInfo: userInfo,
|
||||
Tags: tags,
|
||||
|
@ -443,15 +485,6 @@ func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.Us
|
|||
}
|
||||
}
|
||||
|
||||
func cutOutParsedText(parsedText string) string {
|
||||
parsedText = strings.TrimSpace(parsedText)
|
||||
idx := strings.Index(parsedText, "\n")
|
||||
if idx >= 0 {
|
||||
parsedText = parsedText[0:idx]
|
||||
}
|
||||
return parsedText
|
||||
}
|
||||
|
||||
func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) {
|
||||
relevanceRes := []string{}
|
||||
args = []interface{}{}
|
||||
|
|
|
@ -4,10 +4,9 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/answerdev/answer/internal/service/tag"
|
||||
"github.com/answerdev/answer/internal/service/unique"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"xorm.io/builder"
|
||||
|
@ -23,78 +22,13 @@ type tagRepo struct {
|
|||
func NewTagRepo(
|
||||
data *data.Data,
|
||||
uniqueIDRepo unique.UniqueIDRepo,
|
||||
) tagcommon.TagRepo {
|
||||
) tag.TagRepo {
|
||||
return &tagRepo{
|
||||
data: data,
|
||||
uniqueIDRepo: uniqueIDRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// AddTagList add tag
|
||||
func (tr *tagRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
|
||||
for _, item := range tagList {
|
||||
item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.RevisionID = "0"
|
||||
}
|
||||
_, err = tr.data.DB.Insert(tagList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagListByIDs get tag list all
|
||||
func (tr *tagRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
session := tr.data.DB.In("id", ids)
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
err = session.Find(&tagList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagBySlugName get tag by slug name
|
||||
func (tr *tagRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
|
||||
tagInfo = &entity.Tag{}
|
||||
session := tr.data.DB.Where("slug_name = ?", slugName)
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
exist, err = session.Get(tagInfo)
|
||||
if err != nil {
|
||||
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagListByName get tag list all like name
|
||||
func (tr *tagRepo) GetTagListByName(ctx context.Context, name string, limit int) (tagList []*entity.Tag, err error) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
session := tr.data.DB.Where("slug_name LIKE ?", name+"%")
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
session.Limit(limit).Asc("slug_name")
|
||||
err = session.Find(&tagList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagListByNames get tag list all like name
|
||||
func (tr *tagRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
session := tr.data.DB.In("slug_name", names)
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
err = session.Find(&tagList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveTag delete tag
|
||||
func (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) {
|
||||
session := tr.data.DB.Where(builder.Eq{"id": tagID})
|
||||
|
@ -114,16 +48,6 @@ func (tr *tagRepo) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// UpdateTagQuestionCount update tag question count
|
||||
func (tr *tagRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) {
|
||||
cond := &entity.Tag{QuestionCount: questionCount}
|
||||
_, err = tr.data.DB.Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateTagSynonym update synonym tag
|
||||
func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64,
|
||||
mainTagSlugName string,
|
||||
|
@ -137,20 +61,6 @@ func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []strin
|
|||
return
|
||||
}
|
||||
|
||||
// GetTagByID get tag one
|
||||
func (tr *tagRepo) GetTagByID(ctx context.Context, tagID string) (
|
||||
tag *entity.Tag, exist bool, err error,
|
||||
) {
|
||||
tag = &entity.Tag{}
|
||||
session := tr.data.DB.Where(builder.Eq{"id": tagID})
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
exist, err = session.Get(tag)
|
||||
if err != nil {
|
||||
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagList get tag list all
|
||||
func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
|
@ -161,33 +71,3 @@ func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagPage get tag page
|
||||
func (tr *tagRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (
|
||||
tagList []*entity.Tag, total int64, err error,
|
||||
) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
session := tr.data.DB.NewSession()
|
||||
|
||||
if len(tag.SlugName) > 0 {
|
||||
session.Where(builder.Or(builder.Like{"slug_name", tag.SlugName}, builder.Like{"display_name", tag.SlugName}))
|
||||
tag.SlugName = ""
|
||||
}
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
session.Where("main_tag_id = 0") // if this tag is synonym, exclude it
|
||||
|
||||
switch queryCond {
|
||||
case "popular":
|
||||
session.Desc("question_count")
|
||||
case "name":
|
||||
session.Asc("slug_name")
|
||||
case "newest":
|
||||
session.Desc("created_at")
|
||||
}
|
||||
|
||||
total, err = pager.Help(page, pageSize, &tagList, tag, session)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
package tag_common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/answerdev/answer/internal/service/unique"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// tagCommonRepo tag repository
|
||||
type tagCommonRepo struct {
|
||||
data *data.Data
|
||||
uniqueIDRepo unique.UniqueIDRepo
|
||||
}
|
||||
|
||||
// NewTagCommonRepo new repository
|
||||
func NewTagCommonRepo(
|
||||
data *data.Data,
|
||||
uniqueIDRepo unique.UniqueIDRepo,
|
||||
) tagcommon.TagCommonRepo {
|
||||
return &tagCommonRepo{
|
||||
data: data,
|
||||
uniqueIDRepo: uniqueIDRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTagListByIDs get tag list all
|
||||
func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
session := tr.data.DB.In("id", ids)
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagBySlugName get tag by slug name
|
||||
func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
|
||||
tagInfo = &entity.Tag{}
|
||||
session := tr.data.DB.Where("slug_name = ?", slugName)
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
exist, err = session.Get(tagInfo)
|
||||
if err != nil {
|
||||
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagListByName get tag list all like name
|
||||
func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
cond := &entity.Tag{}
|
||||
session := tr.data.DB.Where("")
|
||||
if name != "" {
|
||||
session.Where("slug_name LIKE ?", name+"%")
|
||||
} else {
|
||||
cond.Recommend = true
|
||||
}
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
session.Limit(limit).Asc("slug_name")
|
||||
if !hasReserved {
|
||||
cond.Reserved = false
|
||||
session.UseBool("recommend", "reserved")
|
||||
} else {
|
||||
session.UseBool("recommend")
|
||||
}
|
||||
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList, cond)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (tr *tagCommonRepo) GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
cond := &entity.Tag{}
|
||||
session := tr.data.DB.Where("")
|
||||
cond.Recommend = true
|
||||
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
session.Asc("slug_name")
|
||||
session.UseBool("recommend")
|
||||
err = session.Find(&tagList, cond)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
cond := &entity.Tag{}
|
||||
session := tr.data.DB.Where("")
|
||||
cond.Reserved = true
|
||||
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
session.Asc("slug_name")
|
||||
session.UseBool("reserved")
|
||||
err = session.Find(&tagList, cond)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagListByNames get tag list all like name
|
||||
func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
session := tr.data.DB.In("slug_name", names).UseBool("recommend", "reserved")
|
||||
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagByID get tag one
|
||||
func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string) (
|
||||
tag *entity.Tag, exist bool, err error,
|
||||
) {
|
||||
tag = &entity.Tag{}
|
||||
session := tr.data.DB.Where(builder.Eq{"id": tagID})
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
exist, err = session.Get(tag)
|
||||
if err != nil {
|
||||
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetTagPage get tag page
|
||||
func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (
|
||||
tagList []*entity.Tag, total int64, err error,
|
||||
) {
|
||||
tagList = make([]*entity.Tag, 0)
|
||||
session := tr.data.DB.NewSession()
|
||||
|
||||
if len(tag.SlugName) > 0 {
|
||||
session.Where(builder.Or(builder.Like{"slug_name", tag.SlugName}, builder.Like{"display_name", tag.SlugName}))
|
||||
tag.SlugName = ""
|
||||
}
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
session.Where("main_tag_id = 0") // if this tag is synonym, exclude it
|
||||
|
||||
switch queryCond {
|
||||
case "popular":
|
||||
session.Desc("question_count")
|
||||
case "name":
|
||||
session.Asc("slug_name")
|
||||
case "newest":
|
||||
session.Desc("created_at")
|
||||
}
|
||||
|
||||
total, err = pager.Help(page, pageSize, &tagList, tag, session)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AddTagList add tag
|
||||
func (tr *tagCommonRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
|
||||
for _, item := range tagList {
|
||||
item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.RevisionID = "0"
|
||||
}
|
||||
_, err = tr.data.DB.Insert(tagList)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateTagQuestionCount update tag question count
|
||||
func (tr *tagCommonRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) {
|
||||
cond := &entity.Tag{QuestionCount: questionCount}
|
||||
_, err = tr.data.DB.Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (tr *tagCommonRepo) UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) {
|
||||
bean := &entity.Tag{}
|
||||
switch attribute {
|
||||
case "recommend":
|
||||
bean.Recommend = value
|
||||
case "reserved":
|
||||
bean.Reserved = value
|
||||
default:
|
||||
return
|
||||
}
|
||||
session := tr.data.DB.In("slug_name", tags).Cols(attribute).UseBool(attribute)
|
||||
_, err = session.Update(bean)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
|
|
|
@ -101,6 +101,14 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err
|
|||
return
|
||||
}
|
||||
|
||||
func (ur *userRepo) UpdateLanguage(ctx context.Context, userID, language string) (err error) {
|
||||
_, err = ur.data.DB.Where("id = ?", userID).Update(&entity.User{Language: language})
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateInfo update user info
|
||||
func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {
|
||||
_, err = ur.data.DB.Where("id = ?", userInfo.ID).
|
||||
|
@ -149,3 +157,12 @@ func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *ent
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (vr *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
|
||||
list := make([]*entity.User, 0)
|
||||
count, err = vr.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ type AnswerAPIRouter struct {
|
|||
siteInfoController *controller_backyard.SiteInfoController
|
||||
siteinfoController *controller.SiteinfoController
|
||||
notificationController *controller.NotificationController
|
||||
dashboardController *controller.DashboardController
|
||||
uploadController *controller.UploadController
|
||||
}
|
||||
|
||||
func NewAnswerAPIRouter(
|
||||
|
@ -50,7 +52,8 @@ func NewAnswerAPIRouter(
|
|||
siteInfoController *controller_backyard.SiteInfoController,
|
||||
siteinfoController *controller.SiteinfoController,
|
||||
notificationController *controller.NotificationController,
|
||||
|
||||
dashboardController *controller.DashboardController,
|
||||
uploadController *controller.UploadController,
|
||||
) *AnswerAPIRouter {
|
||||
return &AnswerAPIRouter{
|
||||
langController: langController,
|
||||
|
@ -73,13 +76,15 @@ func NewAnswerAPIRouter(
|
|||
siteInfoController: siteInfoController,
|
||||
notificationController: notificationController,
|
||||
siteinfoController: siteinfoController,
|
||||
dashboardController: dashboardController,
|
||||
uploadController: uploadController,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
// i18n
|
||||
r.GET("/language/config", a.langController.GetLangMapping)
|
||||
r.GET("/language/options", a.langController.GetLangOptions)
|
||||
r.GET("/language/options", a.langController.GetUserLangOptions)
|
||||
|
||||
// comment
|
||||
r.GET("/comment/page", a.commentController.GetCommentWithPage)
|
||||
|
@ -88,7 +93,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
// user
|
||||
r.GET("/user/info", a.userController.GetUserInfoByUserID)
|
||||
r.GET("/user/status", a.userController.GetUserStatus)
|
||||
r.GET("/user/action/record", a.userController.ActionRecord)
|
||||
r.POST("/user/login/email", a.userController.UserEmailLogin)
|
||||
r.POST("/user/register/email", a.userController.UserRegisterByEmail)
|
||||
|
@ -131,7 +135,8 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage)
|
||||
|
||||
//siteinfo
|
||||
r.GET("/siteinfo", a.siteinfoController.GetInfo)
|
||||
r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
|
||||
r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
|
||||
}
|
||||
|
||||
func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
|
@ -177,8 +182,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
// user
|
||||
r.PUT("/user/password", a.userController.UserModifyPassWord)
|
||||
r.PUT("/user/info", a.userController.UserUpdateInfo)
|
||||
r.POST("/user/avatar/upload", a.userController.UploadUserAvatar)
|
||||
r.POST("/user/post/file", a.userController.UploadUserPostFile)
|
||||
r.PUT("/user/interface", a.userController.UserUpdateInterface)
|
||||
r.POST("/user/notice/set", a.userController.UserNoticeSet)
|
||||
|
||||
// vote
|
||||
|
@ -193,6 +197,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
r.GET("/notification/page", a.notificationController.GetList)
|
||||
r.PUT("/notification/read/state/all", a.notificationController.ClearUnRead)
|
||||
r.PUT("/notification/read/state", a.notificationController.ClearIDUnRead)
|
||||
|
||||
// upload file
|
||||
r.POST("/file", a.uploadController.UploadFile)
|
||||
}
|
||||
|
||||
func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
|
||||
|
@ -213,7 +220,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
|
|||
r.GET("/reasons", a.reasonController.Reasons)
|
||||
|
||||
// language
|
||||
r.GET("/language/options", a.langController.GetLangOptions)
|
||||
r.GET("/language/options", a.langController.GetAdminLangOptions)
|
||||
|
||||
// theme
|
||||
r.GET("/theme/options", a.themeController.GetThemeOptions)
|
||||
|
@ -221,8 +228,17 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
|
|||
// siteinfo
|
||||
r.GET("/siteinfo/general", a.siteInfoController.GetGeneral)
|
||||
r.GET("/siteinfo/interface", a.siteInfoController.GetInterface)
|
||||
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
|
||||
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
|
||||
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
|
||||
r.PUT("/siteinfo/general", a.siteInfoController.UpdateGeneral)
|
||||
r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
|
||||
r.PUT("/siteinfo/branding", a.siteInfoController.UpdateBranding)
|
||||
r.PUT("/siteinfo/write", a.siteInfoController.UpdateSiteWrite)
|
||||
r.PUT("/siteinfo/legal", a.siteInfoController.UpdateSiteLegal)
|
||||
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
|
||||
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
|
||||
|
||||
//dashboard
|
||||
r.GET("/dashboard", a.dashboardController.DashboardInfo)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package router
|
|||
|
||||
// SwaggerConfig struct describes configure for the Swagger API endpoint
|
||||
type SwaggerConfig struct {
|
||||
Show bool `json:"show"`
|
||||
Protocol string `json:"protocol"`
|
||||
Host string `json:"host"`
|
||||
Address string `json:"address"`
|
||||
Show bool `json:"show" mapstructure:"show" yaml:"show"`
|
||||
Protocol string `json:"protocol" mapstructure:"protocol" yaml:"protocol"`
|
||||
Host string `json:"host" mapstructure:"host" yaml:"host"`
|
||||
Address string `json:"address" mapstructure:"address" yaml:"address"`
|
||||
}
|
||||
|
|
|
@ -68,21 +68,23 @@ func (a *UIRouter) Register(r *gin.Engine) {
|
|||
|
||||
// specify the not router for default routes and redirect
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
name := c.Request.URL.Path
|
||||
urlPath := c.Request.URL.Path
|
||||
filePath := ""
|
||||
var file []byte
|
||||
var err error
|
||||
switch name {
|
||||
switch urlPath {
|
||||
case "/favicon.ico":
|
||||
c.Header("content-type", "image/vnd.microsoft.icon")
|
||||
filePath = UIRootFilePath + name
|
||||
filePath = UIRootFilePath + urlPath
|
||||
case "/manifest.json":
|
||||
filePath = UIRootFilePath + name
|
||||
filePath = UIRootFilePath + urlPath
|
||||
case "/install":
|
||||
// if answer is running by run command user can not access install page.
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
default:
|
||||
filePath = UIIndexFilePath
|
||||
c.Header("content-type", "text/html;charset=utf-8")
|
||||
}
|
||||
file, err = ui.Build.ReadFile(filePath)
|
||||
file, err := ui.Build.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
c.Status(http.StatusNotFound)
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package schema
|
||||
|
||||
import "time"
|
||||
|
||||
var AppStartTime time.Time
|
||||
|
||||
const (
|
||||
DashBoardCachekey = "answer@dashboard"
|
||||
DashBoardCacheTime = 60 * time.Minute
|
||||
)
|
||||
|
||||
type DashboardInfo struct {
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
AnswerCount int64 `json:"answer_count"`
|
||||
CommentCount int64 `json:"comment_count"`
|
||||
VoteCount int64 `json:"vote_count"`
|
||||
UserCount int64 `json:"user_count"`
|
||||
ReportCount int64 `json:"report_count"`
|
||||
UploadingFiles bool `json:"uploading_files"`
|
||||
SMTP bool `json:"smtp"`
|
||||
HTTPS bool `json:"https"`
|
||||
TimeZone string `json:"time_zone"`
|
||||
OccupyingStorageSpace string `json:"occupying_storage_space"`
|
||||
AppStartTime string `json:"app_start_time"`
|
||||
VersionInfo DashboardInfoVersion `json:"version_info"`
|
||||
}
|
||||
|
||||
type DashboardInfoVersion struct {
|
||||
Version string `json:"version"`
|
||||
RemoteVersion string `json:"remote_version"`
|
||||
}
|
||||
|
||||
type RemoteVersion struct {
|
||||
Release struct {
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
} `json:"release"`
|
||||
}
|
|
@ -7,3 +7,5 @@ type ErrTypeData struct {
|
|||
var ErrTypeModal = ErrTypeData{ErrType: "modal"}
|
||||
|
||||
var ErrTypeToast = ErrTypeData{ErrType: "toast"}
|
||||
|
||||
var ErrTypeAlert = ErrTypeData{ErrType: "alert"}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
package schema
|
||||
|
||||
// GetLangOption get label option
|
||||
type GetLangOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
var GetLangOptions = []*GetLangOption{
|
||||
{
|
||||
Label: "English(US)",
|
||||
Value: "en_US",
|
||||
},
|
||||
{
|
||||
Label: "中文(CN)",
|
||||
Value: "zh_CN",
|
||||
},
|
||||
}
|
|
@ -10,6 +10,7 @@ type SearchDTO struct {
|
|||
|
||||
type SearchObject struct {
|
||||
ID string `json:"id"`
|
||||
QuestionID string `json:"question_id"`
|
||||
Title string `json:"title"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
CreatedAtParsed int64 `json:"created_at"`
|
||||
|
@ -29,6 +30,8 @@ type TagResp struct {
|
|||
DisplayName string `json:"display_name"`
|
||||
// if main tag slug name is not empty, this tag is synonymous with the main tag
|
||||
MainTagSlugName string `json:"main_tag_slug_name"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Reserved bool `json:"reserved"`
|
||||
}
|
||||
|
||||
type SearchResp struct {
|
||||
|
|
|
@ -1,17 +1,77 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// SiteGeneralReq site general request
|
||||
type SiteGeneralReq struct {
|
||||
Name string `validate:"required,gt=1,lte=128" comment:"site name" form:"name" json:"name"`
|
||||
ShortDescription string `validate:"required,gt=3,lte=255" comment:"short site description" form:"short_description" json:"short_description"`
|
||||
Description string `validate:"required,gt=3,lte=2000" comment:"site description" form:"description" json:"description"`
|
||||
Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"`
|
||||
ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"`
|
||||
Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"`
|
||||
SiteUrl string `validate:"required,gt=1,lte=512,url" form:"site_url" json:"site_url"`
|
||||
ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"`
|
||||
}
|
||||
|
||||
func (r *SiteGeneralReq) FormatSiteUrl() {
|
||||
parsedUrl, err := url.Parse(r.SiteUrl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
r.SiteUrl = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host)
|
||||
}
|
||||
|
||||
// SiteInterfaceReq site interface request
|
||||
type SiteInterfaceReq struct {
|
||||
Logo string `validate:"omitempty,gt=0,lte=256" comment:"logo" form:"logo" json:"logo"`
|
||||
Theme string `validate:"required,gt=1,lte=128" comment:"theme" form:"theme" json:"theme"`
|
||||
Language string `validate:"required,gt=1,lte=128" comment:"interface language" form:"language" json:"language"`
|
||||
Theme string `validate:"required,gt=1,lte=128" form:"theme" json:"theme"`
|
||||
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
|
||||
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
|
||||
}
|
||||
|
||||
// SiteBrandingReq site branding request
|
||||
type SiteBrandingReq struct {
|
||||
Logo string `validate:"required,gt=0,lte=512" form:"logo" json:"logo"`
|
||||
MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"`
|
||||
SquareIcon string `validate:"required,gt=0,lte=512" form:"square_icon" json:"square_icon"`
|
||||
Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"`
|
||||
}
|
||||
|
||||
// SiteWriteReq site write request
|
||||
type SiteWriteReq struct {
|
||||
RequiredTag bool `validate:"omitempty" form:"required_tag" json:"required_tag"`
|
||||
RecommendTags []string `validate:"omitempty" form:"recommend_tags" json:"recommend_tags"`
|
||||
ReservedTags []string `validate:"omitempty" form:"reserved_tags" json:"reserved_tags"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
// SiteLegalReq site branding request
|
||||
type SiteLegalReq struct {
|
||||
TermsOfServiceOriginalText string `json:"terms_of_service_original_text"`
|
||||
TermsOfServiceParsedText string `json:"terms_of_service_parsed_text"`
|
||||
PrivacyPolicyOriginalText string `json:"privacy_policy_original_text"`
|
||||
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text"`
|
||||
}
|
||||
|
||||
// GetSiteLegalInfoReq site site legal request
|
||||
type GetSiteLegalInfoReq struct {
|
||||
InfoType string `validate:"required,oneof=tos privacy" form:"info_type"`
|
||||
}
|
||||
|
||||
func (r *GetSiteLegalInfoReq) IsTOS() bool {
|
||||
return r.InfoType == "tos"
|
||||
}
|
||||
|
||||
func (r *GetSiteLegalInfoReq) IsPrivacy() bool {
|
||||
return r.InfoType == "privacy"
|
||||
}
|
||||
|
||||
// GetSiteLegalInfoResp get site legal info response
|
||||
type GetSiteLegalInfoResp struct {
|
||||
TermsOfServiceOriginalText string `json:"terms_of_service_original_text,omitempty"`
|
||||
TermsOfServiceParsedText string `json:"terms_of_service_parsed_text,omitempty"`
|
||||
PrivacyPolicyOriginalText string `json:"privacy_policy_original_text,omitempty"`
|
||||
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"`
|
||||
}
|
||||
|
||||
// SiteGeneralResp site general response
|
||||
|
@ -20,9 +80,20 @@ type SiteGeneralResp SiteGeneralReq
|
|||
// SiteInterfaceResp site interface response
|
||||
type SiteInterfaceResp SiteInterfaceReq
|
||||
|
||||
// SiteBrandingResp site branding response
|
||||
type SiteBrandingResp SiteBrandingReq
|
||||
|
||||
// SiteWriteResp site write response
|
||||
type SiteWriteResp SiteWriteReq
|
||||
|
||||
// SiteLegalResp site write response
|
||||
type SiteLegalResp SiteLegalReq
|
||||
|
||||
// SiteInfoResp get site info response
|
||||
type SiteInfoResp struct {
|
||||
General *SiteGeneralResp `json:"general"`
|
||||
Face *SiteInterfaceResp `json:"interface"`
|
||||
General *SiteGeneralResp `json:"general"`
|
||||
Interface *SiteInterfaceResp `json:"interface"`
|
||||
Branding *SiteBrandingResp `json:"branding"`
|
||||
}
|
||||
|
||||
// UpdateSMTPConfigReq get smtp config request
|
||||
|
|
|
@ -11,7 +11,8 @@ import (
|
|||
// SearchTagLikeReq get tag list all request
|
||||
type SearchTagLikeReq struct {
|
||||
// tag
|
||||
Tag string `validate:"required,gt=0,lte=35" form:"tag"`
|
||||
Tag string `validate:"omitempty" form:"tag"`
|
||||
IsAdmin bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetTagInfoReq get tag info request
|
||||
|
@ -24,7 +25,7 @@ type GetTagInfoReq struct {
|
|||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
func (r *GetTagInfoReq) Check() (errField *validator.ErrorField, err error) {
|
||||
func (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(r.ID) == 0 && len(r.Name) == 0 {
|
||||
return nil, errors.BadRequest(reason.RequestFormatError)
|
||||
}
|
||||
|
@ -60,6 +61,8 @@ type GetTagResp struct {
|
|||
MemberActions []*PermissionMemberAction `json:"member_actions"`
|
||||
// if main tag slug name is not empty, this tag is synonymous with the main tag
|
||||
MainTagSlugName string `json:"main_tag_slug_name"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Reserved bool `json:"reserved"`
|
||||
}
|
||||
|
||||
func (tr *GetTagResp) GetExcerpt() {
|
||||
|
@ -95,6 +98,8 @@ type GetTagPageResp struct {
|
|||
CreatedAt int64 `json:"created_at"`
|
||||
// updated time
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Reserved bool `json:"reserved"`
|
||||
}
|
||||
|
||||
func (tr *GetTagPageResp) GetExcerpt() {
|
||||
|
@ -150,9 +155,9 @@ type UpdateTagReq struct {
|
|||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
func (r *UpdateTagReq) Check() (errField *validator.ErrorField, err error) {
|
||||
func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(r.EditSummary) == 0 {
|
||||
r.EditSummary = "tag.edit.summary" // to i18n
|
||||
r.EditSummary = "tag.edit.summary" // to do i18n
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -217,4 +222,12 @@ type GetFollowingTagsResp struct {
|
|||
DisplayName string `json:"display_name"`
|
||||
// if main tag slug name is not empty, this tag is synonymous with the main tag
|
||||
MainTagSlugName string `json:"main_tag_slug_name"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Reserved bool `json:"reserved"`
|
||||
}
|
||||
|
||||
type SearchTagLikeResp struct {
|
||||
SlugName string `json:"slug_name"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Reserved bool `json:"reserved"`
|
||||
}
|
||||
|
|
|
@ -62,6 +62,8 @@ type GetUserResp struct {
|
|||
Location string `json:"location"`
|
||||
// ip info
|
||||
IPInfo string `json:"ip_info"`
|
||||
// language
|
||||
Language string `json:"language"`
|
||||
// access token
|
||||
AccessToken string `json:"access_token"`
|
||||
// is admin
|
||||
|
@ -217,10 +219,10 @@ var UserStatusShowMsg = map[int]string{
|
|||
|
||||
// EmailLogin
|
||||
type UserEmailLogin struct {
|
||||
Email string `json:"e_mail" ` // e_mail
|
||||
Pass string `json:"pass" ` // password
|
||||
CaptchaID string `json:"captcha_id" ` // captcha_id
|
||||
CaptchaCode string `json:"captcha_code" ` // captcha_code
|
||||
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` // e_mail
|
||||
Pass string `validate:"required,gte=8,lte=32" json:"pass"` // password
|
||||
CaptchaID string `json:"captcha_id"` // captcha_id
|
||||
CaptchaCode string `json:"captcha_code"` // captcha_code
|
||||
}
|
||||
|
||||
// UserRegisterReq user register request
|
||||
|
@ -234,14 +236,16 @@ type UserRegisterReq struct {
|
|||
IP string `json:"-" `
|
||||
}
|
||||
|
||||
func (u *UserRegisterReq) Check() (errField *validator.ErrorField, err error) {
|
||||
func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
// TODO i18n
|
||||
err = checker.CheckPassword(8, 32, 0, u.Pass)
|
||||
if err != nil {
|
||||
return &validator.ErrorField{
|
||||
Key: "pass",
|
||||
Value: err.Error(),
|
||||
}, err
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: err.Error(),
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -253,14 +257,16 @@ type UserModifyPassWordRequest struct {
|
|||
Pass string `json:"pass" ` // password
|
||||
}
|
||||
|
||||
func (u *UserModifyPassWordRequest) Check() (errField *validator.ErrorField, err error) {
|
||||
func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
// TODO i18n
|
||||
err = checker.CheckPassword(8, 32, 0, u.Pass)
|
||||
if err != nil {
|
||||
return &validator.ErrorField{
|
||||
Key: "pass",
|
||||
Value: err.Error(),
|
||||
}, err
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: err.Error(),
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -290,21 +296,30 @@ type AvatarInfo struct {
|
|||
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
|
||||
}
|
||||
|
||||
func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error) {
|
||||
func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(u.Username) > 0 {
|
||||
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
|
||||
match := re.MatchString(u.Username)
|
||||
if !match {
|
||||
err = errors.BadRequest(reason.UsernameInvalid)
|
||||
return &validator.ErrorField{
|
||||
Key: "username",
|
||||
Value: err.Error(),
|
||||
}, err
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: err.Error(),
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UpdateUserInterfaceRequest update user interface request
|
||||
type UpdateUserInterfaceRequest struct {
|
||||
// language
|
||||
Language string `validate:"required,gt=1,lte=100" json:"language"`
|
||||
// user id
|
||||
UserId string `json:"-" `
|
||||
}
|
||||
|
||||
type UserRetrievePassWordRequest struct {
|
||||
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail
|
||||
CaptchaID string `json:"captcha_id" ` // captcha_id
|
||||
|
@ -317,14 +332,16 @@ type UserRePassWordRequest struct {
|
|||
Content string `json:"-"`
|
||||
}
|
||||
|
||||
func (u *UserRePassWordRequest) Check() (errField *validator.ErrorField, err error) {
|
||||
func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
// TODO i18n
|
||||
err = checker.CheckPassword(8, 32, 0, u.Pass)
|
||||
if err != nil {
|
||||
return &validator.ErrorField{
|
||||
Key: "pass",
|
||||
Value: err.Error(),
|
||||
}, err
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: err.Error(),
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -400,8 +417,3 @@ type UserVerifyEmailSendReq struct {
|
|||
CaptchaID string `validate:"omitempty,gt=0,lte=500" json:"captcha_id"`
|
||||
CaptchaCode string `validate:"omitempty,gt=0,lte=500" json:"captcha_code"`
|
||||
}
|
||||
|
||||
type UserVerifyEmailErrorResponse struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ import (
|
|||
// VoteRepo activity repository
|
||||
type VoteRepo interface {
|
||||
GetVoteStatus(ctx context.Context, objectId, userId string) (status string)
|
||||
GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
)
|
||||
|
||||
type AnswerRepo interface {
|
||||
|
@ -20,6 +21,7 @@ type AnswerRepo interface {
|
|||
SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error)
|
||||
CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error)
|
||||
UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error)
|
||||
GetAnswerCount(ctx context.Context) (count int64, err error)
|
||||
}
|
||||
|
||||
// AnswerCommon user service
|
||||
|
@ -74,11 +76,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer
|
|||
info := schema.AdminAnswerInfo{}
|
||||
info.ID = data.ID
|
||||
info.QuestionID = data.QuestionID
|
||||
info.Description = data.ParsedText
|
||||
info.Adopted = data.Adopted
|
||||
info.VoteCount = data.VoteCount
|
||||
info.CreateTime = data.CreatedAt.Unix()
|
||||
info.UpdateTime = data.UpdatedAt.Unix()
|
||||
info.UserID = data.UserID
|
||||
info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240)
|
||||
return &info
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
|
|||
log.Infof("user status updated: %+v", cacheInfo)
|
||||
userCacheInfo.UserStatus = cacheInfo.UserStatus
|
||||
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
|
||||
userCacheInfo.IsAdmin = cacheInfo.IsAdmin
|
||||
// update current user cache info
|
||||
err := as.authRepo.SetUserCacheInfo(ctx, accessToken, userCacheInfo)
|
||||
if err != nil {
|
||||
|
@ -58,6 +59,10 @@ func (as *AuthService) SetUserCacheInfo(ctx context.Context, userInfo *entity.Us
|
|||
return accessToken, err
|
||||
}
|
||||
|
||||
func (as *AuthService) SetUserStatus(ctx context.Context, userInfo *entity.UserCacheInfo) (err error) {
|
||||
return as.authRepo.SetUserStatus(ctx, userInfo.UserID, userInfo)
|
||||
}
|
||||
|
||||
func (as *AuthService) UpdateUserCacheInfo(ctx context.Context, token string, userInfo *entity.UserCacheInfo) (err error) {
|
||||
err = as.authRepo.SetUserCacheInfo(ctx, token, userInfo)
|
||||
if err != nil {
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
// CommentCommonRepo comment repository
|
||||
type CommentCommonRepo interface {
|
||||
GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)
|
||||
GetCommentCount(ctx context.Context) (count int64, err error)
|
||||
}
|
||||
|
||||
// CommentCommonService user service
|
||||
|
|
|
@ -0,0 +1,236 @@
|
|||
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 nil, statisticalErr
|
||||
}
|
||||
if setCacheErr := ds.SetCache(ctx, info); setCacheErr != nil {
|
||||
log.Errorf("set dashboard statistical failed: %s", setCacheErr)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil {
|
||||
log.Errorf("parsing dashboard information failed: %s", err)
|
||||
return nil, errors.InternalServer(reason.UnknownError)
|
||||
}
|
||||
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
|
|
@ -15,7 +15,7 @@ type FollowRepo interface {
|
|||
}
|
||||
|
||||
type FollowService struct {
|
||||
tagRepo tagcommon.TagRepo
|
||||
tagRepo tagcommon.TagCommonRepo
|
||||
followRepo FollowRepo
|
||||
followCommonRepo activity_common.FollowRepo
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ type FollowService struct {
|
|||
func NewFollowService(
|
||||
followRepo FollowRepo,
|
||||
followCommonRepo activity_common.FollowRepo,
|
||||
tagRepo tagcommon.TagRepo,
|
||||
tagRepo tagcommon.TagCommonRepo,
|
||||
) *FollowService {
|
||||
return &FollowService{
|
||||
followRepo: followRepo,
|
||||
|
|
|
@ -19,7 +19,7 @@ type ObjService struct {
|
|||
answerRepo answercommon.AnswerRepo
|
||||
questionRepo questioncommon.QuestionRepo
|
||||
commentRepo comment_common.CommentCommonRepo
|
||||
tagRepo tagcommon.TagRepo
|
||||
tagRepo tagcommon.TagCommonRepo
|
||||
}
|
||||
|
||||
// NewObjService new object service
|
||||
|
@ -27,7 +27,7 @@ func NewObjService(
|
|||
answerRepo answercommon.AnswerRepo,
|
||||
questionRepo questioncommon.QuestionRepo,
|
||||
commentRepo comment_common.CommentCommonRepo,
|
||||
tagRepo tagcommon.TagRepo) *ObjService {
|
||||
tagRepo tagcommon.TagCommonRepo) *ObjService {
|
||||
return &ObjService{
|
||||
answerRepo: answerRepo,
|
||||
questionRepo: questionRepo,
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||
"github.com/answerdev/answer/internal/service/comment"
|
||||
"github.com/answerdev/answer/internal/service/comment_common"
|
||||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/follow"
|
||||
"github.com/answerdev/answer/internal/service/meta"
|
||||
|
@ -21,6 +22,9 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/report_backyard"
|
||||
"github.com/answerdev/answer/internal/service/report_handle_backyard"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/answerdev/answer/internal/service/search_parser"
|
||||
"github.com/answerdev/answer/internal/service/tag"
|
||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
|
@ -54,6 +58,7 @@ var ProviderSetService = wire.NewSet(
|
|||
revision_common.NewRevisionService,
|
||||
NewRevisionService,
|
||||
rank.NewRankService,
|
||||
search_parser.NewSearchParser,
|
||||
NewSearchService,
|
||||
meta.NewMetaService,
|
||||
object_info.NewObjService,
|
||||
|
@ -61,8 +66,10 @@ var ProviderSetService = wire.NewSet(
|
|||
report_backyard.NewReportBackyardService,
|
||||
user_backyard.NewUserBackyardService,
|
||||
reason.NewReasonService,
|
||||
NewSiteInfoService,
|
||||
siteinfo_common.NewSiteInfoCommonService,
|
||||
siteinfo.NewSiteInfoService,
|
||||
notficationcommon.NewNotificationCommon,
|
||||
notification.NewNotificationService,
|
||||
activity.NewAnswerActivityService,
|
||||
dashboard.NewDashboardService,
|
||||
)
|
||||
|
|
|
@ -38,6 +38,7 @@ type QuestionRepo interface {
|
|||
UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)
|
||||
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
|
||||
CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error)
|
||||
GetQuestionCount(ctx context.Context) (count int64, err error)
|
||||
}
|
||||
|
||||
// QuestionCommon user service
|
||||
|
|
|
@ -3,6 +3,7 @@ package service
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
|
@ -105,6 +106,16 @@ func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language)
|
|||
|
||||
// AddQuestion add question
|
||||
func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo *schema.QuestionInfo, err error) {
|
||||
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !recommendExist {
|
||||
err = fmt.Errorf("recommend is not exist")
|
||||
err = errors.BadRequest(reason.RecommendTagNotExist).WithError(err).WithStack()
|
||||
return
|
||||
}
|
||||
|
||||
questionInfo = &schema.QuestionInfo{}
|
||||
question := &entity.Question{}
|
||||
now := time.Now()
|
||||
|
@ -229,6 +240,28 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
|||
if dbinfo.UserID != req.UserID {
|
||||
return
|
||||
}
|
||||
|
||||
//CheckChangeTag
|
||||
oldTags, err := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tagNameList := make([]string, 0)
|
||||
for _, tag := range req.Tags {
|
||||
tagNameList = append(tagNameList, tag.SlugName)
|
||||
}
|
||||
Tags, err := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
CheckTag, CheckTaglist := qs.CheckChangeTag(ctx, oldTags, Tags)
|
||||
if !CheckTag {
|
||||
err = errors.BadRequest(reason.UnauthorizedError).WithMsg(fmt.Sprintf("tag [%s] cannot be modified",
|
||||
strings.Join(CheckTaglist, ",")))
|
||||
return
|
||||
}
|
||||
|
||||
//update question to db
|
||||
err = qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at"})
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -280,6 +313,10 @@ func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.
|
|||
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData)
|
||||
}
|
||||
|
||||
func (qs *QuestionService) CheckChangeTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
|
||||
return qs.tagCommon.ObjectCheckChangeTag(ctx, oldobjectTagData, objectTagData)
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) {
|
||||
userlist := make([]*schema.UserQuestionInfo, 0)
|
||||
|
||||
|
@ -516,14 +553,13 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin
|
|||
// SearchList
|
||||
func (qs *QuestionService) SearchList(ctx context.Context, req *schema.QuestionSearch, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
|
||||
if len(req.Tag) > 0 {
|
||||
taginfo, has, err := qs.tagCommon.GetTagListByName(ctx, req.Tag)
|
||||
tagInfo, has, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))
|
||||
if err != nil {
|
||||
log.Error("tagCommon.GetTagListByNames error", err)
|
||||
}
|
||||
if has {
|
||||
req.TagIDs = append(req.TagIDs, taginfo.ID)
|
||||
req.TagIDs = append(req.TagIDs, tagInfo.ID)
|
||||
}
|
||||
|
||||
}
|
||||
list := make([]*schema.QuestionInfo, 0)
|
||||
if req.UserName != "" {
|
||||
|
|
|
@ -2,9 +2,8 @@ package report_backyard
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
|
@ -180,20 +179,20 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
|
|||
case "question":
|
||||
r.QuestionID = questionId
|
||||
r.Title = question.Title
|
||||
r.Excerpt = rs.cutOutTagParsedText(question.OriginalText)
|
||||
r.Excerpt = htmltext.FetchExcerpt(question.ParsedText, "...", 240)
|
||||
|
||||
case "answer":
|
||||
r.QuestionID = questionId
|
||||
r.AnswerID = answerId
|
||||
r.Title = question.Title
|
||||
r.Excerpt = rs.cutOutTagParsedText(answer.OriginalText)
|
||||
r.Excerpt = htmltext.FetchExcerpt(answer.ParsedText, "...", 240)
|
||||
|
||||
case "comment":
|
||||
r.QuestionID = questionId
|
||||
r.AnswerID = answerId
|
||||
r.CommentID = commentId
|
||||
r.Title = question.Title
|
||||
r.Excerpt = rs.cutOutTagParsedText(cmt.OriginalText)
|
||||
r.Excerpt = htmltext.FetchExcerpt(cmt.ParsedText, "...", 240)
|
||||
}
|
||||
|
||||
// parse reason
|
||||
|
@ -214,12 +213,3 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
|
|||
}
|
||||
resp = &res
|
||||
}
|
||||
|
||||
func (rs *ReportBackyardService) cutOutTagParsedText(parsedText string) string {
|
||||
parsedText = strings.TrimSpace(parsedText)
|
||||
idx := strings.Index(parsedText, "\n")
|
||||
if idx >= 0 {
|
||||
parsedText = parsedText[0:idx]
|
||||
}
|
||||
return parsedText
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package report_common
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
)
|
||||
|
@ -12,4 +13,5 @@ type ReportRepo interface {
|
|||
GetReportListPage(ctx context.Context, query schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error)
|
||||
GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error)
|
||||
UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error)
|
||||
GetReportCount(ctx context.Context) (count int64, err error)
|
||||
}
|
||||
|
|
|
@ -113,6 +113,8 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi
|
|||
ParsedText: tag.ParsedText,
|
||||
FollowCount: tag.FollowCount,
|
||||
QuestionCount: tag.QuestionCount,
|
||||
Recommend: tag.Recommend,
|
||||
Reserved: tag.Reserved,
|
||||
}
|
||||
tagInfo.GetExcerpt()
|
||||
item.ContentParsed = tagInfo
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/search_common"
|
||||
)
|
||||
|
||||
type AcceptedAnswerSearch struct {
|
||||
repo search_common.SearchRepo
|
||||
w string
|
||||
page int
|
||||
size int
|
||||
order string
|
||||
}
|
||||
|
||||
func NewAcceptedAnswerSearch(repo search_common.SearchRepo) *AcceptedAnswerSearch {
|
||||
return &AcceptedAnswerSearch{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AcceptedAnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
|
||||
var (
|
||||
q,
|
||||
w,
|
||||
p string
|
||||
)
|
||||
|
||||
q = dto.Query
|
||||
w = dto.Query
|
||||
p = `isaccepted:yes`
|
||||
|
||||
if strings.Index(q, p) == 0 {
|
||||
ok = true
|
||||
w = strings.TrimPrefix(q, p)
|
||||
}
|
||||
|
||||
s.w = strings.TrimSpace(w)
|
||||
s.page = dto.Page
|
||||
s.size = dto.Size
|
||||
s.order = dto.Order
|
||||
return
|
||||
}
|
||||
func (s *AcceptedAnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
|
||||
|
||||
words := strings.Split(s.w, " ")
|
||||
if len(words) > 3 {
|
||||
words = words[:4]
|
||||
}
|
||||
|
||||
return s.repo.SearchAnswers(ctx, words, true, "", s.page, s.size, s.order)
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/search_common"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AnswerSearch struct {
|
||||
repo search_common.SearchRepo
|
||||
w string
|
||||
page int
|
||||
size int
|
||||
order string
|
||||
}
|
||||
|
||||
func NewAnswerSearch(repo search_common.SearchRepo) *AnswerSearch {
|
||||
return &AnswerSearch{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
|
||||
var (
|
||||
q,
|
||||
w,
|
||||
p string
|
||||
)
|
||||
|
||||
q = dto.Query
|
||||
w = dto.Query
|
||||
p = `is:answer`
|
||||
|
||||
if strings.Index(q, p) == 0 {
|
||||
ok = true
|
||||
w = strings.TrimPrefix(q, p)
|
||||
}
|
||||
|
||||
s.w = strings.TrimSpace(w)
|
||||
s.page = dto.Page
|
||||
s.size = dto.Size
|
||||
s.order = dto.Order
|
||||
return
|
||||
}
|
||||
func (s *AnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
|
||||
|
||||
words := strings.Split(s.w, " ")
|
||||
if len(words) > 3 {
|
||||
words = words[:4]
|
||||
}
|
||||
|
||||
return s.repo.SearchAnswers(ctx, words, false, "", s.page, s.size, s.order)
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/search_common"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
)
|
||||
|
||||
type AnswersSearch struct {
|
||||
repo search_common.SearchRepo
|
||||
exp int
|
||||
w string
|
||||
page int
|
||||
size int
|
||||
order string
|
||||
}
|
||||
|
||||
func NewAnswersSearch(repo search_common.SearchRepo) *AnswersSearch {
|
||||
return &AnswersSearch{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AnswersSearch) Parse(dto *schema.SearchDTO) (ok bool) {
|
||||
var (
|
||||
q,
|
||||
w,
|
||||
p,
|
||||
exp string
|
||||
)
|
||||
|
||||
q = dto.Query
|
||||
w = dto.Query
|
||||
p = `(?m)^answers:([0-9]+)`
|
||||
|
||||
re := regexp.MustCompile(p)
|
||||
res := re.FindStringSubmatch(q)
|
||||
if len(res) == 2 {
|
||||
exp = res[1]
|
||||
trimLen := len(res[0])
|
||||
w = q[trimLen:]
|
||||
ok = true
|
||||
}
|
||||
|
||||
s.exp = converter.StringToInt(exp)
|
||||
s.w = strings.TrimSpace(w)
|
||||
s.page = dto.Page
|
||||
s.size = dto.Size
|
||||
s.order = dto.Order
|
||||
return
|
||||
}
|
||||
|
||||
func (s *AnswersSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
|
||||
|
||||
words := strings.Split(s.w, " ")
|
||||
if len(words) > 3 {
|
||||
words = words[:4]
|
||||
}
|
||||
|
||||
return s.repo.SearchQuestions(ctx, words, false, s.exp, s.page, s.size, s.order)
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/search_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
)
|
||||
|
||||
type AuthorSearch struct {
|
||||
repo search_common.SearchRepo
|
||||
userCommon *usercommon.UserCommon
|
||||
exp string
|
||||
w string
|
||||
page int
|
||||
size int
|
||||
order string
|
||||
}
|
||||
|
||||
func NewAuthorSearch(repo search_common.SearchRepo, userCommon *usercommon.UserCommon) *AuthorSearch {
|
||||
return &AuthorSearch{
|
||||
repo: repo,
|
||||
userCommon: userCommon,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse
|
||||
// example: "user:12345" -> {exp="" w="12345"}
|
||||
func (s *AuthorSearch) Parse(dto *schema.SearchDTO) (ok bool) {
|
||||
var (
|
||||
exp,
|
||||
q,
|
||||
w,
|
||||
p,
|
||||
me,
|
||||
name string
|
||||
)
|
||||
exp = ""
|
||||
q = dto.Query
|
||||
w = q
|
||||
p = `(?m)^user:([a-z0-9._-]+)`
|
||||
me = "user:me"
|
||||
|
||||
re := regexp.MustCompile(p)
|
||||
res := re.FindStringSubmatch(q)
|
||||
if len(res) == 2 {
|
||||
name = res[1]
|
||||
user, has, err := s.userCommon.GetUserBasicInfoByUserName(nil, name)
|
||||
if err == nil && has {
|
||||
exp = user.ID
|
||||
trimLen := len(res[0])
|
||||
w = q[trimLen:]
|
||||
ok = true
|
||||
}
|
||||
} else if strings.Index(q, me) == 0 {
|
||||
exp = dto.UserID
|
||||
w = strings.TrimPrefix(q, me)
|
||||
ok = true
|
||||
}
|
||||
|
||||
w = strings.TrimSpace(w)
|
||||
s.exp = exp
|
||||
s.w = w
|
||||
s.page = dto.Page
|
||||
s.size = dto.Size
|
||||
s.order = dto.Order
|
||||
return
|
||||
}
|
||||
|
||||
func (s *AuthorSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
|
||||
var (
|
||||
words []string
|
||||
)
|
||||
|
||||
if len(s.exp) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
words = strings.Split(s.w, " ")
|
||||
if len(words) > 3 {
|
||||
words = words[:4]
|
||||
}
|
||||
|
||||
resp, total, err = s.repo.SearchContents(ctx, words, "", s.exp, -1, s.page, s.size, s.order)
|
||||
|
||||
return
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/search_common"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type InQuestionSearch struct {
|
||||
repo search_common.SearchRepo
|
||||
w string
|
||||
exp string
|
||||
page int
|
||||
size int
|
||||
order string
|
||||
}
|
||||
|
||||
func NewInQuestionSearch(repo search_common.SearchRepo) *InQuestionSearch {
|
||||
return &InQuestionSearch{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InQuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) {
|
||||
var (
|
||||
w,
|
||||
q,
|
||||
p,
|
||||
exp string
|
||||
)
|
||||
|
||||
q = dto.Query
|
||||
w = dto.Query
|
||||
p = `(?m)^inquestion:([0-9]+)`
|
||||
|
||||
re := regexp.MustCompile(p)
|
||||
res := re.FindStringSubmatch(q)
|
||||
if len(res) == 2 {
|
||||
exp = res[1]
|
||||
trimLen := len(res[0])
|
||||
w = q[trimLen:]
|
||||
ok = true
|
||||
}
|
||||
|
||||
s.exp = exp
|
||||
s.w = strings.TrimSpace(w)
|
||||
s.page = dto.Page
|
||||
s.size = dto.Size
|
||||
s.order = dto.Order
|
||||
return
|
||||
}
|
||||
func (s *InQuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
|
||||
var (
|
||||
words []string
|
||||
)
|
||||
|
||||
words = strings.Split(s.w, " ")
|
||||
if len(words) > 3 {
|
||||
words = words[:4]
|
||||
}
|
||||
|
||||
return s.repo.SearchAnswers(ctx, words, false, s.exp, s.page, s.size, s.order)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue