Merge branch 'main' into fix/reason

This commit is contained in:
kumfo 2022-10-19 16:17:21 +08:00
commit 00cce27f33
168 changed files with 2832 additions and 1523 deletions

View File

@ -0,0 +1,50 @@
name: Build Docker Hub Image
on:
push:
branches: [ "main" ]
tags:
- 2.*
- 1.*
- 0.*
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v3
with:
images: answerdev/answer
tags: |
type=raw,value=latest
# branch event
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
# tag event
type=ref,enable=true,priority=600,prefix=,suffix=,event=tag
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

65
.github/workflows/build_github_img.yml vendored Normal file
View File

@ -0,0 +1,65 @@
name: Build GitHub Image
on:
push:
branches: [ "main" ]
tags:
- 2.*
- 1.*
- 0.*
pull_request:
branches: [ "main" ]
env:
REGISTRY: ghcr.io
IMAGE: answerdev/answer
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to ghcr.io
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
tags: |
type=raw,value=latest
- name: Build Img
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
builder: ${{ steps.buildx.outputs.name }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# - name: build to hub.docker
# run: |
# docker build -t answerdev/answer -f ./Dockerfile .
# - name: Login to hub.docker Registry
# run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
# - name: Push Image to hub.docker
# run: |
# docker push answerdev/answer

24
.github/workflows/go_build_test.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Go Build Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Go Test Build
run: make clean build

23
.github/workflows/node_build_test.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: Node Build Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-node@v3
with:
node-version: 16
- name: Test Build
run: make install-ui-packages ui

View File

@ -21,8 +21,6 @@ WORKDIR ${BUILD_DIR}
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
RUN make clean build && \
cp answer /usr/bin/answer && \
mkdir -p /tmp/cache && chmod 777 /tmp/cache && \
mkdir /data && chmod 777 /data && cp configs/config.yaml /data/config.yaml && \
mkdir -p /data/upfiles && chmod 777 /data/upfiles && \
mkdir -p /data/i18n && chmod 777 /data/i18n && cp -r i18n/*.yaml /data/i18n
@ -34,7 +32,8 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.li
&& apt -y update \
&& apt -y upgrade \
&& apt -y install ca-certificates openssl tzdata curl netcat dumb-init \
&& apt -y autoremove
&& apt -y autoremove \
&& mkdir -p /tmp/cache
COPY --from=golang-builder /data /data
VOLUME /data

View File

@ -18,7 +18,7 @@ To keep your data out of Docker container, we do a volume (/var/data -> /data) h
```
# Pull image from Docker Hub.
$ docker pull answer/answer
$ docker pull answerdev/answer:latest
# Create local directory for volume.
$ mkdir -p /var/data
@ -27,9 +27,9 @@ $ mkdir -p /var/data
$ docker run --name=answer -p 9080:80 -v /var/data:/data answer/answer
# After the first startup, a configuration file will be generated in the /var/data directory
# /var/data/config.yaml
# /var/data/conf/config.yaml
# Need to modify the Mysql database address in the configuration file
vim /var/data/config.yaml
vim /var/data/conf/config.yaml
# Modify database connection
# connection: [username]:[password]@tcp([host]:[port])/[DbName]
@ -51,7 +51,16 @@ $ docker start answer
6. Modify the database connection address to your database connection address
connection: [username]:[password]@tcp([host]:[port])/[DbName]
7. Exit the data directory and execute ./answer run -c ./data/config.yaml
7. Exit the data directory and execute ./answer run -c ./data/conf/config.yaml
## Available Commands
Usage: answer [command]
- help: Help about any command
- init: Init answer application
- run: Run answer application
- check: Check answer required environment
- dump: Backup answer data
## config.yaml Description

View File

@ -18,7 +18,7 @@
```
# 将镜像从 docker hub 拉到本地
$ docker pull answer/answer
$ docker pull answerdev/answer:latest
# 创建一个挂载目录
$ mkdir -p /var/data
@ -27,9 +27,9 @@ $ mkdir -p /var/data
$ docker run --name=answer -p 9080:80 -v /var/data:/data answer/answer
# 第一次启动后会在/var/data 目录下生成配置文件
# /var/data/config.yaml
π# /var/data/conf/config.yaml
# 需要修改配置文件中的Mysql 数据库地址
vim /var/data/config.yaml
vim /var/data/conf/config.yaml
# 修改数据库连接 connection: [username]:[password]@tcp([host]:[port])/[DbName]
...
@ -50,7 +50,16 @@ $ docker start answer
6. 将数据库连接地址修改为你的数据库连接地址
connection: [username]:[password]@tcp([host]:[port])/[DbName]
7. 退出data 目录 执行 ./answer run -c ./data/config.yaml
7. 退出data 目录 执行 ./answer run -c ./data/conf/config.yaml
## 当前支持的命令
用法: answer [command]
- help: 帮助
- init: 初始化环境
- run: 启动
- check: 环境依赖检查
- dump: 备份数据
## 配置文件 config.yaml 参数说明

214
LICENSE
View File

@ -1,21 +1,201 @@
MIT License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) since 2022 The Segmentfault Development Team.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1. Definitions.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 joyqi
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -6,7 +6,8 @@ DIR_SRC=./cmd/answer
DOCKER_CMD=docker
GO_ENV=CGO_ENABLED=0
GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X 'main.Time=`date`' -extldflags -static"
Revision=$(shell git rev-parse --short HEAD)
GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X main.Revision=$(Revision) -X 'main.Time=`date`' -extldflags -static"
GO=$(GO_ENV) $(shell which go)
build:
@ -31,6 +32,6 @@ install-ui-packages:
ui:
@npm config set registry https://repo.huaweicloud.com/repository/npm/
@cd ui && pnpm install && pnpm build && cd -
@cd ui && echo "REACT_APP_VERSION=$(VERSION)" >> .env && pnpm install && pnpm build && cd -
all: clean build

View File

@ -1,27 +1,28 @@
![logo](docs/img/logo.png)
<a href="https://answer.dev">
<img alt="logo" src="docs/img/answer-logo-flat.svg" height="63px">
</a>
# Answer - Simple Q&A Community
# Answer - Build Q&A community
[![LICENSE](https://img.shields.io/badge/License-MIT-green)](https://github.com/segmentfault/answer/blob/master/LICENSE)
A minimalist open-source knowledge based community software. You can use it to quickly build your Q&A community for product technical support, user Q&A, fans communication, and more.
To learn more about the project, visit [answer.dev](https://answer.dev).
[![LICENSE](https://img.shields.io/badge/License-Apache-green)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/Language-React-blue.svg)](https://reactjs.org/)
## What is Answer?
This is a minimalist open source Q&A community. Users can post questions and others can answer them.
![abstract](docs/img/abstract.png)
## Screenshots
## Why?
- Help companies build knowledge and Q&A communities better and faster.
## Features
- Produce knowledge by asking and answering questions.
- Maintain knowledge by voting and working together.
![screenshot](docs/img/screenshot.png)
## Quick start
### Running with docker-compose
```bash
mkdir answer && cd answer
wget https://github.com/segmentfault/answer/releases/latest/download/docker-compose.yaml
wget https://github.com/answerdev/answer/blob/main/docker-compose.yaml
docker-compose up
```
@ -35,4 +36,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for ways to get started.
## License
[MIT](https://github.com/segmentfault/answer/blob/master/LICENSE)
[Apache](https://github.com/answerdev/answer/blob/main/LICENSE)

View File

@ -1,27 +1,28 @@
![logo](docs/img/logo.png)
<a href="https://answer.dev">
<img alt="logo" src="docs/img/answer-logo-flat.svg" height="63px">
</a>
# Answer - 极简问答社区
# Answer - 构建问答社区
[![LICENSE](https://img.shields.io/badge/License-MIT-green)](https://github.com/segmentfault/answer/blob/master/LICENSE)
一款极简的、问答形式的知识社区开源软件,用来快速构建产品你的产品问答支持社区、用户问答社区、粉丝社区等。
了解更多关于该项目的内容,请访问 [answer.dev](https://answer.dev).
[![LICENSE](https://img.shields.io/badge/License-Apache-green)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/Language-React-blue.svg)](https://reactjs.org/)
## 什么是 Answer?
这是一个极简的开源问答社区。用户可以发布问题,其他人可以回答。
![abstract](docs/img/abstract.png)
## 截图
## 目标
- 帮助企业更好更快构建知识问答社区
## 产品功能
- 通过提问、回答方式生产知识
- 通过投票、共同协作方式维护知识
![screenshot](docs/img/screenshot.png)
## 快速开始
### 使用 docker-compose 快速搭建
```bash
mkdir answer && cd answer
wget https://github.com/segmentfault/answer/releases/latest/download/docker-compose.yaml
wget https://github.com/answerdev/answer/blob/main/docker-compose.yaml
docker-compose up
```
@ -31,8 +32,8 @@ docker-compose up
我们随时欢迎你的贡献!
参考 [CONTRIBUTING.md](CONTRIBUTING.md) 其中的贡献指南
参考 [CONTRIBUTING.md](CONTRIBUTING.md) 开始贡献。
## License
[MIT](https://github.com/segmentfault/answer/blob/master/LICENSE)
[Apache](https://github.com/answerdev/answer/blob/main/LICENSE)

168
cmd/answer/command.go Normal file
View File

@ -0,0 +1,168 @@
package main
import (
"fmt"
"os"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/cli"
"github.com/segmentfault/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
dumpDataPath string
)
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")
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
rootCmd.AddCommand(initCmd)
rootCmd.AddCommand(checkCmd)
rootCmd.AddCommand(runCmd)
rootCmd.AddCommand(dumpCmd)
rootCmd.AddCommand(upgradeCmd)
}
var (
// rootCmd represents the base command when called without any subcommands
rootCmd = &cobra.Command{
Use: "answer",
Short: "Answer is a minimalist open source Q&A community.",
Long: `Answer is a minimalist open source Q&A community.
To run answer, use:
- 'answer init' to initialize the required environment.
- 'answer run' to launch application.`,
}
// runCmd represents the run command
runCmd = &cobra.Command{
Use: "run",
Short: "Run the application",
Long: `Run the application`,
Run: func(cmd *cobra.Command, args []string) {
runApp()
},
}
// initCmd represents the init command
initCmd = &cobra.Command{
Use: "init",
Short: "init answer application",
Long: `init answer application`,
Run: func(cmd *cobra.Command, args []string) {
cli.InstallAllInitialEnvironment(dataDirPath)
c, err := readConfig()
if err != nil {
fmt.Println("read config failed: ", err.Error())
return
}
fmt.Println("read config successfully")
if err := cli.InitDB(c.Data.Database); err != nil {
fmt.Println("init database error: ", err.Error())
return
}
fmt.Println("init database successfully")
},
}
// upgradeCmd represents the upgrade command
upgradeCmd = &cobra.Command{
Use: "upgrade",
Short: "upgrade Answer version",
Long: `upgrade Answer version`,
Run: func(cmd *cobra.Command, args []string) {
c, err := readConfig()
if err != nil {
fmt.Println("read config failed: ", err.Error())
return
}
fmt.Println("read config successfully")
db, err := data.NewDB(false, c.Data.Database)
if err != nil {
fmt.Println("new database failed: ", err.Error())
return
}
if err = migrations.Migrate(db); err != nil {
fmt.Println("migrate failed: ", err.Error())
return
}
fmt.Println("upgrade done")
},
}
// dumpCmd represents the dump command
dumpCmd = &cobra.Command{
Use: "dump",
Short: "back up data",
Long: `back up data`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Answer is backing up data")
c, err := readConfig()
if err != nil {
fmt.Println("read config failed: ", err.Error())
return
}
err = cli.DumpAllData(c.Data.Database, dumpDataPath)
if err != nil {
fmt.Println("dump failed: ", err.Error())
return
}
fmt.Println("Answer backed up the data successfully.")
},
}
// checkCmd represents the check command
checkCmd = &cobra.Command{
Use: "check",
Short: "checking the required environment",
Long: `Check if the current environment meets the startup requirements`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Start checking the required environment...")
if cli.CheckConfigFile(configFilePath) {
fmt.Println("config file exists [✔]")
} else {
fmt.Println("config file not exists [x]")
}
if cli.CheckUploadDir() {
fmt.Println("upload directory exists [✔]")
} else {
fmt.Println("upload directory not exists [x]")
}
c, err := readConfig()
if err != nil {
fmt.Println("read config failed: ", err.Error())
return
}
if cli.CheckDB(c.Data.Database) {
fmt.Println("db connection successfully [✔]")
} else {
fmt.Println("db connection failed [x]")
}
fmt.Println("check environment all done")
},
}
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}

View File

@ -1,8 +1,8 @@
package main
import (
"flag"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/conf"
@ -19,57 +19,30 @@ var (
// Name is the name of the project
Name = "answer"
// Version is the version of the project
Version string
// confFlag is the config flag.
confFlag string
Version = "development"
// Revision is the git short commit revision number
Revision = ""
// Time is the build time of the project
Time = ""
// log level
logLevel = os.Getenv("LOG_LEVEL")
// log path
logPath = os.Getenv("LOG_PATH")
)
func init() {
flag.StringVar(&confFlag, "c", "../../configs/config.yaml", "config path, eg: -c config.yaml")
}
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
func main() {
flag.Parse()
args := flag.Args()
if len(args) < 1 {
cli.Usage()
os.Exit(0)
return
}
if args[0] == "init" {
cli.InitConfig()
return
}
if len(args) >= 3 {
if args[0] == "run" && args[1] == "-c" {
confFlag = args[2]
}
}
Execute()
return
}
func runApp() {
log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName(Name), zap.WithPath(logPath), zap.WithCallerFullPath()))
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
// init config
c := &conf.AllConfig{}
config, err := viper.NewWithPath(confFlag)
if err != nil {
panic(err)
}
if err = config.Parse(&c); err != nil {
panic(err)
}
err = cli.InitDB(c.Data.Database)
c, err := readConfig()
if err != nil {
panic(err)
}
@ -78,13 +51,27 @@ func main() {
if err != nil {
panic(err)
}
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),

View File

@ -74,7 +74,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
return nil, nil, err
}
langController := controller.NewLangController(i18nTranslator)
engine := data.NewDB(debug, dbConf)
engine, err := data.NewDB(debug, dbConf)
if err != nil {
return nil, nil, err
}
cache, cleanup, err := data.NewCache(cacheConf)
if err != nil {
return nil, nil, err

View File

@ -3,6 +3,7 @@ server:
addr: 0.0.0.0:80
data:
database:
driver: "mysql"
connection: root:root@tcp(db:3306)/answer
cache:
file_path: "/tmp/cache/cache.db"
@ -15,5 +16,5 @@ swaggerui:
address: ':80'
service_config:
secret_key: "answer"
web_host: "http://127.0.0.1"
upload_path: "./upfiles"
web_host: "http://127.0.0.1:9080"
upload_path: "/data/upfiles"

View File

@ -1,7 +1,7 @@
version: "3.9"
services:
answer:
image: github.com/segmentfault/answer
image: answerdev/answer:latest
ports:
- '9080:80'
restart: on-failure
@ -10,6 +10,8 @@ services:
condition: service_healthy
links:
- db
volumes:
- ./answer/data:/data
db:
image: mariadb:10.4.7
ports:
@ -22,3 +24,5 @@ services:
test: [ "CMD", "mysqladmin" ,"ping", "-uroot", "-proot"]
timeout: 20s
retries: 10
volumes:
- ./answer/mysql:/var/lib/mysql

View File

@ -1380,7 +1380,7 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "GetRedDot",
"description": "get notification list",
"consumes": [
"application/json"
],
@ -1390,7 +1390,7 @@ const docTemplate = `{
"tags": [
"Notification"
],
"summary": "GetRedDot",
"summary": "get notification list",
"parameters": [
{
"type": "integer",
@ -1412,7 +1412,8 @@ const docTemplate = `{
"type": "string",
"description": "type",
"name": "type",
"in": "query"
"in": "query",
"required": true
}
],
"responses": {
@ -2149,7 +2150,7 @@ const docTemplate = `{
}
},
"/answer/api/v1/question/page": {
"post": {
"get": {
"description": "SearchQuestionList \u003cbr\u003e \"order\" Enums(newest, active,frequent,score,unanswered)",
"consumes": [
"application/json"
@ -2588,6 +2589,19 @@ const docTemplate = `{
"name": "q",
"in": "query",
"required": true
},
{
"enum": [
"newest",
"active",
"score",
"relevance"
],
"type": "string",
"description": "order",
"name": "order",
"in": "query",
"required": true
}
],
"responses": {
@ -3256,7 +3270,7 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "UserUpdateInfo",
"description": "UserUpdateInfo update user info",
"consumes": [
"application/json"
],
@ -3266,7 +3280,7 @@ const docTemplate = `{
"tags": [
"User"
],
"summary": "UserUpdateInfo",
"summary": "UserUpdateInfo update user info",
"parameters": [
{
"type": "string",
@ -3583,12 +3597,12 @@ const docTemplate = `{
"summary": "UserRegisterByEmail",
"parameters": [
{
"description": "UserRegister",
"description": "UserRegisterReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UserRegister"
"$ref": "#/definitions/schema.UserRegisterReq"
}
}
],
@ -4922,7 +4936,7 @@ const docTemplate = `{
"title": {
"description": "question title",
"type": "string",
"maxLength": 64,
"maxLength": 150,
"minLength": 6
}
}
@ -4995,7 +5009,7 @@ const docTemplate = `{
"title": {
"description": "question title",
"type": "string",
"maxLength": 64,
"maxLength": 150,
"minLength": 6
}
}
@ -5075,14 +5089,14 @@ const docTemplate = `{
"schema.ReportHandleReq": {
"type": "object",
"required": [
"flaged_type",
"flagged_type",
"id"
],
"properties": {
"flaged_content": {
"flagged_content": {
"type": "string"
},
"flaged_type": {
"flagged_type": {
"type": "integer"
},
"id": {
@ -5126,6 +5140,10 @@ const docTemplate = `{
"id": {
"type": "string"
},
"status": {
"description": "Status",
"type": "string"
},
"tags": {
"description": "tags",
"type": "array",
@ -5250,7 +5268,7 @@ const docTemplate = `{
"display_name": {
"description": "display_name",
"type": "string",
"maxLength": 50
"maxLength": 35
},
"original_text": {
"description": "original text",
@ -5263,7 +5281,7 @@ const docTemplate = `{
"slug_name": {
"description": "slug_name",
"type": "string",
"maxLength": 50
"maxLength": 35
}
}
},
@ -5345,32 +5363,44 @@ const docTemplate = `{
},
"schema.UpdateInfoRequest": {
"type": "object",
"required": [
"display_name"
],
"properties": {
"avatar": {
"description": "avatar",
"type": "string"
"type": "string",
"maxLength": 500
},
"bio": {
"type": "string"
"description": "bio",
"type": "string",
"maxLength": 4096
},
"bio_html": {
"type": "string"
"description": "bio",
"type": "string",
"maxLength": 4096
},
"display_name": {
"description": "display_name",
"type": "string"
"type": "string",
"maxLength": 30
},
"location": {
"description": "location",
"type": "string"
"type": "string",
"maxLength": 100
},
"username": {
"description": "name",
"type": "string"
"description": "username",
"type": "string",
"maxLength": 30
},
"website": {
"description": "website",
"type": "string"
"type": "string",
"maxLength": 500
}
}
},
@ -5407,7 +5437,7 @@ const docTemplate = `{
"display_name": {
"description": "display_name",
"type": "string",
"maxLength": 50
"maxLength": 35
},
"edit_summary": {
"description": "edit summary",
@ -5424,7 +5454,7 @@ const docTemplate = `{
"slug_name": {
"description": "slug_name",
"type": "string",
"maxLength": 50
"maxLength": 35
},
"tag_id": {
"description": "tag_id",
@ -5617,7 +5647,7 @@ const docTemplate = `{
}
}
},
"schema.UserRegister": {
"schema.UserRegisterReq": {
"type": "object",
"required": [
"e_mail",
@ -5633,7 +5663,7 @@ const docTemplate = `{
"name": {
"description": "name",
"type": "string",
"maxLength": 50
"maxLength": 30
},
"pass": {
"description": "password",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View File

@ -0,0 +1,9 @@
<svg width="110" height="28" viewBox="0 0 110 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 6.4C0 4.15979 0 3.03969 0.435974 2.18404C0.819467 1.43139 1.43139 0.819467 2.18404 0.435974C3.03969 0 4.15979 0 6.4 0H21.6C23.8402 0 24.9603 0 25.816 0.435974C26.5686 0.819467 27.1805 1.43139 27.564 2.18404C28 3.03969 28 4.15979 28 6.4V16.0892C28 18.9608 28 20.3967 27.5628 21.6643C27.1761 22.7853 26.5451 23.8063 25.7153 24.6535C24.7771 25.6115 23.4928 26.2536 20.9244 27.5378L20.9243 27.5378L20 28V24H6.4C4.15979 24 3.03969 24 2.18404 23.564C1.43139 23.1805 0.819467 22.5686 0.435974 21.816C0 20.9603 0 19.8402 0 17.6V6.4ZM20 12C20 15.3137 17.3137 18 14 18C10.6954 18 8.0148 15.3285 8.00006 12.0274V12H20Z" fill="#0033FF"/>
<path d="M35.98 21.5L41.52 7.5H44.4L49.9 21.5H46.46L43.82 14.4C43.7133 14.12 43.6066 13.82 43.5 13.5C43.3933 13.18 43.2866 12.8533 43.18 12.52C43.0733 12.1733 42.9666 11.84 42.86 11.52C42.7666 11.1867 42.6866 10.88 42.62 10.6L43.22 10.58C43.14 10.9133 43.0466 11.24 42.94 11.56C42.8466 11.88 42.7466 12.2 42.64 12.52C42.5466 12.8267 42.44 13.14 42.32 13.46C42.2 13.7667 42.0866 14.0867 41.98 14.42L39.34 21.5H35.98ZM38.62 18.82L39.68 16.26H46.12L47.16 18.82H38.62Z" fill="#212529"/>
<path d="M51.3067 21.5V10.88H54.3467L54.4467 13.04L53.8067 13.28C53.9534 12.8 54.2134 12.3667 54.5867 11.98C54.9734 11.58 55.4334 11.26 55.9667 11.02C56.5 10.78 57.06 10.66 57.6467 10.66C58.4467 10.66 59.12 10.8267 59.6667 11.16C60.2134 11.48 60.6267 11.9667 60.9067 12.62C61.1867 13.26 61.3267 14.0467 61.3267 14.98V21.5H58.1067V15.24C58.1067 14.8133 58.0467 14.46 57.9267 14.18C57.8067 13.9 57.62 13.6933 57.3667 13.56C57.1267 13.4133 56.8267 13.3467 56.4667 13.36C56.1867 13.36 55.9267 13.4067 55.6867 13.5C55.4467 13.58 55.24 13.7067 55.0667 13.88C54.8934 14.04 54.7534 14.2267 54.6467 14.44C54.5534 14.6533 54.5067 14.8867 54.5067 15.14V21.5H52.9267C52.5534 21.5 52.2334 21.5 51.9667 21.5C51.7 21.5 51.48 21.5 51.3067 21.5Z" fill="#212529"/>
<path d="M67.582 21.7C66.542 21.7 65.622 21.5333 64.822 21.2C64.0353 20.8667 63.4087 20.42 62.942 19.86L64.882 18.18C65.2953 18.5933 65.762 18.9 66.282 19.1C66.802 19.2867 67.2953 19.38 67.762 19.38C67.9487 19.38 68.1153 19.36 68.262 19.32C68.4087 19.28 68.5287 19.2267 68.622 19.16C68.7287 19.08 68.8087 18.9933 68.862 18.9C68.9153 18.7933 68.942 18.6733 68.942 18.54C68.942 18.2733 68.822 18.0667 68.582 17.92C68.462 17.8533 68.262 17.7733 67.982 17.68C67.702 17.5867 67.342 17.48 66.902 17.36C66.2753 17.2 65.7287 17.0133 65.262 16.8C64.8087 16.5733 64.4353 16.3133 64.142 16.02C63.8753 15.74 63.6687 15.4333 63.522 15.1C63.3753 14.7533 63.302 14.3667 63.302 13.94C63.302 13.4467 63.4153 13 63.642 12.6C63.882 12.2 64.1953 11.8533 64.582 11.56C64.982 11.2667 65.4353 11.0467 65.942 10.9C66.462 10.74 66.9953 10.66 67.542 10.66C68.1287 10.66 68.6887 10.7267 69.222 10.86C69.7553 10.9933 70.2487 11.18 70.702 11.42C71.1687 11.66 71.582 11.9467 71.942 12.28L70.262 14.16C70.022 13.9333 69.7487 13.7333 69.442 13.56C69.1487 13.3733 68.842 13.2267 68.522 13.12C68.202 13.0133 67.9087 12.96 67.642 12.96C67.442 12.96 67.262 12.98 67.102 13.02C66.9553 13.0467 66.8287 13.1 66.722 13.18C66.6153 13.2467 66.5353 13.3333 66.482 13.44C66.4287 13.5333 66.402 13.6467 66.402 13.78C66.402 13.9133 66.4353 14.04 66.502 14.16C66.582 14.28 66.6887 14.38 66.822 14.46C66.9553 14.54 67.1687 14.6333 67.462 14.74C67.7553 14.8333 68.1553 14.9533 68.662 15.1C69.2887 15.2733 69.8287 15.4667 70.282 15.68C70.7353 15.8933 71.0953 16.1467 71.362 16.44C71.5753 16.6667 71.7353 16.9333 71.842 17.24C71.9487 17.5333 72.002 17.8533 72.002 18.2C72.002 18.88 71.8087 19.4867 71.422 20.02C71.0487 20.54 70.5287 20.9533 69.862 21.26C69.1953 21.5533 68.4353 21.7 67.582 21.7Z" fill="#212529"/>
<path d="M76.2244 21.5L72.7244 10.88H76.1044L78.0644 17.78L77.6444 17.7L79.8444 12.34H81.7244L84.0444 17.72L83.5844 17.76L85.5444 10.88H88.9244L85.3044 21.5H82.9444L80.6444 15.8L80.8844 15.84L78.5844 21.5H76.2244Z" fill="#212529"/>
<path d="M95.3511 21.7C94.1777 21.7 93.1577 21.4667 92.2911 21C91.4244 20.52 90.7511 19.8733 90.2711 19.06C89.7911 18.2333 89.5511 17.2933 89.5511 16.24C89.5511 15.4267 89.6844 14.68 89.9511 14C90.2177 13.32 90.5911 12.7333 91.0711 12.24C91.5511 11.7333 92.1177 11.3467 92.7711 11.08C93.4377 10.8 94.1644 10.66 94.9511 10.66C95.6977 10.66 96.3777 10.7933 96.9911 11.06C97.6177 11.3267 98.1577 11.7 98.6111 12.18C99.0644 12.66 99.4111 13.2267 99.6511 13.88C99.8911 14.5333 99.9977 15.2467 99.9711 16.02L99.9511 16.88H91.4911L91.0311 15.08H97.3511L97.0111 15.46V15.06C96.9844 14.7267 96.8777 14.4333 96.6911 14.18C96.5177 13.9133 96.2844 13.7067 95.9911 13.56C95.6977 13.4133 95.3644 13.34 94.9911 13.34C94.4711 13.34 94.0244 13.4467 93.6511 13.66C93.2911 13.86 93.0177 14.16 92.8311 14.56C92.6444 14.9467 92.5511 15.4267 92.5511 16C92.5511 16.5867 92.6711 17.1 92.9111 17.54C93.1644 17.9667 93.5244 18.3 93.9911 18.54C94.4711 18.78 95.0377 18.9 95.6911 18.9C96.1444 18.9 96.5444 18.8333 96.8911 18.7C97.2511 18.5667 97.6377 18.34 98.0511 18.02L99.5511 20.14C99.1377 20.5 98.6977 20.7933 98.2311 21.02C97.7644 21.2467 97.2844 21.4133 96.7911 21.52C96.3111 21.64 95.8311 21.7 95.3511 21.7Z" fill="#212529"/>
<path d="M102.01 21.5V10.88H105.05L105.19 14.34L104.59 13.68C104.75 13.1067 105.01 12.5933 105.37 12.14C105.743 11.6867 106.176 11.3267 106.67 11.06C107.163 10.7933 107.69 10.66 108.25 10.66C108.49 10.66 108.71 10.68 108.91 10.72C109.123 10.76 109.316 10.8067 109.49 10.86L108.61 14.4C108.463 14.3067 108.263 14.2333 108.01 14.18C107.77 14.1133 107.516 14.08 107.25 14.08C106.956 14.08 106.683 14.1333 106.43 14.24C106.176 14.3333 105.963 14.4733 105.79 14.66C105.616 14.8467 105.476 15.0667 105.37 15.32C105.276 15.5733 105.23 15.86 105.23 16.18V21.5H102.01Z" fill="#212529"/>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

BIN
docs/img/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -1368,7 +1368,7 @@
"ApiKeyAuth": []
}
],
"description": "GetRedDot",
"description": "get notification list",
"consumes": [
"application/json"
],
@ -1378,7 +1378,7 @@
"tags": [
"Notification"
],
"summary": "GetRedDot",
"summary": "get notification list",
"parameters": [
{
"type": "integer",
@ -1400,7 +1400,8 @@
"type": "string",
"description": "type",
"name": "type",
"in": "query"
"in": "query",
"required": true
}
],
"responses": {
@ -2137,7 +2138,7 @@
}
},
"/answer/api/v1/question/page": {
"post": {
"get": {
"description": "SearchQuestionList \u003cbr\u003e \"order\" Enums(newest, active,frequent,score,unanswered)",
"consumes": [
"application/json"
@ -2576,6 +2577,19 @@
"name": "q",
"in": "query",
"required": true
},
{
"enum": [
"newest",
"active",
"score",
"relevance"
],
"type": "string",
"description": "order",
"name": "order",
"in": "query",
"required": true
}
],
"responses": {
@ -3244,7 +3258,7 @@
"ApiKeyAuth": []
}
],
"description": "UserUpdateInfo",
"description": "UserUpdateInfo update user info",
"consumes": [
"application/json"
],
@ -3254,7 +3268,7 @@
"tags": [
"User"
],
"summary": "UserUpdateInfo",
"summary": "UserUpdateInfo update user info",
"parameters": [
{
"type": "string",
@ -3571,12 +3585,12 @@
"summary": "UserRegisterByEmail",
"parameters": [
{
"description": "UserRegister",
"description": "UserRegisterReq",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UserRegister"
"$ref": "#/definitions/schema.UserRegisterReq"
}
}
],
@ -4910,7 +4924,7 @@
"title": {
"description": "question title",
"type": "string",
"maxLength": 64,
"maxLength": 150,
"minLength": 6
}
}
@ -4983,7 +4997,7 @@
"title": {
"description": "question title",
"type": "string",
"maxLength": 64,
"maxLength": 150,
"minLength": 6
}
}
@ -5063,14 +5077,14 @@
"schema.ReportHandleReq": {
"type": "object",
"required": [
"flaged_type",
"flagged_type",
"id"
],
"properties": {
"flaged_content": {
"flagged_content": {
"type": "string"
},
"flaged_type": {
"flagged_type": {
"type": "integer"
},
"id": {
@ -5114,6 +5128,10 @@
"id": {
"type": "string"
},
"status": {
"description": "Status",
"type": "string"
},
"tags": {
"description": "tags",
"type": "array",
@ -5238,7 +5256,7 @@
"display_name": {
"description": "display_name",
"type": "string",
"maxLength": 50
"maxLength": 35
},
"original_text": {
"description": "original text",
@ -5251,7 +5269,7 @@
"slug_name": {
"description": "slug_name",
"type": "string",
"maxLength": 50
"maxLength": 35
}
}
},
@ -5333,32 +5351,44 @@
},
"schema.UpdateInfoRequest": {
"type": "object",
"required": [
"display_name"
],
"properties": {
"avatar": {
"description": "avatar",
"type": "string"
"type": "string",
"maxLength": 500
},
"bio": {
"type": "string"
"description": "bio",
"type": "string",
"maxLength": 4096
},
"bio_html": {
"type": "string"
"description": "bio",
"type": "string",
"maxLength": 4096
},
"display_name": {
"description": "display_name",
"type": "string"
"type": "string",
"maxLength": 30
},
"location": {
"description": "location",
"type": "string"
"type": "string",
"maxLength": 100
},
"username": {
"description": "name",
"type": "string"
"description": "username",
"type": "string",
"maxLength": 30
},
"website": {
"description": "website",
"type": "string"
"type": "string",
"maxLength": 500
}
}
},
@ -5395,7 +5425,7 @@
"display_name": {
"description": "display_name",
"type": "string",
"maxLength": 50
"maxLength": 35
},
"edit_summary": {
"description": "edit summary",
@ -5412,7 +5442,7 @@
"slug_name": {
"description": "slug_name",
"type": "string",
"maxLength": 50
"maxLength": 35
},
"tag_id": {
"description": "tag_id",
@ -5605,7 +5635,7 @@
}
}
},
"schema.UserRegister": {
"schema.UserRegisterReq": {
"type": "object",
"required": [
"e_mail",
@ -5621,7 +5651,7 @@
"name": {
"description": "name",
"type": "string",
"maxLength": 50
"maxLength": 30
},
"pass": {
"description": "password",

View File

@ -791,7 +791,7 @@ definitions:
type: array
title:
description: question title
maxLength: 64
maxLength: 150
minLength: 6
type: string
required:
@ -845,7 +845,7 @@ definitions:
type: array
title:
description: question title
maxLength: 64
maxLength: 150
minLength: 6
type: string
required:
@ -905,14 +905,14 @@ definitions:
type: object
schema.ReportHandleReq:
properties:
flaged_content:
flagged_content:
type: string
flaged_type:
flagged_type:
type: integer
id:
type: string
required:
- flaged_type
- flagged_type
- id
type: object
schema.SearchListResp:
@ -939,6 +939,9 @@ definitions:
type: string
id:
type: string
status:
description: Status
type: string
tags:
description: tags
items:
@ -1027,7 +1030,7 @@ definitions:
properties:
display_name:
description: display_name
maxLength: 50
maxLength: 35
type: string
original_text:
description: original text
@ -1037,7 +1040,7 @@ definitions:
type: string
slug_name:
description: slug_name
maxLength: 50
maxLength: 35
type: string
type: object
schema.TagResp:
@ -1097,23 +1100,34 @@ definitions:
properties:
avatar:
description: avatar
maxLength: 500
type: string
bio:
description: bio
maxLength: 4096
type: string
bio_html:
description: bio
maxLength: 4096
type: string
display_name:
description: display_name
maxLength: 30
type: string
location:
description: location
maxLength: 100
type: string
username:
description: name
description: username
maxLength: 30
type: string
website:
description: website
maxLength: 500
type: string
required:
- display_name
type: object
schema.UpdateNotificationReadReq:
properties:
@ -1136,7 +1150,7 @@ definitions:
properties:
display_name:
description: display_name
maxLength: 50
maxLength: 35
type: string
edit_summary:
description: edit summary
@ -1149,7 +1163,7 @@ definitions:
type: string
slug_name:
description: slug_name
maxLength: 50
maxLength: 35
type: string
tag_id:
description: tag_id
@ -1287,7 +1301,7 @@ definitions:
- code
- pass
type: object
schema.UserRegister:
schema.UserRegisterReq:
properties:
e_mail:
description: email
@ -1295,7 +1309,7 @@ definitions:
type: string
name:
description: name
maxLength: 50
maxLength: 30
type: string
pass:
description: password
@ -2176,7 +2190,7 @@ paths:
get:
consumes:
- application/json
description: GetRedDot
description: get notification list
parameters:
- description: page size
in: query
@ -2192,6 +2206,7 @@ paths:
- achievement
in: query
name: type
required: true
type: string
produces:
- application/json
@ -2202,7 +2217,7 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: GetRedDot
summary: get notification list
tags:
- Notification
/answer/api/v1/notification/read/state:
@ -2645,7 +2660,7 @@ paths:
tags:
- api-question
/answer/api/v1/question/page:
post:
get:
consumes:
- application/json
description: SearchQuestionList <br> "order" Enums(newest, active,frequent,score,unanswered)
@ -2914,6 +2929,16 @@ paths:
name: q
required: true
type: string
- description: order
enum:
- newest
- active
- score
- relevance
in: query
name: order
required: true
type: string
produces:
- application/json
responses:
@ -3318,7 +3343,7 @@ paths:
put:
consumes:
- application/json
description: UserUpdateInfo
description: UserUpdateInfo update user info
parameters:
- description: access-token
in: header
@ -3340,7 +3365,7 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: UserUpdateInfo
summary: UserUpdateInfo update user info
tags:
- User
/answer/api/v1/user/login/email:
@ -3514,12 +3539,12 @@ paths:
- application/json
description: UserRegisterByEmail
parameters:
- description: UserRegister
- description: UserRegisterReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UserRegister'
$ref: '#/definitions/schema.UserRegisterReq'
produces:
- application/json
responses:

7
go.mod
View File

@ -5,7 +5,6 @@ go 1.18
require (
github.com/Chain-Zhang/pinyin v0.1.3
github.com/bwmarrin/snowflake v0.3.0
github.com/davecgh/go-spew v1.1.1
github.com/gin-gonic/gin v1.8.1
github.com/go-playground/locales v0.14.0
github.com/go-playground/universal-translator v0.18.0
@ -17,6 +16,8 @@ require (
github.com/jinzhu/copier v0.3.5
github.com/jinzhu/now v1.1.5
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/lib/pq v1.10.2
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mojocn/base64Captcha v1.3.5
github.com/segmentfault/pacman v1.0.1
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347
@ -24,6 +25,7 @@ require (
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347
github.com/spf13/cobra v1.5.0
github.com/stretchr/testify v1.8.0
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
github.com/swaggo/gin-swagger v1.5.3
@ -37,6 +39,7 @@ require (
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
@ -46,6 +49,7 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
@ -54,7 +58,6 @@ require (
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect

20
go.sum
View File

@ -97,6 +97,7 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -285,6 +286,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
@ -383,6 +385,7 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
@ -517,30 +520,21 @@ github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentfault/pacman v1.0.1 h1:GFdvPtNxvVVjnDM4ty02D/+4unHwG9PmjcOZSc2wRXE=
github.com/segmentfault/pacman v1.0.1/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs=
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220926035018-18f894415e5b h1:jSnRy3z3KVtVuGM2YTZihXwc4zEhW+TvyyJbBm8rjh4=
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220926035018-18f894415e5b/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 h1:0xWBBXHHuemzMY61KYJXh7F5FW/4K8g98RYKNXodTCc=
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220926035018-18f894415e5b h1:Gx3Brm+VMAyBJn4aBsxgKl+EIhFHc/YH5cLGeFHAW4g=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220926035018-18f894415e5b/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347 h1:WpnEbmZFE8FYIgvseX+NJtDgGJlM1KSaKJhoxJywUgo=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220926035018-18f894415e5b h1:uQmSgcV2w4OVXU6l3bQb9O+cSAVuzDQ9adJArQyFBa4=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220926035018-18f894415e5b/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347 h1:Q29Ky9ZUGhdLIygfX6jwPYeEa7Wqn8o3f1NJWb8LvvE=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8=
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220926035018-18f894415e5b h1:TaOBmAglooq+qKdnNTK2sy11t26ud7psHFB7/AV7l5U=
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220926035018-18f894415e5b/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk=
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347 h1:7Adjc296AKv32dg88S0T8t9K3+N+PFYLSCctpPnCUr0=
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk=
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220926035018-18f894415e5b h1:n5n5VPeYGuZCmVppKPgWR/CaINHnL+ipEp9iE1XkcQc=
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220926035018-18f894415e5b/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40=
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347 h1:CfuRhTPK2CBQIZruq5ceuTVthspe8U1FDjWXXI2RWdo=
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@ -559,6 +553,8 @@ github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcD
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@ -740,8 +736,6 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220926192436-02166a98028e h1:I51lVG9ykW5AQeTE50sJ0+gJCAF0J78Hf1+1VUCGxDI=
golang.org/x/net v0.0.0-20220926192436-02166a98028e/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -824,8 +818,6 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25 h1:nwzwVf0l2Y/lkov/+IYgMMbFyI+QypZDds9RxlSmsFQ=
golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=

View File

@ -74,15 +74,17 @@ error:
other: "user not found"
suspended:
other: "user is suspended"
username_invalid:
other: "username is invalid"
username_duplicate:
other: "username is already in use"
report:
spam:
name:
other: "spam"
description:
other: "This post is an advertisement,or vandalism.It is not useful or relevant to the current topic."
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."
rude:
name:
other: "rude or abusive"
@ -97,12 +99,12 @@ report:
name:
other: "not an answer"
description:
other: "This was posted as an answer,but it does not attempt to answer the question. It should possibly be an edit,a comment,another question,or deleted altogether."
other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether."
not_need:
name:
other: "no longer needed"
description:
other: "This comment is outdated,conversational or not relevant to this post."
other: "This comment is outdated, conversational or not relevant to this post."
other:
name:
other: "something else"
@ -135,26 +137,26 @@ question:
notification:
action:
update_question:
other: "update question"
other: "updated question"
answer_the_question:
other: "answer the question"
other: "answered question"
update_answer:
other: "update answer"
other: "updated answer"
adopt_answer:
other: "adopt answer"
other: "accepted answer"
comment_question:
other: "comment question"
other: "commented question"
comment_answer:
other: "comment answer"
other: "commented answer"
reply_to_you:
other: "reply to you"
other: "replied to you"
mention_you:
other: "mention you"
other: "mentioned you"
your_question_is_closed:
other: "your question is closed"
other: "your question has been closed"
your_question_was_deleted:
other: "your question was deleted"
other: "your question has been deleted"
your_answer_was_deleted:
other: "your answer was deleted"
other: "your answer has been deleted"
your_comment_was_deleted:
other: "your comment was deleted"
other: "your comment has been deleted"

View File

@ -2,6 +2,7 @@ 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"`

View File

@ -4,11 +4,15 @@ import (
"time"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"github.com/segmentfault/pacman/cache"
"github.com/segmentfault/pacman/contrib/cache/memory"
"github.com/segmentfault/pacman/log"
"xorm.io/core"
"xorm.io/xorm"
ormlog "xorm.io/xorm/log"
"xorm.io/xorm/schemas"
)
// Data data
@ -27,18 +31,23 @@ func NewData(db *xorm.Engine, cache cache.Cache) (*Data, func(), error) {
}
// NewDB new database instance
func NewDB(debug bool, dataConf *Database) *xorm.Engine {
engine, err := xorm.NewEngine("mysql", dataConf.Connection)
if err != nil {
panic(err)
func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) {
if dataConf.Driver == "" {
dataConf.Driver = string(schemas.MYSQL)
}
if err = engine.Ping(); err != nil {
panic(err)
engine, err := xorm.NewEngine(dataConf.Driver, dataConf.Connection)
if err != nil {
return nil, err
}
if debug {
engine.ShowSQL(true)
} else {
engine.SetLogLevel(ormlog.LOG_ERR)
}
if err = engine.Ping(); err != nil {
return nil, err
}
if dataConf.MaxIdleConn > 0 {
@ -51,7 +60,7 @@ func NewDB(debug bool, dataConf *Database) *xorm.Engine {
engine.SetConnMaxLifetime(time.Duration(dataConf.ConnMaxLifeTime) * time.Second)
}
engine.SetColumnMapper(core.GonicMapper{})
return engine
return engine, nil
}
// NewCache new cache instance

View File

@ -55,7 +55,7 @@ func BindAndCheck(ctx *gin.Context, data interface{}) bool {
errField, err := validator.GetValidatorByLang(lang.Abbr()).Check(data)
if err != nil {
HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError).WithMsg(err.Error()), errField)
HandleResponse(ctx, err, errField)
return true
}
return false

View File

@ -3,7 +3,6 @@ package middleware
import (
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/segmentfault/answer/internal/schema"
"github.com/gin-gonic/gin"
@ -39,20 +38,13 @@ func (am *AuthUserMiddleware) Auth() gin.HandlerFunc {
ctx.Next()
return
}
if token == "888" {
userInfo := &entity.UserCacheInfo{}
userInfo.UserID = "2"
spew.Dump("开发环境 Auth", userInfo)
userInfo, err := am.authService.GetUserCacheInfo(ctx, token)
if err != nil {
ctx.Next()
return
}
if userInfo != nil {
ctx.Set(ctxUuidKey, userInfo)
} else {
userInfo, err := am.authService.GetUserCacheInfo(ctx, token)
if err != nil {
ctx.Next()
return
}
if userInfo != nil {
ctx.Set(ctxUuidKey, userInfo)
}
}
ctx.Next()
}
@ -67,38 +59,30 @@ func (am *AuthUserMiddleware) MustAuth() gin.HandlerFunc {
ctx.Abort()
return
}
if token == "888" {
userInfo := &entity.UserCacheInfo{}
userInfo.UserID = "2"
spew.Dump("开发环境 MustAuth", userInfo)
ctx.Set(ctxUuidKey, userInfo)
} else {
userInfo, err := am.authService.GetUserCacheInfo(ctx, token)
spew.Dump(userInfo, err)
if err != nil || userInfo == nil {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
if userInfo.EmailStatus != entity.EmailStatusAvailable {
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailNeedToBeVerified),
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeInactive})
ctx.Abort()
return
}
if userInfo.UserStatus == entity.UserStatusSuspended {
handler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended),
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended})
ctx.Abort()
return
}
if userInfo.UserStatus == entity.UserStatusDeleted {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
ctx.Set(ctxUuidKey, userInfo)
userInfo, err := am.authService.GetUserCacheInfo(ctx, token)
if err != nil || userInfo == nil {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
if userInfo.EmailStatus != entity.EmailStatusAvailable {
handler.HandleResponse(ctx, errors.Forbidden(reason.EmailNeedToBeVerified),
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeInactive})
ctx.Abort()
return
}
if userInfo.UserStatus == entity.UserStatusSuspended {
handler.HandleResponse(ctx, errors.Forbidden(reason.UserSuspended),
&schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUserSuspended})
ctx.Abort()
return
}
if userInfo.UserStatus == entity.UserStatusDeleted {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
ctx.Set(ctxUuidKey, userInfo)
ctx.Next()
}
}
@ -111,26 +95,19 @@ func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc {
ctx.Abort()
return
}
if token == "888" {
userInfo := &entity.UserCacheInfo{}
userInfo.UserID = "2"
spew.Dump("开发环境 CmsAuth", userInfo)
ctx.Set(ctxUuidKey, userInfo)
} else {
userInfo, err := am.authService.GetCmsUserCacheInfo(ctx, token)
if err != nil {
userInfo, err := am.authService.GetCmsUserCacheInfo(ctx, token)
if err != nil {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
if userInfo != nil {
if userInfo.UserStatus == entity.UserStatusDeleted {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
if userInfo != nil {
if userInfo.UserStatus == entity.UserStatusDeleted {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
ctx.Set(ctxUuidKey, userInfo)
}
ctx.Set(ctxUuidKey, userInfo)
}
ctx.Next()
}

View File

@ -24,6 +24,8 @@ const (
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
UserNotFound = "error.user.not_found"
UsernameInvalid = "error.user.username_invalid"
UsernameDuplicate = "error.user.username_duplicate"
EmailDuplicate = "error.email.duplicate"
EmailVerifyUrlExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"

View File

@ -11,7 +11,9 @@ import (
"github.com/go-playground/validator/v10"
"github.com/go-playground/validator/v10/translations/en"
"github.com/go-playground/validator/v10/translations/zh"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/base/translator"
myErrors "github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
)
@ -98,7 +100,7 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
Key: translator.GlobalTrans.Tr(m.Lang, fieldError.Field()),
Value: fieldError.Translate(m.Tran),
}
return errField, errors.New(fieldError.Translate(m.Tran))
return errField, myErrors.BadRequest(reason.RequestFormatError).WithMsg(fieldError.Translate(m.Tran))
}
}

24
internal/cli/dump.go Normal file
View File

@ -0,0 +1,24 @@
package cli
import (
"fmt"
"path/filepath"
"time"
"github.com/segmentfault/answer/internal/base/data"
"xorm.io/xorm/schemas"
)
// DumpAllData dump all database data to sql
func DumpAllData(dataConf *data.Database, dumpDataPath string) error {
db, err := data.NewDB(false, dataConf)
if err != nil {
return err
}
if err = db.Ping(); err != nil {
return err
}
name := filepath.Join(dumpDataPath, fmt.Sprintf("answer_dump_data_%s.sql", time.Now().Format("2006-01-02")))
return db.DumpAllToFile(name, schemas.MYSQL)
}

View File

@ -2,88 +2,150 @@ package cli
import (
"bufio"
"bytes"
"fmt"
"os"
"path/filepath"
"github.com/segmentfault/answer/assets"
"github.com/segmentfault/answer/configs"
"github.com/segmentfault/answer/i18n"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/entity"
"github.com/segmentfault/answer/pkg/dir"
)
var SuccessMsg = `
answer initialized successfully.
`
const (
DefaultConfigFileName = "config.yaml"
)
var HasBeenInitializedMsg = `
Has been initialized.
`
var (
ConfigFilePath = "/conf/"
UploadFilePath = "/upfiles/"
I18nPath = "/i18n/"
)
func InitConfig() {
exist, err := PathExists("data/config.yaml")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
if exist {
fmt.Println(HasBeenInitializedMsg)
os.Exit(0)
// 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()
installUploadDir()
installI18nBundle()
fmt.Println("install all initial environment done")
return
}
func installConfigFile() {
fmt.Println("[config-file] try to install...")
defaultConfigFile := filepath.Join(ConfigFilePath, DefaultConfigFileName)
// if config file already exists do nothing.
if CheckConfigFile(defaultConfigFile) {
fmt.Printf("[config-file] %s already exists\n", defaultConfigFile)
return
}
_, err = dir.CreatePathIsNotExist("data")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
if _, err := dir.CreatePathIsNotExist(ConfigFilePath); err != nil {
fmt.Printf("[config-file] create directory fail %s\n", err.Error())
return
}
WriterFile("data/config.yaml", string(configs.Config))
_, err = dir.CreatePathIsNotExist("data/i18n")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
fmt.Printf("[config-file] create directory success, config file is %s\n", defaultConfigFile)
if err := writerFile(defaultConfigFile, string(configs.Config)); err != nil {
fmt.Printf("[config-file] install fail %s\n", err.Error())
return
}
_, err = dir.CreatePathIsNotExist("data/upfiles")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
fmt.Printf("[config-file] install success\n")
}
func installUploadDir() {
fmt.Println("[upload-dir] try to install...")
if _, err := dir.CreatePathIsNotExist(UploadFilePath); err != nil {
fmt.Printf("[upload-dir] install fail %s\n", err.Error())
} else {
fmt.Printf("[upload-dir] install success, upload directory is %s\n", UploadFilePath)
}
}
func installI18nBundle() {
fmt.Println("[i18n] try to install i18n bundle...")
if _, err := dir.CreatePathIsNotExist(I18nPath); err != nil {
fmt.Println(err.Error())
return
}
i18nList, err := i18n.I18n.ReadDir(".")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
return
}
fmt.Printf("[i18n] find i18n bundle %d\n", len(i18nList))
for _, item := range i18nList {
path := fmt.Sprintf("data/i18n/%s", item.Name())
path := filepath.Join(I18nPath, item.Name())
content, err := i18n.I18n.ReadFile(item.Name())
if err != nil {
continue
}
WriterFile(path, string(content))
fmt.Printf("[i18n] install %s bundle...\n", item.Name())
err = writerFile(path, string(content))
if err != nil {
fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error())
} else {
fmt.Printf("[i18n] install %s bundle success\n", item.Name())
}
}
fmt.Println(SuccessMsg)
os.Exit(0)
}
func WriterFile(filePath, content string) error {
func writerFile(filePath, content string) error {
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return err
}
defer file.Close()
if err != nil {
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
}
write := bufio.NewWriter(file)
write.WriteString(content)
write.Flush()
return nil
}
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
// InitDB init db
func InitDB(dataConf *data.Database) (err error) {
fmt.Println("[database] try to initialize database")
db, err := data.NewDB(false, dataConf)
if err != nil {
return err
}
if os.IsNotExist(err) {
return false, nil
// check db connection
if err = db.Ping(); err != nil {
return err
}
return false, err
fmt.Println("[database] connect success")
exist, err := db.IsTableExist(&entity.User{})
if err != nil {
return err
}
if exist {
fmt.Println("[database] already exists")
return nil
}
// create table if not exist
s := &bytes.Buffer{}
s.Write(assets.AnswerSql)
_, err = db.Import(s)
if err != nil {
return err
}
fmt.Println("[database] execute sql successfully")
return nil
}

View File

@ -0,0 +1,25 @@
package cli
import (
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/pkg/dir"
)
func CheckConfigFile(configPath string) bool {
return dir.CheckPathExist(configPath)
}
func CheckUploadDir() bool {
return dir.CheckPathExist(UploadFilePath)
}
func CheckDB(dataConf *data.Database) bool {
db, err := data.NewDB(false, dataConf)
if err != nil {
return false
}
if err = db.Ping(); err != nil {
return false
}
return true
}

View File

@ -1,36 +0,0 @@
package cli
import (
"bytes"
"github.com/segmentfault/answer/assets"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/entity"
)
// InitDB init db
func InitDB(dataConf *data.Database) (err error) {
db := data.NewDB(false, dataConf)
// check db connection
err = db.Ping()
if err != nil {
return err
}
exist, err := db.IsTableExist(&entity.User{})
if err != nil {
return err
}
if exist {
return nil
}
// create table if not exist
s := &bytes.Buffer{}
s.Write(assets.AnswerSql)
_, err = db.Import(s)
if err != nil {
return err
}
return nil
}

View File

@ -1,21 +0,0 @@
package cli
import "fmt"
var usageDoc = `
Welcome to answer
VERSION:
1.0.0
USAGE:
answer [global options] command [command options] [arguments...]
COMMANDS:
init Init config, eg:./answer init
run Start web server, eg:./answer run -c data/config.yaml
`
func Usage() {
fmt.Println(usageDoc)
}

View File

@ -85,7 +85,7 @@ func (qc *QuestionController) GetQuestion(c *gin.Context) {
id := c.Query("id")
ctx := context.Background()
userID := middleware.GetLoginUserIDFromContext(c)
info, err := qc.questionService.GetQuestion(ctx, id, userID)
info, err := qc.questionService.GetQuestion(ctx, id, userID, true)
if err != nil {
handler.HandleResponse(c, err, nil)
return
@ -125,7 +125,7 @@ func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) {
// @Produce json
// @Param data body schema.QuestionSearch true "QuestionSearch"
// @Success 200 {string} string ""
// @Router /answer/api/v1/question/page [post]
// @Router /answer/api/v1/question/page [get]
func (qc *QuestionController) Index(ctx *gin.Context) {
req := &schema.QuestionSearch{}
if handler.BindAndCheck(ctx, req) {

View File

@ -28,15 +28,17 @@ func NewSearchController(searchService *service.SearchService) *SearchController
// @Produce json
// @Security ApiKeyAuth
// @Param q query string true "query string"
// @Param order query string true "order" Enums(newest,active,score,relevance)
// @Success 200 {object} handler.RespBody{data=schema.SearchListResp}
// @Router /answer/api/v1/search [get]
func (sc *SearchController) Search(ctx *gin.Context) {
var (
q string
page string
q,
order,
page,
size string
ok bool
dto schema.SearchDTO
ok bool
dto schema.SearchDTO
)
q, ok = ctx.GetQuery("q")
if len(q) == 0 || !ok {
@ -51,12 +53,17 @@ func (sc *SearchController) Search(ctx *gin.Context) {
if !ok {
size = "30"
}
order, ok = ctx.GetQuery("order")
if !ok || (order != "newest" && order != "active" && order != "score" && order != "relevance") {
order = "newest"
}
dto = schema.SearchDTO{
Query: q,
Page: converter.StringToInt(page),
Size: converter.StringToInt(size),
UserID: middleware.GetLoginUserIDFromContext(ctx),
Order: order,
}
resp, total, extra, err := sc.searchService.Search(ctx, &dto)

View File

@ -206,11 +206,11 @@ func (uc *UserController) UserLogout(ctx *gin.Context) {
// @Tags User
// @Accept json
// @Produce json
// @Param data body schema.UserRegister true "UserRegister"
// @Param data body schema.UserRegisterReq true "UserRegisterReq"
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/register/email [post]
func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
req := &schema.UserRegister{}
req := &schema.UserRegisterReq{}
if handler.BindAndCheck(ctx, req) {
return
}

View File

@ -56,7 +56,9 @@ func (sc *SiteInfoController) GetInterface(ctx *gin.Context) {
// @Router /answer/admin/api/siteinfo/general [put]
func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) {
req := schema.SiteGeneralReq{}
handler.BindAndCheck(ctx, &req)
if handler.BindAndCheck(ctx, &req) {
return
}
err := sc.siteInfoService.SaveSiteGeneral(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@ -72,7 +74,9 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) {
// @Router /answer/admin/api/siteinfo/interface [put]
func (sc *SiteInfoController) UpdateInterface(ctx *gin.Context) {
req := schema.SiteInterfaceReq{}
handler.BindAndCheck(ctx, &req)
if handler.BindAndCheck(ctx, &req) {
return
}
err := sc.siteInfoService.SaveSiteInterface(ctx, req)
handler.HandleResponse(ctx, err, nil)
}

View File

@ -16,6 +16,12 @@ var CmsQuestionSearchStatus = map[string]int{
"deleted": QuestionStatusDeleted,
}
var CmsQuestionSearchStatusIntToString = map[int]string{
QuestionStatusAvailable: "available",
QuestionStatusclosed: "closed",
QuestionStatusDeleted: "deleted",
}
type QuestionTag struct {
Question `xorm:"extends"`
TagRel `xorm:"extends"`

View File

@ -27,8 +27,8 @@ type Report struct {
ObjectType int `xorm:"not null default 0 comment('revision type') INT(11) object_type"`
ReportType int `xorm:"not null default 0 comment('report type') INT(11) report_type"`
Content string `xorm:"not null comment('report content') TEXT content"`
FlagedType int `xorm:"not null default 0 comment('flaged type') INT(11) flaged_type"`
FlagedContent string `xorm:"not null comment('flaged content') TEXT flaged_content"`
FlaggedType int `xorm:"not null default 0 comment('flaged type') INT(11) flaged_type"`
FlaggedContent string `xorm:"not null comment('flaged content') TEXT flaged_content"`
Status int `xorm:"not null default 1 comment('status(normal: 1; delete 2)') INT(11) status"`
}

View File

@ -0,0 +1,108 @@
package migrations
import (
"fmt"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
const minDBVersion = 0 // answer 1.0.0
// Migration describes on migration from lower version to high version
type Migration interface {
Description() string
Migrate(*xorm.Engine) error
}
type migration struct {
description string
migrate func(*xorm.Engine) error
}
// Description returns the migration's description
func (m *migration) Description() string {
return m.description
}
// Migrate executes the migration
func (m *migration) Migrate(x *xorm.Engine) error {
return m.migrate(x)
}
// NewMigration creates a new migration
func NewMigration(desc string, fn func(*xorm.Engine) error) Migration {
return &migration{description: desc, migrate: fn}
}
// Version version
type Version struct {
ID int `xorm:"not null pk autoincr comment('id') INT(11) id"`
VersionNumber int64 `xorm:"not null default 0 comment('version_number') INT(11) version_number"`
}
// TableName config table name
func (Version) TableName() string {
return "version"
}
// Use noopMigration when there is a migration that has been no-oped
var noopMigration = func(_ *xorm.Engine) error { return nil }
var migrations = []Migration{
// 0->1
NewMigration("this is first version, no operation", noopMigration),
}
// GetCurrentDBVersion returns the current db version
func GetCurrentDBVersion(engine *xorm.Engine) (int64, error) {
if err := engine.Sync(new(Version)); err != nil {
return -1, fmt.Errorf("sync version failed: %v", err)
}
currentVersion := &Version{ID: 1}
has, err := engine.Get(currentVersion)
if err != nil {
return -1, fmt.Errorf("get first version failed: %v", err)
}
if !has {
_, err := engine.InsertOne(&Version{ID: 1, VersionNumber: 0})
if err != nil {
return -1, fmt.Errorf("insert first version failed: %v", err)
}
return 0, nil
}
return currentVersion.VersionNumber, nil
}
// ExpectedVersion returns the expected db version
func ExpectedVersion() int64 {
return int64(minDBVersion + len(migrations))
}
// Migrate database to current version
func Migrate(engine *xorm.Engine) error {
currentDBVersion, err := GetCurrentDBVersion(engine)
if err != nil {
return err
}
expectedVersion := ExpectedVersion()
for currentDBVersion < expectedVersion {
log.Infof("[migrate] current db version is %d, try to migrate version %d, latest version is %d",
currentDBVersion, currentDBVersion+1, expectedVersion)
migrationFunc := migrations[currentDBVersion]
log.Infof("[migrate] try to migrate db version %d, description: %s", currentDBVersion+1, migrationFunc.Description())
if err := migrationFunc.Migrate(engine); err != nil {
log.Errorf("[migrate] migrate to db version %d failed: ", currentDBVersion+1, err.Error())
return err
}
log.Infof("[migrate] migrate to db version %d success", currentDBVersion+1)
if _, err := engine.Update(&Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil {
log.Errorf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
return err
}
currentDBVersion++
}
return nil
}

View File

@ -20,7 +20,7 @@ var (
func init() {
s, _ := os.LookupEnv("TESTDATA-DB-CONNECTION")
cache, _, _ := data.NewCache(log.Getlog(), &data.CacheConf{})
dataSource, _, _ = data.NewData(log.Getlog(), data.NewDB(true, &data.Database{
dataSource, _, _ = data.NewData(log.Getlog(), data.NewDB(true, &data.Database{
Connection: s,
}), cache)
log = log.Getlog()

View File

@ -66,7 +66,6 @@ func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Que
func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionId string) (err error) {
question := &entity.Question{}
qr.data.DB.ShowSQL()
_, err = qr.data.DB.Where("id =?", questionId).Incr("view_count", 1).Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -218,7 +217,6 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS
session = session.Limit(search.PageSize, offset)
session = session.Select("question.id,question.user_id,question.title,question.original_text,question.parsed_text,question.status,question.view_count,question.unique_view_count,question.vote_count,question.answer_count,question.collection_count,question.follow_count,question.accepted_answer_id,question.last_answer_id,question.created_at,question.updated_at,question.post_update_time,question.revision_id")
count, err = session.FindAndCount(&rows)
//spew.Dump("search", err, count, rows)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return rows, count, err

View File

@ -1,49 +0,0 @@
package revision
import (
"context"
"fmt"
"os"
"testing"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/entity"
repo2 "github.com/segmentfault/answer/internal/repo"
"github.com/segmentfault/answer/internal/repo/unique"
"github.com/segmentfault/pacman/log"
"github.com/stretchr/testify/assert"
)
var (
dataSource *data.Data
log log.log
)
func Init() {
s, _ := os.LookupEnv("TESTDATA-DB-CONNECTION")
fmt.Println(s)
cache, _, _ := data.NewCache(log.Getlog(), &data.CacheConf{})
dataSource, _, _ = data.NewData(log.Getlog(), data.NewDB(true, &data.Database{
Connection: s,
}), cache)
log = log.Getlog()
}
func TestRevisionRepo_AddRevision(t *testing.T) {
Init()
ctx := context.Background()
uniqueIDRepo := unique.NewUniqueIDRepo(log, dataSource)
questionRepo := repo2.NewQuestionRepo(log, dataSource, uniqueIDRepo)
question, _, _ := questionRepo.GetQuestion(ctx, "10010000000000048")
repo := NewRevisionRepo(log, dataSource, uniqueIDRepo)
revision := &entity.Revision{
UserID: question.UserID,
ObjectType: 0,
ObjectID: question.ID,
Title: question.Title,
Content: question.OriginalText,
Status: 1,
}
err := repo.AddRevision(ctx, revision, true)
assert.NoError(t, err)
}

View File

@ -2,6 +2,7 @@ package repo
import (
"context"
"fmt"
"strings"
"time"
@ -19,6 +20,35 @@ import (
"xorm.io/builder"
)
var (
q_fields = []string{
"`question`.`id`",
"`question`.`id` as `question_id`",
"`title`",
"`original_text`",
"`question`.`created_at`",
"`user_id`",
"`vote_count`",
"`answer_count`",
"0 as `accepted`",
"`question`.`status` as `status`",
"`post_update_time`",
}
a_fields = []string{
"`answer`.`id` as `id`",
"`question_id`",
"`question`.`title` as `title`",
"`answer`.`original_text` as `original_text`",
"`answer`.`created_at`",
"`answer`.`user_id` as `user_id`",
"`answer`.`vote_count` as `vote_count`",
"0 as `answer_count`",
"`adopted` as `accepted`",
"`answer`.`status` as `status`",
"`answer`.`created_at` as `post_update_time`",
}
)
// searchRepo tag repository
type searchRepo struct {
data *data.Data
@ -35,42 +65,49 @@ func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon
}
}
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, userID string, votes int, page, size int) (resp []schema.SearchResp, total int64, err error) {
// 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) {
var (
b *builder.Builder
ub *builder.Builder
b *builder.Builder
ub *builder.Builder
qfs = q_fields
afs = a_fields
argsQ = []interface{}{}
argsA = []interface{}{}
)
b = builder.Select(
"`question`.`id`",
"`question`.`id` as `question_id`",
"`title`",
"`original_text`",
"`question`.`created_at`",
"`user_id`",
"`vote_count`",
"`answer_count`",
"0 as `accepted`",
).From("`question`")
ub = builder.Select(
"`answer`.`id` as `id`",
"`question_id`",
"`question`.`title` as `title`",
"`answer`.`original_text` as `original_text`",
"`answer`.`created_at`",
"`answer`.`user_id` as `user_id`",
"`answer`.`vote_count` as `vote_count`",
"0 as `answer_count`",
"`adopted` as `accepted`",
).From("`answer`").
if order == "relevance" {
qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs)
afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
}
b = builder.MySQL().Select(qfs...).From("`question`")
ub = builder.MySQL().Select(afs...).From("`answer`").
LeftJoin("`question`", "`question`.id = `answer`.question_id")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted})
ub.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted})
argsQ = append(argsQ, entity.QuestionStatusDeleted)
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
for i, word := range words {
if i == 0 {
b.Where(builder.Like{"original_text", word})
b.Where(builder.Like{"title", word}).
Or(builder.Like{"original_text", word})
argsQ = append(argsQ, "%"+word+"%")
argsQ = append(argsQ, "%"+word+"%")
ub.Where(builder.Like{"`answer`.original_text", word})
argsA = append(argsA, "%"+word+"%")
} else {
b.Or(builder.Like{"original_text", word})
b.Or(builder.Like{"title", word}).
Or(builder.Like{"original_text", word})
argsQ = append(argsQ, "%"+word+"%")
argsQ = append(argsQ, "%"+word+"%")
ub.Or(builder.Like{"`answer`.original_text", word})
argsA = append(argsA, "%"+word+"%")
}
}
@ -78,32 +115,58 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
if tagID != "" {
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
Where(builder.Eq{"tag_rel.tag_id": tagID})
argsQ = append(argsQ, tagID)
}
// check user
if userID != "" {
b.Where(builder.Eq{"question.user_id": userID})
ub.Where(builder.Eq{"answer.user_id": userID})
argsQ = append(argsQ, userID)
argsA = append(argsA, userID)
}
// check vote
if votes == 0 {
b.Where(builder.Eq{"question.vote_count": votes})
ub.Where(builder.Eq{"answer.vote_count": votes})
argsQ = append(argsQ, votes)
argsA = append(argsA, votes)
} else if votes > 0 {
b.Where(builder.Gte{"question.vote_count": votes})
ub.Where(builder.Gte{"answer.vote_count": votes})
argsQ = append(argsQ, votes)
argsA = append(argsA, votes)
}
b = b.Union("all", ub)
_, _, err = b.ToSQL()
querySql, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
if err != nil {
return
}
res, err := sr.data.DB.OrderBy("created_at DESC").Limit(size, page).Query(b)
queryArgs := []interface{}{}
countArgs := []interface{}{}
tr, err := sr.data.DB.Query(builder.Select("count(*) total").From(b, "c"))
queryArgs = append(queryArgs, querySql)
queryArgs = append(queryArgs, argsQ...)
queryArgs = append(queryArgs, argsA...)
countArgs = append(countArgs, countSql)
countArgs = append(countArgs, argsQ...)
countArgs = append(countArgs, argsA...)
res, err := sr.data.DB.Query(queryArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Query(countArgs...)
if len(tr) != 0 {
total = converter.StringToInt64(string(tr[0]["total"]))
}
@ -116,41 +179,74 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
}
}
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int) (resp []schema.SearchResp, total int64, err error) {
b := builder.Select(
"`id`",
"`id` as `question_id`",
"`title`",
"`original_text`",
"`created_at`",
"`user_id`",
"`vote_count`",
"`answer_count`",
"0 as `accepted`",
).From("question")
// 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) {
var (
qfs = q_fields
args = []interface{}{}
)
if order == "relevance" {
qfs, args = addRelevanceField([]string{"title", "original_text"}, words, qfs)
}
b := builder.MySQL().Select(qfs...).From("question")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted})
args = append(args, entity.QuestionStatusDeleted)
for i, word := range words {
if i == 0 {
b.Where(builder.Like{"original_text", word})
b.Where(builder.Like{"title", word}).
Or(builder.Like{"original_text", word})
args = append(args, "%"+word+"%")
args = append(args, "%"+word+"%")
} else {
b.Or(builder.Like{"original_text", word})
args = append(args, "%"+word+"%")
}
}
// check need filter has not accepted
if limitNoAccepted {
b.And(builder.Eq{"accepted_answer_id": 0})
args = append(args, 0)
}
if answers == 0 {
b.And(builder.Eq{"answer_count": 0})
args = append(args, 0)
} else if answers > 0 {
b.And(builder.Gte{"answer_count": answers})
args = append(args, answers)
}
res, err := sr.data.DB.OrderBy("created_at DESC").Limit(size, page).Query(b)
queryArgs := []interface{}{}
countArgs := []interface{}{}
querySql, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
if err != nil {
return
}
queryArgs = append(queryArgs, querySql)
queryArgs = append(queryArgs, args...)
countArgs = append(countArgs, countSql)
countArgs = append(countArgs, args...)
res, err := sr.data.DB.Query(queryArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Query(countArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Query(builder.Select("count(*) total").From(b, "c"))
if len(tr) != 0 {
total = converter.StringToInt64(string(tr[0]["total"]))
}
@ -165,39 +261,70 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
return
}
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int) (resp []schema.SearchResp, total int64, err error) {
b := builder.Select(
"`answer`.`id` as `id`",
"`question_id`",
"`question`.`title` as `title`",
"`answer`.`original_text` as `original_text`",
"`answer`.`created_at`",
"`answer`.`user_id` as `user_id`",
"`answer`.`vote_count` as `vote_count`",
"0 as `answer_count`",
"`adopted` as `accepted`",
).From("`answer`").
// 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) {
var (
afs = a_fields
args = []interface{}{}
)
if order == "relevance" {
afs, args = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
}
b := builder.MySQL().Select(afs...).From("`answer`").
LeftJoin("`question`", "`question`.id = `answer`.question_id")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted})
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
for i, word := range words {
if i == 0 {
b.Where(builder.Like{"`answer`.original_text", word})
args = append(args, "%"+word+"%")
} else {
b.Or(builder.Like{"`answer`.original_text", word})
args = append(args, "%"+word+"%")
}
}
if limitAccepted {
b.Where(builder.Eq{"adopted": 2})
b.Where(builder.Eq{"adopted": schema.Answer_Adopted_Enable})
args = append(args, schema.Answer_Adopted_Enable)
}
if questionID != "" {
b.Where(builder.Eq{"question_id": questionID})
args = append(args, questionID)
}
res, err := sr.data.DB.OrderBy("created_at DESC").Limit(size, page).Query(b)
queryArgs := []interface{}{}
countArgs := []interface{}{}
querySql, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL()
if err != nil {
return
}
queryArgs = append(queryArgs, querySql)
queryArgs = append(queryArgs, args...)
countArgs = append(countArgs, countSql)
countArgs = append(countArgs, args...)
res, err := sr.data.DB.Query(queryArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Query(countArgs...)
if err != nil {
return
}
tr, err := sr.data.DB.Query(builder.Select("count(*) total").From(b, "c"))
total = converter.StringToInt64(string(tr[0]["total"]))
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
@ -210,10 +337,28 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAc
return
}
func (sr *searchRepo) parseOrder(ctx context.Context, order string) (res string) {
switch order {
case "newest":
res = "created_at desc"
case "active":
res = "post_update_time desc"
case "score":
res = "vote_count desc"
case "relevance":
res = "relevance desc"
default:
res = "created_at desc"
}
return
}
func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) (resp []schema.SearchResp, err error) {
for _, r := range res {
var (
objectKey string
objectKey,
status string
tags []schema.TagResp
tagsEntity []entity.Tag
object schema.SearchObject
@ -246,6 +391,22 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
return
}
_ = copier.Copy(&tags, tagsEntity)
switch objectKey {
case "question":
for k, v := range entity.CmsQuestionSearchStatus {
if v == converter.StringToInt(string(r["status"])) {
status = k
break
}
}
case "answer":
for k, v := range entity.CmsAnswerSearchStatus {
if v == converter.StringToInt(string(r["status"])) {
status = k
break
}
}
}
object = schema.SearchObject{
ID: string(r["id"]),
@ -257,6 +418,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
VoteCount: converter.StringToInt(string(r["vote_count"])),
Accepted: string(r["accepted"]) == "2",
AnswerCount: converter.StringToInt(string(r["answer_count"])),
StatusStr: status,
}
resp = append(resp, schema.SearchResp{
ObjectType: objectKey,
@ -288,3 +450,36 @@ func cutOutParsedText(parsedText string) string {
}
return parsedText
}
func addRelevanceField(search_fields, words, fields []string) (res []string, args []interface{}) {
var relevanceRes = []string{}
args = []interface{}{}
for _, search_field := range search_fields {
var (
relevance = "(LENGTH(" + search_field + ") - LENGTH(%s))"
replacement = "REPLACE(%s, ?, '')"
replace_field = search_field
replaced string
argsField = []interface{}{}
)
res = fields
for i, word := range words {
if i == 0 {
argsField = append(argsField, word)
replaced = fmt.Sprintf(replacement, replace_field)
} else {
argsField = append(argsField, word)
replaced = fmt.Sprintf(replacement, replaced)
}
}
args = append(args, argsField...)
relevance = fmt.Sprintf(relevance, replaced)
relevanceRes = append(relevanceRes, relevance)
}
res = append(res, "("+strings.Join(relevanceRes, " + ")+") as relevance")
return
}

View File

@ -75,7 +75,7 @@ func (tr *tagRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagIn
// 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("LIKE ?", name+"%")
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)

View File

@ -108,7 +108,7 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err
// UpdateInfo update user info
func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {
_, err = ur.data.DB.Where("id = ?", userInfo.ID).
Cols("display_name", "avatar", "bio", "bio_html", "website", "location").Update(userInfo)
Cols("username", "display_name", "avatar", "bio", "bio_html", "website", "location").Update(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}

View File

@ -3,15 +3,17 @@ package router
import (
"embed"
"fmt"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/ui"
"github.com/segmentfault/pacman/log"
"io/fs"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/ui"
"github.com/segmentfault/pacman/log"
)
const UIIndexFilePath = "build/index.html"
const UIRootFilePath = "build"
const UIStaticPath = "build/static"
// UIRouter is an interface that provides ui static file routers
@ -66,14 +68,26 @@ func (a *UIRouter) Register(r *gin.Engine) {
// specify the not router for default routes and redirect
r.NoRoute(func(c *gin.Context) {
index, err := ui.Build.ReadFile(UIIndexFilePath)
name := c.Request.URL.Path
filePath := ""
var file []byte
var err error
switch name {
case "/favicon.ico":
c.Header("content-type", "image/vnd.microsoft.icon")
filePath = UIRootFilePath + name
case "/manifest.json":
filePath = UIRootFilePath + name
default:
filePath = UIIndexFilePath
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.Header("content-type", "text/html;charset=utf-8")
c.String(http.StatusOK, string(index))
c.String(http.StatusOK, string(file))
})
}

View File

@ -57,6 +57,7 @@ type QuestionBaseInfo struct {
AnswerCount int `json:"answer_count" xorm:"answer_count"` // 回复总数
CollectionCount int `json:"collection_count" xorm:"collection_count"` // 收藏总数
FollowCount int `json:"follow_count" xorm:"follow_count"` // 关注数
Status string `json:"status"`
AcceptedAnswer bool `json:"accepted_answer"`
}
@ -110,6 +111,7 @@ type Operation struct {
Operation_Type string `json:"operation_type"`
Operation_Description string `json:"operation_description"`
Operation_Msg string `json:"operation_msg"`
Operation_Time int64 `json:"operation_time"`
}
type GetCloseTypeResp struct {
@ -150,6 +152,7 @@ type UserQuestionInfo struct {
CollectionCount int `json:"collection_count"`
CreateTime int `json:"create_time"`
AcceptedAnswerId string `json:"accepted_answer_id"`
Status string `json:"status"`
}
type QuestionSearch struct {

View File

@ -1,8 +1,9 @@
package schema
import (
"github.com/segmentfault/answer/internal/base/constant"
"time"
"github.com/segmentfault/answer/internal/base/constant"
)
// AddReportReq add report request
@ -41,9 +42,9 @@ type GetReportTypeResp struct {
// ReportHandleReq request handle request
type ReportHandleReq struct {
ID string `validate:"required" comment:"report id" form:"id" json:"id"`
FlagedType int `validate:"required" comment:"flaged type" form:"flaged_type" json:"flaged_type"`
FlagedContent string `validate:"omitempty" comment:"flaged content" form:"flaged_content" json:"flaged_content"`
ID string `validate:"required" comment:"report id" form:"id" json:"id"`
FlaggedType int `validate:"required" comment:"flagged type" form:"flagged_type" json:"flagged_type"`
FlaggedContent string `validate:"omitempty" comment:"flagged content" form:"flagged_content" json:"flagged_content"`
}
// GetReportListPageDTO report list data transfer object
@ -60,9 +61,9 @@ type GetReportListPageResp struct {
ReportedUser *UserBasicInfo `json:"reported_user"`
ReportUser *UserBasicInfo `json:"report_user"`
Content string `json:"content"`
FlagedContent string `json:"flaged_content"`
OType string `json:"object_type"`
Content string `json:"content"`
FlaggedContent string `json:"flagged_content"`
OType string `json:"object_type"`
ObjectID string `json:"-"`
QuestionID string `json:"question_id"`
@ -79,15 +80,15 @@ type GetReportListPageResp struct {
UpdatedAt time.Time `json:"_"`
UpdatedAtParsed int64 `json:"updated_at"`
Reason *ReasonItem `json:"reason"`
FlagedReason *ReasonItem `json:"flaged_reason"`
Reason *ReasonItem `json:"reason"`
FlaggedReason *ReasonItem `json:"flagged_reason"`
UserID string `json:"-"`
ReportedUserID string `json:"-"`
Status int `json:"-"`
ObjectType int `json:"-"`
ReportType int `json:"-"`
FlagedType int `json:"-"`
FlaggedType int `json:"-"`
}
// Format format result

View File

@ -7,6 +7,7 @@ type SearchDTO struct {
UserID string
Page int
Size int
Order string
}
type SearchObject struct {
@ -21,11 +22,13 @@ type SearchObject struct {
UserInfo *UserBasicInfo `json:"user_info"`
// tags
Tags []TagResp `json:"tags"`
// Status
StatusStr string `json:"status"`
}
type TagResp struct {
SlugName string `json:"display_name"`
DisplayName string `json:"slug_name"`
SlugName string `json:"slug_name"`
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"`
}

View File

@ -2,12 +2,14 @@ package schema
import (
"encoding/json"
"regexp"
"github.com/davecgh/go-spew/spew"
"github.com/jinzhu/copier"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/base/validator"
"github.com/segmentfault/answer/internal/entity"
"github.com/segmentfault/answer/pkg/checker"
"github.com/segmentfault/pacman/errors"
)
// UserVerifyEmailReq user verify email request
@ -133,7 +135,6 @@ func (r *GetOtherUserInfoByUsernameResp) GetFromUserEntity(userInfo *entity.User
if ok {
r.Status = statusShow
}
spew.Dump(userInfo)
if userInfo.MailStatus == entity.EmailStatusToBeVerified {
statusMsgShow, ok := UserStatusShowMsg[11]
if ok {
@ -146,8 +147,6 @@ func (r *GetOtherUserInfoByUsernameResp) GetFromUserEntity(userInfo *entity.User
}
}
spew.Dump(r)
}
const (
@ -183,10 +182,10 @@ type UserEmailLogin struct {
CaptchaCode string `json:"captcha_code" ` // captcha_code
}
// Register
type UserRegister struct {
// UserRegisterReq user register request
type UserRegisterReq struct {
// name
Name string `validate:"required,gt=5,lte=50" json:"name"`
Name string `validate:"required,gt=4,lte=30" json:"name"`
// email
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" `
// password
@ -194,7 +193,7 @@ type UserRegister struct {
IP string `json:"-" `
}
func (u *UserRegister) Check() (errField *validator.ErrorField, err error) {
func (u *UserRegisterReq) Check() (errField *validator.ErrorField, err error) {
// TODO i18n
err = checker.PassWordCheck(8, 32, 0, u.Pass)
if err != nil {
@ -228,6 +227,8 @@ func (u *UserModifyPassWordRequest) Check() (errField *validator.ErrorField, err
type UpdateInfoRequest struct {
// display_name
DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"`
// username
Username string `validate:"omitempty,gt=0,lte=30" json:"username"`
// avatar
Avatar string `validate:"omitempty,gt=0,lte=500" json:"avatar"`
// bio
@ -242,6 +243,21 @@ type UpdateInfoRequest struct {
UserId string `json:"-" `
}
func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, 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
}
}
return nil, nil
}
type UserRetrievePassWordRequest struct {
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail
CaptchaID string `json:"captcha_id" ` // captcha_id

View File

@ -61,13 +61,11 @@ func (cs *CollectionService) CollectionSwitch(ctx context.Context, dto *schema.C
return nil, err
}
if !has {
defaultGroup, err := cs.collectionGroupRepo.AddCollectionDefaultGroup(ctx, dto.UserID)
dbdefaultGroup, err := cs.collectionGroupRepo.AddCollectionDefaultGroup(ctx, dto.UserID)
if err != nil {
return nil, err
}
dto.GroupID = defaultGroup.ID
dto.GroupID = dbdefaultGroup.ID
} else {
dto.GroupID = defaultGroup.ID
}

View File

@ -164,6 +164,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUser
operation.Operation_Type = closeinfo.Name
operation.Operation_Description = closeinfo.Description
operation.Operation_Msg = closemsg.CloseMsg
operation.Operation_Time = metainfo.CreatedAt.Unix()
showinfo.Operation = operation
}

View File

@ -149,7 +149,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
log.Error("user IncreaseQuestionCount error", err.Error())
}
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID)
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false)
return
}
@ -229,20 +229,23 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
return
}
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID)
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false)
return
}
// GetQuestion get question one
func (qs *QuestionService) GetQuestion(ctx context.Context, id, loginUserID string) (resp *schema.QuestionInfo, err error) {
func (qs *QuestionService) GetQuestion(ctx context.Context, id, loginUserID string, addpv bool) (resp *schema.QuestionInfo, err error) {
question, err := qs.questioncommon.Info(ctx, id, loginUserID)
if err != nil {
return
}
err = qs.questioncommon.UpdataPv(ctx, id)
if err != nil {
log.Error("UpdataPv", err)
if addpv {
err = qs.questioncommon.UpdataPv(ctx, id)
if err != nil {
log.Error("UpdataPv", err)
}
}
question.MemberActions = permission.GetQuestionPermission(loginUserID, question.UserId)
return question, nil
}
@ -273,6 +276,10 @@ func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order s
for _, item := range questionlist {
info := &schema.UserQuestionInfo{}
_ = copier.Copy(info, item)
status, ok := entity.CmsQuestionSearchStatusIntToString[item.Status]
if ok {
info.Status = status
}
userlist = append(userlist, info)
}
return userlist, count, nil
@ -446,6 +453,10 @@ func (qs *QuestionService) SearchByTitleLike(ctx context.Context, title string,
item.AnswerCount = question.AnswerCount
item.CollectionCount = question.CollectionCount
item.FollowCount = question.FollowCount
status, ok := entity.CmsQuestionSearchStatusIntToString[question.Status]
if ok {
item.Status = status
}
if question.AcceptedAnswerID != "0" {
item.AcceptedAnswer = true
}
@ -458,7 +469,7 @@ func (qs *QuestionService) SearchByTitleLike(ctx context.Context, title string,
// SimilarQuestion
func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
list := make([]*schema.QuestionInfo, 0)
questionInfo, err := qs.GetQuestion(ctx, questionID, loginUserID)
questionInfo, err := qs.GetQuestion(ctx, questionID, loginUserID, false)
if err != nil {
return list, 0, err
}

View File

@ -62,10 +62,10 @@ func (rs *ReportBackyardService) ListReportPage(ctx context.Context, dto schema.
flags []entity.Report
total int64
flagedUserIds,
flaggedUserIds,
userIds []string
flagedUsers,
flaggedUsers,
users map[string]*schema.UserBasicInfo
)
@ -78,18 +78,18 @@ func (rs *ReportBackyardService) ListReportPage(ctx context.Context, dto schema.
_ = copier.Copy(&resp, flags)
for _, r := range resp {
flagedUserIds = append(flagedUserIds, r.ReportedUserID)
flaggedUserIds = append(flaggedUserIds, r.ReportedUserID)
userIds = append(userIds, r.UserID)
r.Format()
}
// flaged users
flagedUsers, err = rs.commonUser.BatchUserBasicInfoByID(ctx, flagedUserIds)
// flagged users
flaggedUsers, err = rs.commonUser.BatchUserBasicInfoByID(ctx, flaggedUserIds)
// flag users
users, err = rs.commonUser.BatchUserBasicInfoByID(ctx, userIds)
for _, r := range resp {
r.ReportedUser = flagedUsers[r.ReportedUserID]
r.ReportedUser = flaggedUsers[r.ReportedUserID]
r.ReportUser = users[r.UserID]
}
@ -102,9 +102,9 @@ func (rs *ReportBackyardService) HandleReported(ctx context.Context, req schema.
var (
reported = entity.Report{}
handleData = entity.Report{
FlagedContent: req.FlagedContent,
FlagedType: req.FlagedType,
Status: entity.ReportStatusCompleted,
FlaggedContent: req.FlaggedContent,
FlaggedType: req.FlaggedType,
Status: entity.ReportStatusCompleted,
}
exist = false
)
@ -203,11 +203,11 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
}
err = rs.configRepo.GetConfigById(r.ReportType, r.Reason)
}
if r.FlagedType > 0 {
r.FlagedReason = &schema.ReasonItem{
ReasonType: r.FlagedType,
if r.FlaggedType > 0 {
r.FlaggedReason = &schema.ReasonItem{
ReasonType: r.FlaggedType,
}
_ = rs.configRepo.GetConfigById(r.FlagedType, r.FlagedReason)
_ = rs.configRepo.GetConfigById(r.FlaggedType, r.FlaggedReason)
}
res[i] = r

View File

@ -2,6 +2,7 @@ package report_handle_backyard
import (
"context"
"github.com/segmentfault/answer/internal/service/config"
"github.com/segmentfault/answer/internal/base/constant"
@ -46,23 +47,23 @@ func (rh *ReportHandle) HandleObject(ctx context.Context, reported entity.Report
}
switch objectKey {
case "question":
switch req.FlagedType {
switch req.FlaggedType {
case reasonDelete:
err = rh.questionCommon.RemoveQuestion(ctx, &schema.RemoveQuestionReq{ID: objectID})
case reasonClose:
err = rh.questionCommon.CloseQuestion(ctx, &schema.CloseQuestionReq{
ID: objectID,
CloseType: req.FlagedType,
CloseMsg: req.FlagedContent,
CloseType: req.FlaggedType,
CloseMsg: req.FlaggedContent,
})
}
case "answer":
switch req.FlagedType {
switch req.FlaggedType {
case reasonDelete:
err = rh.questionCommon.RemoveAnswer(ctx, objectID)
}
case "comment":
switch req.FlagedType {
switch req.FlaggedType {
case reasonDelete:
err = rh.commentRepo.RemoveComment(ctx, objectID)
rh.sendNotification(ctx, reportedUserID, objectID, constant.YourCommentWasDeleted)

View File

@ -9,10 +9,11 @@ import (
)
type AcceptedAnswerSearch struct {
repo search_common.SearchRepo
w string
page int
size int
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewAcceptedAnswerSearch(repo search_common.SearchRepo) *AcceptedAnswerSearch {
@ -40,6 +41,7 @@ func (s *AcceptedAnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
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) {
@ -49,5 +51,5 @@ func (s *AcceptedAnswerSearch) Search(ctx context.Context) (resp []schema.Search
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, true, "", s.page, s.size)
return s.repo.SearchAnswers(ctx, words, true, "", s.page, s.size, s.order)
}

View File

@ -8,10 +8,11 @@ import (
)
type AnswerSearch struct {
repo search_common.SearchRepo
w string
page int
size int
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewAnswerSearch(repo search_common.SearchRepo) *AnswerSearch {
@ -39,6 +40,7 @@ func (s *AnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
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) {
@ -48,5 +50,5 @@ func (s *AnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, to
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, false, "", s.page, s.size)
return s.repo.SearchAnswers(ctx, words, false, "", s.page, s.size, s.order)
}

View File

@ -11,11 +11,12 @@ import (
)
type AnswersSearch struct {
repo search_common.SearchRepo
exp int
w string
page int
size int
repo search_common.SearchRepo
exp int
w string
page int
size int
order string
}
func NewAnswersSearch(repo search_common.SearchRepo) *AnswersSearch {
@ -49,6 +50,7 @@ func (s *AnswersSearch) Parse(dto *schema.SearchDTO) (ok bool) {
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
@ -59,5 +61,5 @@ func (s *AnswersSearch) Search(ctx context.Context) (resp []schema.SearchResp, t
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, false, s.exp, s.page, s.size)
return s.repo.SearchQuestions(ctx, words, false, s.exp, s.page, s.size, s.order)
}

View File

@ -17,6 +17,7 @@ type AuthorSearch struct {
w string
page int
size int
order string
}
func NewAuthorSearch(repo search_common.SearchRepo, userCommon *usercommon.UserCommon) *AuthorSearch {
@ -65,6 +66,7 @@ func (s *AuthorSearch) Parse(dto *schema.SearchDTO) (ok bool) {
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
@ -82,7 +84,7 @@ func (s *AuthorSearch) Search(ctx context.Context) (resp []schema.SearchResp, to
words = words[:4]
}
resp, total, err = s.repo.SearchContents(ctx, words, "", s.exp, -1, s.page, s.size)
resp, total, err = s.repo.SearchContents(ctx, words, "", s.exp, -1, s.page, s.size, s.order)
return
}

View File

@ -9,11 +9,12 @@ import (
)
type InQuestionSearch struct {
repo search_common.SearchRepo
w string
exp string
page int
size int
repo search_common.SearchRepo
w string
exp string
page int
size int
order string
}
func NewInQuestionSearch(repo search_common.SearchRepo) *InQuestionSearch {
@ -47,6 +48,7 @@ func (s *InQuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) {
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) {
@ -59,5 +61,5 @@ func (s *InQuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, false, s.exp, s.page, s.size)
return s.repo.SearchAnswers(ctx, words, false, s.exp, s.page, s.size, s.order)
}

View File

@ -9,10 +9,11 @@ import (
)
type NotAcceptedQuestion struct {
repo search_common.SearchRepo
w string
page int
size int
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewNotAcceptedQuestion(repo search_common.SearchRepo) *NotAcceptedQuestion {
@ -40,6 +41,7 @@ func (s *NotAcceptedQuestion) Parse(dto *schema.SearchDTO) (ok bool) {
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *NotAcceptedQuestion) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
@ -52,5 +54,5 @@ func (s *NotAcceptedQuestion) Search(ctx context.Context) (resp []schema.SearchR
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, true, -1, s.page, s.size)
return s.repo.SearchQuestions(ctx, words, true, -1, s.page, s.size, s.order)
}

View File

@ -9,10 +9,11 @@ import (
)
type ObjectSearch struct {
repo search_common.SearchRepo
w string
page int
size int
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewObjectSearch(repo search_common.SearchRepo) *ObjectSearch {
@ -33,6 +34,7 @@ func (s *ObjectSearch) Parse(dto *schema.SearchDTO) (ok bool) {
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *ObjectSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
@ -41,5 +43,5 @@ func (s *ObjectSearch) Search(ctx context.Context) (resp []schema.SearchResp, to
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchContents(ctx, words, "", "", -1, s.page, s.size)
return s.repo.SearchContents(ctx, words, "", "", -1, s.page, s.size, s.order)
}

View File

@ -8,10 +8,11 @@ import (
)
type QuestionSearch struct {
repo search_common.SearchRepo
w string
page int
size int
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewQuestionSearch(repo search_common.SearchRepo) *QuestionSearch {
@ -39,6 +40,7 @@ func (s *QuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) {
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
@ -49,5 +51,5 @@ func (s *QuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp,
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, false, -1, s.page, s.size)
return s.repo.SearchQuestions(ctx, words, false, -1, s.page, s.size, s.order)
}

View File

@ -11,11 +11,12 @@ import (
)
type ScoreSearch struct {
repo search_common.SearchRepo
exp int
w string
page int
size int
repo search_common.SearchRepo
exp int
w string
page int
size int
order string
}
func NewScoreSearch(repo search_common.SearchRepo) *ScoreSearch {
@ -44,6 +45,7 @@ func (s *ScoreSearch) Parse(dto *schema.SearchDTO) (ok bool) {
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *ScoreSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
@ -56,6 +58,6 @@ func (s *ScoreSearch) Search(ctx context.Context) (resp []schema.SearchResp, tot
words = words[:4]
}
resp, total, err = s.repo.SearchContents(ctx, words, "", "", s.exp, s.page, s.size)
resp, total, err = s.repo.SearchContents(ctx, words, "", "", s.exp, s.page, s.size, s.order)
return
}

View File

@ -21,6 +21,7 @@ type TagSearch struct {
w string
userID string
Extra schema.GetTagPageResp
order string
}
func NewTagSearch(repo search_common.SearchRepo, tagRepo tagcommon.TagRepo, followCommon activity_common.FollowRepo) *TagSearch {
@ -53,6 +54,7 @@ func (ts *TagSearch) Parse(dto *schema.SearchDTO) (ok bool) {
ts.page = dto.Page
ts.size = dto.Size
ts.userID = dto.UserID
ts.order = dto.Order
return ok
}
@ -90,7 +92,7 @@ func (ts *TagSearch) Search(ctx context.Context) (resp []schema.SearchResp, tota
words = words[:4]
}
resp, total, err = ts.repo.SearchContents(ctx, words, tag.ID, "", -1, ts.page, ts.size)
resp, total, err = ts.repo.SearchContents(ctx, words, tag.ID, "", -1, ts.page, ts.size, ts.order)
return
}

View File

@ -9,9 +9,10 @@ import (
)
type ViewsSearch struct {
repo search_common.SearchRepo
exp string
q string
repo search_common.SearchRepo
exp string
q string
order string
}
func NewViewsSearch(repo search_common.SearchRepo) *ViewsSearch {
@ -38,6 +39,7 @@ func (s *ViewsSearch) Parse(dto *schema.SearchDTO) (ok bool) {
q = strings.TrimSpace(q)
s.exp = exp
s.q = q
s.order = dto.Order
return
}
func (s *ViewsSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {

View File

@ -8,10 +8,11 @@ import (
)
type WithinSearch struct {
repo search_common.SearchRepo
w string
page int
size int
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewWithinSearch(repo search_common.SearchRepo) *WithinSearch {
@ -49,9 +50,10 @@ func (s *WithinSearch) Parse(dto *schema.SearchDTO) (ok bool) {
s.w = string(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *WithinSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
return s.repo.SearchContents(ctx, []string{s.w}, "", "", -1, s.page, s.size)
return s.repo.SearchContents(ctx, []string{s.w}, "", "", -1, s.page, s.size, s.order)
}

View File

@ -6,7 +6,7 @@ import (
)
type SearchRepo interface {
SearchContents(ctx context.Context, words []string, tagID, userID string, votes int, page, size int) (resp []schema.SearchResp, total int64, err error)
SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int) (resp []schema.SearchResp, total int64, err error)
SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int) (resp []schema.SearchResp, total int64, err error)
SearchContents(ctx context.Context, words []string, tagID, userID string, votes, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
}

View File

@ -57,6 +57,10 @@ func NewSearchService(
func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp []schema.SearchResp, total int64, extra interface{}, err error) {
extra = nil
if dto.Page < 1 {
dto.Page = 1
}
switch {
case ss.tagSearch.Parse(dto):
resp, total, err = ss.tagSearch.Search(ctx)

View File

@ -2,7 +2,9 @@ package service
import (
"context"
"encoding/hex"
"fmt"
"math/rand"
"regexp"
"strings"
@ -17,7 +19,6 @@ import (
"github.com/segmentfault/answer/internal/service/service_config"
usercommon "github.com/segmentfault/answer/internal/service/user_common"
"github.com/segmentfault/answer/pkg/checker"
"github.com/segmentfault/answer/pkg/uid"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"golang.org/x/crypto/bcrypt"
@ -233,18 +234,28 @@ func (us *UserService) UserModifyPassWord(ctx context.Context, request *schema.U
return nil
}
// UpdateInfo
func (us *UserService) UpdateInfo(ctx context.Context, request *schema.UpdateInfoRequest) error {
userinfo := entity.User{}
userinfo.ID = request.UserId
userinfo.Avatar = request.Avatar
userinfo.DisplayName = request.DisplayName
userinfo.Bio = request.Bio
userinfo.BioHtml = request.BioHtml
userinfo.Location = request.Location
userinfo.Website = request.Website
err := us.userRepo.UpdateInfo(ctx, &userinfo)
if err != nil {
// UpdateInfo update user info
func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (err error) {
if len(req.Username) > 0 {
userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username)
if err != nil {
return err
}
if exist && userInfo.ID != req.UserId {
return errors.BadRequest(reason.UsernameDuplicate)
}
}
userInfo := entity.User{}
userInfo.ID = req.UserId
userInfo.Avatar = req.Avatar
userInfo.DisplayName = req.DisplayName
userInfo.Bio = req.Bio
userInfo.BioHtml = req.BioHtml
userInfo.Location = req.Location
userInfo.Website = req.Website
userInfo.Username = req.Username
if err := us.userRepo.UpdateInfo(ctx, &userInfo); err != nil {
return err
}
return nil
@ -259,7 +270,7 @@ func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, er
}
// UserRegisterByEmail user register
func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegister) (
func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) (
resp *schema.GetUserResp, err error) {
_, has, err := us.userRepo.GetByEmail(ctx, registerUserInfo.Email)
if err != nil {
@ -276,7 +287,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
if err != nil {
return nil, err
}
userInfo.Username, err = us.makeUserName(ctx, registerUserInfo.Name)
userInfo.Username, err = us.makeUsername(ctx, registerUserInfo.Name)
if err != nil {
return nil, err
}
@ -408,58 +419,42 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
return resp, nil
}
// makeUserName
// Generate a unique Username based on the NickName
// todo Waiting to be realized
func (us *UserService) makeUserName(ctx context.Context, userName string) (string, error) {
userName = us.formatUserName(ctx, userName)
_, has, err := us.userRepo.GetByUsername(ctx, userName)
if err != nil {
return "", err
}
//If the user name is duplicated, it is generated recursively from the new one.
if has {
userName = uid.IDStr()
return us.makeUserName(ctx, userName)
}
return userName, nil
}
// formatUserName
// Generate a Username through a nickname
func (us *UserService) formatUserName(ctx context.Context, Name string) string {
formatName, pass := us.CheckUserName(ctx, Name)
if !pass {
//todo 重新给用户 生成随机 username
return uid.IDStr()
}
return formatName
}
func (us *UserService) CheckUserName(ctx context.Context, name string) (string, bool) {
name = strings.Replace(name, " ", "_", -1)
name = strings.ToLower(name)
//Chinese processing
has := checker.IsChinese(name)
if has {
str, err := pinyin.New(name).Split("").Mode(pinyin.WithoutTone).Convert()
// makeUsername
// Generate a unique Username based on the displayName
func (us *UserService) makeUsername(ctx context.Context, displayName string) (username string, err error) {
// Chinese processing
if has := checker.IsChinese(displayName); has {
str, err := pinyin.New(displayName).Split("").Mode(pinyin.WithoutTone).Convert()
if err != nil {
log.Error("pinyin Error", err)
return "", false
return "", err
} else {
name = str
displayName = str
}
}
//Format filtering
re, err := regexp.Compile(`^[a-z0-9._-]{4,20}$`)
if err != nil {
log.Error("regexp.Compile Error", err, "name", name)
}
match := re.MatchString(name)
username = strings.ReplaceAll(displayName, " ", "_")
username = strings.ToLower(username)
suffix := ""
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
match := re.MatchString(username)
if !match {
return "", false
return "", errors.BadRequest(reason.UsernameInvalid)
}
return name, true
for {
_, has, err := us.userRepo.GetByUsername(ctx, username+suffix)
if err != nil {
return "", err
}
if !has {
break
}
bytes := make([]byte, 2)
_, _ = rand.Read(bytes)
suffix = hex.EncodeToString(bytes)
}
return username + suffix, nil
}
// verifyPassword

View File

@ -17,3 +17,11 @@ func CreatePathIsNotExist(path string) (bool, error) {
}
return false, err
}
func CheckPathExist(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
return false
}

View File

@ -1,5 +1,3 @@
#!/bin/bash
if [ ! -f "/data/config.yaml" ]; then
/usr/bin/answer init
fi
/usr/bin/answer run -c /data/config.yaml
/usr/bin/answer init
/usr/bin/answer run -c /data/conf/config.yaml

View File

@ -1,20 +0,0 @@
#!/usr/bin/env bash
git clone https://git.backyard.segmentfault.com/opensource/pacman.git /tmp/sf-pacman
cat <<'EOF' > go.work
go 1.18
use (
.
/tmp/sf-pacman
/tmp/sf-pacman/contrib/cache/redis
/tmp/sf-pacman/contrib/cache/memory
/tmp/sf-pacman/contrib/conf/viper
/tmp/sf-pacman/contrib/log/zap
/tmp/sf-pacman/contrib/i18n
/tmp/sf-pacman/contrib/server/http
)
EOF
go work sync

View File

@ -1,2 +1 @@
REACT_APP_API_URL=/
CDN_PATH=/
PUBLIC_URL=

View File

@ -1,2 +1 @@
REACT_APP_API_URL=http://10.0.10.98:2060
CDN_PATH=https://cdn.dev.segmentfault.com/<projectName>/<version>/

View File

@ -1,2 +1,3 @@
REACT_APP_API_URL=http://10.0.10.98:2060
CDN_PATH=https://static.segmentfault.com/<projectName>/<version>/
REACT_APP_API_URL=/
REACT_APP_PUBLIC_PATH=/
REACT_APP_VERSION=

View File

@ -1,2 +0,0 @@
REACT_APP_LANG = en
CDN_PATH=https://static-test.segmentfault.com/<projectName>/<version>/

View File

@ -27,8 +27,8 @@ corepack prepare pnpm@latest --activate
clone the repo locally and run following command in your terminal:
```shell
$ git clone `answer repo` answer
$ cd answer
$ git clone git@github.com:answerdev/answer.git answer
$ cd answer/ui
$ pnpm install
$ pnpm run start
```

View File

@ -1 +1 @@
<!doctype html><html><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/></head><body>answer<div id="root"></div></body></html>
<!doctype html><html><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Answer</title><script defer="defer" src="/static/js/main.1bed8401.js"></script><link href="/static/css/main.6975b122.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

View File

@ -2,10 +2,9 @@ const path = require('path');
module.exports = {
webpack: function (config, env) {
if (process.env.NODE_ENV === 'production') {
config.output.publicPath = process.env.CDN_PATH;
if (env === 'production') {
config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH;
}
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, 'src'),

View File

@ -2,7 +2,7 @@
"name": "answer-static",
"version": "0.1.0",
"private": true,
"homepage": ".",
"homepage": "/",
"scripts": {
"start": "react-app-rewired start",
"build:dev": "env-cmd -f .env.development react-app-rewired build",
@ -108,4 +108,4 @@
"pnpm": ">=7"
},
"license": "MIT"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -2,10 +2,10 @@
<html>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />-->
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -1,21 +1,11 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "Answer",
"name": "Answer.dev",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,3 @@
$link-hover-decoration: none;
$enable-negative-margins: true;
$blue: #0033FF !default;

View File

@ -195,7 +195,7 @@ export interface PostAnswerReq {
}
export interface PageUser {
id;
id?;
displayName;
userName?;
avatar_url?;
@ -301,6 +301,7 @@ export interface SearchResItem {
answer_count: number;
accepted: boolean;
tags: TagBase[];
status?: string;
};
}
export interface SearchRes extends ListResult<SearchResItem> {

View File

@ -1 +0,0 @@
$link-hover-decoration: none;

View File

@ -45,7 +45,7 @@ const Index: FC<Props> = ({ className, data }) => {
count: data?.collectCount,
});
}
}, [data]);
}, []);
const handleVote = (type: 'up' | 'down') => {
if (!isLogin(true)) {
@ -107,7 +107,7 @@ const Index: FC<Props> = ({ className, data }) => {
onClick={() => handleVote('up')}>
<Icon name="hand-thumbs-up-fill" />
</Button>
<Button variant="outline-secondary text-body" disabled>
<Button variant="outline-dark text-body" disabled>
{votes}
</Button>
<Button

View File

@ -3,6 +3,8 @@ import { Link } from 'react-router-dom';
import { Avatar } from '@answer/components';
import { formatCount } from '@/utils';
interface Props {
data: any;
showAvatar?: boolean;
@ -34,7 +36,9 @@ const Index: FC<Props> = ({
</>
)}
<span className="fw-bold">{data?.rank}</span>
<span className="fw-bold" title="Reputation">
{formatCount(data?.rank)}
</span>
</div>
);
};

View File

@ -1,5 +1,5 @@
import { memo } from 'react';
import { Button } from 'react-bootstrap';
import { Button, Dropdown } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
@ -23,7 +23,7 @@ const ActionBar = ({
return (
<div className="d-flex justify-content-between fs-14">
<div className="d-flex align-items-center text-secondary">
<div className="d-flex align-items-center link-secondary">
{userStatus !== 'deleted' ? (
<Link to={`/users/${username}`}>{nickName}</Link>
) : (
@ -34,7 +34,7 @@ const ActionBar = ({
<Button
variant="link"
size="sm"
className={`me-3 btn-no-border p-0 ${isVote ? '' : 'text-secondary'}`}
className={`me-3 btn-no-border p-0 ${isVote ? '' : 'link-secondary'}`}
onClick={onVote}>
<Icon name="hand-thumbs-up-fill" />
{voteCount > 0 && <span className="ms-2">{voteCount}</span>}
@ -42,7 +42,7 @@ const ActionBar = ({
<Button
variant="link"
size="sm"
className="text-secondary m-0 p-0 btn-no-border"
className="link-secondary m-0 p-0 btn-no-border"
onClick={onReply}>
{t('btn_reply')}
</Button>
@ -55,7 +55,7 @@ const ActionBar = ({
variant="link"
size="sm"
className={classNames(
'text-secondary btn-no-border m-0 p-0',
'link-secondary btn-no-border m-0 p-0',
index > 0 && 'ms-3',
)}
onClick={() => onAction(action)}>
@ -64,6 +64,29 @@ const ActionBar = ({
);
})}
</div>
<Dropdown className="d-block d-md-none">
<Dropdown.Toggle
as="div"
variant="success"
className="no-toggle"
id="dropdown-comment">
<Icon name="three-dots" className="text-secondary" />
</Dropdown.Toggle>
<Dropdown.Menu align="end">
{memberActions.map((action) => {
return (
<Dropdown.Item
key={action.name}
variant="link"
size="sm"
onClick={() => onAction(action)}>
{action.name}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
</Dropdown>
</div>
);
};

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