Merge branch 'main' into fix/reason
|
@ -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 }}
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
17
INSTALL.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
@ -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.
|
||||
|
|
5
Makefile
|
@ -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
|
||||
|
|
29
README.md
|
@ -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)
|
||||
|
|
31
README_CN.md
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
84
docs/docs.go
|
@ -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",
|
||||
|
|
Before Width: | Height: | Size: 118 KiB |
|
@ -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 |
Before Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 60 KiB |
|
@ -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",
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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=
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,2 +1 @@
|
|||
REACT_APP_API_URL=http://10.0.10.98:2060
|
||||
CDN_PATH=https://cdn.dev.segmentfault.com/<projectName>/<version>/
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
REACT_APP_LANG = en
|
||||
CDN_PATH=https://static-test.segmentfault.com/<projectName>/<version>/
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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>
|
|
@ -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'),
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.2 KiB |
|
@ -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/
|
||||
|
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB |
|
@ -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": ".",
|
||||
|
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 29 KiB |
|
@ -0,0 +1,3 @@
|
|||
$link-hover-decoration: none;
|
||||
$enable-negative-margins: true;
|
||||
$blue: #0033FF !default;
|
|
@ -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> {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
$link-hover-decoration: none;
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|