Merge branch 'ui' of git.backyard.segmentfault.com:opensource/answer into ui

This commit is contained in:
haitao(lj) 2022-09-29 18:24:38 +08:00
commit bf85f586d8
79 changed files with 1429 additions and 789 deletions

2
.gitignore vendored
View File

@ -10,7 +10,7 @@
/.vscode/*.log
/cmd/answer/*.sh
/cmd/logs
/configs/*
/configs/config-dev.yaml
/go.work*
/logs
/ui/build

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:

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

@ -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)

581
assets/answer.sql Normal file
View File

@ -0,0 +1,581 @@
-- --------------------------------------------------------
--
-- 表的结构 `activity`
--
CREATE TABLE `activity` (
`id` bigint(20) NOT NULL COMMENT 'Activity ID autoincrement',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'create time',
`updated_at` timestamp NULL DEFAULT NULL COMMENT 'update time',
`user_id` bigint(20) NOT NULL COMMENT 'the user ID that generated the activity or affected by the activity',
`trigger_user_id` bigint(20) NOT NULL DEFAULT '0',
`object_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'the object ID that affected by the activity',
`activity_type` int(11) NOT NULL COMMENT 'activity type, correspond to config id',
`cancelled` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'mark this activity if cancelled or not,default 0(not cancelled)',
`rank` int(11) NOT NULL DEFAULT '0' COMMENT 'rank of current operating user affected',
`has_rank` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'this activity has rank or not'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='activity';
-- --------------------------------------------------------
--
-- 表的结构 `answer`
--
CREATE TABLE `answer` (
`id` bigint(20) NOT NULL COMMENT 'answer id',
`question_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'question id',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'answer user id',
`original_text` mediumtext NOT NULL COMMENT 'original text',
`parsed_text` mediumtext NOT NULL COMMENT 'parsed text',
`status` int(11) NOT NULL DEFAULT '1' COMMENT ' answer status(available: 1; deleted: 10)',
`adopted` int(11) NOT NULL DEFAULT '1' COMMENT 'adopted (1 failed 2 adopted)',
`comment_count` int(11) NOT NULL DEFAULT '0' COMMENT 'comment count',
`vote_count` int(11) NOT NULL DEFAULT '0' COMMENT 'vote count',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`revision_id` bigint(20) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='answer';
-- --------------------------------------------------------
--
-- 表的结构 `collection`
--
CREATE TABLE `collection` (
`id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'collection id',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'user id',
`object_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'object id',
`user_collection_group_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'user collection group id',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='collection';
-- --------------------------------------------------------
--
-- 表的结构 `collection_group`
--
CREATE TABLE `collection_group` (
`id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL DEFAULT '0',
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'the collection group name',
`default_group` int(11) NOT NULL DEFAULT '1' COMMENT 'mark this group is default, default 1',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='collection group';
-- --------------------------------------------------------
--
-- 表的结构 `comment`
--
CREATE TABLE `comment` (
`id` bigint(20) NOT NULL COMMENT 'comment id',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'create time',
`updated_at` timestamp NULL DEFAULT NULL COMMENT 'update time',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'user id',
`reply_user_id` bigint(20) DEFAULT NULL COMMENT 'reply user id',
`reply_comment_id` bigint(20) DEFAULT NULL COMMENT 'reply comment id',
`object_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'object id',
`question_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'question id',
`vote_count` int(11) NOT NULL DEFAULT '0' COMMENT 'user vote amount',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'comment status(available: 0; deleted: 10)',
`original_text` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'original comment content',
`parsed_text` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'parsed comment content'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='comment';
-- --------------------------------------------------------
--
-- 表的结构 `config`
--
CREATE TABLE `config` (
`id` int(11) NOT NULL COMMENT 'config id',
`key` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'the config key',
`value` text COLLATE utf8mb4_unicode_ci COMMENT 'the config value, custom data structures and types'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='config';
-- --------------------------------------------------------
--
-- 表的结构 `meta`
--
CREATE TABLE `meta` (
`id` int(10) UNSIGNED NOT NULL COMMENT 'id',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'created time',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'updated time',
`object_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'object id',
`key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'key',
`value` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'value'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='meta';
-- --------------------------------------------------------
--
-- 表的结构 `notification`
--
CREATE TABLE `notification` (
`id` bigint(20) NOT NULL COMMENT 'notification id',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'user_id',
`object_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'object id',
`content` text NOT NULL COMMENT 'notification content',
`type` int(11) NOT NULL DEFAULT '0' COMMENT '1 inbox 2 achievements',
`is_read` int(11) NOT NULL DEFAULT '1' COMMENT 'read status(unread: 1; read 2)',
`status` int(11) NOT NULL DEFAULT '1' COMMENT 'notification status(normal: 1; delete 2)',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'create time',
`updated_at` timestamp NULL DEFAULT NULL COMMENT 'update time'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='notification';
-- --------------------------------------------------------
--
-- 表的结构 `notification_read`
--
CREATE TABLE `notification_read` (
`id` int(11) NOT NULL COMMENT 'id',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'create time',
`updated_at` timestamp NULL DEFAULT NULL COMMENT 'update time',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'user id',
`message_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'message id',
`is_read` int(11) NOT NULL DEFAULT '1' COMMENT 'read status(unread: 1; read 2)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='notification read record';
-- --------------------------------------------------------
--
-- 表的结构 `question`
--
CREATE TABLE `question` (
`id` bigint(20) NOT NULL COMMENT 'question id',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'user id',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT 'question title',
`original_text` mediumtext NOT NULL COMMENT 'original text',
`parsed_text` mediumtext NOT NULL COMMENT 'parsed text',
`status` int(11) NOT NULL DEFAULT '1' COMMENT ' question status(available: 1; deleted: 10)',
`view_count` int(11) NOT NULL DEFAULT '0' COMMENT 'view count',
`unique_view_count` int(11) NOT NULL DEFAULT '0' COMMENT 'unique view count',
`vote_count` int(11) NOT NULL DEFAULT '0' COMMENT 'vote count',
`answer_count` int(11) NOT NULL DEFAULT '0' COMMENT 'answer count',
`collection_count` int(11) NOT NULL DEFAULT '0' COMMENT 'collection count',
`follow_count` int(11) NOT NULL DEFAULT '0' COMMENT 'follow count',
`accepted_answer_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'accepted answer id',
`last_answer_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'last answer id',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time',
`post_update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'answer the last update time',
`revision_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'revision id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='question';
-- --------------------------------------------------------
--
-- 表的结构 `report`
--
CREATE TABLE `report` (
`id` bigint(20) NOT NULL COMMENT 'id',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'create time',
`updated_at` timestamp NULL DEFAULT NULL COMMENT 'update time',
`user_id` bigint(20) NOT NULL COMMENT 'reporter user id',
`object_id` bigint(20) NOT NULL COMMENT 'object id',
`reported_user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'reported user id',
`object_type` int(11) NOT NULL DEFAULT '0' COMMENT 'revision type',
`report_type` int(11) NOT NULL DEFAULT '0' COMMENT 'report type',
`content` text NOT NULL COMMENT 'report content',
`flaged_type` int(11) NOT NULL DEFAULT '0',
`flaged_content` text,
`status` int(11) NOT NULL DEFAULT '1' COMMENT 'status(normal: 1; pending:2; delete: 10)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='report';
-- --------------------------------------------------------
--
-- 表的结构 `revision`
--
CREATE TABLE `revision` (
`id` bigint(20) NOT NULL COMMENT 'id',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'create time',
`updated_at` timestamp NULL DEFAULT NULL COMMENT 'update time',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'user id',
`object_type` int(11) NOT NULL DEFAULT '0' COMMENT 'revision type(question: 1; answer 2)',
`object_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'object id',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT 'title',
`content` text NOT NULL COMMENT 'content',
`log` varchar(255) DEFAULT NULL,
`status` int(11) NOT NULL DEFAULT '1' COMMENT 'revision status(normal: 1; delete 2)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='revision';
-- --------------------------------------------------------
--
-- 表的结构 `site_info`
--
CREATE TABLE `site_info` (
`id` int(10) UNSIGNED NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'update time',
`type` varchar(64) DEFAULT NULL,
`content` mediumtext,
`status` int(11) NOT NULL DEFAULT '1'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------
--
-- 表的结构 `tag`
--
CREATE TABLE `tag` (
`id` bigint(20) NOT NULL COMMENT 'tag_id',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'create time',
`updated_at` timestamp NULL DEFAULT NULL COMMENT 'update time',
`main_tag_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'main tag id',
`main_tag_slug_name` varchar(50) NOT NULL DEFAULT '' COMMENT 'main tag slug name',
`slug_name` varchar(50) NOT NULL DEFAULT '' COMMENT 'slug name',
`display_name` varchar(50) NOT NULL DEFAULT '' COMMENT 'display name',
`original_text` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'original comment content',
`parsed_text` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'parsed comment content',
`follow_count` int(11) NOT NULL DEFAULT '0' COMMENT 'follow count',
`question_count` int(11) NOT NULL COMMENT 'question count',
`status` int(11) NOT NULL DEFAULT '1' COMMENT 'tag status(available: 1; deleted: 10)',
`revision_id` bigint(20) NOT NULL DEFAULT '0' COMMENT 'revision id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='tag';
--
-- 转存表中的数据 `tag`
--
INSERT INTO `tag` (`id`, `created_at`, `updated_at`, `main_tag_id`, `main_tag_slug_name`, `slug_name`, `display_name`, `original_text`, `parsed_text`, `follow_count`, `question_count`, `status`, `revision_id`) VALUES
(10030000000000007, '2022-09-07 02:07:16', '2022-09-28 06:56:32', 10030000000000364, 'javascript', 'js', 'Js', 'js', 'Js', 7, 21, 1, 90),
(10030000000000008, '2022-09-07 02:07:16', '2022-09-19 08:40:36', 0, '', 'php', 'PHP', '2122121对外只有3001一个端口群集内有负载策略会分发给不同实例2121211212121212121', '<p>2122121对外只有3001一个端口群集内有负载策略会分发给不同实例2121211212121212121</p>\n', 2, 3, 10, 101),
(10030000000000009, '2022-09-07 02:07:16', '2022-09-28 08:00:48', 0, '', 'go', 'Go', 'abcbcsasasasasa', '<p>abcbcsasasasasa</p>\n', 7, 17, 1, 259),
(10030000000000010, '2022-09-07 02:07:16', '2022-09-28 05:16:00', 0, '', 'apple', 'Apple', '对外只有3001一个端口群集内有负载策略会分发给不同实例', '<p>对外只有3001一个端口群集内有负载策略会分发给不同实例</p>', 5, 3, 1, 0),
(10030000000000113, '2022-09-07 02:07:16', '2022-09-26 08:44:04', 0, '', 'lua', 'lua', '对外只有3001一个端口群集内有负载策略会分发给不同实例', '<p>对外只有3001一个端口群集内有负载策略会分发给不同实例</p>', 4, 4, 1, 0),
(10030000000000114, '2022-09-07 02:07:16', '2022-09-27 09:57:35', 0, '', 'dell', 'dell', '对外只有3001一个端口群集内有负载策略会分发给不同实例', '<p>对外只有3001一个端口群集内有负载策略会分发给不同实例</p>', 3, 0, 1, 0),
(10030000000000118, '2022-09-07 02:07:16', '2022-09-28 08:20:13', 0, '', 'dells', 'dells', '对外只有3001一个端口群集内有负载策略会分发给不同实例', '<p>对外只有3001一个端口群集内有负载策略会分发给不同实例</p>', 2, 2, 1, 0),
(10030000000000183, '2022-09-07 02:07:16', '2022-09-27 08:41:31', 0, '', 'string', 'string', '对外只有3001一个端口群集内有负载策略会分发给不同实例\n\n好不错', '<p>对外只有3001一个端口群集内有负载策略会分发给不同实例</p>\n<p>好不错</p>\n', 2, 22, 1, 186),
(10030000000000311, '2022-09-07 02:07:16', '2022-09-28 07:59:36', 10030000000000009, 'go', 'golang', 'golang', '', '', 4, 6, 1, 0),
(10030000000000312, '2022-09-07 02:12:06', '2022-09-15 08:56:04', 0, '', '算法', '算法', '', '', 1, 0, 1, 0),
(10030000000000314, '2022-09-07 02:14:08', '2022-09-26 06:54:36', 0, '', 'java', 'java', '', '', 1, 6, 1, 0),
(10030000000000316, '2022-09-07 02:14:28', '2022-09-28 08:20:13', 10030000000000009, 'go', 'golang2', 'golang2', 'golang2', 'golang2', 1, 2, 1, 28),
(10030000000000324, '2022-09-07 07:14:19', '2022-09-20 03:27:14', 0, '', 'python', 'python', '', '', 0, 2, 1, 41),
(10030000000000325, '2022-09-07 07:17:52', '2022-09-27 08:41:31', 0, '', 'spring boot', 'spring boot', '', '', 2, 2, 1, 43),
(10030000000000364, '2022-09-08 01:45:26', '2022-09-27 08:41:31', 0, '', 'javascript', 'javascript', 'JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. While it is most well-known as the scripting language for Web pages, many non-browser environments also use it, such as Node.js, Apache CouchDB and Adobe Acrobat.\n\nJavaScript is a prototype-based, multi-paradigm, single-threaded, dynamic language, supporting object-oriented, imperative, and declarative (e.g. functional programming) styles. Read more about JavaScript. This section is dedicated to the JavaScript language itself, and not the parts that are specific to Web pages or other host environments. For information about APIs that are specific to Web pages, please see Web APIs and DOM.', '<p>JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. While it is most well-known as the scripting language for Web pages, many non-browser environments also use it, such as Node.js, Apache CouchDB and Adobe Acrobat.</p>\n<p>JavaScript is a prototype-based, multi-paradigm, single-threaded, dynamic language, supporting object-oriented, imperative, and declarative (e.g. functional programming) styles. Read more about JavaScript. This section is dedicated to the JavaScript language itself, and not the parts that are specific to Web pages or other host environments. For information about APIs that are specific to Web pages, please see Web APIs and DOM.</p>\n', 3, 3, 1, 292),
(10030000000000401, '2022-09-08 07:26:38', '2022-09-28 08:20:13', 0, '', 'test', 'test', '111111\ngjsdghjfghjgjfhgasjdhgfhjasg[hello](https://www.baidu.com)\n\n22222222222\n\n3333333\n\n', '<p>111111\ngjsdghjfghjgjfhgasjdhgfhjasg<a href=\"https://www.baidu.com\">hello</a></p>\n<p>22222222222</p>\n<p>3333333</p>\n', 1, 3, 1, 303),
(10030000000000408, '2022-09-08 08:38:03', '2022-09-08 08:38:03', 0, '', 'code ', 'code ', 'code is code', '<p>code is code</p>\n', 0, 0, 1, 70),
(10030000000000428, '2022-09-08 11:44:10', '2022-09-26 06:54:31', 0, '', 'abc', 'abc', 'JavaScript 是一门弱类型的动态脚本语言,支持多种编程范式,包括面向对象和函数式编程,被广泛用于 Web 开发。\n\n一般来说完整的JavaScript包括以下几个部分\n- ECMAScript描述了该语言的语法和基本对象\n- 文档对象模型DOM描述处理网页内容的方法和接口\n- 浏览器对象模型BOM描述与浏览器进行交互的方法和接口\n\n它的基本特点如下\n- 是一种解释性脚本语言(代码不进行预编译)。\n- 主要用来向HTML页面添加交互行为。\n- 可以直接嵌入HTML页面但写成单独的js文件有利于结构和行为的分离。\n\nJavaScript常用来完成以下任务\n- 嵌入动态文本于HTML页面\n- 对浏览器事件作出响应\n- 读写HTML元素\n- 在数据被提交到服务器之前验证数据\n- 检测访客的浏览器信息\n\n![《 Javascript 优点在整个语言中占多大比例?][1]\n\n [1]: http://segmentfault.com/img/bVFXU', '<p>JavaScript 是一门弱类型的动态脚本语言,支持多种编程范式,包括面向对象和函数式编程,被广泛用于 Web 开发。</p>\n<p>一般来说完整的JavaScript包括以下几个部分</p>\n<ul>\n<li>ECMAScript描述了该语言的语法和基本对象</li>\n<li>文档对象模型DOM描述处理网页内容的方法和接口</li>\n<li>浏览器对象模型BOM描述与浏览器进行交互的方法和接口</li>\n</ul>\n<p>它的基本特点如下:</p>\n<ul>\n<li>是一种解释性脚本语言(代码不进行预编译)。</li>\n<li>主要用来向HTML页面添加交互行为。</li>\n<li>可以直接嵌入HTML页面但写成单独的js文件有利于结构和行为的分离。</li>\n</ul>\n<p>JavaScript常用来完成以下任务</p>\n<ul>\n<li>嵌入动态文本于HTML页面</li>\n<li>对浏览器事件作出响应</li>\n<li>读写HTML元素</li>\n<li>在数据被提交到服务器之前验证数据</li>\n<li>检测访客的浏览器信息</li>\n</ul>\n<p><img src=\"http://segmentfault.com/img/bVFXU\" alt=\"《 Javascript 优点在整个语言中占多大比例?\"></p>\n', 0, 0, 1, 113),
(10030000000000507, '2022-09-16 06:32:36', '2022-09-16 08:14:27', 0, '', 'dddddd', 'dddddd', 'ddddddddddddddddd', 'ddddddddddddddddddd', 0, 1, 1, 144),
(10030000000000510, '2022-09-16 07:04:34', '2022-09-16 07:30:43', 0, '', 'vim', 'vim', 'vim', '<p>vim</p>\n', 0, 0, 1, 147),
(10030000000000592, '2022-09-20 10:23:28', '2022-09-27 08:41:31', 0, '', 'android-studio', 'Android-Studio', '', '', 3, 2, 1, 170),
(10030000000000644, '2022-09-23 09:25:39', '2022-09-27 08:41:31', 0, '', 'great', 'great', 'great', '<p>great</p>\n', 2, 1, 1, 272),
(10030000000000687, '2022-09-26 10:48:07', '2022-09-28 01:39:19', 0, '', 'qwer', 'qwerDis', 'qwert', '<p>qwert</p>\n', 0, 0, 1, 316),
(10030000000000688, '2022-09-26 10:49:39', '2022-09-28 08:20:13', 0, '', 'qwers', 'qwerDiss', 'qwerts', 'qwerts', 0, 1, 1, 300),
(10030000000000689, '2022-09-26 10:50:34', '2022-09-28 08:20:13', 10030000000000364, 'javascript', 'jss', '', '', '', 0, 0, 1, 302),
(10030000000000708, '2022-09-27 02:12:03', '2022-09-28 05:00:51', 0, '', 'flutter', 'Flutter', '', '<p>212121212121</p>\n', 3, 1, 1, 305),
(10030000000000712, '2022-09-27 02:32:53', '2022-09-27 10:09:37', 0, '', 'nextjs', 'Nextjs', '', '<p>2121212121</p>\n', 1, 1, 1, 307),
(10030000000000713, '2022-09-27 02:36:29', '2022-09-27 08:41:31', 0, '', 'umijs', 'Umijs', '212121212112121211', '<p>212121212112121211</p>\n', 1, 1, 1, 309),
(10030000000000739, '2022-09-28 01:45:09', '2022-09-28 01:45:18', 0, '', '', '', '', '', 0, 0, 1, 320);
-- --------------------------------------------------------
--
-- 表的结构 `tag_rel`
--
CREATE TABLE `tag_rel` (
`id` bigint(20) NOT NULL COMMENT 'tag_list_id',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'create time',
`updated_at` timestamp NULL DEFAULT NULL COMMENT 'update time',
`object_id` bigint(20) NOT NULL COMMENT 'object_id',
`tag_id` bigint(20) NOT NULL COMMENT 'tag_id',
`status` int(11) NOT NULL DEFAULT '1' COMMENT 'tag_list_status(available: 1; deleted: 10)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='tag relation';
-- --------------------------------------------------------
--
-- 表的结构 `uniqid`
--
CREATE TABLE `uniqid` (
`id` bigint(20) NOT NULL COMMENT 'uniqid_id',
`uniqid_type` int(11) NOT NULL DEFAULT '0' COMMENT 'uniqid_type'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='uniqid';
-- --------------------------------------------------------
--
-- 表的结构 `user`
--
CREATE TABLE `user` (
`id` bigint(20) NOT NULL COMMENT 'user id',
`created_at` timestamp NULL DEFAULT NULL COMMENT 'create time',
`updated_at` timestamp NULL DEFAULT NULL COMMENT 'update time',
`suspended_at` timestamp NULL DEFAULT NULL COMMENT 'suspended time',
`deleted_at` timestamp NULL DEFAULT NULL COMMENT 'delete time',
`last_login_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'last login date',
`username` varchar(50) NOT NULL DEFAULT '' COMMENT 'username',
`pass` varchar(255) NOT NULL DEFAULT '' COMMENT 'password',
`e_mail` varchar(100) NOT NULL COMMENT 'email',
`mail_status` tinyint(4) NOT NULL DEFAULT '2' COMMENT 'mail status(1 pass 2 to be verified)',
`notice_status` int(11) NOT NULL DEFAULT '2' COMMENT 'notice status(1 on 2off)',
`follow_count` int(11) NOT NULL DEFAULT '0' COMMENT 'follow count',
`answer_count` int(11) NOT NULL DEFAULT '0' COMMENT 'answer_count',
`question_count` int(11) NOT NULL DEFAULT '0' COMMENT 'question_count',
`rank` int(11) NOT NULL DEFAULT '0' COMMENT 'rank',
`status` int(11) NOT NULL DEFAULT '1' COMMENT 'user status(available: 0; deleted: 10)',
`authority_group` int(11) NOT NULL DEFAULT '1' COMMENT 'authority group',
`display_name` varchar(50) NOT NULL DEFAULT '' COMMENT 'display name',
`avatar` varchar(255) NOT NULL DEFAULT '' COMMENT 'avatar',
`mobile` varchar(20) NOT NULL COMMENT 'mobile',
`bio` text NOT NULL COMMENT 'bio markdown',
`bio_html` text NOT NULL COMMENT 'bio html',
`website` varchar(255) NOT NULL DEFAULT '' COMMENT 'website',
`location` varchar(100) NOT NULL DEFAULT '' COMMENT 'location',
`ip_info` varchar(255) NOT NULL DEFAULT '' COMMENT 'ip info',
`is_admin` int(11) NOT NULL COMMENT 'admin flag'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='user';
-- --------------------------------------------------------
--
-- 表的结构 `user_group`
--
CREATE TABLE `user_group` (
`id` bigint(20) UNSIGNED NOT NULL COMMENT 'user group id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='user group';
--
-- 转储表的索引
--
--
-- 表的索引 `activity`
--
ALTER TABLE `activity`
ADD PRIMARY KEY (`id`) USING BTREE,
ADD KEY `post_id` (`object_id`),
ADD KEY `user_id` (`user_id`),
ADD KEY `trigger_user_id` (`trigger_user_id`);
--
-- 表的索引 `answer`
--
ALTER TABLE `answer`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `collection`
--
ALTER TABLE `collection`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `collection_group`
--
ALTER TABLE `collection_group`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `comment`
--
ALTER TABLE `comment`
ADD PRIMARY KEY (`id`) USING BTREE;
--
-- 表的索引 `config`
--
ALTER TABLE `config`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `key` (`key`);
--
-- 表的索引 `meta`
--
ALTER TABLE `meta`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `notification`
--
ALTER TABLE `notification`
ADD PRIMARY KEY (`id`) USING BTREE,
ADD KEY `idx_objectid` (`object_id`);
--
-- 表的索引 `notification_read`
--
ALTER TABLE `notification_read`
ADD PRIMARY KEY (`id`) USING BTREE;
--
-- 表的索引 `question`
--
ALTER TABLE `question`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `report`
--
ALTER TABLE `report`
ADD PRIMARY KEY (`id`) USING BTREE;
--
-- 表的索引 `revision`
--
ALTER TABLE `revision`
ADD PRIMARY KEY (`id`) USING BTREE;
--
-- 表的索引 `site_info`
--
ALTER TABLE `site_info`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `tag`
--
ALTER TABLE `tag`
ADD PRIMARY KEY (`id`) USING BTREE,
ADD UNIQUE KEY `slug_name` (`slug_name`);
--
-- 表的索引 `tag_rel`
--
ALTER TABLE `tag_rel`
ADD PRIMARY KEY (`id`) USING BTREE,
ADD UNIQUE KEY `idx_obj_tag_id` (`object_id`,`tag_id`) USING BTREE,
ADD KEY `idx_questionid` (`object_id`),
ADD KEY `idx_tagid` (`tag_id`) USING BTREE;
--
-- 表的索引 `uniqid`
--
ALTER TABLE `uniqid`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `user`
--
ALTER TABLE `user`
ADD PRIMARY KEY (`id`) USING BTREE,
ADD UNIQUE KEY `username` (`username`);
--
-- 表的索引 `user_group`
--
ALTER TABLE `user_group`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `id` (`id`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `activity`
--
ALTER TABLE `activity`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Activity ID autoincrement';
--
-- 使用表AUTO_INCREMENT `answer`
--
ALTER TABLE `answer`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'answer id';
--
-- 使用表AUTO_INCREMENT `collection_group`
--
ALTER TABLE `collection_group`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `comment`
--
ALTER TABLE `comment`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'comment id';
--
-- 使用表AUTO_INCREMENT `config`
--
ALTER TABLE `config`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'config id';
--
-- 使用表AUTO_INCREMENT `meta`
--
ALTER TABLE `meta`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id';
--
-- 使用表AUTO_INCREMENT `notification`
--
ALTER TABLE `notification`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'notification id';
--
-- 使用表AUTO_INCREMENT `notification_read`
--
ALTER TABLE `notification_read`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id';
--
-- 使用表AUTO_INCREMENT `report`
--
ALTER TABLE `report`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id';
--
-- 使用表AUTO_INCREMENT `revision`
--
ALTER TABLE `revision`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id';
--
-- 使用表AUTO_INCREMENT `site_info`
--
ALTER TABLE `site_info`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `tag_rel`
--
ALTER TABLE `tag_rel`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'tag_list_id';
--
-- 使用表AUTO_INCREMENT `uniqid`
--
ALTER TABLE `uniqid`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'uniqid_id';
--
-- 使用表AUTO_INCREMENT `user`
--
ALTER TABLE `user`
MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'user id';
--
-- 使用表AUTO_INCREMENT `user_group`
--
ALTER TABLE `user_group`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'user group id';
COMMIT;

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()))
@ -55,13 +74,18 @@ func main() {
if err != nil {
panic(err)
}
err = cli.CommandCli.InitDB()
if err != nil {
panic(err)
}
defer cleanup()
if err := app.Run(); err != nil {
panic(err)
}
}
func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application {
func newApplication(serverConf *conf.Server, server *gin.Engine, cli *cli.Cli) *pacman.Application {
return pacman.NewApp(
pacman.WithName(Name),
pacman.WithVersion(Version),

View File

@ -12,6 +12,7 @@ import (
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/server"
"github.com/segmentfault/answer/internal/base/translator"
"github.com/segmentfault/answer/internal/cli"
"github.com/segmentfault/answer/internal/controller"
"github.com/segmentfault/answer/internal/controller_backyard"
"github.com/segmentfault/answer/internal/repo"
@ -33,6 +34,7 @@ func initApplication(
serviceConf *service_config.ServiceConfig,
logConf log.Logger) (*pacman.Application, func(), error) {
panic(wire.Build(
cli.ProviderSetCli,
server.ProviderSetServer,
router.ProviderSetRouter,
controller.ProviderSetController,

View File

@ -12,6 +12,7 @@ import (
"github.com/segmentfault/answer/internal/base/middleware"
"github.com/segmentfault/answer/internal/base/server"
"github.com/segmentfault/answer/internal/base/translator"
"github.com/segmentfault/answer/internal/cli"
"github.com/segmentfault/answer/internal/controller"
"github.com/segmentfault/answer/internal/controller_backyard"
"github.com/segmentfault/answer/internal/repo"
@ -101,13 +102,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 +129,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 +143,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,10 +170,11 @@ 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)
application := newApplication(serverConf, ginEngine)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware)
cliCli := cli.NewCli(dataData)
application := newApplication(serverConf, ginEngine, cliCli)
return application, func() {
cleanup2()
cleanup()

6
configs/config.go Normal file
View File

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

19
configs/config.yaml Normal file
View File

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

View File

@ -4,4 +4,6 @@ services:
build:
context: .
image: github.com/segmentfault/answer
volumes:
- ./data:/data
restart: on-failure

View File

@ -884,7 +884,7 @@ const docTemplate = `{
}
},
"/answer/api/v1/answer/list": {
"post": {
"get": {
"security": [
{
"ApiKeyAuth": []
@ -3614,6 +3614,46 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/status": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get user status info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "get user status info",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetUserResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/vote/down": {
"post": {
"security": [
@ -4206,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"
@ -4222,6 +4266,10 @@ const docTemplate = `{
"description": "user id",
"type": "string"
},
"user_status": {
"description": "user status",
"type": "string"
},
"username": {
"description": "username",
"type": "string"
@ -5464,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

@ -872,7 +872,7 @@
}
},
"/answer/api/v1/answer/list": {
"post": {
"get": {
"security": [
{
"ApiKeyAuth": []
@ -3602,6 +3602,46 @@
}
}
},
"/answer/api/v1/user/status": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get user status info",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"User"
],
"summary": "get user status info",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetUserResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/vote/down": {
"post": {
"security": [
@ -4194,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"
@ -4210,6 +4254,10 @@
"description": "user id",
"type": "string"
},
"user_status": {
"description": "user status",
"type": "string"
},
"username": {
"description": "username",
"type": "string"
@ -5452,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
@ -1877,7 +1883,7 @@ paths:
tags:
- api-answer
/answer/api/v1/answer/list:
post:
get:
consumes:
- application/json
description: AnswerList <br> <b>order</b> (default or updated)
@ -3529,6 +3535,28 @@ paths:
summary: UserRegisterByEmail
tags:
- User
/answer/api/v1/user/status:
get:
consumes:
- application/json
description: get user status info
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetUserResp'
type: object
security:
- ApiKeyAuth: []
summary: get user status info
tags:
- User
/answer/api/v1/vote/down:
post:
consumes:

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

@ -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)

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

@ -0,0 +1,93 @@
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 InitDB() {
}
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
}

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

@ -0,0 +1,50 @@
package cli
import (
"bytes"
"github.com/google/wire"
"github.com/segmentfault/answer/assets"
"github.com/segmentfault/answer/internal/base/data"
"github.com/segmentfault/answer/internal/entity"
)
// ProviderSetCli is providers.
var ProviderSetCli = wire.NewSet(NewCli)
type Cli struct {
DataSource *data.Data
}
var CommandCli *Cli
func NewCli(dataSource *data.Data) *Cli {
CommandCli = &Cli{DataSource: dataSource}
return CommandCli
}
// InitDB init db
func (c *Cli) InitDB() (err error) {
// check db connection
err = c.DataSource.DB.Ping()
if err != nil {
return err
}
exist, err := c.DataSource.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 = c.DataSource.DB.Import(s)
if err != nil {
return err
}
return nil
}

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

@ -0,0 +1,18 @@
package cli
import "fmt"
var usageDoc = `
answer
USAGE
answer command
COMMANDS
init init answer config, eg:answer init
run config path, eg:answer run -c data/config.yaml
`
func Usage() {
fmt.Println(usageDoc)
}

View File

@ -1,7 +1,6 @@
package controller
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
@ -176,23 +175,19 @@ func (ac *AnswerController) Update(ctx *gin.Context) {
// @Produce json
// @Param data body schema.AnswerList true "AnswerList"
// @Success 200 {string} string ""
// @Router /answer/api/v1/answer/list [post]
func (ac *AnswerController) AnswerList(c *gin.Context) {
input := new(schema.AnswerList)
err := c.BindJSON(input)
if err != nil {
handler.HandleResponse(c, err, nil)
// @Router /answer/api/v1/answer/list [get]
func (ac *AnswerController) AnswerList(ctx *gin.Context) {
req := &schema.AnswerList{}
if handler.BindAndCheck(ctx, req) {
return
}
ctx := context.Background()
userId := middleware.GetLoginUserIDFromContext(c)
input.LoginUserID = userId
list, count, err := ac.answerService.SearchList(ctx, input)
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
list, count, err := ac.answerService.SearchList(ctx, req)
if err != nil {
handler.HandleResponse(c, err, nil)
handler.HandleResponse(ctx, err, nil)
return
}
handler.HandleResponse(c, nil, gin.H{
handler.HandleResponse(ctx, nil, gin.H{
"list": list,
"count": count,
})

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

@ -80,6 +80,21 @@ func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// GetUserStatus get user status info
// @Summary get user status info
// @Description get user status info
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/status [get]
func (uc *UserController) GetUserStatus(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
resp, err := uc.userService.GetUserStatus(ctx, userID)
handler.HandleResponse(ctx, err, resp)
}
// UserEmailLogin godoc
// @Summary UserEmailLogin
// @Description UserEmailLogin

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

@ -2,6 +2,7 @@ package repo
import (
"context"
"time"
"github.com/segmentfault/answer/internal/base/constant"
"github.com/segmentfault/answer/internal/base/data"
@ -77,7 +78,9 @@ func (ar *answerRepo) UpdateAnswer(ctx context.Context, answer *entity.Answer, C
}
func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error) {
_, err = ar.data.DB.Where("id =?", answer.ID).Cols("status").Update(answer)
now := time.Now()
answer.UpdatedAt = now
_, err = ar.data.DB.Where("id =?", answer.ID).Cols("status", "updated_at").Update(answer)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@ -129,7 +132,7 @@ func (ar *answerRepo) UpdateAdopted(ctx context.Context, id string, questionId s
if err != nil {
return err
}
if id != "" {
if id != "0" {
data.Adopted = schema.Answer_Adopted_Enable
_, err = ar.data.DB.Where("id = ?", id).Cols("adopted").Update(&data)
if err != nil {
@ -181,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 {
@ -191,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()
}
@ -216,7 +219,7 @@ func (ar *answerRepo) CmsSearchList(ctx context.Context, search *entity.CmsAnswe
offset := search.Page * search.PageSize
session := ar.data.DB.Where("")
session = session.And("status =?", search.Status)
session = session.OrderBy("created_at desc")
session = session.OrderBy("updated_at desc")
session = session.Limit(search.PageSize, offset)
count, err = session.FindAndCount(&rows)
if err != nil {

View File

@ -2,6 +2,7 @@ package repo
import (
"context"
"time"
"github.com/segmentfault/answer/internal/base/constant"
"github.com/segmentfault/answer/internal/base/data"
@ -92,7 +93,9 @@ func (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionId st
}
func (qr *questionRepo) UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error) {
_, err = qr.data.DB.Where("id =?", question.ID).Cols("status").Update(question)
now := time.Now()
question.UpdatedAt = now
_, err = qr.data.DB.Where("id =?", question.ID).Cols("status", "updated_at").Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@ -238,7 +241,7 @@ func (qr *questionRepo) CmsSearchList(ctx context.Context, search *schema.CmsQue
offset := search.Page * search.PageSize
session := qr.data.DB.Table("question")
session = session.And("status =?", search.Status)
session = session.OrderBy("created_at desc")
session = session.OrderBy("updated_at desc")
session = session.Limit(search.PageSize, offset)
count, err = session.FindAndCount(&rows)
if err != nil {

View File

@ -66,7 +66,7 @@ func (rr *reportRepo) GetReportListPage(ctx context.Context, dto schema.GetRepor
}
// order
session.OrderBy("created_at desc")
session.OrderBy("updated_at desc")
total, err = pager.Help(dto.Page, dto.PageSize, &reports, cond, session)
if err != nil {

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,
}
}
@ -213,9 +213,7 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAc
func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) (resp []schema.SearchResp, err error) {
for _, r := range res {
var (
objectKey string
uInfo *schema.UserBasicInfo
objectKey string
tags []schema.TagResp
tagsEntity []entity.Tag
object schema.SearchObject
@ -228,16 +226,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`").
@ -258,7 +252,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",
@ -275,8 +269,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

@ -87,6 +87,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
r.GET("/comment", a.commentController.GetComment)
// user
r.GET("/user/status", a.userController.GetUserStatus)
r.GET("/user/action/record", a.userController.ActionRecord)
r.POST("/user/login/email", a.userController.UserEmailLogin)
r.POST("/user/register/email", a.userController.UserRegisterByEmail)
@ -100,7 +101,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
//answer
r.GET("/answer/info", a.answerController.Get)
r.POST("/answer/list", a.answerController.AnswerList)
r.GET("/answer/page", a.answerController.AnswerList)
r.GET("/personal/answer/page", a.questionController.UserAnswerList)
//question

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
@ -113,10 +31,10 @@ type AnswerUpdateReq struct {
}
type AnswerList struct {
QuestionId string `json:"question_id" ` // question_id
Order string `json:"order" ` // 1 Default 2 time
Page int `json:"page" form:"page"` //Query number of pages
PageSize int `json:"page_size" form:"page_size"` //Search page size
QuestionId string `json:"question_id" form:"question_id"` // question_id
Order string `json:"order" form:"order"` // 1 Default 2 time
Page int `json:"page" form:"page"` //Query number of pages
PageSize int `json:"page_size" form:"page_size"` //Search page size
LoginUserID string `json:"-" `
}

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

@ -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

@ -76,7 +76,12 @@ func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) {
if ok {
r.Status = statusShow
}
}
// GetUserStatusResp get user status info
type GetUserStatusResp struct {
// user status
Status string `json:"status"`
}
// GetOtherUserInfoByUsernameResp get user response
@ -161,7 +166,7 @@ const (
var UserStatusShow = map[int]string{
1: "normal",
9: "forbidden",
10: "delete",
10: "deleted",
}
var UserStatusShowMsg = map[int]string{
1: "",
@ -277,15 +282,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

@ -191,16 +191,25 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq
// UpdateAdopted
func (as *AnswerService) UpdateAdopted(ctx context.Context, req *schema.AnswerAdoptedReq) error {
if req.AnswerID == "" {
req.AnswerID = "0"
}
if req.UserID == "" {
return nil
}
newAnswerInfo, exist, err := as.answerRepo.GetByID(ctx, req.AnswerID)
if err != nil {
return err
}
if !exist {
return errors.BadRequest(reason.AnswerNotFound)
newAnswerInfo := &entity.Answer{}
newAnswerInfoexist := false
var err error
if req.AnswerID != "0" {
newAnswerInfo, newAnswerInfoexist, err = as.answerRepo.GetByID(ctx, req.AnswerID)
if err != nil {
return err
}
if !newAnswerInfoexist {
return errors.BadRequest(reason.AnswerNotFound)
}
}
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)
@ -250,12 +259,14 @@ func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string,
log.Error(err)
}
}
err := as.answerActivityService.AcceptAnswer(
ctx, newAnswerInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID)
if err != nil {
log.Error(err)
if newAnswerInfo.ID != "" {
err := as.answerActivityService.AcceptAnswer(
ctx, newAnswerInfo.ID, questionInfo.UserID, newAnswerInfo.UserID, newAnswerInfo.UserID == userID)
if err != nil {
log.Error(err)
}
}
}
func (as *AnswerService) Get(ctx context.Context, answerID, loginUserId string) (*schema.AnswerInfo, *schema.QuestionInfo, bool, error) {
@ -413,7 +424,7 @@ func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionU
Type: schema.NotificationTypeInbox,
ObjectID: answerID,
}
msg.ObjectType = constant.QuestionObjectType
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.UpdateAnswer
notice_queue.AddNotification(msg)
}
@ -425,7 +436,7 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context, ques
Type: schema.NotificationTypeInbox,
ObjectID: answerID,
}
msg.ObjectType = constant.QuestionObjectType
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.AnswerTheQuestion
notice_queue.AddNotification(msg)
}

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

@ -3,12 +3,13 @@ package questioncommon
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/segmentfault/answer/internal/base/reason"
"github.com/segmentfault/answer/internal/service/activity_common"
"github.com/segmentfault/answer/internal/service/config"
"github.com/segmentfault/answer/internal/service/meta"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/answer/internal/entity"
"github.com/segmentfault/answer/internal/schema"
@ -139,7 +140,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUser
return showinfo, err
}
if !has {
return showinfo, fmt.Errorf("the question could not be found")
return showinfo, errors.BadRequest(reason.QuestionNotFound)
}
showinfo = qs.ShowFormat(ctx, dbinfo)

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

@ -13,25 +13,24 @@ import (
"github.com/segmentfault/answer/internal/service/revision"
usercommon "github.com/segmentfault/answer/internal/service/user_common"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// 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,
}
@ -70,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)
@ -80,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
}
@ -125,7 +118,6 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi
}
questionInfo = rs.questionCommon.ShowFormat(ctx, &question)
item.ContentParsed = questionInfo
log.Error(item.ContentParsed)
case constant.ObjectTypeStrMapping["answer"]:
err = json.Unmarshal([]byte(item.Content), &answer)
if err != nil {
@ -156,6 +148,5 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi
if err != nil {
item.ContentParsed = item.Content
}
item.CreatedAtParsed = item.CreatedAt.Unix()
}

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

@ -358,7 +358,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

@ -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

@ -63,6 +63,25 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
return resp, nil
}
// GetUserStatus get user info by user id
func (us *UserService) GetUserStatus(ctx context.Context, userID string) (resp *schema.GetUserStatusResp, err error) {
resp = &schema.GetUserStatusResp{}
if len(userID) == 0 {
return resp, nil
}
userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
resp = &schema.GetUserStatusResp{
Status: schema.UserStatusShow[userInfo.Status],
}
return resp, nil
}
func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) (
resp *schema.GetOtherUserInfoResp, err error) {
userInfo, exist, err := us.userRepo.GetByUsername(ctx, username)

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

@ -18,7 +18,7 @@ const Index: FC<Props> = ({
}) => {
return (
<div className={`text-secondary ${className}`}>
{data.status !== 'deleted' ? (
{data?.status !== 'deleted' ? (
<Link to={`/users/${data?.username}`}>
{showAvatar && (
<Avatar avatar={data?.avatar} size={avatarSize} className="me-1" />

9
ui/static.go Normal file
View File

@ -0,0 +1,9 @@
package ui
import (
"embed"
_ "embed"
)
//go:embed build
var Build embed.FS

View File

@ -1,9 +0,0 @@
package web
import "embed"
//go:embed html/index.html
var Html []byte
//go:embed html/static
var Static embed.FS