fix: merge from main

This commit is contained in:
kumfo 2022-09-30 10:41:46 +08:00
commit d4f02c668d
211 changed files with 2654 additions and 2408 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
.DS_Store
._*
/.idea
/.fleet
/.vscode/*.log
/cmd/answer/*.sh
/cmd/logs

View File

@ -27,11 +27,8 @@ stages:
"compile the golang project":
image: golang:1.18
stage: compile-golang
before_script:
- export GOPROXY="https://goproxy.cn"
- export GOPRIVATE=git.backyard.segmentfault.com
- sh ./script/prebuild.sh
script:
- make generate
- make build
artifacts:
paths:

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"eslint.workingDirectories": [
"ui"
]
}

4
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,4 @@
# Contributing to answer
## Coding and documentation Style
## Submitting Modifications

View File

@ -4,12 +4,7 @@ LABEL maintainer="mingcheng<mc@sf.com>"
COPY . /answer
WORKDIR /answer
RUN make install-ui-packages ui
RUN mv ui/build /tmp
CMD ls -al /tmp
RUN du -sh /tmp/build
RUN make install-ui-packages ui && mv ui/build /tmp
FROM golang:1.18 AS golang-builder
LABEL maintainer="aichy"
@ -23,16 +18,15 @@ ENV GOPRIVATE git.backyard.segmentfault.com
# Build
COPY . ${BUILD_DIR}
WORKDIR ${BUILD_DIR}
COPY --from=node-builder /tmp/build ${BUILD_DIR}/web/html
CMD ls -al ${BUILD_DIR}/web/html
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
RUN make clean build && \
cp answer /usr/bin/answer && \
cp configs/config.yaml /etc/config.yaml && \
mkdir -p /tmp/cache && chmod 777 /tmp/cache && \
mkdir -p /data/upfiles && chmod 777 /data/upfiles && cp -r i18n /data
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
FROM debian:bullseye
ENV TZ "Asia/Shanghai"
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list \
@ -45,9 +39,9 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.li
COPY --from=golang-builder /data /data
VOLUME /data
COPY --from=golang-builder /etc/config.yaml /etc/answer.yaml
COPY --from=golang-builder /usr/bin/answer /usr/bin/answer
COPY /script/entrypoint.sh /entrypoint.sh
RUN chmod 755 /entrypoint.sh
EXPOSE 80
ENTRYPOINT ["dumb-init", "/usr/bin/answer", "-c", "/etc/answer.yaml"]
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -1 +1,86 @@
# How to build and install
Before installing Answer, you need to install the base environment first.
- database
- [MySQL](http://dev.mysql.com) Version >= 5.7
You can then install Answer in several ways:
- Deploy with Docker
- binary installation
- Source installation
## Docker for Answer
Visit Docker Hub or GitHub Container registry to see all available images and tags.
### Usage
To keep your data out of Docker container, we do a volume (/var/data -> /data) here, and you can change it based on your situation.
```
# Pull image from Docker Hub.
$ docker pull answer/answer
# Create local directory for volume.
$ mkdir -p /var/data
# Run the image first
$ 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
# Need to modify the Mysql database address in the configuration file
vim /var/data/config.yaml
# Modify database connection
# connection: [username]:[password]@tcp([host]:[port])/[DbName]
...
# After configuring the configuration file, you can start the mirror again to start the service
$ docker start answer
```
## Binary for Answer
## Install Answer using binary
1. Unzip the compressed package.
2. Use the command cd to enter the directory you just created.
3. Execute the command ./answer init.
4. Answer will generate a ./data directory in the current directory
5. Enter the data directory and modify the config.yaml file
6. Modify the database connection 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
## config.yaml Description
```
server:
http:
addr: 0.0.0.0:80 #Project access port number
data:
database:
connection: root:root@tcp(127.0.0.1:3306)/answer #MySQL database connection address
cache:
file_path: "/tmp/cache/cache.db" #Cache file storage path
i18n:
bundle_dir: "/data/i18n" #Internationalized file storage directory
swaggerui:
show: true #Whether to display the swaggerapi documentation, address /swagger/index.html
protocol: http #swagger protocol header
host: 127.0.0.1 #An accessible IP address or domain name
address: ':80' #accessible port number
service_config:
secret_key: "answer" #encryption key
web_host: "http://127.0.0.1" #Page access using domain name address
upload_path: "./upfiles" #upload directory
```
## Compile the image
If you have modified the source files and want to repackage the image, you can use the following statement to repackage the image
```
docker build -t answer:v1.0.0 .
```
## common problem
1. The project cannot be started, answer the main program startup depends on the configuration file config.yaml, the internationalization translation directory/i18n, the upload file storage directory/upfiles, you need to ensure that the configuration file is loaded when the project starts, answer run -c config.yaml and the correct config.yaml The configuration items that specify the i18n and upfiles directories

View File

@ -1,9 +1,85 @@
# Answer 安装指引
## 前端安装
安装 Answer 之前,您需要先安装基本环境。
- 数据库
- [MySQL](http://dev.mysql.com):版本 >= 5.7
## 后端安装
然后,您可以通过以下几种种方式来安装 Answer
- 采用 Docker 部署
- 二进制安装
- 源码安装
## 使用Docker 安装 Answer
可以从 Docker Hub 或者 GitHub Container registry 下载最新的tags 镜像
### 用法
将配置和存储目录挂在到镜像之外 volume (/var/data -> /data),你可以修改外部挂载地址
```
# 将镜像从 docker hub 拉到本地
$ docker pull answer/answer
# 创建一个挂载目录
$ mkdir -p /var/data
# 先运行一遍镜像
$ docker run --name=answer -p 9080:80 -v /var/data:/data answer/answer
# 第一次启动后会在/var/data 目录下生成配置文件
# /var/data/config.yaml
# 需要修改配置文件中的Mysql 数据库地址
vim /var/data/config.yaml
# 修改数据库连接 connection: [username]:[password]@tcp([host]:[port])/[DbName]
...
# 配置好配置文件后可以再次启动镜像即可启动服务
$ docker start answer
```
## 使用二进制 安装 Answer
可以使用编译完成的各个平台的二进制文件运行 Answer 项目
### 用法
从GitHub 最新版本的tag中下载对应平台的二进制文件压缩包
1. 解压压缩包。
2. 使用命令 cd 进入到刚刚创建的目录。
3. 执行命令 ./answer init。
4. Answer 会在当前目录生成./data 目录
5. 进入data目录修改config.yaml文件
6. 将数据库连接地址修改为你的数据库连接地址
connection: [username]:[password]@tcp([host]:[port])/[DbName]
7. 退出data 目录 执行 ./answer run -c ./data/config.yaml
## 配置文件 config.yaml 参数说明
```
server:
http:
addr: 0.0.0.0:80 #项目访问端口号
data:
database:
connection: root:root@tcp(127.0.0.1:3306)/answer #mysql数据库连接地址
cache:
file_path: "/tmp/cache/cache.db" #缓存文件存放路径
i18n:
bundle_dir: "/data/i18n" #国际化文件存放目录
swaggerui:
show: true #是否显示swaggerapi文档,地址 /swagger/index.html
protocol: http #swagger 协议头
host: 127.0.0.1 #可被访问的ip地址或域名
address: ':80' #可被访问的端口号
service_config:
secret_key: "answer" #加密key
web_host: "http://127.0.0.1" #页面访问使用域名地址
upload_path: "./upfiles" #上传目录
```
## 编译镜像
如果修改了源文件并且要重新打包镜像可以使用以下语句重新打包镜像
```
docker build -t answer:v1.0.0 .
```
## 常见问题
1. 项目无法启动,answer主程序启动依赖配置文件config.yaml 、国际化翻译目录 /i18n 、上传文件存放目录/upfiles 需要确保项目启动时加载了配置文件 answer run -c config.yaml 以及在config.yaml 正确的指定i18n 和 upfiles 目录的配置项

View File

@ -12,6 +12,11 @@ GO=$(GO_ENV) $(shell which go)
build:
@$(GO_ENV) $(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
generate:
go get github.com/google/wire/cmd/wire@latest
go generate ./...
go mod tidy
test:
@$(GO) test ./...

View File

@ -1,35 +1,38 @@
# answer
![logo](docs/img/logo.png)
问答社区主项目代码
# Answer - Simple Q&A Community
# Dependence
github.com/segmentfault/pacman
* config-file `viper` https://github.com/spf13/viper
* web `gin` https://gin-gonic.com/zh-cn/
* log `zap` https://github.com/uber-go/zap
* orm `xorm` https://xorm.io/zh/
* redis `go-redis` https://github.com/go-redis/redis
[![LICENSE](https://img.shields.io/badge/License-MIT-green)](https://github.com/segmentfault/answer/blob/master/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/)
# module
- email github.com/jordan-wright/email
- session github.com/gin-contrib/sessions
- Captcha github.com/mojocn/base64Captcha
## 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)
# Run
```
cd cmd
export GOPRIVATE=git.backyard.segmentfault.com
go mod tidy
./dev.sh
## 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.
## Quick start
### Running with docker-compose
```bash
mkdir answer && cd answer
wget https://github.com/segmentfault/answer/releases/latest/download/docker-compose.yaml
docker-compose up
```
# pprof
For more information you can see [INSTALL.md](./INSTALL.md)
```
# Installation dependency
go get -u github.com/google/pprof
brew install graphviz
```
```
pprof -http :8082 http://XXX/debug/pprof/profile\?seconds\=10
```
## Contributing
Contributions are always welcome!
See [CONTRIBUTING.md](CONTRIBUTING.md) for ways to get started.
## License
[MIT](https://github.com/segmentfault/answer/blob/master/LICENSE)

View File

@ -1,9 +1,38 @@
# Answer 问答社区
![logo](docs/img/logo.png)
## 功能说明
# Answer - 极简问答社区
## 安装
[![LICENSE](https://img.shields.io/badge/License-MIT-green)](https://github.com/segmentfault/answer/blob/master/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)
## 常见问题
## 目标
- 帮助企业更好更快构建知识问答社区
## 产品功能
- 通过提问、回答方式生产知识
- 通过投票、共同协作方式维护知识
## 快速开始
### 使用 docker-compose 快速搭建
```bash
mkdir answer && cd answer
wget https://github.com/segmentfault/answer/releases/latest/download/docker-compose.yaml
docker-compose up
```
其他安装配置细节请参考 [INSTALL.md](./INSTALL.md)
## 贡献
我们随时欢迎你的贡献!
参考 [CONTRIBUTING.md](CONTRIBUTING.md) 其中的贡献指南
## License
[MIT](https://github.com/segmentfault/answer/blob/master/LICENSE)

255
assets/answer.sql Normal file

File diff suppressed because one or more lines are too long

6
assets/assets.go Normal file
View File

@ -0,0 +1,6 @@
package assets
import _ "embed"
//go:embed answer.sql
var AnswerSql []byte

View File

@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/conf"
"github.com/segmentfault/answer/internal/cli"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/contrib/conf/viper"
"github.com/segmentfault/pacman/contrib/log/zap"
@ -37,6 +38,24 @@ func init() {
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]
}
}
log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName(Name), zap.WithPath(logPath), zap.WithCallerFullPath()))
@ -50,11 +69,16 @@ func main() {
panic(err)
}
err = cli.InitDB(c.Data.Database)
if err != nil {
panic(err)
}
app, cleanup, err := initApplication(
c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger())
if err != nil {
panic(err)
}
defer cleanup()
if err := app.Run(); err != nil {
panic(err)

View File

@ -101,13 +101,14 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService)
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo)
userCommon := usercommon.NewUserCommon(userRepo)
answerRepo := repo.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
questionRepo := repo.NewQuestionRepo(dataData, uniqueIDRepo)
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagRepo)
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userRepo, objService, voteRepo)
rankService := rank2.NewRankService(userRepo, userRankRepo, objService, configRepo)
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo)
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, configRepo)
commentController := controller.NewCommentController(commentService, rankService)
reportRepo := report.NewReportRepo(dataData, uniqueIDRepo)
reportService := report2.NewReportService(reportRepo, objService)
@ -127,7 +128,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
collectionGroupRepo := collection.NewCollectionGroupRepo(dataData)
tagRelRepo := tag.NewTagListRepo(dataData)
tagCommonService := tagcommon.NewTagCommonService(tagRepo, tagRelRepo, revisionService)
userCommon := usercommon.NewUserCommon(userRepo)
collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo)
answerCommon := answercommon.NewAnswerCommon(answerRepo)
metaRepo := meta.NewMetaRepo(dataData)
@ -142,10 +142,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
questionController := controller.NewQuestionController(questionService, rankService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
answerController := controller.NewAnswerController(answerService, rankService)
searchRepo := repo.NewSearchRepo(dataData, uniqueIDRepo, userRepo)
searchService := service.NewSearchService(searchRepo, tagRepo, userRepo, followRepo)
searchRepo := repo.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo)
searchController := controller.NewSearchController(searchService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userRepo, questionCommon, answerService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService)
revisionController := controller.NewRevisionController(serviceRevisionService)
rankController := controller.NewRankController(rankService)
commonRepo := common.NewCommonRepo(dataData, uniqueIDRepo)
@ -169,9 +169,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
notificationController := controller.NewNotificationController(notificationService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
viewRouter := router.NewViewRouter()
uiRouter := router.NewUIRouter()
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, viewRouter, authUserMiddleware)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware)
application := newApplication(serverConf, ginEngine)
return application, func() {
cleanup2()

6
configs/config.go Normal file
View File

@ -0,0 +1,6 @@
package configs
import _ "embed"
//go:embed config.yaml
var Config []byte

View File

@ -1,19 +1,19 @@
server:
http:
addr: ""
addr: 0.0.0.0:80
data:
database:
connection: ""
connection: root:root@tcp(db:3306)/answer
cache:
file_path: ""
file_path: "/tmp/cache/cache.db"
i18n:
bundle_dir: ""
bundle_dir: "/data/i18n"
swaggerui:
show: true
protocol: http
host: 0
address: ':'
host: 127.0.0.1
address: ':80'
service_config:
secret_key: ""
web_host: ""
upload_path: ""
secret_key: "answer"
web_host: "http://127.0.0.1"
upload_path: "./upfiles"

View File

@ -1,7 +1,24 @@
version: "3"
version: "3.9"
services:
answer:
build:
context: .
image: github.com/segmentfault/answer
ports:
- '9080:80'
restart: on-failure
depends_on:
db:
condition: service_healthy
links:
- db
db:
image: mariadb:10.4.7
ports:
- '13306:3306'
restart: on-failure
environment:
MYSQL_DATABASE: answer
MYSQL_ROOT_PASSWORD: root
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-uroot", "-proot"]
timeout: 20s
retries: 10

View File

@ -4246,6 +4246,10 @@ const docTemplate = `{
"description": "reply user id",
"type": "string"
},
"reply_user_status": {
"description": "reply user status",
"type": "string"
},
"reply_username": {
"description": "reply user username",
"type": "string"
@ -4262,6 +4266,10 @@ const docTemplate = `{
"description": "user id",
"type": "string"
},
"user_status": {
"description": "user status",
"type": "string"
},
"username": {
"description": "username",
"type": "string"
@ -5504,7 +5512,7 @@ const docTemplate = `{
},
"status": {
"description": "status",
"type": "integer"
"type": "string"
},
"username": {
"description": "name",

BIN
docs/img/abstract.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
docs/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -4234,6 +4234,10 @@
"description": "reply user id",
"type": "string"
},
"reply_user_status": {
"description": "reply user status",
"type": "string"
},
"reply_username": {
"description": "reply user username",
"type": "string"
@ -4250,6 +4254,10 @@
"description": "user id",
"type": "string"
},
"user_status": {
"description": "user status",
"type": "string"
},
"username": {
"description": "username",
"type": "string"
@ -5492,7 +5500,7 @@
},
"status": {
"description": "status",
"type": "integer"
"type": "string"
},
"username": {
"description": "name",

View File

@ -299,6 +299,9 @@ definitions:
reply_user_id:
description: reply user id
type: string
reply_user_status:
description: reply user status
type: string
reply_username:
description: reply user username
type: string
@ -311,6 +314,9 @@ definitions:
user_id:
description: user id
type: string
user_status:
description: user status
type: string
username:
description: username
type: string
@ -1209,7 +1215,7 @@ definitions:
type: integer
status:
description: status
type: integer
type: string
username:
description: name
type: string

14
go.mod
View File

@ -19,17 +19,17 @@ require (
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+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-20220926035018-18f894415e5b
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220926035018-18f894415e5b
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220926035018-18f894415e5b
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220926035018-18f894415e5b
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220926035018-18f894415e5b
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347
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/stretchr/testify v1.8.0
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
github.com/swaggo/gin-swagger v1.5.3
github.com/swaggo/swag v1.8.6
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
golang.org/x/net v0.0.0-20220926192436-02166a98028e
golang.org/x/net v0.0.0-20220927171203-f486391704dc
xorm.io/builder v0.3.12
xorm.io/core v0.7.3
xorm.io/xorm v1.3.2
@ -76,7 +76,7 @@ require (
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect
golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25 // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/protobuf v1.28.1 // indirect

21
go.sum
View File

@ -525,14 +525,24 @@ github.com/segmentfault/pacman v1.0.1 h1:GFdvPtNxvVVjnDM4ty02D/+4unHwG9PmjcOZSc2
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=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -584,7 +594,6 @@ github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBn
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
@ -648,8 +657,6 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7 h1:WJywXQVIb56P2kAvXeMGTIgQ1ZHQxR60+F9dLsodECc=
golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -733,10 +740,10 @@ 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-20220923203811-8be639271d50 h1:vKyz8L3zkd+xrMeIaBsQ/MNVPVFSffdaU3ZyYlBGFnI=
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
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=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -817,10 +824,10 @@ 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-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/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=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

6
i18n/i18n.go Normal file
View File

@ -0,0 +1,6 @@
package i18n
import "embed"
//go:embed *.yaml
var I18n embed.FS

View File

@ -28,44 +28,3 @@ type Data struct {
Database *data.Database `json:"database" mapstructure:"database"`
Cache *data.CacheConf `json:"cache" mapstructure:"cache"`
}
// ------------------ remove
// log .
type log struct {
Dir string `json:"dir"`
Name string `json:"name"`
Access bool `json:"access"`
Level string `json:"level"`
MaxSize int `json:"max_size"`
MaxBackups int `json:"max_backups"`
MaxAge int `json:"max_age"`
}
// Local .
type Local struct {
Address string `json:"address"`
Debug bool `json:"debug"`
log log `json:"log"`
}
// // SwaggerConfig .
// type SwaggerConfig struct {
// Show bool `json:"show"`
// Protocol string `json:"protocol"`
// Host string `json:"host"`
// Address string `json:"address"`
// }
// Answer .
type Answer struct {
MaxIdle int `json:"max_idle"`
MaxOpen int `json:"max_open"`
IsDebug bool `json:"is_debug"`
Datasource string `json:"datasource"`
}
// Mysql .
type Mysql struct {
Answer Answer `json:"answer"`
}

View File

@ -13,6 +13,7 @@ const (
UserTokenCacheTime = 7 * 24 * time.Hour
AdminTokenCacheKey = "answer:admin:token:"
AdminTokenCacheTime = 7 * 24 * time.Hour
AcceptLanguageFlag = "Accept-Language"
)
const (

View File

@ -5,6 +5,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/constant"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/base/validator"
myErrors "github.com/segmentfault/pacman/errors"
@ -44,13 +45,15 @@ func HandleResponse(ctx *gin.Context, err error, data interface{}) {
// BindAndCheck bind request and check
func BindAndCheck(ctx *gin.Context, data interface{}) bool {
lang := GetLang(ctx)
ctx.Set(constant.AcceptLanguageFlag, lang)
if err := ctx.ShouldBind(data); err != nil {
log.Errorf("http_handle BindAndCheck fail, %s", err.Error())
HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil)
return true
}
errField, err := validator.GetValidatorByLang(GetLang(ctx).Abbr()).Check(data)
errField, err := validator.GetValidatorByLang(lang.Abbr()).Check(data)
if err != nil {
HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError).WithMsg(err.Error()), errField)
return true

View File

@ -2,12 +2,13 @@ package handler
import (
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/internal/base/constant"
"github.com/segmentfault/pacman/i18n"
)
// GetLang get language from header
func GetLang(ctx *gin.Context) i18n.Language {
acceptLanguage := ctx.GetHeader("Accept-Language")
acceptLanguage := ctx.GetHeader(constant.AcceptLanguageFlag)
switch i18n.Language(acceptLanguage) {
case i18n.LanguageChinese:
return i18n.LanguageChinese

View File

@ -17,7 +17,7 @@ type PageCond struct {
}
// NewPageModel new page model
func NewPageModel(page, pageSize int, totalRecords int64, records interface{}) *PageModel {
func NewPageModel(totalRecords int64, records interface{}) *PageModel {
sliceValue := reflect.Indirect(reflect.ValueOf(records))
if sliceValue.Kind() != reflect.Slice {
panic("not a slice")

View File

@ -11,7 +11,7 @@ func NewHTTPServer(debug bool,
staticRouter *router.StaticRouter,
answerRouter *router.AnswerAPIRouter,
swaggerRouter *router.SwaggerRouter,
viewRouter *router.ViewRouter,
viewRouter *router.UIRouter,
authUserMiddleware *middleware.AuthUserMiddleware) *gin.Engine {
if debug {
@ -22,7 +22,7 @@ func NewHTTPServer(debug bool,
r := gin.New()
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
viewRouter.RegisterViewRouter(r)
viewRouter.Register(r)
rootGroup := r.Group("")
swaggerRouter.Register(rootGroup)

89
internal/cli/install.go Normal file
View File

@ -0,0 +1,89 @@
package cli
import (
"bufio"
"fmt"
"os"
"github.com/segmentfault/answer/configs"
"github.com/segmentfault/answer/i18n"
"github.com/segmentfault/answer/pkg/dir"
)
var SuccessMsg = `
answer initialized successfully.
`
var HasBeenInitializedMsg = `
Has been initialized.
`
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)
}
_, err = dir.CreatePathIsNotExist("data")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
WriterFile("data/config.yaml", string(configs.Config))
_, err = dir.CreatePathIsNotExist("data/i18n")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
_, err = dir.CreatePathIsNotExist("data/upfiles")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
i18nList, err := i18n.I18n.ReadDir(".")
if err != nil {
fmt.Println(err.Error())
os.Exit(2)
}
for _, item := range i18nList {
path := fmt.Sprintf("data/i18n/%s", item.Name())
content, err := i18n.I18n.ReadFile(item.Name())
if err != nil {
continue
}
WriterFile(path, string(content))
}
fmt.Println(SuccessMsg)
os.Exit(0)
}
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 {
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
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

36
internal/cli/provider.go Normal file
View File

@ -0,0 +1,36 @@
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
}

21
internal/cli/usage.go Normal file
View File

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

View File

@ -93,16 +93,16 @@ func (nc *NotificationController) ClearIDUnRead(ctx *gin.Context) {
handler.HandleResponse(ctx, err, gin.H{})
}
// GetList
// @Summary GetRedDot
// @Description GetRedDot
// GetList get notification list
// @Summary get notification list
// @Description get notification list
// @Tags Notification
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Param type query string false "type" Enums(inbox,achievement)
// @Param type query string true "type" Enums(inbox,achievement)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/notification/page [get]
func (nc *NotificationController) GetList(ctx *gin.Context) {
@ -111,9 +111,6 @@ func (nc *NotificationController) GetList(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
list, count, err := nc.notificationService.GetList(ctx, req)
handler.HandleResponse(ctx, err, gin.H{
"list": list,
"count": count,
})
resp, err := nc.notificationService.GetList(ctx, req)
handler.HandleResponse(ctx, err, resp)
}

View File

@ -328,15 +328,15 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UserUpdateInfo godoc
// @Summary UserUpdateInfo
// @Description UserUpdateInfo
// UserUpdateInfo update user info
// @Summary UserUpdateInfo update user info
// @Description UserUpdateInfo update user info
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param Authorization header string true "access-token"
// @Param data body schema.UpdateInfoRequest true "UpdateInfoRequest"
// @Param data body schema.UpdateInfoRequest true "UpdateInfoRequest"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/user/info [put]
func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {

View File

@ -20,7 +20,7 @@ var CmsAnswerSearchStatus = map[string]int{
type Answer struct {
ID string `xorm:"not null pk autoincr comment('answer id') BIGINT(20) id"`
CreatedAt time.Time `xorm:"created not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"`
UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP updated_at"`
QuestionID string `xorm:"not null default 0 comment('question id') BIGINT(20) question_id"`
UserID string `xorm:"not null default 0 comment('answer user id') BIGINT(20) user_id"`
OriginalText string `xorm:"not null comment('original content') MEDIUMTEXT original_text"`

View File

@ -24,6 +24,8 @@ type QuestionTag struct {
// Question question
type Question struct {
ID string `xorm:"not null pk comment('question id') BIGINT(20) id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP comment('update time') TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 comment('user id') BIGINT(20) user_id"`
Title string `xorm:"not null default '' comment('question title') VARCHAR(255) title"`
OriginalText string `xorm:"not null comment('original content') MEDIUMTEXT original_text"`
@ -37,8 +39,6 @@ type Question struct {
FollowCount int `xorm:"not null default 0 comment('follow count') INT(11) follow_count"`
AcceptedAnswerID string `xorm:"not null default 0 comment('accepted answer id') BIGINT(20) accepted_answer_id"`
LastAnswerID string `xorm:"not null default 0 comment('last answer id') BIGINT(20) last_answer_id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP comment('create time') TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP comment('update time') TIMESTAMP updated_at"`
PostUpdateTime time.Time `xorm:"default CURRENT_TIMESTAMP comment('answer the last update time') TIMESTAMP post_update_time"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}

View File

@ -36,7 +36,7 @@ type User struct {
Rank int `xorm:"not null default 0 comment('rank') INT(11) rank"`
Status int `xorm:"not null default 1 comment('user status(available: 1; deleted: 10)') INT(11) status"`
AuthorityGroup int `xorm:"not null default 1 comment('authority group') INT(11) authority_group"`
DisplayName string `xorm:"not null default '' comment('display name') VARCHAR(50) display_name"`
DisplayName string `xorm:"not null default '' comment('display name') VARCHAR(30) display_name"`
Avatar string `xorm:"not null default '' comment('avatar') VARCHAR(255) avatar"`
Mobile string `xorm:"not null comment('mobile') VARCHAR(20) mobile"`
Bio string `xorm:"not null comment('bio markdown') TEXT bio"`

View File

@ -184,7 +184,7 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
session = session.And("user_id = ?", search.UserID)
}
if search.Order == entity.Answer_Search_OrderBy_Time {
session = session.OrderBy("updated_at desc")
session = session.OrderBy("created_at desc")
} else if search.Order == entity.Answer_Search_OrderBy_Vote {
session = session.OrderBy("vote_count desc")
} else {
@ -194,7 +194,7 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
session = session.And("status = ?", entity.AnswerStatusAvailable)
session = session.Limit(search.PageSize, offset)
count, err = session.OrderBy("updated_at desc").FindAndCount(&rows)
count, err = session.FindAndCount(&rows)
if err != nil {
return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}

View File

@ -22,16 +22,16 @@ import (
// searchRepo tag repository
type searchRepo struct {
data *data.Data
userRepo usercommon.UserRepo
userCommon *usercommon.UserCommon
uniqueIDRepo unique.UniqueIDRepo
}
// NewSearchRepo new repository
func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userRepo usercommon.UserRepo) search_common.SearchRepo {
func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon *usercommon.UserCommon) search_common.SearchRepo {
return &searchRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
userRepo: userRepo,
userCommon: userCommon,
}
}
@ -219,7 +219,6 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
var (
objectKey,
status string
uInfo *schema.UserBasicInfo
tags []schema.TagResp
tagsEntity []entity.Tag
@ -233,16 +232,12 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
tp, _ := time.ParseInLocation("2006-01-02 15:04:05", string(r["created_at"]), time.Local)
// get user info
userInfo, exist, e := sr.userRepo.GetByUserID(ctx, string(r["user_id"]))
userInfo, _, e := sr.userCommon.GetUserBasicInfoByID(ctx, string(r["user_id"]))
if e != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
return
}
if exist {
uInfo = sr.userBasicInfoFormat(ctx, userInfo)
}
// get tags
err = sr.data.DB.
Select("`display_name`,`slug_name`,`main_tag_slug_name`").
@ -279,7 +274,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
Title: string(r["title"]),
Excerpt: cutOutParsedText(string(r["original_text"])),
CreatedAtParsed: tp.Unix(),
UserInfo: uInfo,
UserInfo: userInfo,
Tags: tags,
VoteCount: converter.StringToInt(string(r["vote_count"])),
Accepted: string(r["accepted"]) == "2",
@ -297,8 +292,8 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
// userBasicInfoFormat
func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.User) *schema.UserBasicInfo {
return &schema.UserBasicInfo{
UserId: dbinfo.ID,
UserName: dbinfo.Username,
ID: dbinfo.ID,
Username: dbinfo.Username,
Rank: dbinfo.Rank,
DisplayName: dbinfo.DisplayName,
Avatar: dbinfo.Avatar,

View File

@ -73,10 +73,11 @@ 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) (tagList []*entity.Tag, err error) {
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(builder.Like{"slug_name", name})
session := tr.data.DB.Where("LIKE ?", name+"%")
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Limit(limit).Asc("slug_name")
err = session.Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()

View File

@ -3,4 +3,4 @@ package router
import "github.com/google/wire"
// ProviderSetRouter is providers.
var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewViewRouter)
var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter)

79
internal/router/ui.go Normal file
View File

@ -0,0 +1,79 @@
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"
)
const UIIndexFilePath = "build/index.html"
const UIStaticPath = "build/static"
// UIRouter is an interface that provides ui static file routers
type UIRouter struct {
}
// NewUIRouter creates a new UIRouter instance with the embed resources
func NewUIRouter() *UIRouter {
return &UIRouter{}
}
// _resource is an interface that provides static file, it's a private interface
type _resource struct {
fs embed.FS
}
// Open to implement the interface by http.FS required
func (r *_resource) Open(name string) (fs.File, error) {
name = fmt.Sprintf(UIStaticPath+"/%s", name)
log.Debugf("open static path %s", name)
return r.fs.Open(name)
}
// Register a new static resource which generated by ui directory
func (a *UIRouter) Register(r *gin.Engine) {
staticPath := os.Getenv("ANSWER_STATIC_PATH")
// if ANSWER_STATIC_PATH is set and not empty, ignore embed resource
if staticPath != "" {
info, err := os.Stat(staticPath)
if err != nil || !info.IsDir() {
log.Error(err)
} else {
log.Debugf("registering static path %s", staticPath)
r.LoadHTMLGlob(staticPath + "/*.html")
r.Static("/static", staticPath+"/static")
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
})
// return immediately if the static path is set
return
}
}
// handle the static file by default ui static files
r.StaticFS("/static", http.FS(&_resource{
fs: ui.Build,
}))
// specify the not router for default routes and redirect
r.NoRoute(func(c *gin.Context) {
index, err := ui.Build.ReadFile(UIIndexFilePath)
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))
})
}

View File

@ -0,0 +1,36 @@
package router
import (
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestUIRouter_Register(t *testing.T) {
r := gin.Default()
NewUIRouter().Register(r)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestUIRouter_Static(t *testing.T) {
r := gin.Default()
NewUIRouter().Register(r)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/static/version.txt", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "OK", w.Body.String())
}

View File

@ -1,67 +0,0 @@
package router
import (
"embed"
"errors"
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/segmentfault/answer/web"
)
// RegisterViewRouter
type ViewRouter struct {
}
// NewRegisterViewRouter
func NewViewRouter() *ViewRouter {
return &ViewRouter{}
}
type Resource struct {
fs embed.FS
path string
}
func NewResource() *Resource {
return &Resource{
fs: web.Static,
path: "html",
}
}
func (r *Resource) Open(name string) (fs.File, error) {
if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
return nil, errors.New("http: invalid character in file path")
}
fullName := filepath.Join(r.path, filepath.FromSlash(path.Clean("/static/"+name)))
file, err := r.fs.Open(fullName)
return file, err
}
func (a *ViewRouter) RegisterViewRouter(r *gin.Engine) {
//export answer_html_static_path="../../web/static"
//export answer_html_page_path="../../web"
static := os.Getenv("answer_html_static_path")
index := os.Getenv("answer_html_page_path")
if len(static) > 0 && len(index) > 0 {
r.LoadHTMLGlob(index + "/*.html")
r.Static("/static", static)
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
})
return
} else {
r.StaticFS("/static", http.FS(NewResource()))
r.NoRoute(func(c *gin.Context) {
c.Header("content-type", "text/html;charset=utf-8")
c.String(200, string(web.Html))
})
}
}

View File

@ -1,95 +1,13 @@
package schema
import (
"time"
)
// RemoveAnswerReq delete answer request
type RemoveAnswerReq struct {
// answer id
ID string `validate:"required" comment:"answer id" json:"id"`
ID string `validate:"required" json:"id"`
// user id
UserID string `json:"-"`
}
// GetAnswerListReq get answer list all request
type GetAnswerListReq struct {
// question id
QuestionID int64 `validate:"omitempty" comment:"question id" form:"question_id"`
// answer user id
UserID int64 `validate:"omitempty" comment:"answer user id" form:"user_id"`
// content markdown
Content string `validate:"omitempty" comment:"content markdown" form:"content"`
// content html
Html string `validate:"omitempty" comment:"content html" form:"html"`
// answer status(available: 1; deleted: 10)
Status int `validate:"omitempty" comment:" answer status(available: 1; deleted: 10)" form:"status"`
// adopted (1 failed 2 adopted)
Adopted int `validate:"omitempty" comment:"adopted (1 failed 2 adopted)" form:"adopted"`
// comment count
CommentCount int `validate:"omitempty" comment:"comment count" form:"comment_count"`
// vote count
VoteCount int `validate:"omitempty" comment:"vote count" form:"vote_count"`
//
CreateTime time.Time `validate:"omitempty" comment:"" form:"create_time"`
//
UpdateTime time.Time `validate:"omitempty" comment:"" form:"update_time"`
}
// GetAnswerWithPageReq get answer list page request
type GetAnswerWithPageReq struct {
// page
Page int `validate:"omitempty,min=1" form:"page"`
// page size
PageSize int `validate:"omitempty,min=1" form:"page_size"`
// question id
QuestionID int64 `validate:"omitempty" comment:"question id" form:"question_id"`
// answer user id
UserID int64 `validate:"omitempty" comment:"answer user id" form:"user_id"`
// content markdown
Content string `validate:"omitempty" comment:"content markdown" form:"content"`
// content html
Html string `validate:"omitempty" comment:"content html" form:"html"`
// answer status(available: 1; deleted: 10)
Status int `validate:"omitempty" comment:" answer status(available: 1; deleted: 10)" form:"status"`
// adopted (1 failed 2 adopted)
Adopted int `validate:"omitempty" comment:"adopted (1 failed 2 adopted)" form:"adopted"`
// comment count
CommentCount int `validate:"omitempty" comment:"comment count" form:"comment_count"`
// vote count
VoteCount int `validate:"omitempty" comment:"vote count" form:"vote_count"`
//
CreateTime time.Time `validate:"omitempty" comment:"" form:"create_time"`
//
UpdateTime time.Time `validate:"omitempty" comment:"" form:"update_time"`
}
// GetAnswerResp get answer response
type GetAnswerResp struct {
// answer id
ID int64 `json:"id"`
// question id
QuestionID int64 `json:"question_id"`
// answer user id
UserID int64 `json:"user_id"`
// content markdown
Content string `json:"content"`
// content html
Html string `json:"html"`
// answer status(available: 1; deleted: 10)
Status int `json:"status"`
// adopted (1 failed 2 adopted)
Adopted int `json:"adopted"`
// comment count
CommentCount int `json:"comment_count"`
// vote count
VoteCount int `json:"vote_count"`
//
CreateTime time.Time `json:"create_time"`
//
UpdateTime time.Time `json:"update_time"`
}
const (
Answer_Adopted_Failed = 1
Answer_Adopted_Enable = 2

View File

@ -9,16 +9,16 @@ type UpdateUserStatusReq struct {
}
const (
Normal = "normal"
Suspended = "suspended"
Deleted = "deleted"
Inactive = "inactive"
UserNormal = "normal"
UserSuspended = "suspended"
UserDeleted = "deleted"
UserInactive = "inactive"
)
func (r *UpdateUserStatusReq) IsNormal() bool { return r.Status == Normal }
func (r *UpdateUserStatusReq) IsSuspended() bool { return r.Status == Suspended }
func (r *UpdateUserStatusReq) IsDeleted() bool { return r.Status == Deleted }
func (r *UpdateUserStatusReq) IsInactive() bool { return r.Status == Inactive }
func (r *UpdateUserStatusReq) IsNormal() bool { return r.Status == UserNormal }
func (r *UpdateUserStatusReq) IsSuspended() bool { return r.Status == UserSuspended }
func (r *UpdateUserStatusReq) IsDeleted() bool { return r.Status == UserDeleted }
func (r *UpdateUserStatusReq) IsInactive() bool { return r.Status == UserInactive }
// GetUserPageReq get user list page request
type GetUserPageReq struct {
@ -34,9 +34,9 @@ type GetUserPageReq struct {
Status string `validate:"omitempty,oneof=suspended deleted inactive" form:"status"`
}
func (r *GetUserPageReq) IsSuspended() bool { return r.Status == Suspended }
func (r *GetUserPageReq) IsDeleted() bool { return r.Status == Deleted }
func (r *GetUserPageReq) IsInactive() bool { return r.Status == Inactive }
func (r *GetUserPageReq) IsSuspended() bool { return r.Status == UserSuspended }
func (r *GetUserPageReq) IsDeleted() bool { return r.Status == UserDeleted }
func (r *GetUserPageReq) IsInactive() bool { return r.Status == UserInactive }
// GetUserPageResp get user response
type GetUserPageResp struct {

View File

@ -43,12 +43,6 @@ type AddCollectionGroupReq struct {
UpdateTime time.Time `validate:"required" comment:"" json:"update_time"`
}
// RemoveCollectionGroupReq delete collection group request
type RemoveCollectionGroupReq struct {
//
ID int64 `validate:"required" comment:"" json:"id"`
}
// UpdateCollectionGroupReq update collection group request
type UpdateCollectionGroupReq struct {
//
@ -65,38 +59,6 @@ type UpdateCollectionGroupReq struct {
UpdateTime time.Time `validate:"omitempty" comment:"" json:"update_time"`
}
// GetCollectionGroupListReq get collection group list all request
type GetCollectionGroupListReq struct {
//
UserID int64 `validate:"omitempty" comment:"" form:"user_id"`
// the collection group name
Name string `validate:"omitempty,gt=0,lte=50" comment:"the collection group name" form:"name"`
// mark this group is default, default 1
DefaultGroup int `validate:"omitempty" comment:"mark this group is default, default 1" form:"default_group"`
//
CreateTime time.Time `validate:"omitempty" comment:"" form:"create_time"`
//
UpdateTime time.Time `validate:"omitempty" comment:"" form:"update_time"`
}
// GetCollectionGroupWithPageReq get collection group list page request
type GetCollectionGroupWithPageReq struct {
// page
Page int `validate:"omitempty,min=1" form:"page"`
// page size
PageSize int `validate:"omitempty,min=1" form:"page_size"`
//
UserID int64 `validate:"omitempty" comment:"" form:"user_id"`
// the collection group name
Name string `validate:"omitempty,gt=0,lte=50" comment:"the collection group name" form:"name"`
// mark this group is default, default 1
DefaultGroup int `validate:"omitempty" comment:"mark this group is default, default 1" form:"default_group"`
//
CreateTime time.Time `validate:"omitempty" comment:"" form:"create_time"`
//
UpdateTime time.Time `validate:"omitempty" comment:"" form:"update_time"`
}
// GetCollectionGroupResp get collection group response
type GetCollectionGroupResp struct {
//

View File

@ -1,87 +0,0 @@
package schema
import "time"
// AddCollectionReq add collection request
type AddCollectionReq struct {
// user id
UserID int64 `validate:"required" comment:"user id" json:"user_id"`
// object id
ObjectID int64 `validate:"required" comment:"object id" json:"object_id"`
// user collection group id
UserCollectionGroupID int64 `validate:"required" comment:"user collection group id" json:"user_collection_group_id"`
//
CreateTime time.Time `validate:"required" comment:"" json:"create_time"`
//
UpdateTime time.Time `validate:"required" comment:"" json:"update_time"`
}
// RemoveCollectionReq delete collection request
type RemoveCollectionReq struct {
// collection id
ID int64 `validate:"required" comment:"collection id" json:"id"`
}
// UpdateCollectionReq update collection request
type UpdateCollectionReq struct {
// collection id
ID int64 `validate:"required" comment:"collection id" json:"id"`
// user id
UserID int64 `validate:"omitempty" comment:"user id" json:"user_id"`
// object id
ObjectID int64 `validate:"omitempty" comment:"object id" json:"object_id"`
// user collection group id
UserCollectionGroupID int64 `validate:"omitempty" comment:"user collection group id" json:"user_collection_group_id"`
//
CreateTime time.Time `validate:"omitempty" comment:"" json:"create_time"`
//
UpdateTime time.Time `validate:"omitempty" comment:"" json:"update_time"`
}
// GetCollectionListReq get collection list all request
type GetCollectionListReq struct {
// user id
UserID int64 `validate:"omitempty" comment:"user id" form:"user_id"`
// object id
ObjectID int64 `validate:"omitempty" comment:"object id" form:"object_id"`
// user collection group id
UserCollectionGroupID int64 `validate:"omitempty" comment:"user collection group id" form:"user_collection_group_id"`
//
CreateTime time.Time `validate:"omitempty" comment:"" form:"create_time"`
//
UpdateTime time.Time `validate:"omitempty" comment:"" form:"update_time"`
}
// GetCollectionWithPageReq get collection list page request
type GetCollectionWithPageReq struct {
// page
Page int `validate:"omitempty,min=1" form:"page"`
// page size
PageSize int `validate:"omitempty,min=1" form:"page_size"`
// user id
UserID int64 `validate:"omitempty" comment:"user id" form:"user_id"`
// object id
ObjectID int64 `validate:"omitempty" comment:"object id" form:"object_id"`
// user collection group id
UserCollectionGroupID int64 `validate:"omitempty" comment:"user collection group id" form:"user_collection_group_id"`
//
CreateTime time.Time `validate:"omitempty" comment:"" form:"create_time"`
//
UpdateTime time.Time `validate:"omitempty" comment:"" form:"update_time"`
}
// GetCollectionResp get collection response
type GetCollectionResp struct {
// collection id
ID int64 `json:"id"`
// user id
UserID int64 `json:"user_id"`
// object id
ObjectID int64 `json:"object_id"`
// user collection group id
UserCollectionGroupID int64 `json:"user_collection_group_id"`
//
CreateTime time.Time `json:"create_time"`
//
UpdateTime time.Time `json:"update_time"`
}

View File

@ -109,6 +109,8 @@ type GetCommentResp struct {
UserDisplayName string `json:"user_display_name"`
// user avatar
UserAvatar string `json:"user_avatar"`
// user status
UserStatus string `json:"user_status"`
// reply user id
ReplyUserID string `json:"reply_user_id"`
@ -118,6 +120,8 @@ type GetCommentResp struct {
ReplyUserDisplayName string `json:"reply_user_display_name"`
// reply comment id
ReplyCommentID string `json:"reply_comment_id"`
// reply user status
ReplyUserStatus string `json:"reply_user_status"`
// MemberActions
MemberActions []*PermissionMemberAction `json:"member_actions"`

View File

@ -22,7 +22,7 @@ type CloseQuestionMeta struct {
type QuestionAdd struct {
// question title
Title string `validate:"required,gte=6,lte=64" json:"title"`
Title string `validate:"required,gte=6,lte=150" json:"title"`
// content
Content string `validate:"required,gte=6,lte=65535" json:"content"`
// html
@ -37,7 +37,7 @@ type QuestionUpdate struct {
// question id
ID string `validate:"required" json:"id"`
// question title
Title string `validate:"required,gte=6,lte=64" json:"title"`
Title string `validate:"required,gte=6,lte=150" json:"title"`
// content
Content string `validate:"required,gte=6,lte=65535" json:"content"`
// html
@ -166,7 +166,7 @@ type CmsQuestionSearch struct {
Page int `json:"page" form:"page"` //Query number of pages
PageSize int `json:"page_size" form:"page_size"` //Search page size
Status int `json:"-" form:"-"`
StatusStr string `json:"status" form:"status"` //Status 1 Available 2 closed 10 Deleted
StatusStr string `json:"status" form:"status"` //Status 1 Available 2 closed 10 UserDeleted
}
type AdminSetQuestionStatusRequest struct {

View File

@ -11,7 +11,7 @@ import (
// SearchTagLikeReq get tag list all request
type SearchTagLikeReq struct {
// tag
Tag string `validate:"required,gt=0,lte=50" form:"tag"`
Tag string `validate:"required,gt=0,lte=35" form:"tag"`
}
// GetTagInfoReq get tag info request
@ -19,7 +19,7 @@ type GetTagInfoReq struct {
// tag id
ID string `validate:"omitempty" form:"id"`
// tag slug name
Name string `validate:"omitempty,gt=0,lte=50" form:"name"`
Name string `validate:"omitempty,gt=0,lte=35" form:"name"`
// user id
UserID string `json:"-"`
}
@ -115,9 +115,9 @@ type TagChange struct {
type TagItem struct {
// slug_name
SlugName string `validate:"omitempty,gt=0,lte=50" json:"slug_name"`
SlugName string `validate:"omitempty,gt=0,lte=35" json:"slug_name"`
// display_name
DisplayName string `validate:"omitempty,gt=0,lte=50" json:"display_name"`
DisplayName string `validate:"omitempty,gt=0,lte=35" json:"display_name"`
// original text
OriginalText string `validate:"omitempty" json:"original_text"`
// parsed text
@ -137,9 +137,9 @@ type UpdateTagReq struct {
// tag_id
TagID string `validate:"required" json:"tag_id"`
// slug_name
SlugName string `validate:"omitempty,gt=0,lte=50" json:"slug_name"`
SlugName string `validate:"omitempty,gt=0,lte=35" json:"slug_name"`
// display_name
DisplayName string `validate:"omitempty,gt=0,lte=50" json:"display_name"`
DisplayName string `validate:"omitempty,gt=0,lte=35" json:"display_name"`
// original text
OriginalText string `validate:"omitempty" json:"original_text"`
// parsed text
@ -164,9 +164,9 @@ type GetTagWithPageReq struct {
// page size
PageSize int `validate:"omitempty,min=1" form:"page_size"`
// slug_name
SlugName string `validate:"omitempty,gt=0,lte=50" form:"slug_name"`
SlugName string `validate:"omitempty,gt=0,lte=35" form:"slug_name"`
// display_name
DisplayName string `validate:"omitempty,gt=0,lte=50" form:"display_name"`
DisplayName string `validate:"omitempty,gt=0,lte=35" form:"display_name"`
// query condition
QueryCond string `validate:"omitempty,oneof=popular name newest" form:"query_cond"`
// user id

View File

@ -166,7 +166,7 @@ const (
var UserStatusShow = map[int]string{
1: "normal",
9: "forbidden",
10: "delete",
10: "deleted",
}
var UserStatusShowMsg = map[int]string{
1: "",
@ -226,14 +226,20 @@ func (u *UserModifyPassWordRequest) Check() (errField *validator.ErrorField, err
}
type UpdateInfoRequest struct {
UserId string `json:"-" ` // user_id
UserName string `json:"username"` // name
DisplayName string `json:"display_name" ` // display_name
Avatar string `json:"avatar" ` // avatar
Bio string `json:"bio"`
BioHtml string `json:"bio_html"`
Website string `json:"website" ` // website
Location string `json:"location"` // location
// display_name
DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"`
// avatar
Avatar string `validate:"omitempty,gt=0,lte=500" json:"avatar"`
// bio
Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"`
// bio
BioHtml string `validate:"omitempty,gt=0,lte=4096" json:"bio_html"`
// website
Website string `validate:"omitempty,gt=0,lte=500" json:"website"`
// location
Location string `validate:"omitempty,gt=0,lte=100" json:"location"`
// user id
UserId string `json:"-" `
}
type UserRetrievePassWordRequest struct {
@ -282,15 +288,15 @@ type ActionRecordResp struct {
}
type UserBasicInfo struct {
UserId string `json:"-" ` // user_id
UserName string `json:"username" ` // name
ID string `json:"-" ` // user_id
Username string `json:"username" ` // name
Rank int `json:"rank" ` // rank
DisplayName string `json:"display_name"` // display_name
Avatar string `json:"avatar" ` // avatar
Website string `json:"website" ` // website
Location string `json:"location" ` // location
IpInfo string `json:"ip_info"` // ip info
Status int `json:"status"` // status
Status string `json:"status"` // status
}
type GetOtherUserInfoByUsernameReq struct {

View File

@ -32,7 +32,7 @@ type CommentRepo interface {
type CommentService struct {
commentRepo CommentRepo
commentCommonRepo comment_common.CommentCommonRepo
userRepo usercommon.UserRepo
userCommon *usercommon.UserCommon
voteCommon activity_common.VoteRepo
objectInfoService *object_info.ObjService
}
@ -61,13 +61,13 @@ func (c *CommentQuery) GetOrderBy() string {
func NewCommentService(
commentRepo CommentRepo,
commentCommonRepo comment_common.CommentCommonRepo,
userRepo usercommon.UserRepo,
userCommon *usercommon.UserCommon,
objectInfoService *object_info.ObjService,
voteCommon activity_common.VoteRepo) *CommentService {
return &CommentService{
commentRepo: commentRepo,
commentCommonRepo: commentCommonRepo,
userRepo: userRepo,
userCommon: userCommon,
voteCommon: voteCommon,
objectInfoService: objectInfoService,
}
@ -124,19 +124,20 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
// get reply user info
if len(resp.ReplyUserID) > 0 {
replyUser, exist, err := cs.userRepo.GetByUserID(ctx, resp.ReplyUserID)
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
resp.ReplyUsername = replyUser.Username
resp.ReplyUserDisplayName = replyUser.DisplayName
resp.ReplyUserStatus = replyUser.Status
}
cs.notificationCommentReply(ctx, replyUser.ID, objInfo.QuestionID, req.UserID)
}
// get user info
userInfo, exist, err := cs.userRepo.GetByUserID(ctx, resp.UserID)
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID)
if err != nil {
return nil, err
}
@ -144,6 +145,7 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
resp.Username = userInfo.Username
resp.UserDisplayName = userInfo.DisplayName
resp.UserAvatar = userInfo.Avatar
resp.UserStatus = userInfo.Status
}
return resp, nil
}
@ -191,7 +193,7 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
// get comment user info
if len(resp.UserID) > 0 {
commentUser, exist, err := cs.userRepo.GetByUserID(ctx, resp.UserID)
commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.UserID)
if err != nil {
return nil, err
}
@ -199,18 +201,20 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
resp.Username = commentUser.Username
resp.UserDisplayName = commentUser.DisplayName
resp.UserAvatar = commentUser.Avatar
resp.UserStatus = commentUser.Status
}
}
// get reply user info
if len(resp.ReplyUserID) > 0 {
replyUser, exist, err := cs.userRepo.GetByUserID(ctx, resp.ReplyUserID)
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
resp.ReplyUsername = replyUser.Username
resp.ReplyUserDisplayName = replyUser.DisplayName
resp.ReplyUserStatus = replyUser.Status
}
}
@ -249,7 +253,7 @@ func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.Ge
// get comment user info
if len(commentResp.UserID) > 0 {
commentUser, exist, err := cs.userRepo.GetByUserID(ctx, commentResp.UserID)
commentUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.UserID)
if err != nil {
return nil, err
}
@ -257,18 +261,20 @@ func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.Ge
commentResp.Username = commentUser.Username
commentResp.UserDisplayName = commentUser.DisplayName
commentResp.UserAvatar = commentUser.Avatar
commentResp.UserStatus = commentUser.Status
}
}
// get reply user info
if len(commentResp.ReplyUserID) > 0 {
replyUser, exist, err := cs.userRepo.GetByUserID(ctx, commentResp.ReplyUserID)
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, commentResp.ReplyUserID)
if err != nil {
return nil, err
}
if exist {
commentResp.ReplyUsername = replyUser.Username
commentResp.ReplyUserDisplayName = replyUser.DisplayName
commentResp.ReplyUserStatus = replyUser.Status
}
}
@ -278,7 +284,7 @@ func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.Ge
commentResp.MemberActions = permission.GetCommentPermission(req.UserID, commentResp.UserID)
resp = append(resp, commentResp)
}
return pager.NewPageModel(req.Page, req.PageSize, total, resp), nil
return pager.NewPageModel(total, resp), nil
}
func (cs *CommentService) checkCommentWhetherOwner(ctx context.Context, userID, commentID string) error {
@ -305,7 +311,7 @@ func (cs *CommentService) checkIsVote(ctx context.Context, userID, commentID str
func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *schema.GetCommentPersonalWithPageReq) (
pageModel *pager.PageModel, err error) {
if len(req.Username) > 0 {
userInfo, exist, err := cs.userRepo.GetByUsername(ctx, req.Username)
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
if err != nil {
return nil, err
}
@ -348,7 +354,7 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s
}
resp = append(resp, commentResp)
}
return pager.NewPageModel(req.Page, req.PageSize, total, resp), nil
return pager.NewPageModel(total, resp), nil
}
func (cs *CommentService) notificationQuestionComment(ctx context.Context, questionUserID, commentID, commentUserID string) {
@ -389,7 +395,7 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
func (cs *CommentService) notificationMention(ctx context.Context, mentionUsernameList []string, commentID, commentUserID string) {
for _, username := range mentionUsernameList {
userInfo, exist, err := cs.userRepo.GetByUsername(ctx, username)
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username)
if err != nil {
log.Error(err)
continue

View File

@ -97,5 +97,5 @@ func (ns *NotificationReadService) GetNotificationReadWithPage(ctx context.Conte
resp := &[]schema.GetNotificationReadResp{}
_ = copier.Copy(resp, notificationReads)
return pager.NewPageModel(page, pageSize, total, resp), nil
return pager.NewPageModel(total, resp), nil
}

View File

@ -5,9 +5,13 @@ import (
"encoding/json"
"fmt"
"github.com/segmentfault/answer/internal/base/constant"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/base/pager"
"github.com/segmentfault/answer/internal/base/translator"
"github.com/segmentfault/answer/internal/schema"
notficationcommon "github.com/segmentfault/answer/internal/service/notification_common"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
)
@ -91,30 +95,33 @@ func (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string,
return nil
}
func (ns *NotificationService) GetList(ctx context.Context, search *schema.NotificationSearch) ([]*schema.NotificationContent, int64, error) {
list := make([]*schema.NotificationContent, 0)
func (ns *NotificationService) GetList(ctx context.Context, search *schema.NotificationSearch) (
pageModel *pager.PageModel, err error) {
resp := make([]*schema.NotificationContent, 0)
searchType, ok := schema.NotificationType[search.TypeStr]
if !ok {
return list, 0, nil
return pager.NewPageModel(0, resp), nil
}
search.Type = searchType
dblist, count, err := ns.notificationRepo.SearchList(ctx, search)
notifications, count, err := ns.notificationRepo.SearchList(ctx, search)
if err != nil {
return list, count, err
return nil, err
}
for _, dbitem := range dblist {
for _, notificationInfo := range notifications {
item := &schema.NotificationContent{}
err := json.Unmarshal([]byte(dbitem.Content), item)
err := json.Unmarshal([]byte(notificationInfo.Content), item)
if err != nil {
log.Error("NotificationContent Unmarshal Error", err.Error())
continue
}
item.ID = dbitem.ID
item.UpdateTime = dbitem.UpdatedAt.Unix()
if dbitem.IsRead == schema.NotificationRead {
lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language)
item.NotificationAction = translator.GlobalTrans.Tr(lang, item.NotificationAction)
item.ID = notificationInfo.ID
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
if notificationInfo.IsRead == schema.NotificationRead {
item.IsRead = true
}
list = append(list, item)
resp = append(resp, item)
}
return list, count, nil
return pager.NewPageModel(count, resp), nil
}

View File

@ -265,7 +265,7 @@ func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order s
search.Order = order
search.Page = page
search.PageSize = pageSize
search.UserID = userinfo.UserId
search.UserID = userinfo.ID
questionlist, count, err := qs.SearchList(ctx, search, loginUserID)
if err != nil {
return userlist, 0, err
@ -289,7 +289,7 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o
return userAnswerlist, 0, nil
}
answersearch := &entity.AnswerSearch{}
answersearch.UserID = userinfo.UserId
answersearch.UserID = userinfo.ID
answersearch.PageSize = pageSize
answersearch.Page = page
if order == "newest" {
@ -337,7 +337,7 @@ func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, p
return list, 0, nil
}
collectionSearch := &entity.CollectionSearch{}
collectionSearch.UserID = userinfo.UserId
collectionSearch.UserID = userinfo.ID
collectionSearch.Page = page
collectionSearch.PageSize = pageSize
collectionlist, count, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
@ -384,13 +384,13 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin
search.Order = "score"
search.Page = 0
search.PageSize = 5
search.UserID = userinfo.UserId
search.UserID = userinfo.ID
questionlist, _, err := qs.SearchList(ctx, search, loginUserID)
if err != nil {
return userQuestionlist, userAnswerlist, err
}
answersearch := &entity.AnswerSearch{}
answersearch.UserID = userinfo.UserId
answersearch.UserID = userinfo.ID
answersearch.PageSize = 5
answersearch.Order = entity.Answer_Search_OrderBy_Vote
questionIDs := make([]string, 0)
@ -494,7 +494,7 @@ func (qs *QuestionService) SearchList(ctx context.Context, req *schema.QuestionS
if !exist {
return list, 0, err
}
req.UserID = userinfo.UserId
req.UserID = userinfo.ID
}
questionList, count, err := qs.questionRepo.SearchList(ctx, req)
if err != nil {

View File

@ -47,7 +47,7 @@ type UserRankRepo interface {
// RankService rank service
type RankService struct {
userRepo usercommon.UserRepo
userCommon *usercommon.UserCommon
configRepo config.ConfigRepo
userRankRepo UserRankRepo
objectInfoService *object_info.ObjService
@ -55,12 +55,12 @@ type RankService struct {
// NewRankService new rank service
func NewRankService(
userRepo usercommon.UserRepo,
userCommon *usercommon.UserCommon,
userRankRepo UserRankRepo,
objectInfoService *object_info.ObjService,
configRepo config.ConfigRepo) *RankService {
return &RankService{
userRepo: userRepo,
userCommon: userCommon,
configRepo: configRepo,
userRankRepo: userRankRepo,
objectInfoService: objectInfoService,
@ -74,7 +74,7 @@ func (rs *RankService) CheckRankPermission(ctx context.Context, userID string, a
}
// get the rank of the current user
userInfo, exist, err := rs.userRepo.GetByUserID(ctx, userID)
userInfo, exist, err := rs.userCommon.GetUserBasicInfoByID(ctx, userID)
if err != nil {
return false, err
}
@ -101,7 +101,7 @@ func (rs *RankService) CheckRankPermission(ctx context.Context, userID string, a
func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.GetRankPersonalWithPageReq) (
pageModel *pager.PageModel, err error) {
if len(req.Username) > 0 {
userInfo, exist, err := rs.userRepo.GetByUsername(ctx, req.Username)
userInfo, exist, err := rs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
if err != nil {
return nil, err
}
@ -140,5 +140,5 @@ func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.
}
resp = append(resp, commentResp)
}
return pager.NewPageModel(req.Page, req.PageSize, total, resp), nil
return pager.NewPageModel(total, resp), nil
}

View File

@ -2,9 +2,10 @@ package report_backyard
import (
"context"
"github.com/segmentfault/answer/internal/service/config"
"strings"
"github.com/segmentfault/answer/internal/service/config"
"github.com/jinzhu/copier"
"github.com/segmentfault/answer/internal/base/pager"
"github.com/segmentfault/answer/internal/base/reason"
@ -93,7 +94,7 @@ func (rs *ReportBackyardService) ListReportPage(ctx context.Context, dto schema.
}
rs.parseObject(ctx, &resp)
return pager.NewPageModel(dto.Page, dto.PageSize, total, resp), nil
return pager.NewPageModel(total, resp), nil
}
// HandleReported handle the reported object

View File

@ -18,19 +18,19 @@ import (
// RevisionService user service
type RevisionService struct {
revisionRepo revision.RevisionRepo
userRepo usercommon.UserRepo
userCommon *usercommon.UserCommon
questionCommon *questioncommon.QuestionCommon
answerService *AnswerService
}
func NewRevisionService(
revisionRepo revision.RevisionRepo,
userRepo usercommon.UserRepo,
userCommon *usercommon.UserCommon,
questionCommon *questioncommon.QuestionCommon,
answerService *AnswerService) *RevisionService {
return &RevisionService{
revisionRepo: revisionRepo,
userRepo: userRepo,
userCommon: userCommon,
questionCommon: questionCommon,
answerService: answerService,
}
@ -69,7 +69,6 @@ func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetR
)
resp = []schema.GetRevisionResp{}
_ = copier.Copy(&rev, req)
revs, err = rs.revisionRepo.GetRevisionList(ctx, &rev)
@ -79,29 +78,24 @@ func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetR
for _, r := range revs {
var (
userInfo *entity.User
uinfo schema.UserBasicInfo
item schema.GetRevisionResp
exists bool
uinfo schema.UserBasicInfo
item schema.GetRevisionResp
)
_ = copier.Copy(&item, r)
rs.parseItem(ctx, &item)
// get user info
userInfo, exists, err = rs.userRepo.GetByUserID(ctx, item.UserID)
if err != nil {
return
userInfo, exists, e := rs.userCommon.GetUserBasicInfoByID(ctx, item.UserID)
if e != nil {
return nil, e
}
if exists {
err = copier.Copy(&uinfo, userInfo)
item.UserInfo = uinfo
}
resp = append(resp, item)
}
return
}

View File

@ -5,25 +5,24 @@ import (
"regexp"
"strings"
"github.com/segmentfault/answer/internal/entity"
"github.com/segmentfault/answer/internal/schema"
"github.com/segmentfault/answer/internal/service/search_common"
usercommon "github.com/segmentfault/answer/internal/service/user_common"
)
type AuthorSearch struct {
repo search_common.SearchRepo
userRepo usercommon.UserRepo
exp string
w string
page int
size int
repo search_common.SearchRepo
userCommon *usercommon.UserCommon
exp string
w string
page int
size int
}
func NewAuthorSearch(repo search_common.SearchRepo, userRepo usercommon.UserRepo) *AuthorSearch {
func NewAuthorSearch(repo search_common.SearchRepo, userCommon *usercommon.UserCommon) *AuthorSearch {
return &AuthorSearch{
repo: repo,
userRepo: userRepo,
repo: repo,
userCommon: userCommon,
}
}
@ -37,9 +36,6 @@ func (s *AuthorSearch) Parse(dto *schema.SearchDTO) (ok bool) {
p,
me,
name string
user *entity.User
has bool
err error
)
exp = ""
q = dto.Query
@ -51,8 +47,7 @@ func (s *AuthorSearch) Parse(dto *schema.SearchDTO) (ok bool) {
res := re.FindStringSubmatch(q)
if len(res) == 2 {
name = res[1]
user, has, err = s.userRepo.GetByUsername(nil, name)
user, has, err := s.userCommon.GetUserBasicInfoByUserName(nil, name)
if err == nil && has {
exp = user.ID
trimLen := len(res[0])

View File

@ -35,14 +35,14 @@ type SearchService struct {
func NewSearchService(
searchRepo search_common.SearchRepo,
tagRepo tagcommon.TagRepo,
userRepo usercommon.UserRepo,
userCommon *usercommon.UserCommon,
followCommon activity_common.FollowRepo,
) *SearchService {
return &SearchService{
searchRepo: searchRepo,
tagSearch: search.NewTagSearch(searchRepo, tagRepo, followCommon),
withinSearch: search.NewWithinSearch(searchRepo),
authorSearch: search.NewAuthorSearch(searchRepo, userRepo),
authorSearch: search.NewAuthorSearch(searchRepo, userCommon),
scoreSearch: search.NewScoreSearch(searchRepo),
answersSearch: search.NewAnswersSearch(searchRepo),
acceptedAnswerSearch: search.NewAcceptedAnswerSearch(searchRepo),

View File

@ -41,12 +41,11 @@ func NewTagService(
// SearchTagLike get tag list all
func (ts *TagService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []string, err error) {
tags, err := ts.tagRepo.GetTagListByName(ctx, req.Tag)
tags, err := ts.tagRepo.GetTagListByName(ctx, req.Tag, 5)
if err != nil {
return
}
for _, tag := range tags {
//resp = append(resp, tag.DisplayName)
resp = append(resp, tag.SlugName)
}
return resp, nil
@ -358,7 +357,7 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
UpdatedAt: tag.UpdatedAt.Unix(),
})
}
return pager.NewPageModel(page, pageSize, total, resp), nil
return pager.NewPageModel(total, resp), nil
}
// checkTagIsFollow get tag list page

View File

@ -16,7 +16,7 @@ type TagRepo interface {
AddTagList(ctx context.Context, tagList []*entity.Tag) (err error)
GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error)
GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error)
GetTagListByName(ctx context.Context, name string) (tagList []*entity.Tag, err error)
GetTagListByName(ctx context.Context, name string, limit int) (tagList []*entity.Tag, err error)
GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error)
RemoveTag(ctx context.Context, tagID string) (err error)
UpdateTag(ctx context.Context, tag *entity.Tag) (err error)

View File

@ -108,17 +108,17 @@ func (us *UserBackyardService) GetUserPage(ctx context.Context, req *schema.GetU
Avatar: u.Avatar,
}
if u.Status == entity.UserStatusDeleted {
t.Status = schema.Deleted
t.Status = schema.UserDeleted
t.DeletedAt = u.DeletedAt.Unix()
} else if u.Status == entity.UserStatusSuspended {
t.Status = schema.Suspended
t.Status = schema.UserSuspended
t.SuspendedAt = u.SuspendedAt.Unix()
} else if u.MailStatus == entity.EmailStatusToBeVerified {
t.Status = schema.Inactive
t.Status = schema.UserInactive
} else {
t.Status = schema.Normal
t.Status = schema.UserNormal
}
resp = append(resp, t)
}
return pager.NewPageModel(req.Page, req.PageSize, total, resp), nil
return pager.NewPageModel(total, resp), nil
}

View File

@ -35,12 +35,12 @@ func NewUserCommon(userRepo UserRepo) *UserCommon {
}
func (us *UserCommon) GetUserBasicInfoByID(ctx context.Context, ID string) (*schema.UserBasicInfo, bool, error) {
dbInfo, has, err := us.userRepo.GetByUserID(ctx, ID)
userInfo, exist, err := us.userRepo.GetByUserID(ctx, ID)
if err != nil {
return nil, has, err
return nil, exist, err
}
info := us.UserBasicInfoFormat(ctx, dbInfo)
return info, has, nil
info := us.UserBasicInfoFormat(ctx, userInfo)
return info, exist, nil
}
func (us *UserCommon) GetUserBasicInfoByUserName(ctx context.Context, username string) (*schema.UserBasicInfo, bool, error) {
@ -74,16 +74,20 @@ func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, IDs []string)
}
// UserBasicInfoFormat
func (us *UserCommon) UserBasicInfoFormat(ctx context.Context, dbinfo *entity.User) *schema.UserBasicInfo {
info := new(schema.UserBasicInfo)
info.UserId = dbinfo.ID
info.UserName = dbinfo.Username
info.Rank = dbinfo.Rank
info.DisplayName = dbinfo.DisplayName
info.Avatar = dbinfo.Avatar
info.Website = dbinfo.Website
info.Location = dbinfo.Location
info.IpInfo = dbinfo.IPInfo
info.Status = dbinfo.Status
return info
func (us *UserCommon) UserBasicInfoFormat(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo {
userBasicInfo := &schema.UserBasicInfo{}
userBasicInfo.ID = userInfo.ID
userBasicInfo.Username = userInfo.Username
userBasicInfo.Rank = userInfo.Rank
userBasicInfo.DisplayName = userInfo.DisplayName
userBasicInfo.Avatar = userInfo.Avatar
userBasicInfo.Website = userInfo.Website
userBasicInfo.Location = userInfo.Location
userBasicInfo.IpInfo = userInfo.IPInfo
userBasicInfo.Status = schema.UserStatusShow[userInfo.Status]
if userBasicInfo.Status == schema.UserDeleted {
userBasicInfo.Avatar = ""
userBasicInfo.DisplayName = "Anonymous"
}
return userBasicInfo
}

View File

@ -97,5 +97,5 @@ func (us *UserGroupService) GetUserGroupWithPage(ctx context.Context, req *schem
resp := &[]schema.GetUserGroupResp{}
_ = copier.Copy(resp, userGroups)
return pager.NewPageModel(page, pageSize, total, resp), nil
return pager.NewPageModel(total, resp), nil
}

View File

@ -235,30 +235,8 @@ func (us *UserService) UserModifyPassWord(ctx context.Context, request *schema.U
// UpdateInfo
func (us *UserService) UpdateInfo(ctx context.Context, request *schema.UpdateInfoRequest) error {
// formatName, pass := us.CheckUserName(ctx, request.Username)
// if !pass {
// return fmt.Errorf("username format error")
// }
// dbuserinfo, has, err := us.userRepo.GetUserInfoByUserID(ctx, request.UserID)
// if err != nil {
// return err
// }
// if !has {
// return fmt.Errorf("user does not exist")
// }
// dbNameUserInfo, has, err := us.userRepo.GetOtherUserInfoByUsername(ctx, formatName)
// if err != nil {
// return err
// }
// if has {
// if dbuserinfo.TagID != dbNameUserInfo.TagID {
// return fmt.Errorf("username already exists")
// }
// }
userinfo := entity.User{}
userinfo.ID = request.UserId
//userinfo.Username = formatName
userinfo.Avatar = request.Avatar
userinfo.DisplayName = request.DisplayName
userinfo.Bio = request.Bio

View File

@ -193,5 +193,5 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith
resp = append(resp, item)
}
return pager.NewPageModel(req.Page, req.PageSize, total, resp), err
return pager.NewPageModel(total, resp), err
}

View File

@ -1,75 +0,0 @@
package captcha
import (
"fmt"
"image/color"
"sync"
"github.com/mojocn/base64Captcha"
)
var store base64Captcha.Store
var once sync.Once
func NewCaptcha() {
once.Do(func() {
//var err error
//RedisDb, err = arch.App.Cache.GetQuestion("cache")
//if err != nil {
// store = base64Captcha.DefaultMemStore
// return
//}
//var ctx = context.Background()
//_, err = RedisDb.Ping(ctx).Result()
//
//if err != nil {
// store = base64Captcha.DefaultMemStore
// return
//}
store = RedisStore{}
})
}
// CaptchaClient
type CaptchaClient struct {
}
// NewCaptchaClient
func NewCaptchaClient() *CaptchaClient {
return &CaptchaClient{}
}
func MakeCaptcha() (id, b64s string, err error) {
var driver base64Captcha.Driver
//Configure the parameters of the CAPTCHA
driverString := base64Captcha.DriverString{
Height: 40,
Width: 100,
NoiseCount: 0,
ShowLineOptions: 2 | 4,
Length: 4,
Source: "1234567890qwertyuioplkjhgfdsazxcvbnm",
BgColor: &color.RGBA{R: 3, G: 102, B: 214, A: 125},
Fonts: []string{"wqy-microhei.ttc"},
}
//ConvertFonts Load fonts by name
driver = driverString.ConvertFonts()
//Create Captcha
captcha := base64Captcha.NewCaptcha(driver, store)
//Generate
id, b64s, err = captcha.Generate()
return id, b64s, err
}
// VerifyCaptcha Verification code
func VerifyCaptcha(id string, VerifyValue string) bool {
fmt.Println(id, VerifyValue)
if store.Verify(id, VerifyValue, true) {
//verify successfully
return true
} else {
//Verification failed
return false
}
}

View File

@ -1,46 +0,0 @@
package captcha
import (
"context"
"fmt"
"github.com/segmentfault/pacman/contrib/cache/memory"
)
const CAPTCHA = "captcha:"
var RedisDb = memory.NewCache()
type RedisStore struct {
}
func (r RedisStore) Set(id string, value string) error {
key := CAPTCHA + id
ctx := context.Background()
err := RedisDb.SetString(ctx, key, value, 2)
return err
}
func (r RedisStore) Get(id string, clear bool) string {
key := CAPTCHA + id
ctx := context.Background()
val, err := RedisDb.GetString(ctx, key)
if err != nil {
fmt.Println(err)
return ""
}
if clear {
err := RedisDb.Del(ctx, key)
if err != nil {
fmt.Println(err)
return ""
}
}
return val
}
func (r RedisStore) Verify(id, answer string, clear bool) bool {
v := RedisStore{}.Get(id, clear)
fmt.Println("key:" + id + ";value:" + v + ";answer:" + answer)
return v == answer
}

View File

@ -1,103 +0,0 @@
package email
import (
"bytes"
"fmt"
"net/smtp"
"text/template"
"github.com/jordan-wright/email"
"github.com/segmentfault/pacman/log"
)
// EmailClient
type EmailClient struct {
email *email.Email
config *Config
}
// Config .
type Config struct {
WebName string `json:"web_name"`
WebHost string `json:"web_host"`
SecretKey string `json:"secret_key"`
UserSessionKey string `json:"user_session_key"`
EmailFrom string `json:"email_from"`
EmailFromPass string `json:"email_from_pass"`
EmailFromHostname string `json:"email_from_hostname"`
EmailFromSMTP string `json:"email_from_smtp"`
EmailFromName string `json:"email_from_name"`
RegisterTitle string `json:"register_title"`
RegisterBody string `json:"register_body"`
PassResetTitle string `json:"pass_reset_title"`
PassResetBody string `json:"pass_reset_body"`
}
// NewEmailClient
func NewEmailClient() *EmailClient {
return &EmailClient{
email: email.NewEmail(),
}
}
func (s *EmailClient) Send(ToEmail, Title, Body string) {
from := s.config.EmailFrom
fromPass := s.config.EmailFromPass
fromName := s.config.EmailFromName
fromSmtp := s.config.EmailFromSMTP
fromHostName := s.config.EmailFromHostname
s.email.From = fmt.Sprintf("%s <%s>", fromName, from)
s.email.To = []string{ToEmail}
s.email.Subject = Title
s.email.HTML = []byte(Body)
err := s.email.Send(fromSmtp, smtp.PlainAuth("", from, fromPass, fromHostName))
if err != nil {
log.Error(err)
}
}
func (s *EmailClient) RegisterTemplate(RegisterUrl string) (Title, Body string, err error) {
webName := s.config.WebName
templateData := RegisterTemplateData{webName, RegisterUrl}
tmpl, err := template.New("register_title").Parse(s.config.RegisterTitle)
if err != nil {
return "", "", err
}
title := new(bytes.Buffer)
body := new(bytes.Buffer)
err = tmpl.Execute(title, templateData)
if err != nil {
return "", "", err
}
tmpl, err = template.New("register_body").Parse(s.config.RegisterBody)
err = tmpl.Execute(body, templateData)
if err != nil {
return "", "", err
}
return title.String(), body.String(), nil
}
func (s *EmailClient) PassResetTemplate(PassResetUrl string) (Title, Body string, err error) {
webName := s.config.WebName
templateData := PassResetTemplateData{webName, PassResetUrl}
tmpl, err := template.New("pass_reset_title").Parse(s.config.PassResetTitle)
if err != nil {
return "", "", err
}
title := new(bytes.Buffer)
body := new(bytes.Buffer)
err = tmpl.Execute(title, templateData)
if err != nil {
return "", "", err
}
tmpl, err = template.New("pass_reset_body").Parse(s.config.PassResetBody)
err = tmpl.Execute(body, templateData)
if err != nil {
return "", "", err
}
return title.String(), body.String(), nil
}

View File

@ -1,11 +0,0 @@
package email
type RegisterTemplateData struct {
SiteName string
RegisterUrl string
}
type PassResetTemplateData struct {
SiteName string
PassResetUrl string
}

5
script/entrypoint.sh Executable file
View File

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

View File

View File

@ -1,9 +0,0 @@
# The MIT License
Copyright 2022 robin, contributors
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.

View File

@ -49,9 +49,9 @@ when cloning repo, and run `pnpm install` to init dependencies. you can use proj
## 🖥 Environment Support
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Edge / IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Safari |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Edge, IE11 | last 2 versions | last 2 versions | last 2 versions |
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Safari |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Build with

View File

@ -15,7 +15,7 @@ module.exports = {
'@answer/hooks': path.resolve(__dirname, 'src/hooks'),
'@answer/utils': path.resolve(__dirname, 'src/utils'),
'@answer/common': path.resolve(__dirname, 'src/common'),
'@answer/services': path.resolve(__dirname, 'src/services'),
'@answer/api': path.resolve(__dirname, 'src/services/api'),
};
return config;
@ -26,8 +26,8 @@ module.exports = {
const config = configFunction(proxy, allowedHost);
config.proxy = {
'/answer': {
target: 'http://10.0.20.84:8080',
// target: 'http://10.0.10.98:2060',
// target: "http://10.0.20.84:8080",
target: 'http://10.0.10.98:2060',
changeOrigin: true,
secure: false,
},

View File

@ -19,7 +19,7 @@
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
"path": "ui/node_modules/cz-conventional-changelog"
}
},
"dependencies": {
@ -46,7 +46,7 @@
"react": "^18.2.0",
"react-bootstrap": "^2.5.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-helmet-async": "^1.3.0",
"react-i18next": "^11.18.3",
"react-router-dom": "^6.4.0",
"swr": "^1.3.0",
@ -102,9 +102,10 @@
"typescript": "*",
"web-vitals": "^2.1.4"
},
"license": "MIT",
"packageManager": "pnpm@7.9.5",
"engines": {
"node": ">=16.17",
"pnpm": ">=7"
}
},
"license": "MIT"
}

View File

@ -67,7 +67,7 @@ specifiers:
react-app-rewired: ^2.2.1
react-bootstrap: ^2.5.0
react-dom: ^18.2.0
react-helmet: ^6.1.0
react-helmet-async: ^1.3.0
react-i18next: ^11.18.3
react-router-dom: ^6.4.0
react-scripts: 5.0.1
@ -102,7 +102,7 @@ dependencies:
react: 18.2.0
react-bootstrap: 2.5.0_7ey2zzynotv32rpkwno45fsx4e
react-dom: 18.2.0_react@18.2.0
react-helmet: 6.1.0_react@18.2.0
react-helmet-async: 1.3.0_biqbaboplfbrettd7655fr4n2y
react-i18next: 11.18.6_ulhmqqxshznzmtuvahdi5nasbq
react-router-dom: 6.4.0_biqbaboplfbrettd7655fr4n2y
swr: 1.3.0_react@18.2.0
@ -9581,16 +9581,19 @@ packages:
resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==}
dev: false
/react-helmet/6.1.0_react@18.2.0:
resolution: {integrity: sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==}
/react-helmet-async/1.3.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==}
peerDependencies:
react: '>=16.3.0'
react: ^16.6.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0
dependencies:
object-assign: 4.1.1
'@babel/runtime': 7.19.0
invariant: 2.2.4
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-fast-compare: 3.2.0
react-side-effect: 2.1.2_react@18.2.0
shallowequal: 1.1.0
dev: false
/react-i18next/11.18.6_ulhmqqxshznzmtuvahdi5nasbq:
@ -9747,14 +9750,6 @@ packages:
- webpack-hot-middleware
- webpack-plugin-serve
/react-side-effect/2.1.2_react@18.2.0:
resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==}
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@ -10257,6 +10252,10 @@ packages:
/setprototypeof/1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
/shallowequal/1.1.0:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
dev: false
/shebang-command/2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}

View File

@ -34,3 +34,25 @@ export const ADMIN_LIST_STATUS = {
name: 'deleted',
},
};
export const ADMIN_NAV_MENUS = [
{
name: 'dashboard',
children: [],
},
{
name: 'contents',
child: [{ name: 'questions' }, { name: 'answers' }],
},
{
name: 'users',
},
{
name: 'flags',
// badgeContent: 5,
},
{
name: 'settings',
child: [{ name: 'general' }, { name: 'interface' }],
},
];

View File

@ -7,3 +7,302 @@ export interface FormValue<T = any> {
export interface FormDataType {
[prop: string]: FormValue;
}
export interface Paging {
page: number;
page_size?: number;
}
export type ReportType = 'question' | 'answer' | 'comment' | 'user';
export type ReportAction = 'close' | 'flag' | 'review';
export interface ReportParams {
type: ReportType;
action: ReportAction;
}
export interface TagBase {
display_name: string;
slug_name: string;
}
export interface Tag extends TagBase {
main_tag_slug_name?: string;
original_text?: string;
parsed_text?: string;
}
export interface SynonymsTag extends Tag {
tag_id: string;
tag?: string;
}
export interface TagInfo extends TagBase {
tag_id: string;
original_text: string;
parsed_text: string;
follow_count: number;
question_count: number;
is_follower: boolean;
member_actions;
created_at?;
updated_at?;
main_tag_slug_name?: string;
excerpt?;
}
export interface QuestionParams {
title: string;
content: string;
html: string;
tags: Tag[];
}
export interface ListResult<T = any> {
count: number;
list: T[];
}
export interface AnswerParams {
content: string;
html: string;
question_id: string;
id: string;
}
export interface LoginReqParams {
e_mail: string;
/** password */
pass: string;
captcha_id?: string;
captcha_code?: string;
}
export interface RegisterReqParams extends LoginReqParams {
name: string;
}
export interface ModifyPasswordReq {
old_pass: string;
pass: string;
}
/** User */
export interface ModifyUserReq {
display_name: string;
username?: string;
avatar: string;
bio: string;
bio_html?: string;
location: string;
website: string;
}
export interface UserInfoBase {
avatar: string;
username: string;
display_name: string;
rank: number;
website: string;
location: string;
ip_info?: string;
/** 'forbidden' | 'normal' | 'delete'
*/
status?: string;
/** roles */
is_admin?: true;
}
export interface UserInfoRes extends UserInfoBase {
bio: string;
bio_html: string;
create_time?: string;
/** value = 1 active; value = 2 inactivated
*/
mail_status: number;
e_mail?: string;
[prop: string]: any;
}
export interface AvatarUploadReq {
file: FormData;
}
export interface ImgCodeReq {
captcha_id?: string;
captcha_code?: string;
}
export interface ImgCodeRes {
captcha_id: string;
captcha_img: string;
verify: boolean;
}
export interface PasswordResetReq extends ImgCodeReq {
e_mail: string;
}
export interface CheckImgReq {
action: 'login' | 'e_mail' | 'find_pass';
}
export interface SetNoticeReq {
notice_switch: boolean;
}
export interface QuestionDetailRes {
id: string;
title: string;
content: string;
html: string;
tags: any[];
view_count: number;
unique_view_count?: number;
answer_count: number;
favorites_count: number;
follow_counts: 0;
accepted_answer_id: string;
last_answer_id: string;
create_time: string;
update_time: string;
user_info: UserInfoBase;
answered: boolean;
collected: boolean;
[prop: string]: any;
}
export interface AnswersReq extends Paging {
order?: 'default' | 'updated';
question_id: string;
}
export interface AnswerItem {
id: string;
question_id: string;
content: string;
html: string;
create_time: string;
update_time: string;
user_info: UserInfoBase;
[prop: string]: any;
}
export interface PostAnswerReq {
content: string;
html: string;
question_id: string;
}
export interface PageUser {
id;
displayName;
userName?;
avatar_url?;
}
export interface LangsType {
label: string;
value: string;
}
/**
* @description interface for Question
*/
export type QuestionOrderBy =
| 'newest'
| 'active'
| 'frequent'
| 'score'
| 'unanswered';
export interface QueryQuestionsReq extends Paging {
order: QuestionOrderBy;
tags?: string[];
}
export type AdminQuestionStatus = 'available' | 'closed' | 'deleted';
export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted';
export interface AdminContentsReq extends Paging {
status: AdminContentsFilterBy;
}
/**
* @description interface for Answer
*/
export type AdminAnswerStatus = 'available' | 'deleted';
/**
* @description interface for Users
*/
export type UserFilterBy = 'all' | 'inactive' | 'suspended' | 'deleted';
/**
* @description interface for Flags
*/
export type FlagStatus = 'pending' | 'completed';
export type FlagType = 'all' | 'question' | 'answer' | 'comment';
export interface AdminFlagsReq extends Paging {
status: FlagStatus;
object_type: FlagType;
}
/**
* @description interface for Admin Settings
*/
export interface AdminSettingsGeneral {
name: string;
short_description: string;
description: string;
}
export interface AdminSettingsInterface {
logo: string;
language: string;
theme: string;
}
export interface SiteSettings {
general: AdminSettingsGeneral;
interface: AdminSettingsInterface;
}
/**
* @description interface for Activity
*/
export interface FollowParams {
is_cancel: boolean;
object_id: string;
}
/**
* @description search request params
*/
export interface SearchParams {
q: string;
order: string;
page: number;
size?: number;
}
/**
* @description search response data
*/
export interface SearchResItem {
object_type: string;
object: {
id: string;
title: string;
excerpt: string;
created_at: number;
user_info: UserInfoBase;
vote_count: number;
answer_count: number;
accepted: boolean;
tags: TagBase[];
};
}
export interface SearchRes extends ListResult<SearchResItem> {
extra: any;
}

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import { Icon } from '@answer/components';
import { bookmark, postVote } from '@answer/services/api';
import { bookmark, postVote } from '@answer/api';
import { isLogin } from '@answer/utils';
import { userInfoStore } from '@answer/stores';
import { useToast } from '@answer/hooks';

View File

@ -5,23 +5,35 @@ import { Avatar } from '@answer/components';
interface Props {
data: any;
showAvatar?: boolean;
avatarSize?: string;
className?: string;
}
const Index: FC<Props> = ({
data,
showAvatar = true,
avatarSize = '20px',
className = 'fs-14',
}) => {
return (
<div className={`text-secondary ${className}`}>
<Link to={`/users/${data?.username}`}>
<Avatar avatar={data?.avatar} size={avatarSize} className="me-1" />
</Link>
<Link to={`/users/${data?.username}`} className="me-1 text-break">
{data?.display_name}
</Link>
{data?.status !== 'deleted' ? (
<Link to={`/users/${data?.username}`}>
{showAvatar && (
<Avatar avatar={data?.avatar} size={avatarSize} className="me-1" />
)}
<span className="me-1 text-break">{data?.display_name}</span>
</Link>
) : (
<>
{showAvatar && (
<Avatar avatar={data?.avatar} size={avatarSize} className="me-1" />
)}
<span className="me-1 text-break">{data?.display_name}</span>
</>
)}
<span className="fw-bold">{data?.rank}</span>
</div>
);

View File

@ -17,15 +17,20 @@ const ActionBar = ({
onReply,
onVote,
onAction,
userStatus = '',
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'comment' });
return (
<div className="d-flex justify-content-between fs-14">
<div className="d-flex align-items-center">
<Link to={`/users/${username}`}>{nickName}</Link>
<span className="mx-1">·</span>
<FormatTime time={createdAt} className="text-secondary me-3" />
<div className="d-flex align-items-center text-secondary">
{userStatus !== 'deleted' ? (
<Link to={`/users/${username}`}>{nickName}</Link>
) : (
<span>{nickName}</span>
)}
<span className="mx-1"></span>
<FormatTime time={createdAt} className="me-3" />
<Button
variant="link"
size="sm"

View File

@ -36,7 +36,7 @@ const Form = ({
<Mentions pageUsers={pageUsers.getUsers()}>
<TextArea size="sm" value={value} onChange={handleChange} />
</Mentions>
<div className="text-muted fs-14">{t(`tip_${mode}`)}</div>
<div className="form-text">{t(`tip_${mode}`)}</div>
</div>
{type === 'edit' ? (
<div className="d-flex flex-column">

View File

@ -22,7 +22,7 @@ const Form = ({ userName, onSendReply, onCancel, mode }) => {
<Mentions pageUsers={pageUsers.getUsers()}>
<TextArea size="sm" value={value} onChange={handleChange} />
</Mentions>
<div className="text-muted fs-14">{t(`tip_${mode}`)}</div>
<div className="form-text">{t(`tip_${mode}`)}</div>
</div>
<div className="d-flex flex-column">
<Button

View File

@ -7,16 +7,16 @@ import classNames from 'classnames';
import { unionBy } from 'lodash';
import { marked } from 'marked';
import * as Types from '@answer/common/interface';
import {
useQueryComments,
addComment,
deleteComment,
updateComment,
postVote,
} from '@answer/services/api';
} from '@answer/api';
import { Modal } from '@answer/components';
import { usePageUsers, useReportModal } from '@answer/hooks';
import * as Types from '@answer/services/types';
import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils';
import { Form, ActionBar, Reply } from './components';
@ -269,6 +269,7 @@ const Comment = ({ objectId, mode }) => {
voteCount={item.vote_count}
isVote={item.is_vote}
memberActions={item.member_actions}
userStatus={item.user_status}
onReply={() => {
handleReply(item.comment_id);
}}

View File

@ -90,7 +90,7 @@ const Editor = ({
return;
}
if (editor.getValue() !== value) {
// editor.setValue(value);
editor.setValue(value);
}
}, [editor, value]);

View File

@ -3,7 +3,7 @@ import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Modal as AnswerModal } from '@answer/components';
import { uploadImage } from '@answer/services/api';
import { uploadImage } from '@answer/api';
import ToolItem from '../toolItem';
import { IEditorContext } from '../types';
@ -232,10 +232,12 @@ const Image: FC<IEditorContext> = ({ editor }) => {
</Modal.Header>
<Modal.Body>
<Tabs onSelect={handleSelect}>
<Tab eventKey="localImage" title={t('image.tab_1')}>
<Tab eventKey="localImage" title={t('image.tab_image')}>
<Form className="mt-3" onSubmit={handleClick}>
<Form.Group controlId="editor.imgLink" className="mb-3">
<Form.Label>{t('image.form1.fields.file.label')}</Form.Label>
<Form.Label>
{t('image.form_image.fields.file.label')}
</Form.Label>
<Form.Control
type="file"
onChange={onUpload}
@ -243,13 +245,13 @@ const Image: FC<IEditorContext> = ({ editor }) => {
/>
<Form.Control.Feedback type="invalid">
{t('image.form1.fields.file.msg.empty')}
{t('image.form_image.fields.file.msg.empty')}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="editor.imgDescription" className="mb-3">
<Form.Label>
{t('image.form1.fields.description.label')}
{t('image.form_image.fields.description.label')}
</Form.Label>
<Form.Control
type="text"
@ -262,10 +264,12 @@ const Image: FC<IEditorContext> = ({ editor }) => {
</Form.Group>
</Form>
</Tab>
<Tab eventKey="remoteImage" title={t('image.tab_2')}>
<Tab eventKey="remoteImage" title={t('image.tab_url')}>
<Form className="mt-3" onSubmit={handleClick}>
<Form.Group controlId="editor.imgUrl" className="mb-3">
<Form.Label>{t('image.form2.fields.url.label')}</Form.Label>
<Form.Label>
{t('image.form_url.fields.url.label')}
</Form.Label>
<Form.Control
type="text"
value={link.value}
@ -275,12 +279,14 @@ const Image: FC<IEditorContext> = ({ editor }) => {
isInvalid={currentTab === 'remoteImage' && link.isInvalid}
/>
<Form.Control.Feedback type="invalid">
{t('image.form2.fields.url.msg.empty')}
{t('image.form_url.fields.url.msg.empty')}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="editor.imgName" className="mb-3">
<Form.Label>{t('image.form2.fields.name.label')}</Form.Label>
<Form.Label>
{t('image.form_url.fields.name.label')}
</Form.Label>
<Form.Control
type="text"
value={imageName.value}

View File

@ -14,7 +14,7 @@ const Link: FC<IEditorContext> = ({ editor }) => {
};
const [visible, setVisible] = useState(false);
const [link, setLink] = useState({
value: 'http://',
value: 'https://',
isInvalid: false,
errorMsg: '',
});

View File

@ -5,8 +5,7 @@ import { NavLink } from 'react-router-dom';
import { TagSelector, Tag } from '@answer/components';
import { isLogin } from '@answer/utils';
import { useFollowingTags, followTags } from '@/services/tag.api';
import { useFollowingTags, followTags } from '@answer/api';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'question' });

View File

@ -10,6 +10,10 @@
font-weight: bold;
color: #fff;
}
&.icon-link {
width: 46px;
height: 38px;
}
}
.placeholder-search::placeholder {
color: rgba(255, 255, 255, 0.75);

View File

@ -14,8 +14,7 @@ import { useSearchParams, NavLink, Link, useNavigate } from 'react-router-dom';
import { Avatar, Icon } from '@answer/components';
import { userInfoStore, siteInfoStore, interfaceStore } from '@answer/stores';
import { logout } from '@answer/services/api';
import { useQueryNotificationRedDot } from '@answer/services/notification.api';
import { logout, useQueryNotificationStatus } from '@answer/api';
import Storage from '@answer/utils/storage';
import './index.scss';
@ -29,7 +28,7 @@ const Header: FC = () => {
const [searchStr, setSearch] = useState('');
const siteInfo = siteInfoStore((state) => state.siteInfo);
const { interface: interfaceInfo } = interfaceStore();
const { data: redDot } = useQueryNotificationRedDot();
const { data: redDot } = useQueryNotificationStatus();
const handleInput = (val) => {
setSearch(val);
};
@ -108,8 +107,8 @@ const Header: FC = () => {
<Nav.Link
as={NavLink}
to="/users/notifications/inbox"
className="me-2 position-relative">
<div className="px-2 text-white text-opacity-75">
className="icon-link d-flex align-items-center justify-content-center p-0 me-2 position-relative">
<div className="text-white text-opacity-75">
<Icon name="bell-fill" className="fs-5" />
</div>
{(redDot?.inbox || 0) > 0 && (
@ -120,8 +119,8 @@ const Header: FC = () => {
<Nav.Link
as={Link}
to="/users/notifications/achievement"
className="me-2 position-relative">
<div className="px-2 text-white text-opacity-75">
className="icon-link d-flex align-items-center justify-content-center p-0 me-2 position-relative">
<div className="text-white text-opacity-75">
<Icon name="trophy-fill" className="fs-5" />
</div>
{(redDot?.achievement || 0) > 0 && (
@ -135,7 +134,7 @@ const Header: FC = () => {
id="dropdown-basic"
as="a"
className="no-toggle pointer">
<Avatar size="38px" avatar={user?.avatar} />
<Avatar size="36px" avatar={user?.avatar} />
</Dropdown.Toggle>
<Dropdown.Menu>

View File

@ -3,8 +3,8 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useHotQuestions } from '@/services/question.api';
import { Icon } from '@/components';
import { useHotQuestions } from '@answer/api';
import { Icon } from '@answer/components';
const HotQuestions: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'question' });

View File

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState, FC } from 'react';
import { Dropdown } from 'react-bootstrap';
import * as Types from '@answer/services/types';
import * as Types from '@answer/common/interface';
interface IProps {
children: React.ReactNode;

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