Compare commits

...

159 Commits

Author SHA1 Message Date
aichy126 919ad47e4a Merge branch 'main' of github.com:answerdev/answer
Build Docker Hub Image / build (push) Has been cancelled
2022-11-29 13:12:35 +08:00
aichy cfa984d6aa Merge branch 'hot-fix/0.4.2/panic' into 'main'
Hot fix/0.4.2/panic

See merge request opensource/answer!289
2022-11-29 04:56:47 +00:00
aichy126 bc54bc5f7c fix isadmin 2022-11-29 12:14:51 +08:00
aichy126 c6491762d5 fix isadmin 2022-11-29 11:55:41 +08:00
LinkinStar 8c599136d6 fix(auth): add get admin flag from context function 2022-11-29 11:54:12 +08:00
贾海涛(龙笛) e9a0b517e5 Merge branch 'revert-762628bb' into 'main'
Revert "Merge branch 'feat/ui-0.5.0' into 'main'"

See merge request opensource/answer!283
2022-11-28 09:19:24 +00:00
贾海涛(龙笛) 741bab581d Revert "Merge branch 'feat/ui-0.5.0' into 'main'"
This reverts merge request !281
2022-11-28 09:12:52 +00:00
贾海涛(龙笛) 762628bb1a Merge branch 'feat/ui-0.5.0' into 'main'
Feat/ui 0.5.0

See merge request opensource/answer!281
2022-11-28 08:59:11 +00:00
LinkinStar 88df9a4732 Merge branch 'main' into github-main
Build Docker Hub Image / build (push) Has been cancelled
2022-11-23 17:06:25 +08:00
linkinstar 8c1c1a664c fix: #80 #92 2022-11-23 08:38:26 +00:00
aichy126 98114a6a05 fix en_us 2022-11-23 16:32:06 +08:00
haitao(lj) 9c2e05d280 fix(router): remove unnecessary guard for password-reset @github: #92 2022-11-23 10:49:00 +08:00
aichy126 b7e6d3ee35 fix delete question 2022-11-22 18:03:18 +08:00
aichy126 6ec9dbec87 update permission 2022-11-22 17:35:41 +08:00
aichy126 0fac38860f en_US.yaml 2022-11-22 17:06:15 +08:00
aichy126 2676822331 fix question answer delete 2022-11-22 16:54:28 +08:00
aichy126 f216536e26 update question delete 2022-11-22 16:22:30 +08:00
aichy126 761944440c Merge branch 'main' into ai_0.3_fix_del 2022-11-22 16:19:42 +08:00
aichy126 4f1762f01a fix question delete 2022-11-22 16:13:55 +08:00
aichy126 76ace2c9a3 fix question delete 2022-11-22 11:12:08 +08:00
Fen 7004bbe230 fix: discord invite link 2022-11-22 10:24:21 +08:00
linkinstar 661d8a7f8f Merge branch 'release/0.4.0' into 'main'
Build Docker Hub Image / build (push) Has been cancelled
upgrade v0.4.0

See merge request opensource/answer!268
2022-11-21 06:33:47 +00:00
linkinstar 738febd893 Merge branch 'pr/github' into 'main'
Workflows/docker (#88)

See merge request opensource/answer!267
2022-11-21 06:30:04 +00:00
LinkinStar 418a4a33dc Merge branch 'github-main' into pr/github 2022-11-21 14:23:48 +08:00
LinkinStar 51dc24e226 doc: update makefile version 2022-11-21 14:20:34 +08:00
贾海涛(龙笛) ce5bf43948 Merge branch 'feat/ui-v0.4' into 'test'
Feat/ui v0.4

See merge request opensource/answer!266
2022-11-21 04:27:09 +00:00
robin 9caddd22b3 fix(ui): Fixed the problem that select cannot be selected 2022-11-21 12:21:37 +08:00
robin dc8407447c refactor(ui): remove obsolete code 2022-11-21 12:19:43 +08:00
贾海涛(龙笛) fd27fcca13 Merge branch 'feat/ui-v0.4' into 'test'
fix(legal&write): avoid some read properties of null/undefined error

See merge request opensource/answer!265
2022-11-21 03:51:10 +00:00
haitao(lj) 3aec45e287 fix(legal&write): avoid some read properties of null/undefined error 2022-11-21 11:44:19 +08:00
LinkinStar c3d0b110b7 fix: site info write set response empty array when site info not found 2022-11-21 11:30:44 +08:00
LinkinStar 17dd8e8930 Merge branch 'ai/0.4.0/tag' into test
# Conflicts:
#	internal/service/siteinfo/siteinfo_service.go
#	internal/service/siteinfo_common/siteinfo_service.go
2022-11-21 11:27:28 +08:00
LinkinStar ed9702ab8b fix: site info write set response empty array when site info not found 2022-11-21 11:24:41 +08:00
LinkinStar 3a713fb0c1 fix: set default version is 0.0.0, return site info is empty when site info can't found. fix dashboard return error when cache not found. 2022-11-21 11:16:37 +08:00
LinkinStar 04a12ee832 fix: set default version is 0.0.0, return site info is empty when site info can't found. fix dashboard return error when cache not found. 2022-11-21 11:10:13 +08:00
fen ae137d912c fix: legal layout 2022-11-21 11:09:07 +08:00
Li Shuailing 8e8cbf6621 Merge branch 'feat/ui-v0.4' into 'test'
feat: form for swwitch add label

See merge request opensource/answer!264
2022-11-21 03:05:51 +00:00
shuai 8501ef4c64 feat: form classname adjustment 2022-11-21 10:51:47 +08:00
shuai 1d3819673a feat: form for swwitch add label 2022-11-21 10:46:07 +08:00
LinkinStar ba02cf4095 feat: regenerate wire 2022-11-19 17:07:20 +08:00
LinkinStar 89a79a02fa Merge branch 'ai/0.4.0/tag' into test
# Conflicts:
#	cmd/answer/wire_gen.go
#	internal/service/search_service.go
#	internal/service/tag_common/tag_common.go
2022-11-19 17:06:19 +08:00
LinkinStar 747323b49c fix: add tag common service to controller 2022-11-19 17:04:46 +08:00
LinkinStar 697a98951f doc: regenerate swagger doc 2022-11-19 14:03:16 +08:00
LinkinStar 3a2c8700c9 feat: add tag common repo unit test 2022-11-19 14:00:49 +08:00
LinkinStar dc1f83f819 refactor: tag repo remove site info service 2022-11-19 14:00:49 +08:00
LinkinStar 0467165264 refactor: separate tag common repo and tag repo 2022-11-19 14:00:40 +08:00
LinkinStar d833cadae2 fix: repo test set notification userID to correct 2022-11-19 13:59:51 +08:00
aichy126 a0b0adde8d update answer order by 2022-11-19 13:59:51 +08:00
贾海涛(龙笛) 784f745cdb Merge branch 'feat/ui-v0.4' into 'test'
Feat/ui v0.4

See merge request opensource/answer!263
2022-11-19 05:46:10 +00:00
haitao(lj) c9799feb2a Merge branch 'feat/ui-v0.4' of git.backyard.segmentfault.com:opensource/answer into feat/ui-v0.4 2022-11-19 13:39:42 +08:00
haitao(lj) d7de1f2468 chore: update agreements, fix some issue dashbaord 2022-11-19 13:39:37 +08:00
LinkinStar 7868adcd7e feat: add tag common repo unit test 2022-11-18 20:11:19 +08:00
LinkinStar 432f8c3309 refactor: tag repo remove site info service 2022-11-18 19:56:10 +08:00
Li Shuailing 2f5f97510f Merge branch 'feat/ui-v0.4' into 'test'
fix: lang

See merge request opensource/answer!261
2022-11-18 11:14:58 +00:00
shuai 696ff4a94d fix: lang 2022-11-18 19:08:18 +08:00
Li Shuailing 68a7de29fe Merge branch 'feat/ui-v0.4' into 'test'
fix: tag selector component style adjustment

See merge request opensource/answer!260
2022-11-18 10:53:58 +00:00
shuai b5068db443 fix: write page styles adjustment 2022-11-18 18:46:13 +08:00
shuai d01e7f5529 fix: tag selector component style adjustment 2022-11-18 18:42:06 +08:00
LinkinStar fd4469f3db refactor: separate tag common repo and tag repo 2022-11-18 18:23:27 +08:00
LinkinStar 4075739fec fix: repo test set notification userID to correct 2022-11-18 16:52:05 +08:00
aichy126 0c1a77d52c update answer order by 2022-11-18 16:40:09 +08:00
Li Shuailing f001b63ad3 Merge branch 'feat/ui-v0.4' into 'test'
refactor(layout): remove min-height calc form page-wrap, prevent unexpected...

See merge request opensource/answer!259
2022-11-18 08:32:46 +00:00
Li Shuailing 908d78d3f4 Merge branch 'feat/ui-v0.4' into 'test'
fix: handle form error

See merge request opensource/answer!257
2022-11-18 07:27:12 +00:00
Fen 7c725f535d update: badge color 2022-11-18 13:48:15 +08:00
Fen 62b0521b9f update: discord badge 2022-11-18 12:22:47 +08:00
Fen 4d63d72a0b Update README.md
fix: typo
2022-11-18 10:59:45 +08:00
Li Shuailing 8caad012d5 Merge branch 'feat/ui-v0.4' into 'test'
Feat/ui v0.4

See merge request opensource/answer!256
2022-11-18 02:42:46 +00:00
Fen b47b19bb5e Update README.md
add discord
2022-11-18 10:38:38 +08:00
aichy 7336bae653 Merge branch 'ai/0.4.0/tag' into 'test'
Ai/0.4.0/tag

See merge request opensource/answer!255
2022-11-17 11:28:36 +00:00
aichy126 6742c5d707 update synonym error 2022-11-17 19:23:02 +08:00
aichy126 30039173c0 Merge branch 'ai/0.4.0/tag' of git.backyard.segmentfault.com:opensource/answer into ai/0.4.0/tag 2022-11-17 19:09:58 +08:00
aichy126 41bb72b110 check tags 2022-11-17 19:09:56 +08:00
Li Shuailing d39759007d Merge branch 'feat/ui-v0.4' into 'test'
Feat/ui v0.4

See merge request opensource/answer!253
2022-11-17 11:08:43 +00:00
LinkinStar 5c9e8333d3 Merge branch 'ai/0.4.0/tag' into test
# Conflicts:
#	internal/base/reason/reason.go
2022-11-17 19:02:11 +08:00
LinkinStar 8084870096 feat: change the original single form error return to multiple form error return 2022-11-17 18:59:09 +08:00
linkinstar e9dfeeb638 Merge branch 'ai/0.4.0/tag' into 'test'
Ai/0.4.0/tag

See merge request opensource/answer!252
2022-11-17 10:02:57 +00:00
LinkinStar 83ad1a185a feat: change the original single form error return to multiple form error return 2022-11-17 17:57:13 +08:00
aichy126 0519b558b7 update tag error 2022-11-17 17:46:02 +08:00
aichy126 045257cd07 Merge branch 'ai/0.4.0/tag' into test 2022-11-17 17:12:40 +08:00
aichy126 177b99f475 update tag recommend show 2022-11-17 17:12:27 +08:00
linkinstar 73c7ab7094 Merge branch 'ai/0.4.0/tag' into 'test'
feat: add recommend and reserved tag field migration

See merge request opensource/answer!251
2022-11-17 08:38:34 +00:00
Li Shuailing 5f4e20c8fd Merge branch 'feat/ui-v0.4' into 'test'
Feat/ui v0.4

See merge request opensource/answer!250
2022-11-17 07:00:42 +00:00
贾海涛(龙笛) 4a93337a2b Merge branch 'feat/ui-v0.4' into 'test'
bug fixes

See merge request opensource/answer!249
2022-11-17 06:51:37 +00:00
杨光富 7c7a32d91d Merge branch 'fix/search' into 'test'
Fix/search

See merge request opensource/answer!248
2022-11-17 06:19:41 +00:00
kumfo 1b242a182c feat: add question_id to search result 2022-11-17 14:14:02 +08:00
linkinstar e47c5a7e26 Merge branch 'fix/0.4.0/user-status' into 'test'
Fix/0.4.0/user status

See merge request opensource/answer!247
2022-11-17 04:16:59 +00:00
kumfo 7bdb6d2016 Merge branch 'dev' into fix/search 2022-11-17 12:12:23 +08:00
LinkinStar e8ab1c4202 fix: user verified email will update email status. so user status cache should be updated. 2022-11-17 12:10:20 +08:00
LinkinStar f6095fd476 feat: add recommend and reserved tag field migration 2022-11-17 10:09:18 +08:00
Li Shuailing 20b2d84aa9 Merge branch 'feat/ui-v0.4' into 'test'
Feat/ui v0.4

See merge request opensource/answer!246
2022-11-16 07:56:05 +00:00
aichy126 c9821f80ff Merge branch 'ai/0.4.0/tag' into test 2022-11-16 15:36:21 +08:00
aichy126 82130b70b1 swagger 2022-11-16 15:35:59 +08:00
aichy126 19075b869e fix ExistRecommend 2022-11-16 15:35:43 +08:00
aichy126 dc1a672a33 swagger 2022-11-16 15:31:24 +08:00
aichy126 aca215a731 Merge branch 'ai/0.4.0/tag' into test 2022-11-16 15:31:20 +08:00
aichy126 952757e2da Merge branch 'ai/0.4.0/tag' of git.backyard.segmentfault.com:opensource/answer into ai/0.4.0/tag 2022-11-16 15:28:25 +08:00
aichy126 0863eec2a0 update tag show 2022-11-16 15:28:23 +08:00
LinkinStar 5361315104 Merge branch 'ai/0.4.0/tag' into test 2022-11-16 15:02:36 +08:00
LinkinStar 5dd8fa4e7f fix: remove site write required tag val 2022-11-16 15:02:11 +08:00
Li Shuailing c1de1db15f Merge branch 'feat/ui-v0.4' into 'test'
fix: tags color

See merge request opensource/answer!244
2022-11-16 06:55:07 +00:00
LinkinStar e91afef608 Merge branch 'ai/0.4.0/tag' into test 2022-11-16 14:40:25 +08:00
LinkinStar 59daaa5c5d fix: remove site write tag val 2022-11-16 14:40:04 +08:00
Li Shuailing a63f9935c4 Merge branch 'feat/ui-v0.4' into 'test'
Feat/ui v0.4

See merge request opensource/answer!243
2022-11-16 06:01:50 +00:00
Li Shuailing 70efa79547 Merge branch 'feat/ui-v0.4' into 'dev'
Feat/ui v0.4

See merge request opensource/answer!242
2022-11-16 04:17:22 +00:00
aichy 8ba55f5955 Merge branch 'ai/0.4.0/tag' into 'test'
update search show tags

See merge request opensource/answer!241
2022-11-16 03:56:20 +00:00
aichy126 5e26663906 update search show tags 2022-11-16 11:48:02 +08:00
LinkinStar 3a817bb6c8 fix: rename icon ext is ico 2022-11-16 10:58:17 +08:00
LinkinStar 53f6413d45 doc: update site legal info get swagger doc 2022-11-16 10:58:17 +08:00
aichy b08e20ff99 Merge branch 'ai/0.4.0/tag' into 'test'
Ai/0.4.0/tag

See merge request opensource/answer!240
2022-11-16 02:57:49 +00:00
aichy126 233afca9ca Merge branch 'ai/0.4.0/tag' of git.backyard.segmentfault.com:opensource/answer into ai/0.4.0/tag 2022-11-16 10:51:56 +08:00
aichy126 167bac52db update tags order 2022-11-16 10:51:54 +08:00
LinkinStar 59fb67c651 feat: save site write config return form error when tag not found 2022-11-16 10:38:15 +08:00
aichy126 67b7e7d2b1 error msg 2022-11-16 10:32:29 +08:00
aichy 5f1ebbd950 Merge branch 'ai/0.4.0/tag' into 'test'
update tag info add reserved

See merge request opensource/answer!238
2022-11-15 11:06:57 +00:00
aichy126 2abec78482 update tag info add reserved 2022-11-15 19:00:48 +08:00
LinkinStar 4b7b40152b doc: generate swagger doc 2022-11-15 17:12:33 +08:00
LinkinStar b473a9e62a feat: tos save original and parsed text 2022-11-15 17:11:42 +08:00
LinkinStar d78d9e0415 fix: if site info is empty return empty json 2022-11-15 17:11:42 +08:00
aichy beb5603b01 Merge branch 'ai/0.4.0/tag' into 'test'
update search add question search tags

See merge request opensource/answer!236
2022-11-15 09:06:11 +00:00
aichy126 9e5196c0c2 update search add question search tags 2022-11-15 16:59:27 +08:00
linkinstar ef2b524f05 Merge branch 'feat/0.4.0/icon-customize' into 'test'
feat: branding support upload icon image

See merge request opensource/answer!235
2022-11-15 07:26:26 +00:00
LinkinStar 70842e18d8 feat: branding support upload icon image 2022-11-15 15:20:41 +08:00
linkinstar c299304b66 Merge branch 'ai/0.4.0/tag' into 'test'
Ai/0.4.0/tag

See merge request opensource/answer!234
2022-11-15 06:19:38 +00:00
LinkinStar 72866de5c1 fix: tag not exist error 2022-11-15 14:14:20 +08:00
aichy126 a66cf236ef admin add tag 2022-11-15 11:53:46 +08:00
杨光富 96f2ad92d1 Merge branch 'fix/search' into 'test'
Fix/search

See merge request opensource/answer!233
2022-11-15 03:41:54 +00:00
kumfo baa24ec0d1 style: merge from test 2022-11-15 11:35:14 +08:00
LinkinStar ba64be6845 feat: site info update write config 2022-11-15 11:24:39 +08:00
LinkinStar bcaa8b94cb doc: update swagger 2022-11-15 10:54:30 +08:00
LinkinStar f17c28ae1d Merge remote-tracking branch 'origin/ai/0.4.0/tag' into test
# Conflicts:
#	docs/docs.go
#	docs/swagger.json
#	docs/swagger.yaml
#	internal/base/constant/constant.go
#	internal/base/reason/reason.go
#	internal/controller_backyard/siteinfo_controller.go
#	internal/router/answer_api_router.go
#	internal/schema/siteinfo_schema.go
#	internal/service/siteinfo/siteinfo_service.go
2022-11-15 10:54:01 +08:00
aichy126 3662182ff3 update tags attribute 2022-11-15 10:40:54 +08:00
aichy126 3a992965df update recommendTag 2022-11-14 18:55:09 +08:00
kumfo 39f4b7342c feat: restructure the search 2022-11-14 18:47:22 +08:00
aichy126 baf0fce1c1 update reserved tags 2022-11-14 18:07:28 +08:00
aichy126 a390227c17 update reserved tags 2022-11-14 18:06:58 +08:00
linkinstar 81dc48c00b Merge branch 'feat/0.4.0/icon-customize' into 'test'
Feat/0.4.0/icon customize

See merge request opensource/answer!231
2022-11-14 09:11:19 +00:00
aichy126 678d903a09 verify question has recommend tag 2022-11-14 17:05:49 +08:00
LinkinStar d02f64b7a9 feat: site info add term of service and private policy 2022-11-14 17:04:25 +08:00
aichy126 d701ae60fb question tags add recommend 2022-11-14 16:44:48 +08:00
aichy126 2f6448b2c7 update recommend tag 2022-11-14 16:41:10 +08:00
LinkinStar f6fa850aa0 feat: unified file upload interface 2022-11-14 16:27:10 +08:00
LinkinStar 9e5c6c7a44 feat: add site info common service in tag service 2022-11-14 15:25:34 +08:00
LinkinStar 00336de025 feat: add site info write common service 2022-11-14 15:21:38 +08:00
LinkinStar cca5bcb59b feat: add site info tag config 2022-11-14 15:21:38 +08:00
LinkinStar 74ba5620bb feat: move logo field to site info 2022-11-14 15:21:38 +08:00
LinkinStar 6b63d17623 feat: add site info about branding 2022-11-14 15:21:38 +08:00
LinkinStar 23217f92b7 feat: add site info write common service 2022-11-14 15:21:17 +08:00
LinkinStar 5fed2e2608 feat: add site info tag config 2022-11-14 15:17:49 +08:00
LinkinStar 94c86fe3f8 feat: move logo field to site info 2022-11-14 14:56:56 +08:00
LinkinStar 6f815fb860 feat: add site info about branding 2022-11-14 11:50:00 +08:00
aichy 17ee34b49d Workflows/docker (#88)
* Github action add v prefix
2022-11-14 11:08:28 +08:00
Fen ab6cdc2ce3 fix pull request template path 2022-11-12 00:12:24 +08:00
Fen 701b711a7d update pull request template 2022-11-12 00:09:50 +08:00
Fen ded031ede2 Update issue templates 2022-11-12 00:04:43 +08:00
Fen b1a9ca56a4 Merge pull request #79 from answerdev/issue_template
add github issue template
2022-11-11 23:51:42 +08:00
fen 0141c1ec6e add github issue template 2022-11-11 23:50:14 +08:00
aichy126 b3d7816331 Merge branch 'ai_0.3_fix_del' into test 2022-11-11 15:51:15 +08:00
aichy126 c413bf3b09 update del Answer 2022-11-11 15:35:08 +08:00
kumfo 902e6636be Merge branch 'main' into fix/search 2022-11-10 16:00:45 +08:00
134 changed files with 3635 additions and 2913 deletions
+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,7 @@
Fixes #
## Proposed Changes
-
-
-
+3 -3
View File
@@ -4,9 +4,9 @@ on:
push:
branches: [ "main" ]
tags:
- 2.*
- 1.*
- 0.*
- v2.*
- v1.*
- v0.*
# pull_request:
# branches: [ "main" ]
+1
View File
@@ -20,5 +20,6 @@
Thumbs*.db
tmp
vendor/
.husky
/answer-data/
/answer
-1
View File
@@ -1 +0,0 @@
_
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd ui && pnpm commitlint --edit $1 --config commitlint.config.js
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd ui && npm run pre-commit
+2 -3
View File
@@ -1,6 +1,6 @@
.PHONY: build clean ui
VERSION=0.3.0
VERSION=0.4.0
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
@@ -26,11 +26,10 @@ generate:
go mod tidy
test:
@$(GO) test ./...
@$(GO) test ./internal/repo/repo_test
# clean all build result
clean:
@$(GO) clean ./...
@rm -f $(BIN)
+4 -3
View File
@@ -8,10 +8,11 @@ An open-source knowledge-based community software. You can use it to quickly bui
To learn more about the project, visit [answer.dev](https://answer.dev).
[![LICENSE](https://img.shields.io/badge/License-Apache-green)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/Language-React-blue.svg)](https://reactjs.org/)
[![LICENSE](https://img.shields.io/github/license/answerdev/answer)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/language-go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/language-react-blue.svg)](https://reactjs.org/)
[![Go Report Card](https://goreportcard.com/badge/github.com/answerdev/answer)](https://goreportcard.com/report/github.com/answerdev/answer)
[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/a6PZZbfnFx)
## Screenshots
+5 -4
View File
@@ -4,14 +4,15 @@
# Answer - 构建问答社区
一款极简的、问答形式的知识社区开源软件,用来快速构建产品你的产品问答支持社区、用户问答社区、粉丝社区等。
一款问答形式的知识社区开源软件,用来快速构建产品你的产品技术社区、客户支持社区、用户社区等。
了解更多关于该项目的内容,请访问 [answer.dev](https://answer.dev).
[![LICENSE](https://img.shields.io/badge/License-Apache-green)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/Language-React-blue.svg)](https://reactjs.org/)
[![LICENSE](https://img.shields.io/github/license/answerdev/answer)](https://github.com/answerdev/answer/blob/main/LICENSE)
[![Language](https://img.shields.io/badge/language-go-blue.svg)](https://golang.org/)
[![Language](https://img.shields.io/badge/language-react-blue.svg)](https://reactjs.org/)
[![Go Report Card](https://goreportcard.com/badge/github.com/answerdev/answer)](https://goreportcard.com/report/github.com/answerdev/answer)
[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/a6PZZbfnFx)
## 截图
+1 -1
View File
@@ -20,7 +20,7 @@ var (
// Name is the name of the project
Name = "answer"
// Version is the version of the project
Version = "development"
Version = "0.0.0"
// Revision is the git short commit revision number
Revision = ""
// Time is the build time of the project
+16 -11
View File
@@ -34,6 +34,7 @@ import (
"github.com/answerdev/answer/internal/repo/search_common"
"github.com/answerdev/answer/internal/repo/site_info"
"github.com/answerdev/answer/internal/repo/tag"
"github.com/answerdev/answer/internal/repo/tag_common"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/internal/repo/user"
"github.com/answerdev/answer/internal/router"
@@ -58,11 +59,12 @@ import (
"github.com/answerdev/answer/internal/service/report_backyard"
"github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/search_parser"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/answerdev/answer/internal/service/siteinfo_common"
tag2 "github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/tag_common"
tag_common2 "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/internal/service/user_backyard"
"github.com/answerdev/answer/internal/service/user_common"
@@ -115,8 +117,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
userCommon := usercommon.NewUserCommon(userRepo)
answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo)
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagRepo)
tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo)
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo)
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo)
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, configRepo)
@@ -127,18 +129,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
serviceVoteRepo := activity.NewVoteRepo(dataData, uniqueIDRepo, configRepo, activityRepo, userRankRepo, voteRepo)
voteService := service.NewVoteService(serviceVoteRepo, uniqueIDRepo, configRepo, questionRepo, answerRepo, commentCommonRepo, objService)
voteController := controller.NewVoteController(voteService)
tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo)
tagRelRepo := tag.NewTagRelRepo(dataData)
revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo)
revisionService := revision_common.NewRevisionService(revisionRepo, userRepo)
tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, revisionService, siteInfoCommonService)
followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
tagService := tag2.NewTagService(tagRepo, revisionService, followRepo)
tagController := controller.NewTagController(tagService, rankService)
tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService)
tagController := controller.NewTagController(tagService, tagCommonService, rankService)
followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo)
followService := follow.NewFollowService(followFollowRepo, followRepo, tagRepo)
followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo)
followController := controller.NewFollowController(followService)
collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo)
collectionGroupRepo := collection.NewCollectionGroupRepo(dataData)
tagRelRepo := tag.NewTagRelRepo(dataData)
tagCommonService := tagcommon.NewTagCommonService(tagRepo, tagRelRepo, revisionService)
collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo)
answerCommon := answercommon.NewAnswerCommon(answerRepo)
metaRepo := meta.NewMetaRepo(dataData)
@@ -154,8 +157,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo)
searchService := service.NewSearchService(searchParser, searchRepo)
searchController := controller.NewSearchController(searchService)
serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService)
revisionController := controller.NewRevisionController(serviceRevisionService)
@@ -171,7 +175,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reasonService := reason2.NewReasonService(reasonRepo)
reasonController := controller.NewReasonController(reasonService)
themeController := controller_backyard.NewThemeController()
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService)
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService, tagCommonService)
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData)
@@ -179,7 +183,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon)
notificationController := controller.NewNotificationController(notificationService)
dashboardController := controller.NewDashboardController(dashboardService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController)
uploadController := controller.NewUploadController(uploaderService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter()
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)
+493 -102
View File
@@ -503,6 +503,77 @@ const docTemplate = `{
}
}
},
"/answer/admin/api/siteinfo/branding": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site interface",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site interface",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteBrandingResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site info branding",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site info branding",
"parameters": [
{
"description": "branding info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteBrandingReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/general": {
"get": {
"security": [
@@ -645,6 +716,148 @@ const docTemplate = `{
}
}
},
"/answer/admin/api/siteinfo/legal": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Set the legal information for the site",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Set the legal information for the site",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteLegalResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site legal info",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site legal info",
"parameters": [
{
"description": "write info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteLegalReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/write": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site interface",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site interface",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteWriteResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site write info",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site write info",
"parameters": [
{
"description": "write info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteWriteReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/theme/options": {
"get": {
"security": [
@@ -1323,6 +1536,64 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "upload file",
"consumes": [
"multipart/form-data"
],
"tags": [
"Upload"
],
"summary": "upload file",
"parameters": [
{
"enum": [
"post",
"avatar",
"branding"
],
"type": "string",
"description": "identify the source of the file upload",
"name": "source",
"in": "formData",
"required": true
},
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/follow": {
"post": {
"security": [
@@ -2747,6 +3018,51 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/siteinfo/legal": {
"get": {
"description": "get site legal info",
"produces": [
"application/json"
],
"tags": [
"site"
],
"summary": "get site legal info",
"parameters": [
{
"enum": [
"tos",
"privacy"
],
"type": "string",
"description": "legal information type",
"name": "info_type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetSiteLegalInfoResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/tag": {
"get": {
"description": "get tag one",
@@ -3106,52 +3422,6 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/avatar/upload": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "UserUpdateInfo",
"consumes": [
"multipart/form-data"
],
"tags": [
"User"
],
"summary": "UserUpdateInfo",
"parameters": [
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/email": {
"put": {
"security": [
@@ -3671,52 +3941,6 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/user/post/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "upload user post file",
"consumes": [
"multipart/form-data"
],
"tags": [
"User"
],
"summary": "upload user post file",
"parameters": [
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/register/email": {
"post": {
"description": "UserRegisterByEmail",
@@ -4634,6 +4858,12 @@ const docTemplate = `{
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug name",
"type": "string"
@@ -4877,6 +5107,23 @@ const docTemplate = `{
}
}
},
"schema.GetSiteLegalInfoResp": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.GetTagPageResp": {
"type": "object",
"properties": {
@@ -4912,6 +5159,12 @@ const docTemplate = `{
"description": "question amount",
"type": "integer"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug_name",
"type": "string"
@@ -4972,6 +5225,12 @@ const docTemplate = `{
"description": "question amount",
"type": "integer"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug name",
"type": "string"
@@ -5535,6 +5794,9 @@ const docTemplate = `{
"id": {
"type": "string"
},
"question_id": {
"type": "string"
},
"status": {
"description": "Status",
"type": "string"
@@ -5571,6 +5833,56 @@ const docTemplate = `{
}
}
},
"schema.SiteBrandingReq": {
"type": "object",
"required": [
"logo",
"square_icon"
],
"properties": {
"favicon": {
"type": "string",
"maxLength": 512
},
"logo": {
"type": "string",
"maxLength": 512
},
"mobile_logo": {
"type": "string",
"maxLength": 512
},
"square_icon": {
"type": "string",
"maxLength": 512
}
}
},
"schema.SiteBrandingResp": {
"type": "object",
"required": [
"logo",
"square_icon"
],
"properties": {
"favicon": {
"type": "string",
"maxLength": 512
},
"logo": {
"type": "string",
"maxLength": 512
},
"mobile_logo": {
"type": "string",
"maxLength": 512
},
"square_icon": {
"type": "string",
"maxLength": 512
}
}
},
"schema.SiteGeneralReq": {
"type": "object",
"required": [
@@ -5647,10 +5959,6 @@ const docTemplate = `{
"type": "string",
"maxLength": 128
},
"logo": {
"type": "string",
"maxLength": 256
},
"theme": {
"type": "string",
"maxLength": 128
@@ -5673,10 +5981,6 @@ const docTemplate = `{
"type": "string",
"maxLength": 128
},
"logo": {
"type": "string",
"maxLength": 256
},
"theme": {
"type": "string",
"maxLength": 128
@@ -5687,6 +5991,80 @@ const docTemplate = `{
}
}
},
"schema.SiteLegalReq": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.SiteLegalResp": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.SiteWriteReq": {
"type": "object",
"properties": {
"recommend_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"required_tag": {
"type": "boolean"
},
"reserved_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.SiteWriteResp": {
"type": "object",
"properties": {
"recommend_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"required_tag": {
"type": "boolean"
},
"reserved_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.TagItem": {
"type": "object",
"properties": {
@@ -5720,6 +6098,12 @@ const docTemplate = `{
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"type": "string"
}
@@ -6003,6 +6387,10 @@ const docTemplate = `{
},
"schema.UserEmailLogin": {
"type": "object",
"required": [
"e_mail",
"pass"
],
"properties": {
"captcha_code": {
"description": "captcha_code",
@@ -6014,11 +6402,14 @@ const docTemplate = `{
},
"e_mail": {
"description": "e_mail",
"type": "string"
"type": "string",
"maxLength": 500
},
"pass": {
"description": "password",
"type": "string"
"type": "string",
"maxLength": 32,
"minLength": 8
}
}
},
+493 -102
View File
@@ -491,6 +491,77 @@
}
}
},
"/answer/admin/api/siteinfo/branding": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site interface",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site interface",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteBrandingResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site info branding",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site info branding",
"parameters": [
{
"description": "branding info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteBrandingReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/general": {
"get": {
"security": [
@@ -633,6 +704,148 @@
}
}
},
"/answer/admin/api/siteinfo/legal": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Set the legal information for the site",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "Set the legal information for the site",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteLegalResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site legal info",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site legal info",
"parameters": [
{
"description": "write info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteLegalReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/write": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site interface",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site interface",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteWriteResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site write info",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site write info",
"parameters": [
{
"description": "write info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteWriteReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/theme/options": {
"get": {
"security": [
@@ -1311,6 +1524,64 @@
}
}
},
"/answer/api/v1/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "upload file",
"consumes": [
"multipart/form-data"
],
"tags": [
"Upload"
],
"summary": "upload file",
"parameters": [
{
"enum": [
"post",
"avatar",
"branding"
],
"type": "string",
"description": "identify the source of the file upload",
"name": "source",
"in": "formData",
"required": true
},
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/follow": {
"post": {
"security": [
@@ -2735,6 +3006,51 @@
}
}
},
"/answer/api/v1/siteinfo/legal": {
"get": {
"description": "get site legal info",
"produces": [
"application/json"
],
"tags": [
"site"
],
"summary": "get site legal info",
"parameters": [
{
"enum": [
"tos",
"privacy"
],
"type": "string",
"description": "legal information type",
"name": "info_type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetSiteLegalInfoResp"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/tag": {
"get": {
"description": "get tag one",
@@ -3094,52 +3410,6 @@
}
}
},
"/answer/api/v1/user/avatar/upload": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "UserUpdateInfo",
"consumes": [
"multipart/form-data"
],
"tags": [
"User"
],
"summary": "UserUpdateInfo",
"parameters": [
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/email": {
"put": {
"security": [
@@ -3659,52 +3929,6 @@
}
}
},
"/answer/api/v1/user/post/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "upload user post file",
"consumes": [
"multipart/form-data"
],
"tags": [
"User"
],
"summary": "upload user post file",
"parameters": [
{
"type": "file",
"description": "file",
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/answer/api/v1/user/register/email": {
"post": {
"description": "UserRegisterByEmail",
@@ -4622,6 +4846,12 @@
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug name",
"type": "string"
@@ -4865,6 +5095,23 @@
}
}
},
"schema.GetSiteLegalInfoResp": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.GetTagPageResp": {
"type": "object",
"properties": {
@@ -4900,6 +5147,12 @@
"description": "question amount",
"type": "integer"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug_name",
"type": "string"
@@ -4960,6 +5213,12 @@
"description": "question amount",
"type": "integer"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"description": "slug name",
"type": "string"
@@ -5523,6 +5782,9 @@
"id": {
"type": "string"
},
"question_id": {
"type": "string"
},
"status": {
"description": "Status",
"type": "string"
@@ -5559,6 +5821,56 @@
}
}
},
"schema.SiteBrandingReq": {
"type": "object",
"required": [
"logo",
"square_icon"
],
"properties": {
"favicon": {
"type": "string",
"maxLength": 512
},
"logo": {
"type": "string",
"maxLength": 512
},
"mobile_logo": {
"type": "string",
"maxLength": 512
},
"square_icon": {
"type": "string",
"maxLength": 512
}
}
},
"schema.SiteBrandingResp": {
"type": "object",
"required": [
"logo",
"square_icon"
],
"properties": {
"favicon": {
"type": "string",
"maxLength": 512
},
"logo": {
"type": "string",
"maxLength": 512
},
"mobile_logo": {
"type": "string",
"maxLength": 512
},
"square_icon": {
"type": "string",
"maxLength": 512
}
}
},
"schema.SiteGeneralReq": {
"type": "object",
"required": [
@@ -5635,10 +5947,6 @@
"type": "string",
"maxLength": 128
},
"logo": {
"type": "string",
"maxLength": 256
},
"theme": {
"type": "string",
"maxLength": 128
@@ -5661,10 +5969,6 @@
"type": "string",
"maxLength": 128
},
"logo": {
"type": "string",
"maxLength": 256
},
"theme": {
"type": "string",
"maxLength": 128
@@ -5675,6 +5979,80 @@
}
}
},
"schema.SiteLegalReq": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.SiteLegalResp": {
"type": "object",
"properties": {
"privacy_policy_original_text": {
"type": "string"
},
"privacy_policy_parsed_text": {
"type": "string"
},
"terms_of_service_original_text": {
"type": "string"
},
"terms_of_service_parsed_text": {
"type": "string"
}
}
},
"schema.SiteWriteReq": {
"type": "object",
"properties": {
"recommend_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"required_tag": {
"type": "boolean"
},
"reserved_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.SiteWriteResp": {
"type": "object",
"properties": {
"recommend_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"required_tag": {
"type": "boolean"
},
"reserved_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"schema.TagItem": {
"type": "object",
"properties": {
@@ -5708,6 +6086,12 @@
"description": "if main tag slug name is not empty, this tag is synonymous with the main tag",
"type": "string"
},
"recommend": {
"type": "boolean"
},
"reserved": {
"type": "boolean"
},
"slug_name": {
"type": "string"
}
@@ -5991,6 +6375,10 @@
},
"schema.UserEmailLogin": {
"type": "object",
"required": [
"e_mail",
"pass"
],
"properties": {
"captcha_code": {
"description": "captcha_code",
@@ -6002,11 +6390,14 @@
},
"e_mail": {
"description": "e_mail",
"type": "string"
"type": "string",
"maxLength": 500
},
"pass": {
"description": "password",
"type": "string"
"type": "string",
"maxLength": 32,
"minLength": 8
}
}
},
+304 -58
View File
@@ -368,6 +368,10 @@ definitions:
description: if main tag slug name is not empty, this tag is synonymous with
the main tag
type: string
recommend:
type: boolean
reserved:
type: boolean
slug_name:
description: slug name
type: string
@@ -544,6 +548,17 @@ definitions:
smtp_username:
type: string
type: object
schema.GetSiteLegalInfoResp:
properties:
privacy_policy_original_text:
type: string
privacy_policy_parsed_text:
type: string
terms_of_service_original_text:
type: string
terms_of_service_parsed_text:
type: string
type: object
schema.GetTagPageResp:
properties:
created_at:
@@ -570,6 +585,10 @@ definitions:
question_count:
description: question amount
type: integer
recommend:
type: boolean
reserved:
type: boolean
slug_name:
description: slug_name
type: string
@@ -615,6 +634,10 @@ definitions:
question_count:
description: question amount
type: integer
recommend:
type: boolean
reserved:
type: boolean
slug_name:
description: slug name
type: string
@@ -1025,6 +1048,8 @@ definitions:
type: string
id:
type: string
question_id:
type: string
status:
description: Status
type: string
@@ -1050,6 +1075,42 @@ definitions:
description: object_type
type: string
type: object
schema.SiteBrandingReq:
properties:
favicon:
maxLength: 512
type: string
logo:
maxLength: 512
type: string
mobile_logo:
maxLength: 512
type: string
square_icon:
maxLength: 512
type: string
required:
- logo
- square_icon
type: object
schema.SiteBrandingResp:
properties:
favicon:
maxLength: 512
type: string
logo:
maxLength: 512
type: string
mobile_logo:
maxLength: 512
type: string
square_icon:
maxLength: 512
type: string
required:
- logo
- square_icon
type: object
schema.SiteGeneralReq:
properties:
contact_email:
@@ -1103,9 +1164,6 @@ definitions:
language:
maxLength: 128
type: string
logo:
maxLength: 256
type: string
theme:
maxLength: 128
type: string
@@ -1122,9 +1180,6 @@ definitions:
language:
maxLength: 128
type: string
logo:
maxLength: 256
type: string
theme:
maxLength: 128
type: string
@@ -1136,6 +1191,54 @@ definitions:
- theme
- time_zone
type: object
schema.SiteLegalReq:
properties:
privacy_policy_original_text:
type: string
privacy_policy_parsed_text:
type: string
terms_of_service_original_text:
type: string
terms_of_service_parsed_text:
type: string
type: object
schema.SiteLegalResp:
properties:
privacy_policy_original_text:
type: string
privacy_policy_parsed_text:
type: string
terms_of_service_original_text:
type: string
terms_of_service_parsed_text:
type: string
type: object
schema.SiteWriteReq:
properties:
recommend_tags:
items:
type: string
type: array
required_tag:
type: boolean
reserved_tags:
items:
type: string
type: array
type: object
schema.SiteWriteResp:
properties:
recommend_tags:
items:
type: string
type: array
required_tag:
type: boolean
reserved_tags:
items:
type: string
type: array
type: object
schema.TagItem:
properties:
display_name:
@@ -1161,6 +1264,10 @@ definitions:
description: if main tag slug name is not empty, this tag is synonymous with
the main tag
type: string
recommend:
type: boolean
reserved:
type: boolean
slug_name:
type: string
type: object
@@ -1373,10 +1480,16 @@ definitions:
type: string
e_mail:
description: e_mail
maxLength: 500
type: string
pass:
description: password
maxLength: 32
minLength: 8
type: string
required:
- e_mail
- pass
type: object
schema.UserModifyPassWordRequest:
properties:
@@ -1784,6 +1897,47 @@ paths:
summary: update smtp config
tags:
- admin
/answer/admin/api/siteinfo/branding:
get:
description: get site interface
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteBrandingResp'
type: object
security:
- ApiKeyAuth: []
summary: get site interface
tags:
- admin
put:
description: update site info branding
parameters:
- description: branding info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteBrandingReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site info branding
tags:
- admin
/answer/admin/api/siteinfo/general:
get:
description: get site general information
@@ -1866,6 +2020,88 @@ paths:
summary: update site info interface
tags:
- admin
/answer/admin/api/siteinfo/legal:
get:
description: Set the legal information for the site
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteLegalResp'
type: object
security:
- ApiKeyAuth: []
summary: Set the legal information for the site
tags:
- admin
put:
description: update site legal info
parameters:
- description: write info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteLegalReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site legal info
tags:
- admin
/answer/admin/api/siteinfo/write:
get:
description: get site interface
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteWriteResp'
type: object
security:
- ApiKeyAuth: []
summary: get site interface
tags:
- admin
put:
description: update site write info
parameters:
- description: write info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteWriteReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site write info
tags:
- admin
/answer/admin/api/theme/options:
get:
description: Get theme options
@@ -2272,6 +2508,41 @@ paths:
summary: get comment page
tags:
- Comment
/answer/api/v1/file:
post:
consumes:
- multipart/form-data
description: upload file
parameters:
- description: identify the source of the file upload
enum:
- post
- avatar
- branding
in: formData
name: source
required: true
type: string
- description: file
in: formData
name: file
required: true
type: file
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
type: string
type: object
security:
- ApiKeyAuth: []
summary: upload file
tags:
- Upload
/answer/api/v1/follow:
post:
consumes:
@@ -3145,6 +3416,33 @@ paths:
summary: get site info
tags:
- site
/answer/api/v1/siteinfo/legal:
get:
description: get site legal info
parameters:
- description: legal information type
enum:
- tos
- privacy
in: query
name: info_type
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetSiteLegalInfoResp'
type: object
summary: get site legal info
tags:
- site
/answer/api/v1/tag:
delete:
consumes:
@@ -3362,32 +3660,6 @@ paths:
summary: ActionRecord
tags:
- User
/answer/api/v1/user/avatar/upload:
post:
consumes:
- multipart/form-data
description: UserUpdateInfo
parameters:
- description: file
in: formData
name: file
required: true
type: file
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
type: string
type: object
security:
- ApiKeyAuth: []
summary: UserUpdateInfo
tags:
- User
/answer/api/v1/user/email:
put:
consumes:
@@ -3708,32 +3980,6 @@ paths:
summary: RetrievePassWord
tags:
- User
/answer/api/v1/user/post/file:
post:
consumes:
- multipart/form-data
description: upload user post file
parameters:
- description: file
in: formData
name: file
required: true
type: file
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
type: string
type: object
security:
- ApiKeyAuth: []
summary: upload user post file
tags:
- User
/answer/api/v1/user/register/email:
post:
consumes:
+30 -60
View File
@@ -26,6 +26,10 @@ backend:
answer:
not_found:
other: "Answer do not found."
cannot_deleted:
other: "No permission to delete."
cannot_update:
other: "No permission to update."
comment:
edit_without_permission:
other: "Comment are not allowed to edit."
@@ -63,6 +67,12 @@ backend:
question:
not_found:
other: "Question not found."
cannot_deleted:
other: "No permission to delete."
cannot_close:
other: "No permission to close."
cannot_update:
other: "No permission to update."
rank:
fail_to_meet_the_condition:
other: "Rank fail to meet the condition."
@@ -74,6 +84,12 @@ backend:
tag:
not_found:
other: "Tag not found."
recommend_tag_not_found:
other: "Recommend Tag is not exist."
recommend_tag_enter:
other: "Please enter at least one required tag."
not_contain_synonym_tags:
other: "Should not contain synonym tags."
theme:
not_found:
other: "Theme not found."
@@ -90,18 +106,17 @@ backend:
other: "Username is already in use."
set_avatar:
other: "Avatar set failed."
config:
read_config_failed:
other: "Read config failed"
database:
connection_failed:
other: "Database connection failed"
create_table_failed:
other: "Create table failed"
install:
create_config_failed:
other: "Cant create the config.yaml file."
config:
read_config_failed:
other: "Read config failed"
database:
connection_failed:
other: "Database connection failed"
create_table_failed:
other: "Create table failed"
install:
create_config_failed:
other: "Cant create the config.yaml file."
report:
spam:
name:
@@ -380,7 +395,6 @@ ui:
tag_info:
created_at: Created
edited_at: Edited
history: History
synonyms:
title: Synonyms
text: The following tags will be remapped to
@@ -498,11 +512,6 @@ ui:
label: Answer
msg:
empty: Answer cannot be empty.
edit_summary:
label: Edit Summary
placeholder: >-
Briefly explain your changes (corrected spelling, fixed grammar,
improved formatting)
btn_post_question: Post your question
btn_save_edits: Save edits
answer_question: Answer your own question
@@ -523,7 +532,6 @@ ui:
setting: Settings
logout: Log out
admin: Admin
review: Review
search:
placeholder: Search
footer:
@@ -733,9 +741,6 @@ ui:
logout: Log out
verify: Verify
add_question: Add question
approve: Approve
reject: Reject
skip: Skip
search:
title: Search Results
keywords: Keywords
@@ -1173,8 +1178,9 @@ ui:
label: Recommend Tags
text: "Please input tag slug above, one tag per line."
required_tag:
label: Required Tag
text: "Every new question must have at least one recommend tag"
title: Required Tag
label: Set recommend tag as required
text: "Every new question must have at least one recommend tag."
reserved_tags:
label: Reserved Tags
text: "Reserved tags can only be added to a post by moderator."
@@ -1183,39 +1189,3 @@ ui:
invalid: is invalid
btn_submit: Save
not_found_props: "Required property {{ key }} not found."
page_review:
review: Reivew
proposed: proposed
question_edit: Question edit
answer_edit: Answer edit
tag_edit: Tag edit
edit_summary: Edit summary
edit_question: Edit question
edit_answer: Edit answer
edit_tag: Edit tag
empty: No review tasks left.
timeline:
undeleted: undeleted
deleted: deleted
downvote: downvote
upvote: upvote
accept: accept
cancelled: cancelled
commented: commented
rollback: rollback
edited: edited
answered: answered
asked: asked
closed: closed
reopened: reopened
created: created
title: "History for"
show_votes: "Show votes"
n_or_a: N/A
title_for: "Timeline for"
title_for_tag: "Title for tag"
datetime: Datetime
type: Type
by: By
comment: Comment
no_data: "We couldn't find anything."
+1 -1
View File
@@ -571,7 +571,7 @@ ui:
character: '用户名只能由 "a-z", "0-9", " - . _" 组成'
avatar:
label: 头像
text: 您可以上传图片作为头像,也可以 <1>重置</1> 为
text: 您可以上传图片作为头像
bio:
label: 关于我 (可选)
website:
+4
View File
@@ -53,4 +53,8 @@ var (
const (
SiteTypeGeneral = "general"
SiteTypeInterface = "interface"
SiteTypeBranding = "branding"
SiteTypeWrite = "write"
SiteTypeLegal = "legal"
)
+9
View File
@@ -124,6 +124,15 @@ func GetLoginUserIDFromContext(ctx *gin.Context) (userID string) {
return u.UserID
}
// GetIsAdminFromContext get user is admin from context
func GetIsAdminFromContext(ctx *gin.Context) (isAdmin bool) {
userInfo := GetUserInfoFromContext(ctx)
if userInfo == nil {
return false
}
return userInfo.IsAdmin
}
// GetUserInfoFromContext get user info from context
func GetUserInfoFromContext(ctx *gin.Context) (u *entity.UserCacheInfo) {
userInfo, exist := ctx.Get(ctxUUIDKey)
+39 -29
View File
@@ -14,33 +14,43 @@ const (
)
const (
EmailOrPasswordWrong = "error.user.email_or_password_wrong"
CommentNotFound = "error.comment.not_found"
QuestionNotFound = "error.question.not_found"
AnswerNotFound = "error.answer.not_found"
CommentEditWithoutPermission = "error.comment.edit_without_permission"
DisallowVote = "error.object.disallow_vote"
DisallowFollow = "error.object.disallow_follow"
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
UserNotFound = "error.user.not_found"
UsernameInvalid = "error.user.username_invalid"
UsernameDuplicate = "error.user.username_duplicate"
UserSetAvatar = "error.user.set_avatar"
EmailDuplicate = "error.email.duplicate"
EmailVerifyURLExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"
UserSuspended = "error.user.suspended"
ObjectNotFound = "error.object.not_found"
TagNotFound = "error.tag.not_found"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
CommentNotFound = "error.comment.not_found"
QuestionNotFound = "error.question.not_found"
QuestionCannotDeleted = "error.question.cannot_deleted"
QuestionCannotClose = "error.question.cannot_close"
QuestionCannotUpdate = "error.question.cannot_update"
AnswerNotFound = "error.answer.not_found"
AnswerCannotDeleted = "error.answer.cannot_deleted"
AnswerCannotUpdate = "error.answer.cannot_update"
CommentEditWithoutPermission = "error.comment.edit_without_permission"
DisallowVote = "error.object.disallow_vote"
DisallowFollow = "error.object.disallow_follow"
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
OldPasswordVerificationFailed = "error.object.old_password_verification_failed"
NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting"
UserNotFound = "error.user.not_found"
UsernameInvalid = "error.user.username_invalid"
UsernameDuplicate = "error.user.username_duplicate"
UserSetAvatar = "error.user.set_avatar"
EmailDuplicate = "error.email.duplicate"
EmailVerifyURLExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"
UserSuspended = "error.user.suspended"
ObjectNotFound = "error.object.not_found"
TagNotFound = "error.tag.not_found"
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
)
+20 -13
View File
@@ -26,10 +26,10 @@ type MyValidator struct {
Lang i18n.Language
}
// ErrorField error field
type ErrorField struct {
Key string `json:"key"`
Value string `json:"value"`
// FormErrorField indicates the current form error content. which field is error and error message.
type FormErrorField struct {
ErrorField string `json:"error_field"`
ErrorMsg string `json:"error_msg"`
}
// GlobalValidatorMapping is a mapping from validator to translator used
@@ -87,7 +87,7 @@ func GetValidatorByLang(la string) *MyValidator {
}
// Check /
func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error) {
func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err error) {
err = m.Validate.Struct(value)
if err != nil {
var valErrors validator.ValidationErrors
@@ -97,9 +97,9 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
}
for _, fieldError := range valErrors {
errField = &ErrorField{
Key: fieldError.Field(),
Value: fieldError.Translate(m.Tran),
errField := &FormErrorField{
ErrorField: fieldError.Field(),
ErrorMsg: fieldError.Translate(m.Tran),
}
// get original tag name from value for set err field key.
@@ -108,17 +108,24 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
if found {
originalTag := getObjectTagByFieldName(value, fieldName)
if len(originalTag) > 0 {
errField.Key = originalTag
errField.ErrorField = originalTag
}
}
return errField, myErrors.BadRequest(reason.RequestFormatError).WithMsg(fieldError.Translate(m.Tran))
errFields = append(errFields, errField)
}
if len(errFields) > 0 {
errMsg := ""
if len(errFields) == 1 {
errMsg = errFields[0].ErrorMsg
}
return errFields, myErrors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
}
}
if v, ok := value.(Checker); ok {
errField, err = v.Check()
errFields, err = v.Check()
if err != nil {
return errField, err
return errFields, err
}
}
return nil, nil
@@ -126,7 +133,7 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
// Checker .
type Checker interface {
Check() (errField *ErrorField, err error)
Check() (errField []*FormErrorField, err error)
}
func getObjectTagByFieldName(obj interface{}, fieldName string) (tag string) {
+6 -1
View File
@@ -55,7 +55,8 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
IsAdmin := middleware.GetIsAdminFromContext(ctx)
req.IsAdmin = IsAdmin
err := ac.answerService.RemoveAnswer(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@@ -147,6 +148,8 @@ func (ac *AnswerController) Update(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
IsAdmin := middleware.GetIsAdminFromContext(ctx)
req.IsAdmin = IsAdmin
if can, err := ac.rankService.CheckRankPermission(ctx, req.UserID, rank.AnswerEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
@@ -190,6 +193,8 @@ func (ac *AnswerController) AnswerList(ctx *gin.Context) {
return
}
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
IsAdmin := middleware.GetIsAdminFromContext(ctx)
req.IsAdmin = IsAdmin
list, count, err := ac.answerService.SearchList(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, nil)
+1
View File
@@ -21,4 +21,5 @@ var ProviderSetController = wire.NewSet(
NewNotificationController,
NewSiteinfoController,
NewDashboardController,
NewUploadController,
)
+6 -3
View File
@@ -43,9 +43,10 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionDeleteRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), errors.Forbidden(reason.RankFailToMeetTheCondition))
return
}
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
err := qc.questionService.RemoveQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
@@ -67,6 +68,7 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
err := qc.questionService.CloseQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@@ -85,7 +87,8 @@ func (qc *QuestionController) GetQuestion(c *gin.Context) {
id := c.Query("id")
ctx := context.Background()
userID := middleware.GetLoginUserIDFromContext(c)
info, err := qc.questionService.GetQuestion(ctx, id, userID, true)
IsAdmin := middleware.GetIsAdminFromContext(c)
info, err := qc.questionService.GetQuestion(ctx, id, userID, true, IsAdmin)
if err != nil {
handler.HandleResponse(c, err, nil)
return
@@ -213,7 +216,7 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
if can, err := qc.rankService.CheckRankPermission(ctx, req.UserID, rank.QuestionEditRank); err != nil || !can {
handler.HandleResponse(ctx, err, errors.Forbidden(reason.RankFailToMeetTheCondition))
return
+40 -3
View File
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
type SiteinfoController struct {
@@ -30,9 +31,45 @@ func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
resp := &schema.SiteInfoResp{}
resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
handler.HandleResponse(ctx, err, resp)
log.Error(err)
}
resp.Interface, err = sc.siteInfoService.GetSiteInterface(ctx)
if err != nil {
log.Error(err)
}
resp.Branding, err = sc.siteInfoService.GetSiteBranding(ctx)
if err != nil {
log.Error(err)
}
handler.HandleResponse(ctx, nil, resp)
}
// GetSiteLegalInfo get site legal info
// @Summary get site legal info
// @Description get site legal info
// @Tags site
// @Param info_type query string true "legal information type" Enums(tos, privacy)
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.GetSiteLegalInfoResp}
// @Router /answer/api/v1/siteinfo/legal [get]
func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) {
req := &schema.GetSiteLegalInfoReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx)
handler.HandleResponse(ctx, err, resp)
siteLegal, err := sc.siteInfoService.GetSiteLegal(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
resp := &schema.GetSiteLegalInfoResp{}
if req.IsTOS() {
resp.TermsOfServiceOriginalText = siteLegal.TermsOfServiceOriginalText
resp.TermsOfServiceParsedText = siteLegal.TermsOfServiceParsedText
} else if req.IsPrivacy() {
resp.PrivacyPolicyOriginalText = siteLegal.PrivacyPolicyOriginalText
resp.PrivacyPolicyParsedText = siteLegal.PrivacyPolicyParsedText
}
handler.HandleResponse(ctx, nil, resp)
}
+12 -6
View File
@@ -7,19 +7,25 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/tag_common"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
// TagController tag controller
type TagController struct {
tagService *tag.TagService
rankService *rank.RankService
tagService *tag.TagService
tagCommonService *tag_common.TagCommonService
rankService *rank.RankService
}
// NewTagController new controller
func NewTagController(tagService *tag.TagService, rankService *rank.RankService) *TagController {
return &TagController{tagService: tagService, rankService: rankService}
func NewTagController(
tagService *tag.TagService,
tagCommonService *tag_common.TagCommonService,
rankService *rank.RankService,
) *TagController {
return &TagController{tagService: tagService, tagCommonService: tagCommonService, rankService: rankService}
}
// SearchTagLike get tag list
@@ -36,8 +42,8 @@ func (tc *TagController) SearchTagLike(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := tc.tagService.SearchTagLike(ctx, req)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
resp, err := tc.tagCommonService.SearchTagLike(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
+65
View File
@@ -0,0 +1,65 @@
package controller
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
const (
// file is uploaded by markdown(or something else) editor
fileFromPost = "post"
// file is used to change the user's avatar
fileFromAvatar = "avatar"
// file is logo/icon images
fileFromBranding = "branding"
)
// UploadController upload controller
type UploadController struct {
uploaderService *uploader.UploaderService
}
// NewUploadController new controller
func NewUploadController(uploaderService *uploader.UploaderService) *UploadController {
return &UploadController{
uploaderService: uploaderService,
}
}
// UploadFile upload file
// @Summary upload file
// @Description upload file
// @Tags Upload
// @Accept multipart/form-data
// @Security ApiKeyAuth
// @Param source formData string true "identify the source of the file upload" Enums(post, avatar, branding)
// @Param file formData file true "file"
// @Success 200 {object} handler.RespBody{data=string}
// @Router /answer/api/v1/file [post]
func (uc *UploadController) UploadFile(ctx *gin.Context) {
var (
url string
err error
)
source := ctx.PostForm("source")
switch source {
case fileFromAvatar:
url, err = uc.uploaderService.UploadAvatarFile(ctx)
case fileFromPost:
url, err = uc.uploaderService.UploadPostFile(ctx)
case fileFromBranding:
url, err = uc.uploaderService.UploadBrandingFile(ctx)
default:
handler.HandleResponse(ctx, errors.BadRequest(reason.UploadFileSourceUnsupported), nil)
return
}
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
handler.HandleResponse(ctx, err, url)
}
+37 -109
View File
@@ -1,14 +1,11 @@
package controller
import (
"net/http"
"path"
"strings"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/action"
@@ -17,7 +14,6 @@ import (
"github.com/answerdev/answer/internal/service/uploader"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// UserController user controller
@@ -106,24 +102,22 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) {
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "captcha_code",
Value: "error.object.verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
resp, err := uc.userService.EmailLogin(ctx, req)
if err != nil {
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
resp := schema.UserVerifyEmailErrorResponse{
Key: "e_mail",
Value: "error.object.email_or_password_incorrect",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "e_mail",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.EmailOrPasswordWrong),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), errFields)
return
}
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP())
@@ -146,12 +140,11 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
}
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "captcha_code",
Value: "error.object.verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
@@ -277,12 +270,11 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(),
req.CaptchaID, req.CaptchaCode)
if !captchaPass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "captcha_code",
Value: "error.object.verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
@@ -314,22 +306,19 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
return
}
if !oldPassVerification {
resp := schema.UserVerifyEmailErrorResponse{
Key: "old_pass",
Value: "error.object.old_password_verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "old_pass",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.OldPasswordVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.OldPasswordVerificationFailed), errFields)
return
}
if req.OldPass == req.Pass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "pass",
Value: "error.object.new_password_same_as_previous_setting",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.NewPasswordSameAsPreviousSetting),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.NewPasswordSameAsPreviousSetting), errFields)
return
}
err = uc.userService.UserModifyPassword(ctx, req)
@@ -378,66 +367,6 @@ func (uc *UserController) UserUpdateInterface(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UploadUserAvatar godoc
// @Summary UserUpdateInfo
// @Description UserUpdateInfo
// @Tags User
// @Accept multipart/form-data
// @Security ApiKeyAuth
// @Param file formData file true "file"
// @Success 200 {object} handler.RespBody{data=string}
// @Router /answer/api/v1/user/avatar/upload [post]
func (uc *UserController) UploadUserAvatar(ctx *gin.Context) {
// max size
var filesMax int64 = 5 << 20
var valuesMax int64 = 5
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, filesMax+valuesMax)
_, header, err := ctx.Request.FormFile("file")
if err != nil {
log.Error(err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(header.Filename))
if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" {
log.Errorf("upload file format is not supported: %s", fileExt)
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
url, err := uc.uploaderService.UploadAvatarFile(ctx, header, fileExt)
handler.HandleResponse(ctx, err, url)
}
// UploadUserPostFile godoc
// @Summary upload user post file
// @Description upload user post file
// @Tags User
// @Accept multipart/form-data
// @Security ApiKeyAuth
// @Param file formData file true "file"
// @Success 200 {object} handler.RespBody{data=string}
// @Router /answer/api/v1/user/post/file [post]
func (uc *UserController) UploadUserPostFile(ctx *gin.Context) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, header, err := ctx.Request.FormFile("file")
if err != nil {
log.Error(err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(header.Filename))
if fileExt != ".jpg" && fileExt != ".png" && fileExt != ".jpeg" {
log.Errorf("upload file format is not supported: %s", fileExt)
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
url, err := uc.uploaderService.UploadPostFile(ctx, header, fileExt)
handler.HandleResponse(ctx, err, url)
}
// ActionRecord godoc
// @Summary ActionRecord
// @Description ActionRecord
@@ -502,19 +431,18 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
resp := schema.UserVerifyEmailErrorResponse{
Key: "captcha_code",
Value: "error.object.verification_failed",
}
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
resp, err := uc.userService.UserChangeEmailSendCode(ctx, req)
if err != nil {
if resp != nil {
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
resp.ErrorMsg = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.ErrorMsg)
}
handler.HandleResponse(ctx, err, resp)
return
@@ -2,16 +2,18 @@ package controller_backyard
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/gin-gonic/gin"
)
// SiteInfoController site info controller
type SiteInfoController struct {
siteInfoService *siteinfo.SiteInfoService
}
// NewSiteInfoController new siteinfo controller.
// NewSiteInfoController new site info controller
func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController {
return &SiteInfoController{
siteInfoService: siteInfoService,
@@ -44,6 +46,45 @@ func (sc *SiteInfoController) GetInterface(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// GetSiteBranding get site interface
// @Summary get site interface
// @Description get site interface
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteBrandingResp}
// @Router /answer/admin/api/siteinfo/branding [get]
func (sc *SiteInfoController) GetSiteBranding(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteBranding(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetSiteWrite get site interface
// @Summary get site interface
// @Description get site interface
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteWriteResp}
// @Router /answer/admin/api/siteinfo/write [get]
func (sc *SiteInfoController) GetSiteWrite(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteWrite(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetSiteLegal Set the legal information for the site
// @Summary Set the legal information for the site
// @Description Set the legal information for the site
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteLegalResp}
// @Router /answer/admin/api/siteinfo/legal [get]
func (sc *SiteInfoController) GetSiteLegal(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteLegal(ctx)
handler.HandleResponse(ctx, err, resp)
}
// UpdateGeneral update site general information
// @Summary update site general information
// @Description update site general information
@@ -80,6 +121,62 @@ func (sc *SiteInfoController) UpdateInterface(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UpdateBranding update site branding
// @Summary update site info branding
// @Description update site info branding
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteBrandingReq true "branding info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/branding [put]
func (sc *SiteInfoController) UpdateBranding(ctx *gin.Context) {
req := &schema.SiteBrandingReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.SaveSiteBranding(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UpdateSiteWrite update site write info
// @Summary update site write info
// @Description update site write info
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteWriteReq true "write info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/write [put]
func (sc *SiteInfoController) UpdateSiteWrite(ctx *gin.Context) {
req := &schema.SiteWriteReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := sc.siteInfoService.SaveSiteWrite(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UpdateSiteLegal update site legal info
// @Summary update site legal info
// @Description update site legal info
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteLegalReq true "write info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/legal [put]
func (sc *SiteInfoController) UpdateSiteLegal(ctx *gin.Context) {
req := &schema.SiteLegalReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.SaveSiteLegal(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetSMTPConfig get smtp config
// @Summary GetSMTPConfig get smtp config
// @Description GetSMTPConfig get smtp config
+1
View File
@@ -5,4 +5,5 @@ type UserCacheInfo struct {
UserID string `json:"user_id"`
UserStatus int `json:"user_status"`
EmailStatus int `json:"email_status"`
IsAdmin bool `json:"is_admin"`
}
+2
View File
@@ -21,6 +21,8 @@ type Tag struct {
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
QuestionCount int `xorm:"not null default 0 INT(11) question_count"`
Status int `xorm:"not null default 1 INT(11) status"`
Recommend bool `xorm:"not null default false BOOL recommend"`
Reserved bool `xorm:"not null default false BOOL reserved"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
+1
View File
@@ -43,6 +43,7 @@ var migrations = []Migration{
// 0->1
NewMigration("this is first version, no operation", noopMigration),
NewMigration("add user language", addUserLanguage),
NewMigration("add recommend and reserved tag fields", addTagRecommendedAndReserved),
}
// GetCurrentDBVersion returns the current db version
+13
View File
@@ -0,0 +1,13 @@
package migrations
import (
"xorm.io/xorm"
)
func addTagRecommendedAndReserved(x *xorm.Engine) error {
type Tag struct {
Recommend bool `xorm:"not null default false BOOL recommend"`
Reserved bool `xorm:"not null default false BOOL reserved"`
}
return x.Sync(new(Tag))
}
+1 -1
View File
@@ -204,7 +204,7 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc
case entity.AnswerSearchOrderByVote:
session = session.OrderBy("vote_count desc")
default:
session = session.OrderBy("adopted desc,vote_count desc")
session = session.OrderBy("adopted desc,vote_count desc,created_at asc")
}
session = session.And("status = ?", entity.AnswerStatusAvailable)
+2
View File
@@ -22,6 +22,7 @@ import (
"github.com/answerdev/answer/internal/repo/search_common"
"github.com/answerdev/answer/internal/repo/site_info"
"github.com/answerdev/answer/internal/repo/tag"
"github.com/answerdev/answer/internal/repo/tag_common"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/internal/repo/user"
"github.com/google/wire"
@@ -53,6 +54,7 @@ var ProviderSetRepo = wire.NewSet(
activity.NewQuestionActivityRepo,
activity.NewUserActiveActivityRepo,
tag.NewTagRepo,
tag_common.NewTagCommonRepo,
tag.NewTagRelRepo,
collection.NewCollectionRepo,
collection.NewCollectionGroupRepo,
@@ -81,7 +81,7 @@ func Test_notificationRepo_GetNotificationPage(t *testing.T) {
err := notificationRepo.AddNotification(context.TODO(), ent)
assert.NoError(t, err)
notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: userID})
notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: ent.UserID})
assert.NoError(t, err)
assert.True(t, total > 0)
assert.Equal(t, notificationPage[0].UserID, ent.UserID)
+2 -1
View File
@@ -52,7 +52,8 @@ var (
func TestMain(t *testing.M) {
dbSetting, ok := dbSettingMapping[os.Getenv("TEST_DB_DRIVER")]
if !ok {
dbSetting = dbSettingMapping[string(schemas.MYSQL)]
// Use sqlite3 to test.
dbSetting = dbSettingMapping[string(schemas.SQLITE)]
}
defer func() {
if tearDown != nil {
+27 -21
View File
@@ -8,6 +8,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/repo/tag"
"github.com/answerdev/answer/internal/repo/tag_common"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/pkg/converter"
"github.com/stretchr/testify/assert"
@@ -42,8 +43,8 @@ var (
func addTagList() {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)
err := tagRepo.AddTagList(context.TODO(), testTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, uniqueIDRepo)
err := tagCommonRepo.AddTagList(context.TODO(), testTagList)
if err != nil {
panic(err)
}
@@ -51,9 +52,9 @@ func addTagList() {
func Test_tagRepo_GetTagByID(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
@@ -61,9 +62,9 @@ func Test_tagRepo_GetTagByID(t *testing.T) {
func Test_tagRepo_GetTagBySlugName(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName)
gotTag, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
@@ -80,36 +81,36 @@ func Test_tagRepo_GetTagList(t *testing.T) {
func Test_tagRepo_GetTagListByIDs(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID})
gotTags, err := tagCommonRepo.GetTagListByIDs(context.TODO(), []string{testTagList[0].ID})
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagListByName(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, 1)
gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, 1, false)
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagListByNames(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName})
gotTags, err := tagCommonRepo.GetTagListByNames(context.TODO(), []string{testTagList[0].SlugName})
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
func Test_tagRepo_GetTagPage(t *testing.T) {
tagOnce.Do(addTagList)
tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, _, err := tagRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "")
gotTags, _, err := tagCommonRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "")
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
@@ -121,7 +122,9 @@ func Test_tagRepo_RemoveTag(t *testing.T) {
err := tagRepo.RemoveTag(context.TODO(), testTagList[1].ID)
assert.NoError(t, err)
_, exist, err := tagRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
_, exist, err := tagCommonRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName)
assert.NoError(t, err)
assert.False(t, exist)
}
@@ -134,21 +137,22 @@ func Test_tagRepo_UpdateTag(t *testing.T) {
err := tagRepo.UpdateTag(context.TODO(), testTagList[0])
assert.NoError(t, err)
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].DisplayName, gotTag.DisplayName)
}
func Test_tagRepo_UpdateTagQuestionCount(t *testing.T) {
uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource)
tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
testTagList[0].DisplayName = "golang"
err := tagRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)
err := tagCommonRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)
assert.NoError(t, err)
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID)
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, 100, gotTag.QuestionCount)
@@ -166,7 +170,9 @@ func Test_tagRepo_UpdateTagSynonym(t *testing.T) {
converter.StringToInt64(testTagList[0].ID), testTagList[0].SlugName)
assert.NoError(t, err)
gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[2].ID)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID)
assert.NoError(t, err)
assert.True(t, exist)
assert.Equal(t, testTagList[0].ID, fmt.Sprintf("%d", gotTag.MainTagID))
+42 -10
View File
@@ -3,10 +3,11 @@ package search_common
import (
"context"
"fmt"
"github.com/answerdev/answer/pkg/htmltext"
"strings"
"time"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
@@ -67,7 +68,7 @@ func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon
}
// SearchContents search question and answer data
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
@@ -116,10 +117,12 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
}
// check tag
if tagID != "" {
if len(tagIDs) > 0 {
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
Where(builder.Eq{"tag_rel.tag_id": tagID})
argsQ = append(argsQ, tagID)
Where(builder.In("tag_rel.tag_id", tagIDs))
for _, tagID := range tagIDs {
argsQ = append(argsQ, tagID)
}
}
// check user
@@ -193,7 +196,7 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
}
// SearchQuestions search question data
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, notAccepted bool, views, answers int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
@@ -223,11 +226,26 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
}
// check need filter has not accepted
if limitNoAccepted {
if notAccepted {
b.And(builder.Eq{"accepted_answer_id": 0})
args = append(args, 0)
}
// check views
if views > -1 {
b.And(builder.Gte{"view_count": views})
args = append(args, views)
}
// check answers
if answers == 0 {
b.And(builder.Eq{"answer_count": answers})
args = append(args, answers)
} else if answers > 0 {
b.And(builder.Gte{"answer_count": answers})
args = append(args, answers)
}
if answers == 0 {
b.And(builder.Eq{"answer_count": 0})
args = append(args, 0)
@@ -274,7 +292,7 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit
}
// SearchAnswers search answer data
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
@@ -303,11 +321,23 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAc
}
}
if limitAccepted {
// check tags
// check tag
if len(tagIDs) > 0 {
b.Join("INNER", "tag_rel", "question.id = tag_rel.object_id").
Where(builder.In("tag_rel.tag_id", tagIDs))
for _, tagID := range tagIDs {
args = append(args, tagID)
}
}
// check limit accepted
if accepted {
b.Where(builder.Eq{"adopted": schema.AnswerAdoptedEnable})
args = append(args, schema.AnswerAdoptedEnable)
}
// check question id
if questionID != "" {
b.Where(builder.Eq{"question_id": questionID})
args = append(args, questionID)
@@ -390,11 +420,12 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
// get tags
err = sr.data.DB.
Select("`display_name`,`slug_name`,`main_tag_slug_name`").
Select("`display_name`,`slug_name`,`main_tag_slug_name`,`recommend`,`reserved`").
Table("tag").
Join("INNER", "tag_rel", "tag.id = tag_rel.tag_id").
Where(builder.Eq{"tag_rel.object_id": r["question_id"]}).
And(builder.Eq{"tag_rel.status": entity.TagRelStatusAvailable}).
UseBool("recommend", "reserved").
Find(&tagsEntity)
if err != nil {
@@ -421,6 +452,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
object = schema.SearchObject{
ID: string(r["id"]),
QuestionID: string(r["question_id"]),
Title: string(r["title"]),
Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240),
CreatedAtParsed: tp.Unix(),
+2 -122
View File
@@ -4,10 +4,9 @@ import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
@@ -23,78 +22,13 @@ type tagRepo struct {
func NewTagRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
) tagcommon.TagRepo {
) tag.TagRepo {
return &tagRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// AddTagList add tag
func (tr *tagRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
for _, item := range tagList {
item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName())
if err != nil {
return err
}
item.RevisionID = "0"
}
_, err = tr.data.DB.Insert(tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByIDs get tag list all
func (tr *tagRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.In("id", ids)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagBySlugName get tag by slug name
func (tr *tagRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
tagInfo = &entity.Tag{}
session := tr.data.DB.Where("slug_name = ?", slugName)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tagInfo)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByName get tag list all like name
func (tr *tagRepo) GetTagListByName(ctx context.Context, name string, limit int) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.Where("slug_name LIKE ?", name+"%")
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Limit(limit).Asc("slug_name")
err = session.Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByNames get tag list all like name
func (tr *tagRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.In("slug_name", names)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// RemoveTag delete tag
func (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) {
session := tr.data.DB.Where(builder.Eq{"id": tagID})
@@ -114,16 +48,6 @@ func (tr *tagRepo) UpdateTag(ctx context.Context, tag *entity.Tag) (err error) {
return
}
// UpdateTagQuestionCount update tag question count
func (tr *tagRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) {
cond := &entity.Tag{QuestionCount: questionCount}
_, err = tr.data.DB.Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateTagSynonym update synonym tag
func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64,
mainTagSlugName string,
@@ -137,20 +61,6 @@ func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []strin
return
}
// GetTagByID get tag one
func (tr *tagRepo) GetTagByID(ctx context.Context, tagID string) (
tag *entity.Tag, exist bool, err error,
) {
tag = &entity.Tag{}
session := tr.data.DB.Where(builder.Eq{"id": tagID})
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tag)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagList get tag list all
func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
@@ -161,33 +71,3 @@ func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*
}
return
}
// GetTagPage get tag page
func (tr *tagRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (
tagList []*entity.Tag, total int64, err error,
) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.NewSession()
if len(tag.SlugName) > 0 {
session.Where(builder.Or(builder.Like{"slug_name", tag.SlugName}, builder.Like{"display_name", tag.SlugName}))
tag.SlugName = ""
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Where("main_tag_id = 0") // if this tag is synonym, exclude it
switch queryCond {
case "popular":
session.Desc("question_count")
case "name":
session.Asc("slug_name")
case "newest":
session.Desc("created_at")
}
total, err = pager.Help(page, pageSize, &tagList, tag, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+210
View File
@@ -0,0 +1,210 @@
package tag_common
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
)
// tagCommonRepo tag repository
type tagCommonRepo struct {
data *data.Data
uniqueIDRepo unique.UniqueIDRepo
}
// NewTagCommonRepo new repository
func NewTagCommonRepo(
data *data.Data,
uniqueIDRepo unique.UniqueIDRepo,
) tagcommon.TagCommonRepo {
return &tagCommonRepo{
data: data,
uniqueIDRepo: uniqueIDRepo,
}
}
// GetTagListByIDs get tag list all
func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.In("id", ids)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagBySlugName get tag by slug name
func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
tagInfo = &entity.Tag{}
session := tr.data.DB.Where("slug_name = ?", slugName)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tagInfo)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByName get tag list all like name
func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Where("")
if name != "" {
session.Where("slug_name LIKE ?", name+"%")
} else {
cond.Recommend = true
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Limit(limit).Asc("slug_name")
if !hasReserved {
cond.Reserved = false
session.UseBool("recommend", "reserved")
} else {
session.UseBool("recommend")
}
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Where("")
cond.Recommend = true
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Asc("slug_name")
session.UseBool("recommend")
err = session.Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Where("")
cond.Reserved = true
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Asc("slug_name")
session.UseBool("reserved")
err = session.Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagListByNames get tag list all like name
func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.In("slug_name", names).UseBool("recommend", "reserved")
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagByID get tag one
func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string) (
tag *entity.Tag, exist bool, err error,
) {
tag = &entity.Tag{}
session := tr.data.DB.Where(builder.Eq{"id": tagID})
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tag)
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetTagPage get tag page
func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (
tagList []*entity.Tag, total int64, err error,
) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.NewSession()
if len(tag.SlugName) > 0 {
session.Where(builder.Or(builder.Like{"slug_name", tag.SlugName}, builder.Like{"display_name", tag.SlugName}))
tag.SlugName = ""
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Where("main_tag_id = 0") // if this tag is synonym, exclude it
switch queryCond {
case "popular":
session.Desc("question_count")
case "name":
session.Asc("slug_name")
case "newest":
session.Desc("created_at")
}
total, err = pager.Help(page, pageSize, &tagList, tag, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// AddTagList add tag
func (tr *tagCommonRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
for _, item := range tagList {
item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName())
if err != nil {
return err
}
item.RevisionID = "0"
}
_, err = tr.data.DB.Insert(tagList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateTagQuestionCount update tag question count
func (tr *tagCommonRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error) {
cond := &entity.Tag{QuestionCount: questionCount}
_, err = tr.data.DB.Where(builder.Eq{"id": tagID}).MustCols("question_count").Update(cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (tr *tagCommonRepo) UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) {
bean := &entity.Tag{}
switch attribute {
case "recommend":
bean.Recommend = value
case "reserved":
bean.Reserved = value
default:
return
}
session := tr.data.DB.In("slug_name", tags).Cols(attribute).UseBool(attribute)
_, err = session.Update(bean)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+1
View File
@@ -7,6 +7,7 @@ import (
"strings"
"time"
"unicode"
"xorm.io/builder"
"github.com/answerdev/answer/internal/base/data"
+13 -3
View File
@@ -28,6 +28,7 @@ type AnswerAPIRouter struct {
siteinfoController *controller.SiteinfoController
notificationController *controller.NotificationController
dashboardController *controller.DashboardController
uploadController *controller.UploadController
}
func NewAnswerAPIRouter(
@@ -52,7 +53,7 @@ func NewAnswerAPIRouter(
siteinfoController *controller.SiteinfoController,
notificationController *controller.NotificationController,
dashboardController *controller.DashboardController,
uploadController *controller.UploadController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
@@ -76,6 +77,7 @@ func NewAnswerAPIRouter(
notificationController: notificationController,
siteinfoController: siteinfoController,
dashboardController: dashboardController,
uploadController: uploadController,
}
}
@@ -134,6 +136,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
//siteinfo
r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
}
func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
@@ -180,8 +183,6 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.PUT("/user/password", a.userController.UserModifyPassWord)
r.PUT("/user/info", a.userController.UserUpdateInfo)
r.PUT("/user/interface", a.userController.UserUpdateInterface)
r.POST("/user/avatar/upload", a.userController.UploadUserAvatar)
r.POST("/user/post/file", a.userController.UploadUserPostFile)
r.POST("/user/notice/set", a.userController.UserNoticeSet)
// vote
@@ -196,6 +197,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.GET("/notification/page", a.notificationController.GetList)
r.PUT("/notification/read/state/all", a.notificationController.ClearUnRead)
r.PUT("/notification/read/state", a.notificationController.ClearIDUnRead)
// upload file
r.POST("/file", a.uploadController.UploadFile)
}
func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
@@ -224,8 +228,14 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
// siteinfo
r.GET("/siteinfo/general", a.siteInfoController.GetGeneral)
r.GET("/siteinfo/interface", a.siteInfoController.GetInterface)
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
r.PUT("/siteinfo/general", a.siteInfoController.UpdateGeneral)
r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
r.PUT("/siteinfo/branding", a.siteInfoController.UpdateBranding)
r.PUT("/siteinfo/write", a.siteInfoController.UpdateSiteWrite)
r.PUT("/siteinfo/legal", a.siteInfoController.UpdateSiteLegal)
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
+6 -7
View File
@@ -68,24 +68,23 @@ func (a *UIRouter) Register(r *gin.Engine) {
// specify the not router for default routes and redirect
r.NoRoute(func(c *gin.Context) {
name := c.Request.URL.Path
urlPath := c.Request.URL.Path
filePath := ""
var file []byte
var err error
switch name {
switch urlPath {
case "/favicon.ico":
c.Header("content-type", "image/vnd.microsoft.icon")
filePath = UIRootFilePath + name
filePath = UIRootFilePath + urlPath
case "/manifest.json":
filePath = UIRootFilePath + name
filePath = UIRootFilePath + urlPath
case "/install":
// if answer is running by run command user can not access install page.
c.Redirect(http.StatusFound, "/")
return
default:
filePath = UIIndexFilePath
c.Header("content-type", "text/html;charset=utf-8")
}
file, err = ui.Build.ReadFile(filePath)
file, err := ui.Build.ReadFile(filePath)
if err != nil {
log.Error(err)
c.Status(http.StatusNotFound)
+4 -1
View File
@@ -5,7 +5,8 @@ type RemoveAnswerReq struct {
// answer id
ID string `validate:"required" json:"id"`
// user id
UserID string `json:"-"`
UserID string `json:"-"`
IsAdmin bool `json:"-"`
}
const (
@@ -28,6 +29,7 @@ type AnswerUpdateReq struct {
Content string `json:"content"` // content
HTML string `json:"html" ` // html
EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary
IsAdmin bool `json:"-"`
}
type AnswerList struct {
@@ -36,6 +38,7 @@ type AnswerList struct {
Page int `json:"page" form:"page"` // Query number of pages
PageSize int `json:"page_size" form:"page_size"` // Search page size
LoginUserID string `json:"-" `
IsAdmin bool `json:"-"`
}
type AnswerInfo struct {
+7 -4
View File
@@ -3,8 +3,9 @@ package schema
// RemoveQuestionReq delete question request
type RemoveQuestionReq struct {
// question id
ID string `validate:"required" comment:"question id" json:"id"`
UserID string `json:"-" ` // user_id
ID string `validate:"required" comment:"question id" json:"id"`
UserID string `json:"-" ` // user_id
IsAdmin bool `json:"-"`
}
type CloseQuestionReq struct {
@@ -12,6 +13,7 @@ type CloseQuestionReq struct {
UserID string `json:"-" ` // user_id
CloseType int `json:"close_type" ` // close_type
CloseMsg string `json:"close_msg" ` // close_type
IsAdmin bool `json:"-"`
}
type CloseQuestionMeta struct {
@@ -46,7 +48,8 @@ type QuestionUpdate struct {
// edit summary
EditSummary string `validate:"omitempty" json:"edit_summary"`
// user id
UserID string `json:"-"`
UserID string `json:"-"`
IsAdmin bool `json:"-"`
}
type QuestionBaseInfo struct {
@@ -169,7 +172,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 UserDeleted
StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 UserDeleted
Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string
}
+3
View File
@@ -10,6 +10,7 @@ type SearchDTO struct {
type SearchObject struct {
ID string `json:"id"`
QuestionID string `json:"question_id"`
Title string `json:"title"`
Excerpt string `json:"excerpt"`
CreatedAtParsed int64 `json:"created_at"`
@@ -29,6 +30,8 @@ type TagResp struct {
DisplayName string `json:"display_name"`
// if main tag slug name is not empty, this tag is synonymous with the main tag
MainTagSlugName string `json:"main_tag_slug_name"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}
type SearchResp struct {
+58 -3
View File
@@ -24,21 +24,76 @@ func (r *SiteGeneralReq) FormatSiteUrl() {
// SiteInterfaceReq site interface request
type SiteInterfaceReq struct {
Logo string `validate:"omitempty,gt=0,lte=256" form:"logo" json:"logo"`
Theme string `validate:"required,gt=1,lte=128" form:"theme" json:"theme"`
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
}
// SiteBrandingReq site branding request
type SiteBrandingReq struct {
Logo string `validate:"required,gt=0,lte=512" form:"logo" json:"logo"`
MobileLogo string `validate:"omitempty,gt=0,lte=512" form:"mobile_logo" json:"mobile_logo"`
SquareIcon string `validate:"required,gt=0,lte=512" form:"square_icon" json:"square_icon"`
Favicon string `validate:"omitempty,gt=0,lte=512" form:"favicon" json:"favicon"`
}
// SiteWriteReq site write request
type SiteWriteReq struct {
RequiredTag bool `validate:"omitempty" form:"required_tag" json:"required_tag"`
RecommendTags []string `validate:"omitempty" form:"recommend_tags" json:"recommend_tags"`
ReservedTags []string `validate:"omitempty" form:"reserved_tags" json:"reserved_tags"`
UserID string `json:"-"`
}
// SiteLegalReq site branding request
type SiteLegalReq struct {
TermsOfServiceOriginalText string `json:"terms_of_service_original_text"`
TermsOfServiceParsedText string `json:"terms_of_service_parsed_text"`
PrivacyPolicyOriginalText string `json:"privacy_policy_original_text"`
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text"`
}
// GetSiteLegalInfoReq site site legal request
type GetSiteLegalInfoReq struct {
InfoType string `validate:"required,oneof=tos privacy" form:"info_type"`
}
func (r *GetSiteLegalInfoReq) IsTOS() bool {
return r.InfoType == "tos"
}
func (r *GetSiteLegalInfoReq) IsPrivacy() bool {
return r.InfoType == "privacy"
}
// GetSiteLegalInfoResp get site legal info response
type GetSiteLegalInfoResp struct {
TermsOfServiceOriginalText string `json:"terms_of_service_original_text,omitempty"`
TermsOfServiceParsedText string `json:"terms_of_service_parsed_text,omitempty"`
PrivacyPolicyOriginalText string `json:"privacy_policy_original_text,omitempty"`
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"`
}
// SiteGeneralResp site general response
type SiteGeneralResp SiteGeneralReq
// SiteInterfaceResp site interface response
type SiteInterfaceResp SiteInterfaceReq
// SiteBrandingResp site branding response
type SiteBrandingResp SiteBrandingReq
// SiteWriteResp site write response
type SiteWriteResp SiteWriteReq
// SiteLegalResp site write response
type SiteLegalResp SiteLegalReq
// SiteInfoResp get site info response
type SiteInfoResp struct {
General *SiteGeneralResp `json:"general"`
Face *SiteInterfaceResp `json:"interface"`
General *SiteGeneralResp `json:"general"`
Interface *SiteInterfaceResp `json:"interface"`
Branding *SiteBrandingResp `json:"branding"`
}
// UpdateSMTPConfigReq get smtp config request
+17 -4
View File
@@ -11,7 +11,8 @@ import (
// SearchTagLikeReq get tag list all request
type SearchTagLikeReq struct {
// tag
Tag string `validate:"required,gt=0,lte=35" form:"tag"`
Tag string `validate:"omitempty" form:"tag"`
IsAdmin bool `json:"-"`
}
// GetTagInfoReq get tag info request
@@ -24,7 +25,7 @@ type GetTagInfoReq struct {
UserID string `json:"-"`
}
func (r *GetTagInfoReq) Check() (errField *validator.ErrorField, err error) {
func (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
if len(r.ID) == 0 && len(r.Name) == 0 {
return nil, errors.BadRequest(reason.RequestFormatError)
}
@@ -60,6 +61,8 @@ type GetTagResp struct {
MemberActions []*PermissionMemberAction `json:"member_actions"`
// if main tag slug name is not empty, this tag is synonymous with the main tag
MainTagSlugName string `json:"main_tag_slug_name"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}
func (tr *GetTagResp) GetExcerpt() {
@@ -95,6 +98,8 @@ type GetTagPageResp struct {
CreatedAt int64 `json:"created_at"`
// updated time
UpdatedAt int64 `json:"updated_at"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}
func (tr *GetTagPageResp) GetExcerpt() {
@@ -150,9 +155,9 @@ type UpdateTagReq struct {
UserID string `json:"-"`
}
func (r *UpdateTagReq) Check() (errField *validator.ErrorField, err error) {
func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) {
if len(r.EditSummary) == 0 {
r.EditSummary = "tag.edit.summary" // to i18n
r.EditSummary = "tag.edit.summary" // to do i18n
}
return nil, nil
}
@@ -217,4 +222,12 @@ type GetFollowingTagsResp struct {
DisplayName string `json:"display_name"`
// if main tag slug name is not empty, this tag is synonymous with the main tag
MainTagSlugName string `json:"main_tag_slug_name"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}
type SearchTagLikeResp struct {
SlugName string `json:"slug_name"`
Recommend bool `json:"recommend"`
Reserved bool `json:"reserved"`
}
+32 -30
View File
@@ -219,10 +219,10 @@ var UserStatusShowMsg = map[int]string{
// EmailLogin
type UserEmailLogin struct {
Email string `json:"e_mail" ` // e_mail
Pass string `json:"pass" ` // password
CaptchaID string `json:"captcha_id" ` // captcha_id
CaptchaCode string `json:"captcha_code" ` // captcha_code
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` // e_mail
Pass string `validate:"required,gte=8,lte=32" json:"pass"` // password
CaptchaID string `json:"captcha_id"` // captcha_id
CaptchaCode string `json:"captcha_code"` // captcha_code
}
// UserRegisterReq user register request
@@ -236,14 +236,16 @@ type UserRegisterReq struct {
IP string `json:"-" `
}
func (u *UserRegisterReq) Check() (errField *validator.ErrorField, err error) {
func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) {
// TODO i18n
err = checker.CheckPassword(8, 32, 0, u.Pass)
if err != nil {
return &validator.ErrorField{
Key: "pass",
Value: err.Error(),
}, err
errField := &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: err.Error(),
}
errFields = append(errFields, errField)
return errFields, err
}
return nil, nil
}
@@ -255,14 +257,16 @@ type UserModifyPassWordRequest struct {
Pass string `json:"pass" ` // password
}
func (u *UserModifyPassWordRequest) Check() (errField *validator.ErrorField, err error) {
func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
// TODO i18n
err = checker.CheckPassword(8, 32, 0, u.Pass)
if err != nil {
return &validator.ErrorField{
Key: "pass",
Value: err.Error(),
}, err
errField := &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: err.Error(),
}
errFields = append(errFields, errField)
return errFields, err
}
return nil, nil
}
@@ -292,16 +296,17 @@ type AvatarInfo struct {
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
}
func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error) {
func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
if len(u.Username) > 0 {
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
match := re.MatchString(u.Username)
if !match {
err = errors.BadRequest(reason.UsernameInvalid)
return &validator.ErrorField{
Key: "username",
Value: err.Error(),
}, err
errField := &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: err.Error(),
}
errFields = append(errFields, errField)
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
}
return nil, nil
@@ -327,14 +332,16 @@ type UserRePassWordRequest struct {
Content string `json:"-"`
}
func (u *UserRePassWordRequest) Check() (errField *validator.ErrorField, err error) {
func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
// TODO i18n
err = checker.CheckPassword(8, 32, 0, u.Pass)
if err != nil {
return &validator.ErrorField{
Key: "pass",
Value: err.Error(),
}, err
errField := &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: err.Error(),
}
errFields = append(errFields, errField)
return errFields, err
}
return nil, nil
}
@@ -410,8 +417,3 @@ type UserVerifyEmailSendReq struct {
CaptchaID string `validate:"omitempty,gt=0,lte=500" json:"captcha_id"`
CaptchaCode string `validate:"omitempty,gt=0,lte=500" json:"captcha_code"`
}
type UserVerifyEmailErrorResponse struct {
Key string `json:"key"`
Value string `json:"value"`
}
+39 -8
View File
@@ -73,11 +73,29 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
if !exist {
return nil
}
if answerInfo.UserID != req.UserID {
return errors.BadRequest(reason.UnauthorizedError)
}
if answerInfo.Adopted == schema.AnswerAdoptedEnable {
return errors.BadRequest(reason.UnauthorizedError)
if !req.IsAdmin {
if answerInfo.UserID != req.UserID {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if answerInfo.VoteCount > 0 {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if answerInfo.Adopted == schema.AnswerAdoptedEnable {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
if err != nil {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if !exist {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if questionInfo.AnswerCount > 1 {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
if questionInfo.AcceptedAnswerID != "" {
return errors.BadRequest(reason.AnswerCannotDeleted)
}
}
// user add question count
@@ -164,6 +182,19 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq
if !exist {
return "", errors.BadRequest(reason.QuestionNotFound)
}
if !req.IsAdmin {
answerInfo, exist, err := as.answerRepo.GetByID(ctx, req.ID)
if err != nil {
return "", err
}
if !exist {
return "", nil
}
if answerInfo.UserID != req.UserID {
return "", errors.BadRequest(reason.AnswerCannotUpdate)
}
}
now := time.Now()
insertData := new(entity.Answer)
insertData.ID = req.ID
@@ -361,14 +392,14 @@ func (as *AnswerService) SearchList(ctx context.Context, search *schema.AnswerLi
if err != nil {
return list, count, err
}
AnswerList, err := as.SearchFormatInfo(ctx, dblist, search.LoginUserID)
AnswerList, err := as.SearchFormatInfo(ctx, dblist, search.LoginUserID, search.IsAdmin)
if err != nil {
return AnswerList, count, err
}
return AnswerList, count, nil
}
func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity.Answer, loginUserID string) ([]*schema.AnswerInfo, error) {
func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity.Answer, loginUserID string, isAdmin bool) ([]*schema.AnswerInfo, error) {
list := make([]*schema.AnswerInfo, 0)
objectIds := make([]string, 0)
userIds := make([]string, 0)
@@ -411,7 +442,7 @@ func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity.
}
for _, item := range list {
item.MemberActions = permission.GetAnswerPermission(loginUserID, item.UserID)
item.MemberActions = permission.GetAnswerPermission(ctx, loginUserID, item.UserID, isAdmin)
}
return list, nil
+5
View File
@@ -43,6 +43,7 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
log.Infof("user status updated: %+v", cacheInfo)
userCacheInfo.UserStatus = cacheInfo.UserStatus
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
userCacheInfo.IsAdmin = cacheInfo.IsAdmin
// update current user cache info
err := as.authRepo.SetUserCacheInfo(ctx, accessToken, userCacheInfo)
if err != nil {
@@ -58,6 +59,10 @@ func (as *AuthService) SetUserCacheInfo(ctx context.Context, userInfo *entity.Us
return accessToken, err
}
func (as *AuthService) SetUserStatus(ctx context.Context, userInfo *entity.UserCacheInfo) (err error) {
return as.authRepo.SetUserStatus(ctx, userInfo.UserID, userInfo)
}
func (as *AuthService) UpdateUserCacheInfo(ctx context.Context, token string, userInfo *entity.UserCacheInfo) (err error) {
err = as.authRepo.SetUserCacheInfo(ctx, token, userInfo)
if err != nil {
+3 -3
View File
@@ -121,7 +121,7 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
resp = &schema.GetCommentResp{}
resp.SetFromComment(comment)
resp.MemberActions = permission.GetCommentPermission(req.UserID, resp.UserID)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID)
// get reply user info
if len(resp.ReplyUserID) > 0 {
@@ -222,7 +222,7 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
// check if current user vote this comment
resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID)
resp.MemberActions = permission.GetCommentPermission(req.UserID, resp.UserID)
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID)
return resp, nil
}
@@ -282,7 +282,7 @@ func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.Ge
// check if current user vote this comment
commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID)
commentResp.MemberActions = permission.GetCommentPermission(req.UserID, commentResp.UserID)
commentResp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, commentResp.UserID)
resp = append(resp, commentResp)
}
return pager.NewPageModel(total, resp), nil
@@ -76,17 +76,16 @@ func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.Das
if err != nil {
info, statisticalErr := ds.Statistical(ctx)
if statisticalErr != nil {
return dashboardInfo, err
return nil, statisticalErr
}
setCacheErr := ds.SetCache(ctx, info)
if setCacheErr != nil {
log.Error("ds.SetCache", setCacheErr)
if setCacheErr := ds.SetCache(ctx, info); setCacheErr != nil {
log.Errorf("set dashboard statistical failed: %s", setCacheErr)
}
return info, err
return info, nil
}
err = json.Unmarshal([]byte(infoStr), dashboardInfo)
if err != nil {
return dashboardInfo, err
if err = json.Unmarshal([]byte(infoStr), dashboardInfo); err != nil {
log.Errorf("parsing dashboard information failed: %s", err)
return nil, errors.InternalServer(reason.UnknownError)
}
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
+2 -2
View File
@@ -15,7 +15,7 @@ type FollowRepo interface {
}
type FollowService struct {
tagRepo tagcommon.TagRepo
tagRepo tagcommon.TagCommonRepo
followRepo FollowRepo
followCommonRepo activity_common.FollowRepo
}
@@ -23,7 +23,7 @@ type FollowService struct {
func NewFollowService(
followRepo FollowRepo,
followCommonRepo activity_common.FollowRepo,
tagRepo tagcommon.TagRepo,
tagRepo tagcommon.TagCommonRepo,
) *FollowService {
return &FollowService{
followRepo: followRepo,
+2 -2
View File
@@ -19,7 +19,7 @@ type ObjService struct {
answerRepo answercommon.AnswerRepo
questionRepo questioncommon.QuestionRepo
commentRepo comment_common.CommentCommonRepo
tagRepo tagcommon.TagRepo
tagRepo tagcommon.TagCommonRepo
}
// NewObjService new object service
@@ -27,7 +27,7 @@ func NewObjService(
answerRepo answercommon.AnswerRepo,
questionRepo questioncommon.QuestionRepo,
commentRepo comment_common.CommentCommonRepo,
tagRepo tagcommon.TagRepo) *ObjService {
tagRepo tagcommon.TagCommonRepo) *ObjService {
return &ObjService{
answerRepo: answerRepo,
questionRepo: questionRepo,
@@ -1,9 +1,13 @@
package permission
import "github.com/answerdev/answer/internal/schema"
import (
"context"
"github.com/answerdev/answer/internal/schema"
)
// TODO: There is currently no permission management
func GetCommentPermission(userID string, commentCreatorUserID string) (
func GetCommentPermission(ctx context.Context, userID string, commentCreatorUserID string) (
actions []*schema.PermissionMemberAction) {
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
@@ -31,7 +35,7 @@ func GetCommentPermission(userID string, commentCreatorUserID string) (
return actions
}
func GetTagPermission(userID string, tagCreatorUserID string) (
func GetTagPermission(ctx context.Context, userID string, tagCreatorUserID string) (
actions []*schema.PermissionMemberAction) {
if userID != tagCreatorUserID {
return []*schema.PermissionMemberAction{}
@@ -50,18 +54,20 @@ func GetTagPermission(userID string, tagCreatorUserID string) (
}
}
func GetAnswerPermission(userID string, answerAuthID string) (
func GetAnswerPermission(ctx context.Context, userID string, answerAuthID string, isAdmin bool) (
actions []*schema.PermissionMemberAction) {
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
actions = append(actions, &schema.PermissionMemberAction{
Action: "report",
Name: "Flag",
Type: "reason",
})
}
if userID != answerAuthID {
return actions
if !isAdmin {
if len(userID) > 0 {
actions = append(actions, &schema.PermissionMemberAction{
Action: "report",
Name: "Flag",
Type: "reason",
})
}
if userID != answerAuthID {
return actions
}
}
actions = append(actions, []*schema.PermissionMemberAction{
{
@@ -78,18 +84,19 @@ func GetAnswerPermission(userID string, answerAuthID string) (
return actions
}
func GetQuestionPermission(userID string, questionAuthID string) (
actions []*schema.PermissionMemberAction) {
func GetQuestionPermission(ctx context.Context, userID string, questionAuthID string, isAdmin bool) (actions []*schema.PermissionMemberAction) {
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
actions = append(actions, &schema.PermissionMemberAction{
Action: "report",
Name: "Flag",
Type: "reason",
})
}
if userID != questionAuthID {
return actions
if !isAdmin {
if len(userID) > 0 {
actions = append(actions, &schema.PermissionMemberAction{
Action: "report",
Name: "Flag",
Type: "reason",
})
}
if userID != questionAuthID {
return actions
}
}
actions = append(actions, []*schema.PermissionMemberAction{
{
+2
View File
@@ -24,6 +24,7 @@ import (
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/search_parser"
"github.com/answerdev/answer/internal/service/tag"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/uploader"
@@ -57,6 +58,7 @@ var ProviderSetService = wire.NewSet(
revision_common.NewRevisionService,
NewRevisionService,
rank.NewRankService,
search_parser.NewSearchParser,
NewSearchService,
meta.NewMetaService,
object_info.NewObjService,
+78 -17
View File
@@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/answerdev/answer/internal/base/constant"
@@ -70,6 +71,12 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ
if !has {
return nil
}
if !req.IsAdmin {
if questionInfo.UserID != req.UserID {
return errors.BadRequest(reason.QuestionCannotClose)
}
}
questionInfo.Status = entity.QuestionStatusclosed
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
if err != nil {
@@ -105,6 +112,16 @@ func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language)
// AddQuestion add question
func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo *schema.QuestionInfo, err error) {
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
if err != nil {
return
}
if !recommendExist {
err = fmt.Errorf("recommend is not exist")
err = errors.BadRequest(reason.RecommendTagNotExist).WithError(err).WithStack()
return
}
questionInfo = &schema.QuestionInfo{}
question := &entity.Question{}
now := time.Now()
@@ -150,7 +167,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
log.Error("user IncreaseQuestionCount error", err.Error())
}
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false)
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false, false)
return
}
@@ -163,15 +180,31 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
if !has {
return nil
}
if questionInfo.UserID != req.UserID {
return errors.BadRequest(reason.UnauthorizedError)
}
if !req.IsAdmin {
if questionInfo.UserID != req.UserID {
return errors.BadRequest(reason.QuestionCannotDeleted)
}
if questionInfo.AcceptedAnswerID != "" {
return errors.BadRequest(reason.UnauthorizedError)
}
if questionInfo.AnswerCount > 0 {
return errors.BadRequest(reason.UnauthorizedError)
if questionInfo.AcceptedAnswerID != "0" {
return errors.BadRequest(reason.QuestionCannotDeleted)
}
if questionInfo.AnswerCount > 1 {
return errors.BadRequest(reason.QuestionCannotDeleted)
}
if questionInfo.AnswerCount == 1 {
answersearch := &entity.AnswerSearch{}
answersearch.QuestionID = req.ID
answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
if err != nil {
return err
}
for _, answer := range answerList {
if answer.VoteCount > 0 {
return errors.BadRequest(reason.QuestionCannotDeleted)
}
}
}
}
questionInfo.Status = entity.QuestionStatusDeleted
@@ -212,9 +245,34 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
if !has {
return
}
if dbinfo.UserID != req.UserID {
if !req.IsAdmin {
if dbinfo.UserID != req.UserID {
return questionInfo, errors.BadRequest(reason.QuestionCannotUpdate)
}
}
//CheckChangeTag
oldTags, err := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)
if err != nil {
return
}
tagNameList := make([]string, 0)
for _, tag := range req.Tags {
tagNameList = append(tagNameList, tag.SlugName)
}
Tags, err := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if err != nil {
return
}
CheckTag, CheckTaglist := qs.CheckChangeTag(ctx, oldTags, Tags)
if !CheckTag {
err = errors.BadRequest(reason.UnauthorizedError).WithMsg(fmt.Sprintf("tag [%s] cannot be modified",
strings.Join(CheckTaglist, ",")))
return
}
//update question to db
err = qs.questionRepo.UpdateQuestion(ctx, question, []string{"title", "original_text", "parsed_text", "updated_at"})
if err != nil {
return
@@ -241,12 +299,12 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
return
}
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false)
questionInfo, err = qs.GetQuestion(ctx, question.ID, question.UserID, false, false)
return
}
// GetQuestion get question one
func (qs *QuestionService) GetQuestion(ctx context.Context, id, loginUserID string, addpv bool) (resp *schema.QuestionInfo, err error) {
func (qs *QuestionService) GetQuestion(ctx context.Context, id, loginUserID string, addpv bool, isAdmin bool) (resp *schema.QuestionInfo, err error) {
question, err := qs.questioncommon.Info(ctx, id, loginUserID)
if err != nil {
return
@@ -258,7 +316,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, id, loginUserID stri
}
}
question.MemberActions = permission.GetQuestionPermission(loginUserID, question.UserID)
question.MemberActions = permission.GetQuestionPermission(ctx, loginUserID, question.UserID, isAdmin)
return question, nil
}
@@ -266,6 +324,10 @@ func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData)
}
func (qs *QuestionService) CheckChangeTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
return qs.tagCommon.ObjectCheckChangeTag(ctx, oldobjectTagData, objectTagData)
}
func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) {
userlist := make([]*schema.UserQuestionInfo, 0)
@@ -481,7 +543,7 @@ func (qs *QuestionService) SearchByTitleLike(ctx context.Context, title string,
// SimilarQuestion
func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID string, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
list := make([]*schema.QuestionInfo, 0)
questionInfo, err := qs.GetQuestion(ctx, questionID, loginUserID, false)
questionInfo, err := qs.GetQuestion(ctx, questionID, loginUserID, false, false)
if err != nil {
return list, 0, err
}
@@ -502,14 +564,13 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin
// SearchList
func (qs *QuestionService) SearchList(ctx context.Context, req *schema.QuestionSearch, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
if len(req.Tag) > 0 {
taginfo, has, err := qs.tagCommon.GetTagListByName(ctx, req.Tag)
tagInfo, has, err := qs.tagCommon.GetTagBySlugName(ctx, strings.ToLower(req.Tag))
if err != nil {
log.Error("tagCommon.GetTagListByNames error", err)
}
if has {
req.TagIDs = append(req.TagIDs, taginfo.ID)
req.TagIDs = append(req.TagIDs, tagInfo.ID)
}
}
list := make([]*schema.QuestionInfo, 0)
if req.UserName != "" {
+2
View File
@@ -113,6 +113,8 @@ func (rs *RevisionService) parseItem(ctx context.Context, item *schema.GetRevisi
ParsedText: tag.ParsedText,
FollowCount: tag.FollowCount,
QuestionCount: tag.QuestionCount,
Recommend: tag.Recommend,
Reserved: tag.Reserved,
}
tagInfo.GetExcerpt()
item.ContentParsed = tagInfo
@@ -1,55 +0,0 @@
package search
import (
"context"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type AcceptedAnswerSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewAcceptedAnswerSearch(repo search_common.SearchRepo) *AcceptedAnswerSearch {
return &AcceptedAnswerSearch{
repo: repo,
}
}
func (s *AcceptedAnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `isaccepted:yes`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AcceptedAnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, true, "", s.page, s.size, s.order)
}
-54
View File
@@ -1,54 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"strings"
)
type AnswerSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewAnswerSearch(repo search_common.SearchRepo) *AnswerSearch {
return &AnswerSearch{
repo: repo,
}
}
func (s *AnswerSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `is:answer`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AnswerSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, false, "", s.page, s.size, s.order)
}
-65
View File
@@ -1,65 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"github.com/answerdev/answer/pkg/converter"
)
type AnswersSearch struct {
repo search_common.SearchRepo
exp int
w string
page int
size int
order string
}
func NewAnswersSearch(repo search_common.SearchRepo) *AnswersSearch {
return &AnswersSearch{
repo: repo,
}
}
func (s *AnswersSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p,
exp string
)
q = dto.Query
w = dto.Query
p = `(?m)^answers:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
s.exp = converter.StringToInt(exp)
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AnswersSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, false, s.exp, s.page, s.size, s.order)
}
-90
View File
@@ -1,90 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
)
type AuthorSearch struct {
repo search_common.SearchRepo
userCommon *usercommon.UserCommon
exp string
w string
page int
size int
order string
}
func NewAuthorSearch(repo search_common.SearchRepo, userCommon *usercommon.UserCommon) *AuthorSearch {
return &AuthorSearch{
repo: repo,
userCommon: userCommon,
}
}
// Parse
// example: "user:12345" -> {exp="" w="12345"}
func (s *AuthorSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
exp,
q,
w,
p,
me,
name string
)
exp = ""
q = dto.Query
w = q
p = `(?m)^user:([a-z0-9._-]+)`
me = "user:me"
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
name = res[1]
user, has, err := s.userCommon.GetUserBasicInfoByUserName(nil, name)
if err == nil && has {
exp = user.ID
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
} else if strings.Index(q, me) == 0 {
exp = dto.UserID
w = strings.TrimPrefix(q, me)
ok = true
}
w = strings.TrimSpace(w)
s.exp = exp
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *AuthorSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
if len(s.exp) == 0 {
return
}
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
resp, total, err = s.repo.SearchContents(ctx, words, "", s.exp, -1, s.page, s.size, s.order)
return
}
-65
View File
@@ -1,65 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"regexp"
"strings"
)
type InQuestionSearch struct {
repo search_common.SearchRepo
w string
exp string
page int
size int
order string
}
func NewInQuestionSearch(repo search_common.SearchRepo) *InQuestionSearch {
return &InQuestionSearch{
repo: repo,
}
}
func (s *InQuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
w,
q,
p,
exp string
)
q = dto.Query
w = dto.Query
p = `(?m)^inquestion:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
s.exp = exp
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *InQuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchAnswers(ctx, words, false, s.exp, s.page, s.size, s.order)
}
@@ -1,58 +0,0 @@
package search
import (
"context"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type NotAcceptedQuestion struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewNotAcceptedQuestion(repo search_common.SearchRepo) *NotAcceptedQuestion {
return &NotAcceptedQuestion{
repo: repo,
}
}
func (s *NotAcceptedQuestion) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `hasaccepted:no`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *NotAcceptedQuestion) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, true, -1, s.page, s.size, s.order)
}
-47
View File
@@ -1,47 +0,0 @@
package search
import (
"context"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type ObjectSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewObjectSearch(repo search_common.SearchRepo) *ObjectSearch {
return &ObjectSearch{
repo: repo,
}
}
func (s *ObjectSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
w string
)
w = strings.TrimSpace(dto.Query)
if len(w) > 0 {
ok = true
}
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *ObjectSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchContents(ctx, words, "", "", -1, s.page, s.size, s.order)
}
-55
View File
@@ -1,55 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"strings"
)
type QuestionSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewQuestionSearch(repo search_common.SearchRepo) *QuestionSearch {
return &QuestionSearch{
repo: repo,
}
}
func (s *QuestionSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q,
w,
p string
)
q = dto.Query
w = dto.Query
p = `is:question`
if strings.Index(q, p) == 0 {
ok = true
w = strings.TrimPrefix(q, p)
}
s.w = strings.TrimSpace(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *QuestionSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
words := strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
return s.repo.SearchQuestions(ctx, words, false, -1, s.page, s.size, s.order)
}
-63
View File
@@ -1,63 +0,0 @@
package search
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"github.com/answerdev/answer/pkg/converter"
)
type ScoreSearch struct {
repo search_common.SearchRepo
exp int
w string
page int
size int
order string
}
func NewScoreSearch(repo search_common.SearchRepo) *ScoreSearch {
return &ScoreSearch{
repo: repo,
}
}
func (s *ScoreSearch) Parse(dto *schema.SearchDTO) (ok bool) {
exp := ""
q := dto.Query
w := q
p := `(?m)^score:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(w)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
w = q[trimLen:]
ok = true
}
w = strings.TrimSpace(w)
s.exp = converter.StringToInt(exp)
s.w = w
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *ScoreSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
var (
words []string
)
words = strings.Split(s.w, " ")
if len(words) > 3 {
words = words[:4]
}
resp, total, err = s.repo.SearchContents(ctx, words, "", "", s.exp, s.page, s.size, s.order)
return
}
+19 -16
View File
@@ -9,27 +9,28 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/search_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/tag_common"
)
type TagSearch struct {
repo search_common.SearchRepo
tagRepo tagcommon.TagRepo
followCommon activity_common.FollowRepo
page int
size int
exp string
w string
userID string
Extra schema.GetTagPageResp
order string
repo search_common.SearchRepo
tagCommonService *tag_common.TagCommonService
followCommon activity_common.FollowRepo
page int
size int
exp string
w string
userID string
Extra schema.GetTagPageResp
order string
}
func NewTagSearch(repo search_common.SearchRepo, tagRepo tagcommon.TagRepo, followCommon activity_common.FollowRepo) *TagSearch {
func NewTagSearch(repo search_common.SearchRepo,
tagCommonService *tag_common.TagCommonService, followCommon activity_common.FollowRepo) *TagSearch {
return &TagSearch{
repo: repo,
tagRepo: tagRepo,
followCommon: followCommon,
repo: repo,
tagCommonService: tagCommonService,
followCommon: followCommon,
}
}
@@ -65,7 +66,7 @@ func (ts *TagSearch) Search(ctx context.Context) (resp []schema.SearchResp, tota
tag *entity.Tag
exists, followed bool
)
tag, exists, err = ts.tagRepo.GetTagBySlugName(ctx, ts.exp)
tag, exists, err = ts.tagCommonService.GetTagBySlugName(ctx, ts.exp)
if err != nil {
return
}
@@ -82,6 +83,8 @@ func (ts *TagSearch) Search(ctx context.Context) (resp []schema.SearchResp, tota
ParsedText: tag.ParsedText,
QuestionCount: tag.QuestionCount,
IsFollower: followed,
Recommend: tag.Recommend,
Reserved: tag.Reserved,
}
ts.Extra.GetExcerpt()
-47
View File
@@ -1,47 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
"regexp"
"strings"
)
type ViewsSearch struct {
repo search_common.SearchRepo
exp string
q string
order string
}
func NewViewsSearch(repo search_common.SearchRepo) *ViewsSearch {
return &ViewsSearch{
repo: repo,
}
}
func (s *ViewsSearch) Parse(dto *schema.SearchDTO) (ok bool) {
exp := ""
w := dto.Query
q := w
p := `(?m)^views:([0-9]+)`
re := regexp.MustCompile(p)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
exp = res[1]
trimLen := len(res[0])
q = w[trimLen:]
ok = true
}
q = strings.TrimSpace(q)
s.exp = exp
s.q = q
s.order = dto.Order
return
}
func (s *ViewsSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
return
}
-59
View File
@@ -1,59 +0,0 @@
package search
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/search_common"
)
type WithinSearch struct {
repo search_common.SearchRepo
w string
page int
size int
order string
}
func NewWithinSearch(repo search_common.SearchRepo) *WithinSearch {
return &WithinSearch{
repo: repo,
}
}
func (s *WithinSearch) Parse(dto *schema.SearchDTO) (ok bool) {
var (
q string
w []rune
hasEnd bool
)
q = dto.Query
if q[0:1] == `"` {
for _, v := range []rune(q) {
if len(w) == 0 && string(v) == `"` {
continue
} else if string(v) == `"` {
hasEnd = true
break
} else {
w = append(w, v)
}
}
}
if hasEnd {
ok = true
}
s.w = string(w)
s.page = dto.Page
s.size = dto.Size
s.order = dto.Order
return
}
func (s *WithinSearch) Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error) {
return s.repo.SearchContents(ctx, []string{s.w}, "", "", -1, s.page, s.size, s.order)
}
+3 -3
View File
@@ -6,7 +6,7 @@ import (
)
type SearchRepo interface {
SearchContents(ctx context.Context, words []string, tagID, userID string, votes, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchQuestions(ctx context.Context, words []string, limitNoAccepted bool, answers, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchAnswers(ctx context.Context, words []string, limitAccepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchQuestions(ctx context.Context, words []string, notAccepted bool, views, answers int, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
SearchAnswers(ctx context.Context, words []string, tagIDs []string, accepted bool, questionID string, page, size int, order string) (resp []schema.SearchResp, total int64, err error)
}
@@ -0,0 +1,360 @@
package search_parser
import (
"context"
"regexp"
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/converter"
)
type SearchParser struct {
tagCommonService *tag_common.TagCommonService
userCommon *usercommon.UserCommon
}
func NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon *usercommon.UserCommon) *SearchParser {
return &SearchParser{
tagCommonService: tagCommonService,
userCommon: userCommon,
}
}
// ParseStructure parse search structure, maybe match one of type all/questions/answers,
// but if match two type, it will return false
func (sp *SearchParser) ParseStructure(dto *schema.SearchDTO) (
searchType string,
// search all
userID string,
votes int,
// search questions
notAccepted bool,
isQuestion bool,
views,
answers int,
// search answers
accepted bool,
questionID string,
isAnswer bool,
// common fields
tags,
words []string,
) {
var (
query = dto.Query
currentUserID = dto.UserID
all = 0
q = 0
a = 0
withWords = []string{}
limitWords = 5
)
// match tags
tags = sp.parseTags(&query)
// match all
userID = sp.parseUserID(&query, currentUserID)
if userID != "" {
searchType = "all"
all = 1
}
votes = sp.parseVotes(&query)
if votes != -1 {
searchType = "all"
all = 1
}
withWords = sp.parseWithin(&query)
if len(withWords) > 0 {
searchType = "all"
all = 1
}
// match questions
notAccepted = sp.parseNotAccepted(&query)
if notAccepted {
searchType = "question"
q = 1
}
isQuestion = sp.parseIsQuestion(&query)
if isQuestion {
searchType = "question"
q = 1
}
views = sp.parseViews(&query)
if views != -1 {
searchType = "question"
q = 1
}
answers = sp.parseAnswers(&query)
if answers != -1 {
searchType = "question"
q = 1
}
// match answers
accepted = sp.parseAccepted(&query)
if accepted {
searchType = "answer"
a = 1
}
questionID = sp.parseQuestionID(&query)
if questionID != "" {
searchType = "answer"
a = 1
}
isAnswer = sp.parseIsAnswer(&query)
if isAnswer {
searchType = "answer"
a = 1
}
words = strings.Split(query, " ")
if len(withWords) > 0 {
words = append(withWords, words...)
}
// check limit words
if len(words) > limitWords {
words = words[:limitWords]
}
// check tags' search is all or question
if len(tags) > 0 {
if len(words) > 0 {
searchType = "all"
all = 1
} else {
searchType = "question"
q = 1
}
}
// check match types greater than 1
if all+q+a > 1 {
searchType = ""
}
// check not match
if all+q+a == 0 && len(words) > 0 {
searchType = "all"
}
return
}
// parseTags parse search tags, return tag ids array
func (sp *SearchParser) parseTags(query *string) (tags []string) {
var (
// expire tag pattern
exprTag = `(?m)\[([a-zA-Z0-9-\+\.#]+)\]{1}?`
q = *query
limit = 5
)
re := regexp.MustCompile(exprTag)
res := re.FindAllStringSubmatch(q, -1)
if len(res) == 0 {
return
}
tags = make([]string, len(res))
for i, item := range res {
tag, exists, err := sp.tagCommonService.GetTagBySlugName(context.TODO(), item[1])
if err != nil || !exists {
continue
}
tags[i] = tag.ID
}
// limit maximum 5 tags
if len(tags) > limit {
tags = tags[:limit]
}
q = strings.TrimSpace(re.ReplaceAllString(q, ""))
*query = q
return
}
// parseUserID return user id or current login user id
func (sp *SearchParser) parseUserID(query *string, currentUserID string) (userID string) {
var (
exprUserID = `(?m)^user:([a-z0-9._-]+)`
exprMe = "user:me"
q = *query
)
re := regexp.MustCompile(exprUserID)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
name := res[1]
user, has, err := sp.userCommon.GetUserBasicInfoByUserName(nil, name)
if err == nil && has {
userID = user.ID
q = re.ReplaceAllString(q, "")
}
} else if strings.Index(q, exprMe) != -1 {
userID = currentUserID
q = strings.ReplaceAll(q, exprMe, "")
}
*query = strings.TrimSpace(q)
return
}
// parseVotes return the votes of search query
func (sp *SearchParser) parseVotes(query *string) (votes int) {
var (
expr = `(?m)^score:([0-9]+)`
q = *query
)
votes = -1
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
votes = converter.StringToInt(res[1])
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseWithin parse quotes within words like: "hello world"
func (sp *SearchParser) parseWithin(query *string) (words []string) {
var (
q = *query
expr = `(?U)(".+")`
)
re := regexp.MustCompile(expr)
matches := re.FindAllStringSubmatch(q, -1)
words = []string{}
for _, match := range matches {
words = append(words, match[1])
}
q = re.ReplaceAllString(q, "")
*query = strings.TrimSpace(q)
return
}
// parseNotAccepted return the question has not accepted the answer
func (sp *SearchParser) parseNotAccepted(query *string) (notAccepted bool) {
var (
q = *query
expr = `hasaccepted:no`
)
if strings.Index(q, expr) != -1 {
q = strings.ReplaceAll(q, expr, "")
notAccepted = true
}
*query = strings.TrimSpace(q)
return
}
// parseIsQuestion check the result if only limit question or not
func (sp *SearchParser) parseIsQuestion(query *string) (isQuestion bool) {
var (
q = *query
expr = `is:question`
)
if strings.Index(q, expr) == 0 {
q = strings.ReplaceAll(q, expr, "")
isQuestion = true
}
*query = strings.TrimSpace(q)
return
}
// parseViews check search has views or not
func (sp *SearchParser) parseViews(query *string) (views int) {
var (
q = *query
expr = `(?m)^views:([0-9]+)`
)
views = -1
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
views = converter.StringToInt(res[1])
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseAnswers check whether specified answer count for question
func (sp *SearchParser) parseAnswers(query *string) (answers int) {
var (
q = *query
expr = `(?m)^answers:([0-9]+)`
)
answers = -1
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
answers = converter.StringToInt(res[1])
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseAccepted check the search is limit accepted answer or not
func (sp *SearchParser) parseAccepted(query *string) (accepted bool) {
var (
q = *query
expr = `isaccepted:yes`
)
if strings.Index(q, expr) != -1 {
accepted = true
strings.ReplaceAll(q, expr, "")
}
*query = strings.TrimSpace(q)
return
}
// parseQuestionID check whether specified question's id
func (sp *SearchParser) parseQuestionID(query *string) (questionID string) {
var (
q = *query
expr = `(?m)^inquestion:([0-9]+)`
)
re := regexp.MustCompile(expr)
res := re.FindStringSubmatch(q)
if len(res) == 2 {
questionID = res[1]
q = re.ReplaceAllString(q, "")
}
*query = strings.TrimSpace(q)
return
}
// parseIsAnswer check the result if only limit answer or not
func (sp *SearchParser) parseIsAnswer(query *string) (isAnswer bool) {
var (
q = *query
expr = `is:answer`
)
if strings.Index(q, expr) != -1 {
isAnswer = true
q = strings.ReplaceAll(q, expr, "")
}
*query = strings.TrimSpace(q)
return
}
+36 -67
View File
@@ -2,92 +2,61 @@ package service
import (
"context"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/search"
"github.com/answerdev/answer/internal/service/search_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/internal/service/search_parser"
)
type Search interface {
Parse(dto *schema.SearchDTO) (ok bool)
Search(ctx context.Context) (resp []schema.SearchResp, total int64, err error)
}
type SearchService struct {
searchRepo search_common.SearchRepo
tagSearch *search.TagSearch
withinSearch *search.WithinSearch
authorSearch *search.AuthorSearch
scoreSearch *search.ScoreSearch
answersSearch *search.AnswersSearch
notAcceptedQuestion *search.NotAcceptedQuestion
acceptedAnswerSearch *search.AcceptedAnswerSearch
inQuestionSearch *search.InQuestionSearch
questionSearch *search.QuestionSearch
answerSearch *search.AnswerSearch
viewsSearch *search.ViewsSearch
objectSearch *search.ObjectSearch
searchParser *search_parser.SearchParser
searchRepo search_common.SearchRepo
}
func NewSearchService(
searchParser *search_parser.SearchParser,
searchRepo search_common.SearchRepo,
tagRepo tagcommon.TagRepo,
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, userCommon),
scoreSearch: search.NewScoreSearch(searchRepo),
answersSearch: search.NewAnswersSearch(searchRepo),
acceptedAnswerSearch: search.NewAcceptedAnswerSearch(searchRepo),
notAcceptedQuestion: search.NewNotAcceptedQuestion(searchRepo),
inQuestionSearch: search.NewInQuestionSearch(searchRepo),
questionSearch: search.NewQuestionSearch(searchRepo),
answerSearch: search.NewAnswerSearch(searchRepo),
viewsSearch: search.NewViewsSearch(searchRepo),
objectSearch: search.NewObjectSearch(searchRepo),
searchParser: searchParser,
searchRepo: searchRepo,
}
}
// Search search contents
func (ss *SearchService) Search(ctx context.Context, dto *schema.SearchDTO) (resp []schema.SearchResp, total int64, extra interface{}, err error) {
extra = nil
if dto.Page < 1 {
dto.Page = 1
}
switch {
case ss.tagSearch.Parse(dto):
resp, total, err = ss.tagSearch.Search(ctx)
extra = ss.tagSearch.Extra
case ss.withinSearch.Parse(dto):
resp, total, err = ss.withinSearch.Search(ctx)
case ss.authorSearch.Parse(dto):
resp, total, err = ss.authorSearch.Search(ctx)
case ss.scoreSearch.Parse(dto):
resp, total, err = ss.scoreSearch.Search(ctx)
case ss.answersSearch.Parse(dto):
resp, total, err = ss.answersSearch.Search(ctx)
case ss.acceptedAnswerSearch.Parse(dto):
resp, total, err = ss.acceptedAnswerSearch.Search(ctx)
case ss.notAcceptedQuestion.Parse(dto):
resp, total, err = ss.notAcceptedQuestion.Search(ctx)
case ss.inQuestionSearch.Parse(dto):
resp, total, err = ss.inQuestionSearch.Search(ctx)
case ss.questionSearch.Parse(dto):
resp, total, err = ss.questionSearch.Search(ctx)
case ss.answerSearch.Parse(dto):
resp, total, err = ss.answerSearch.Search(ctx)
case ss.viewsSearch.Parse(dto):
resp, total, err = ss.viewsSearch.Search(ctx)
default:
ss.objectSearch.Parse(dto)
resp, total, err = ss.objectSearch.Search(ctx)
// search type
searchType,
// search all
userID,
votes,
// search questions
notAccepted,
_,
views,
answers,
// search answers
accepted,
questionID,
_,
// common fields
tags,
words := ss.searchParser.ParseStructure(dto)
switch searchType {
case "all":
resp, total, err = ss.searchRepo.SearchContents(ctx, words, tags, userID, votes, dto.Page, dto.Size, dto.Order)
if err != nil {
return nil, 0, nil, err
}
case "question":
resp, total, err = ss.searchRepo.SearchQuestions(ctx, words, notAccepted, views, answers, dto.Page, dto.Size, dto.Order)
case "answer":
resp, total, err = ss.searchRepo.SearchAnswers(ctx, words, tags, accepted, questionID, dto.Page, dto.Size, dto.Order)
}
return resp, total, extra, err
return
}
+109 -11
View File
@@ -11,47 +11,107 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type SiteInfoService struct {
siteInfoRepo siteinfo_common.SiteInfoRepo
emailService *export.EmailService
siteInfoRepo siteinfo_common.SiteInfoRepo
emailService *export.EmailService
tagCommonService *tagcommon.TagCommonService
}
func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService *export.EmailService) *SiteInfoService {
func NewSiteInfoService(
siteInfoRepo siteinfo_common.SiteInfoRepo,
emailService *export.EmailService,
tagCommonService *tagcommon.TagCommonService) *SiteInfoService {
return &SiteInfoService{
siteInfoRepo: siteInfoRepo,
emailService: emailService,
siteInfoRepo: siteInfoRepo,
emailService: emailService,
tagCommonService: tagCommonService,
}
}
// GetSiteGeneral get site info general
func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
resp = &schema.SiteGeneralResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
if err != nil {
return nil, err
log.Error(err)
return resp, nil
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
return resp, nil
}
resp = &schema.SiteGeneralResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteInterface get site info interface
func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
resp = &schema.SiteInterfaceResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
if err != nil {
log.Error(err)
return resp, nil
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteBranding get site info branding
func (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingReq, err error) {
resp = &schema.SiteBrandingReq{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeBranding)
if err != nil {
log.Error(err)
return resp, nil
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteWrite get site info write
func (s *SiteInfoService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeWrite)
if err != nil {
log.Error(err)
return resp, nil
}
if exist {
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
}
resp.RecommendTags, err = s.tagCommonService.GetSiteWriteRecommendTag(ctx)
if err != nil {
log.Error(err)
}
resp.ReservedTags, err = s.tagCommonService.GetSiteWriteReservedTag(ctx)
if err != nil {
log.Error(err)
}
return resp, nil
}
// GetSiteLegal get site legal info
func (s *SiteInfoService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
resp = &schema.SiteLegalResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeLegal)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
return resp, nil
}
resp = &schema.SiteInterfaceResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
@@ -109,6 +169,44 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site
return
}
// SaveSiteBranding save site branding information
func (s *SiteInfoService) SaveSiteBranding(ctx context.Context, req *schema.SiteBrandingReq) (err error) {
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeBranding,
Content: string(content),
Status: 1,
}
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeBranding, data)
}
// SaveSiteWrite save site configuration about write
func (s *SiteInfoService) SaveSiteWrite(ctx context.Context, req *schema.SiteWriteReq) (resp interface{}, err error) {
errData, err := s.tagCommonService.SetSiteWriteTag(ctx, req.RecommendTags, req.ReservedTags, req.UserID)
if err != nil {
return errData, err
}
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeWrite,
Content: string(content),
Status: 1,
}
return nil, s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeWrite, data)
}
// SaveSiteLegal save site legal configuration
func (s *SiteInfoService) SaveSiteLegal(ctx context.Context, req *schema.SiteLegalReq) (err error) {
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeLegal,
Content: string(content),
Status: 1,
}
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeLegal, data)
}
// GetSMTPConfig get smtp config
func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) (
resp *schema.GetSMTPConfigResp, err error,
@@ -5,9 +5,7 @@ import (
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/errors"
)
type SiteInfoCommonService struct {
@@ -22,29 +20,70 @@ func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService
// GetSiteGeneral get site info general
func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
resp = &schema.SiteGeneralResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
if err != nil {
return nil, err
return resp, err
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
return resp, nil
}
resp = &schema.SiteGeneralResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteInterface get site info interface
func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
resp = &schema.SiteInterfaceResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
if err != nil {
return resp, err
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteBranding get site info branding
func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {
resp = &schema.SiteBrandingResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeBranding)
if err != nil {
return resp, err
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteWrite get site info write
func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeWrite)
if err != nil {
return resp, err
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteLegal get site info write
func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
resp = &schema.SiteLegalResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeLegal)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
return resp, nil
}
resp = &schema.SiteInterfaceResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
+42 -33
View File
@@ -3,7 +3,10 @@ package tag
import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/internal/base/pager"
@@ -12,44 +15,44 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/permission"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type TagRepo interface {
RemoveTag(ctx context.Context, tagID string) (err error)
UpdateTag(ctx context.Context, tag *entity.Tag) (err error)
UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error)
GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error)
}
// TagService user service
type TagService struct {
tagRepo tagcommon.TagRepo
revisionService *revision_common.RevisionService
followCommon activity_common.FollowRepo
tagRepo TagRepo
tagCommonService *tag_common.TagCommonService
revisionService *revision_common.RevisionService
followCommon activity_common.FollowRepo
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewTagService new tag service
func NewTagService(
tagRepo tagcommon.TagRepo,
tagRepo TagRepo,
tagCommonService *tag_common.TagCommonService,
revisionService *revision_common.RevisionService,
followCommon activity_common.FollowRepo) *TagService {
followCommon activity_common.FollowRepo,
siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService {
return &TagService{
tagRepo: tagRepo,
revisionService: revisionService,
followCommon: followCommon,
tagRepo: tagRepo,
tagCommonService: tagCommonService,
revisionService: revisionService,
followCommon: followCommon,
siteInfoService: siteInfoService,
}
}
// SearchTagLike get tag list all
func (ts *TagService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []string, err error) {
tags, err := ts.tagRepo.GetTagListByName(ctx, req.Tag, 5)
if err != nil {
return
}
for _, tag := range tags {
resp = append(resp, tag.SlugName)
}
return resp, nil
}
// RemoveTag delete tag
func (ts *TagService) RemoveTag(ctx context.Context, tagID string) (err error) {
// TODO permission
@@ -71,7 +74,7 @@ func (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) (
return err
}
tagInfo, exist, err := ts.tagRepo.GetTagByID(ctx, req.TagID)
tagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)
if err != nil {
return err
}
@@ -116,9 +119,9 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
exist bool
)
if len(req.ID) > 0 {
tagInfo, exist, err = ts.tagRepo.GetTagByID(ctx, req.ID)
tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, req.ID)
} else {
tagInfo, exist, err = ts.tagRepo.GetTagBySlugName(ctx, req.Name)
tagInfo, exist, err = ts.tagCommonService.GetTagBySlugName(ctx, req.Name)
}
if err != nil {
return nil, err
@@ -130,7 +133,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
resp = &schema.GetTagResp{}
// if tag is synonyms get original tag info
if tagInfo.MainTagID > 0 {
tagInfo, exist, err = ts.tagRepo.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID))
tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID))
if err != nil {
return nil, err
}
@@ -148,8 +151,10 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
resp.ParsedText = tagInfo.ParsedText
resp.FollowCount = tagInfo.FollowCount
resp.QuestionCount = tagInfo.QuestionCount
resp.Recommend = tagInfo.Recommend
resp.Reserved = tagInfo.Reserved
resp.IsFollower = ts.checkTagIsFollow(ctx, req.UserID, tagInfo.ID)
resp.MemberActions = permission.GetTagPermission(req.UserID, req.UserID)
resp.MemberActions = permission.GetTagPermission(ctx, req.UserID, req.UserID)
resp.GetExcerpt()
return resp, nil
}
@@ -165,7 +170,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) (
if err != nil {
return nil, err
}
tagList, err := ts.tagRepo.GetTagListByIDs(ctx, objIDs)
tagList, err := ts.tagCommonService.GetTagListByIDs(ctx, objIDs)
if err != nil {
return nil, err
}
@@ -174,9 +179,11 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) (
TagID: t.ID,
SlugName: t.SlugName,
DisplayName: t.DisplayName,
Recommend: t.Recommend,
Reserved: t.Reserved,
}
if t.MainTagID > 0 {
mainTag, exist, err := ts.tagRepo.GetTagByID(ctx, converter.IntToString(t.MainTagID))
mainTag, exist, err := ts.tagCommonService.GetTagByID(ctx, converter.IntToString(t.MainTagID))
if err != nil {
return nil, err
}
@@ -192,7 +199,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) (
// GetTagSynonyms get tag synonyms
func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSynonymsReq) (
resp []*schema.GetTagSynonymsResp, err error) {
tag, exist, err := ts.tagRepo.GetTagByID(ctx, req.TagID)
tag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)
if err != nil {
return
}
@@ -241,7 +248,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
req.Format()
addSynonymTagList := make([]string, 0)
removeSynonymTagList := make([]string, 0)
mainTagInfo, exist, err := ts.tagRepo.GetTagByID(ctx, req.TagID)
mainTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID)
if err != nil {
return err
}
@@ -253,7 +260,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
for _, item := range req.SynonymTagList {
addSynonymTagList = append(addSynonymTagList, item.SlugName)
}
tagListInDB, err := ts.tagRepo.GetTagListByNames(ctx, addSynonymTagList)
tagListInDB, err := ts.tagCommonService.GetTagListByNames(ctx, addSynonymTagList)
if err != nil {
return err
}
@@ -278,7 +285,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
}
if len(needAddTagList) > 0 {
err = ts.tagRepo.AddTagList(ctx, needAddTagList)
err = ts.tagCommonService.AddTagList(ctx, needAddTagList)
if err != nil {
return err
}
@@ -336,7 +343,7 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
page := req.Page
pageSize := req.PageSize
tags, total, err := ts.tagRepo.GetTagPage(ctx, page, pageSize, tag, req.QueryCond)
tags, total, err := ts.tagCommonService.GetTagPage(ctx, page, pageSize, tag, req.QueryCond)
if err != nil {
return
}
@@ -355,6 +362,8 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID),
CreatedAt: tag.CreatedAt.Unix(),
UpdatedAt: tag.UpdatedAt.Unix(),
Recommend: tag.Recommend,
Reserved: tag.Reserved,
})
}
return pager.NewPageModel(total, resp), nil
+322 -26
View File
@@ -1,30 +1,34 @@
package tagcommon
package tag_common
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type TagRepo interface {
type TagCommonRepo interface {
AddTagList(ctx context.Context, tagList []*entity.Tag) (err error)
GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error)
GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error)
GetTagListByName(ctx context.Context, name string, limit int) (tagList []*entity.Tag, err error)
GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error)
GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error)
RemoveTag(ctx context.Context, tagID string) (err error)
UpdateTag(ctx context.Context, tag *entity.Tag) (err error)
UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error)
UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, mainTagSlugName string) (err error)
GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error)
GetTagList(ctx context.Context, tag *entity.Tag) (tagList []*entity.Tag, err error)
GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (tagList []*entity.Tag, total int64, err error)
GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error)
GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error)
UpdateTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error)
UpdateTagQuestionCount(ctx context.Context, tagID string, questionCount int) (err error)
}
type TagRelRepo interface {
@@ -40,39 +44,209 @@ type TagRelRepo interface {
// TagCommonService user service
type TagCommonService struct {
revisionService *revision_common.RevisionService
tagRepo TagRepo
tagCommonRepo TagCommonRepo
tagRelRepo TagRelRepo
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewTagCommonService new tag service
func NewTagCommonService(tagRepo TagRepo, tagRelRepo TagRelRepo,
func NewTagCommonService(tagCommonRepo TagCommonRepo, tagRelRepo TagRelRepo,
revisionService *revision_common.RevisionService,
siteInfoService *siteinfo_common.SiteInfoCommonService,
) *TagCommonService {
return &TagCommonService{
tagRepo: tagRepo,
tagCommonRepo: tagCommonRepo,
tagRelRepo: tagRelRepo,
revisionService: revisionService,
siteInfoService: siteInfoService,
}
}
// GetTagListByName
func (ts *TagCommonService) GetTagListByName(ctx context.Context, tagName string) (tagInfo *entity.Tag, exist bool, err error) {
tagName = strings.ToLower(tagName)
return ts.tagRepo.GetTagBySlugName(ctx, tagName)
// SearchTagLike get tag list all
func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []schema.SearchTagLikeResp, err error) {
tags, err := ts.tagCommonRepo.GetTagListByName(ctx, req.Tag, 5, req.IsAdmin)
if err != nil {
return
}
ts.tagsFormatRecommendAndReserved(ctx, tags)
for _, tag := range tags {
item := schema.SearchTagLikeResp{}
item.SlugName = tag.SlugName
item.Recommend = tag.Recommend
item.Reserved = tag.Reserved
resp = append(resp, item)
}
return resp, nil
}
func (ts *TagCommonService) GetSiteWriteRecommendTag(ctx context.Context) (tags []string, err error) {
tags = make([]string, 0)
list, err := ts.tagCommonRepo.GetRecommendTagList(ctx)
if err != nil {
return tags, err
}
for _, item := range list {
tags = append(tags, item.SlugName)
}
return tags, nil
}
func (ts *TagCommonService) SetSiteWriteTag(ctx context.Context, recommendTags, reservedTags []string, userID string) (
errFields []*validator.FormErrorField, err error) {
recommendErr := ts.CheckTag(ctx, recommendTags, userID)
reservedErr := ts.CheckTag(ctx, reservedTags, userID)
if recommendErr != nil {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "recommend_tags",
ErrorMsg: recommendErr.Error(),
})
err = recommendErr
}
if reservedErr != nil {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "reserved_tags",
ErrorMsg: reservedErr.Error(),
})
err = reservedErr
}
if len(errFields) > 0 {
return errFields, err
}
err = ts.SetTagsAttribute(ctx, recommendTags, "recommend")
if err != nil {
return nil, err
}
err = ts.SetTagsAttribute(ctx, reservedTags, "reserved")
if err != nil {
return nil, err
}
return nil, nil
}
func (ts *TagCommonService) GetSiteWriteReservedTag(ctx context.Context) (tags []string, err error) {
tags = make([]string, 0)
list, err := ts.tagCommonRepo.GetReservedTagList(ctx)
if err != nil {
return tags, err
}
for _, item := range list {
tags = append(tags, item.SlugName)
}
return tags, nil
}
// SetTagsAttribute
func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, attribute string) (err error) {
var tagslist []string
switch attribute {
case "recommend":
tagslist, err = ts.GetSiteWriteRecommendTag(ctx)
case "reserved":
tagslist, err = ts.GetSiteWriteReservedTag(ctx)
default:
return
}
err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tagslist, attribute, false)
if err != nil {
return err
}
err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tags, attribute, true)
if err != nil {
return err
}
return nil
}
func (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []string) ([]*entity.Tag, error) {
for k, tagname := range tagNames {
tagNames[k] = strings.ToLower(tagname)
}
return ts.tagRepo.GetTagListByNames(ctx, tagNames)
tagList, err := ts.tagCommonRepo.GetTagListByNames(ctx, tagNames)
if err != nil {
return nil, err
}
ts.tagsFormatRecommendAndReserved(ctx, tagList)
return tagList, nil
}
//
func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.TagItem) (bool, error) {
taginfo, err := ts.siteInfoService.GetSiteWrite(ctx)
if err != nil {
return false, err
}
if !taginfo.RequiredTag {
return true, nil
}
tagNames := make([]string, 0)
for _, item := range tags {
tagNames = append(tagNames, item.SlugName)
}
list, err := ts.GetTagListByNames(ctx, tagNames)
if err != nil {
return false, err
}
for _, item := range list {
if item.Recommend {
return true, nil
}
}
return false, nil
}
// GetObjectTag get object tag
func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (objTags []*schema.TagResp, err error) {
objTags = make([]*schema.TagResp, 0)
tagsInfoList, err := ts.GetObjectEntityTag(ctx, objectId)
return ts.TagFormat(ctx, tagsInfoList)
}
// AddTagList get object tag
func (ts *TagCommonService) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) {
return ts.tagCommonRepo.AddTagList(ctx, tagList)
}
// GetTagByID get object tag
func (ts *TagCommonService) GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error) {
tag, exist, err = ts.tagCommonRepo.GetTagByID(ctx, tagID)
if !exist {
return
}
ts.tagFormatRecommendAndReserved(ctx, tag)
return
}
// GetTagBySlugName get object tag
func (ts *TagCommonService) GetTagBySlugName(ctx context.Context, slugName string) (tag *entity.Tag, exist bool, err error) {
tag, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, slugName)
if !exist {
return
}
ts.tagFormatRecommendAndReserved(ctx, tag)
return
}
// GetTagListByIDs get object tag
func (ts *TagCommonService) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) {
tagList, err = ts.tagCommonRepo.GetTagListByIDs(ctx, ids)
if err != nil {
return nil, err
}
ts.tagsFormatRecommendAndReserved(ctx, tagList)
return
}
// GetTagPage get object tag
func (ts *TagCommonService) GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (
tagList []*entity.Tag, total int64, err error) {
tagList, total, err = ts.tagCommonRepo.GetTagPage(ctx, page, pageSize, tag, queryCond)
if err != nil {
return nil, 0, err
}
ts.tagsFormatRecommendAndReserved(ctx, tagList)
return
}
func (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId string) (objTags []*entity.Tag, err error) {
tagIDList := make([]string, 0)
tagList, err := ts.tagRelRepo.GetObjectTagRelList(ctx, objectId)
if err != nil {
@@ -81,20 +255,57 @@ func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (
for _, tag := range tagList {
tagIDList = append(tagIDList, tag.TagID)
}
tagsInfoList, err := ts.tagRepo.GetTagListByIDs(ctx, tagIDList)
objTags, err = ts.GetTagListByIDs(ctx, tagIDList)
if err != nil {
return nil, err
}
for _, tagInfo := range tagsInfoList {
return objTags, nil
}
func (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) (objTags []*schema.TagResp, err error) {
objTags = make([]*schema.TagResp, 0)
for _, tagInfo := range tags {
objTags = append(objTags, &schema.TagResp{
SlugName: tagInfo.SlugName,
DisplayName: tagInfo.DisplayName,
MainTagSlugName: tagInfo.MainTagSlugName,
Recommend: tagInfo.Recommend,
Reserved: tagInfo.Reserved,
})
}
return objTags, nil
}
func (ts *TagCommonService) tagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) {
if len(tagList) == 0 {
return
}
tagConfig, err := ts.siteInfoService.GetSiteWrite(ctx)
if err != nil {
log.Error(err)
return
}
if !tagConfig.RequiredTag {
for _, tag := range tagList {
tag.Recommend = false
}
}
}
func (ts *TagCommonService) tagFormatRecommendAndReserved(ctx context.Context, tag *entity.Tag) {
if tag == nil {
return
}
tagConfig, err := ts.siteInfoService.GetSiteWrite(ctx)
if err != nil {
log.Error(err)
return
}
if !tagConfig.RequiredTag {
tag.Recommend = false
}
}
// BatchGetObjectTag batch get object tag
func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []string) (map[string][]*schema.TagResp, error) {
objectIDTagMap := make(map[string][]*schema.TagResp)
@@ -108,7 +319,7 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s
for _, tag := range tagList {
tagIDList = append(tagIDList, tag.TagID)
}
tagsInfoList, err := ts.tagRepo.GetTagListByIDs(ctx, tagIDList)
tagsInfoList, err := ts.GetTagListByIDs(ctx, tagIDList)
if err != nil {
return objectIDTagMap, err
}
@@ -123,13 +334,98 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s
SlugName: tagInfo.SlugName,
DisplayName: tagInfo.DisplayName,
MainTagSlugName: tagInfo.MainTagSlugName,
Recommend: tagInfo.Recommend,
Reserved: tagInfo.Reserved,
}
objectIDTagMap[item.ObjectID] = append(objectIDTagMap[item.ObjectID], t)
}
}
for _, taglist := range objectIDTagMap {
sort.SliceStable(taglist, func(i, j int) bool {
return taglist[i].Reserved
})
sort.SliceStable(taglist, func(i, j int) bool {
return taglist[i].Recommend
})
}
return objectIDTagMap, nil
}
func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID string) (err error) {
if len(tags) == 0 {
return nil
}
// find tags name
tagListInDb, err := ts.GetTagListByNames(ctx, tags)
if err != nil {
return err
}
tagInDbMapping := make(map[string]*entity.Tag)
checktags := make([]string, 0)
for _, tag := range tagListInDb {
if tag.MainTagID != 0 {
checktags = append(checktags, fmt.Sprintf("\"%s\"", tag.SlugName))
}
tagInDbMapping[tag.SlugName] = tag
}
if len(checktags) > 0 {
err = errors.BadRequest(reason.TagNotContainSynonym).WithMsg(fmt.Sprintf("Should not contain synonym tags %s", strings.Join(checktags, ",")))
return err
}
addTagList := make([]*entity.Tag, 0)
addTagMsgList := make([]string, 0)
for _, tag := range tags {
_, ok := tagInDbMapping[tag]
if ok {
continue
}
item := &entity.Tag{}
item.SlugName = tag
item.DisplayName = tag
item.OriginalText = ""
item.ParsedText = ""
item.Status = entity.TagStatusAvailable
addTagList = append(addTagList, item)
addTagMsgList = append(addTagMsgList, tag)
}
if len(addTagList) > 0 {
err = errors.BadRequest(reason.TagNotFound).WithMsg(fmt.Sprintf("tag [%s] does not exist",
strings.Join(addTagMsgList, ",")))
return err
}
return nil
}
func (ts *TagCommonService) ObjectCheckChangeTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
reservedTagsMap := make(map[string]bool)
needTagsMap := make([]string, 0)
for _, tag := range objectTagData {
if tag.Reserved {
reservedTagsMap[tag.SlugName] = true
}
}
for _, tag := range oldobjectTagData {
if tag.Reserved {
_, ok := reservedTagsMap[tag.SlugName]
if !ok {
needTagsMap = append(needTagsMap, tag.SlugName)
}
}
}
if len(needTagsMap) > 0 {
return false, needTagsMap
}
return true, []string{}
}
// ObjectChangeTag change object tag list
func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *schema.TagChange) (err error) {
if len(objectTagData.Tags) == 0 {
@@ -144,7 +440,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
}
// find tags name
tagListInDb, err := ts.tagRepo.GetTagListByNames(ctx, thisObjTagNameList)
tagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisObjTagNameList)
if err != nil {
return err
}
@@ -171,7 +467,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
}
if len(addTagList) > 0 {
err = ts.tagRepo.AddTagList(ctx, addTagList)
err = ts.tagCommonRepo.AddTagList(ctx, addTagList)
if err != nil {
return err
}
@@ -205,7 +501,7 @@ func (ts *TagCommonService) RefreshTagQuestionCount(ctx context.Context, tagIDs
if err != nil {
return err
}
err = ts.tagRepo.UpdateTagQuestionCount(ctx, tagID, int(count))
err = ts.tagCommonRepo.UpdateTagQuestionCount(ctx, tagID, int(count))
if err != nil {
return err
}
+79 -23
View File
@@ -4,12 +4,14 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
@@ -24,6 +26,25 @@ const (
avatarSubPath = "avatar"
avatarThumbSubPath = "avatar_thumb"
postSubPath = "post"
brandingSubPath = "branding"
)
var (
subPathList = []string{
avatarSubPath,
avatarThumbSubPath,
postSubPath,
brandingSubPath,
}
FormatExts = map[string]imaging.Format{
".jpg": imaging.JPEG,
".jpeg": imaging.JPEG,
".png": imaging.PNG,
".gif": imaging.GIF,
".tif": imaging.TIFF,
".tiff": imaging.TIFF,
".bmp": imaging.BMP,
}
)
// UploaderService user service
@@ -35,13 +56,11 @@ type UploaderService struct {
// NewUploaderService new upload service
func NewUploaderService(serviceConfig *service_config.ServiceConfig,
siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath))
if err != nil {
panic(err)
}
err = dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, postSubPath))
if err != nil {
panic(err)
for _, subPath := range subPathList {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath))
if err != nil {
panic(err)
}
}
return &UploaderService{
serviceConfig: serviceConfig,
@@ -49,23 +68,26 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig,
}
}
func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) (
url string, err error) {
// UploadAvatarFile upload avatar file
func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(avatarSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath)
}
var FormatExts = map[string]imaging.Format{
".jpg": imaging.JPEG,
".jpeg": imaging.JPEG,
".png": imaging.PNG,
".gif": imaging.GIF,
".tif": imaging.TIFF,
".tiff": imaging.TIFF,
".bmp": imaging.BMP,
}
func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (
avatarfile []byte, err error) {
if size > 1024 {
@@ -73,12 +95,12 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
}
thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName)
thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName)
avatarfile, err = ioutil.ReadFile(thumbfilePath)
avatarfile, err = os.ReadFile(thumbfilePath)
if err == nil {
return avatarfile, nil
}
filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName)
avatarfile, err = ioutil.ReadFile(filePath)
avatarfile, err = os.ReadFile(filePath)
if err != nil {
return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
@@ -117,13 +139,47 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
return buf.Bytes(), nil
}
func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) (
func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
url string, err error) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(postSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
url string, err error) {
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
fileExt := strings.ToLower(path.Ext(file.Filename))
_, ok := FormatExts[fileExt]
if !ok && fileExt != ".ico" {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), nil)
return
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(brandingSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
url string, err error) {
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
+12 -4
View File
@@ -12,6 +12,7 @@ import (
"github.com/Chain-Zhang/pinyin"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
@@ -112,6 +113,7 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
IsAdmin: userInfo.IsAdmin,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
@@ -322,6 +324,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
IsAdmin: userInfo.IsAdmin,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
@@ -408,11 +411,16 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
IsAdmin: userInfo.IsAdmin,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
return nil, err
}
// User verified email will update user email status. So user status cache should be updated.
if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {
return nil, err
}
resp.IsAdmin = userInfo.IsAdmin
if resp.IsAdmin {
err = us.authService.SetCmsUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
@@ -478,7 +486,7 @@ func (us *UserService) encryptPassword(ctx context.Context, Pass string) (string
// UserChangeEmailSendCode user change email verification
func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) (
resp *schema.UserVerifyEmailErrorResponse, err error) {
resp *validator.FormErrorField, err error) {
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return nil, err
@@ -492,9 +500,9 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
return nil, err
}
if exist {
resp = &schema.UserVerifyEmailErrorResponse{
Key: "e_mail",
Value: reason.EmailDuplicate,
resp = &validator.FormErrorField{
ErrorField: "e_mail",
ErrorMsg: reason.EmailDuplicate,
}
return resp, errors.BadRequest(reason.EmailDuplicate)
}
+1 -3
View File
@@ -23,9 +23,8 @@ module.exports = {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
plugins: ['react', '@typescript-eslint', 'prettier'],
plugins: ['react', '@typescript-eslint'],
rules: {
'prettier/prettier': 'error',
'no-unused-vars': 'off',
'no-console': 'off',
'import/prefer-default-export': 'off',
@@ -51,7 +50,6 @@ module.exports = {
'react-hooks/exhaustive-deps': 'off',
'react/jsx-props-no-spreading': 'off',
'@typescript-eslint/default-param-last': 'off',
'no-nested-ternary': 'off',
'import/order': [
'error',
{
+1 -1
View File
@@ -1,5 +1,5 @@
{
"*.{js,jsx,ts,tsx}": ["eslint --cache --fix"],
"*.{js,jsx,scss,md,json}": ["prettier --write"],
"*.{js,jsx,less,md,json}": ["prettier --write"],
"*.ts?(x)": ["prettier --parser=typescript --write"]
}
+1 -1
View File
@@ -1,3 +1,3 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
extends: ['@commitlint/routes-conventional'],
};
+2 -4
View File
@@ -9,8 +9,7 @@
"lint": "eslint . --cache --fix --ext .ts,.tsx",
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
"prepare": "cd .. && husky install",
"preinstall": "node ./scripts/preinstall.js",
"pre-commit": "lint-staged"
"preinstall": "node ./scripts/preinstall.js"
},
"dependencies": {
"axios": "^0.27.2",
@@ -20,7 +19,6 @@
"codemirror": "5.65.0",
"copy-to-clipboard": "^3.3.2",
"dayjs": "^1.11.5",
"diff": "^5.1.0",
"i18next": "^21.9.0",
"katex": "^0.16.2",
"lodash": "^4.17.21",
@@ -35,12 +33,12 @@
"react-helmet-async": "^1.3.0",
"react-i18next": "^11.18.3",
"react-router-dom": "^6.4.0",
"semver": "^7.3.8",
"swr": "^1.3.0",
"zustand": "^4.1.1"
},
"devDependencies": {
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.2.0",
"@fullhuman/postcss-purgecss": "^4.1.3",
"@testing-library/dom": "^8.17.1",
"@testing-library/jest-dom": "^4.2.4",
+53 -37
View File
@@ -2,7 +2,6 @@ lockfileVersion: 5.4
specifiers:
'@commitlint/cli': ^17.0.3
'@commitlint/config-conventional': ^17.2.0
'@fullhuman/postcss-purgecss': ^4.1.3
'@testing-library/dom': ^8.17.1
'@testing-library/jest-dom': ^4.2.4
@@ -26,7 +25,6 @@ specifiers:
copy-to-clipboard: ^3.3.2
customize-cra: ^1.0.0
dayjs: ^1.11.5
diff: ^5.1.0
eslint: ^8.0.1
eslint-config-airbnb: ^19.0.4
eslint-config-airbnb-typescript: ^17.0.0
@@ -61,6 +59,7 @@ specifiers:
react-router-dom: ^6.4.0
react-scripts: 5.0.1
sass: ^1.54.4
semver: ^7.3.8
swr: ^1.3.0
typescript: ^4.8.3
yaml-loader: ^0.8.0
@@ -74,7 +73,6 @@ dependencies:
codemirror: 5.65.0
copy-to-clipboard: 3.3.2
dayjs: 1.11.5
diff: 5.1.0
i18next: 21.9.2
katex: 0.16.2
lodash: 4.17.21
@@ -89,12 +87,12 @@ dependencies:
react-helmet-async: 1.3.0_biqbaboplfbrettd7655fr4n2y
react-i18next: 11.18.6_ulhmqqxshznzmtuvahdi5nasbq
react-router-dom: 6.4.0_biqbaboplfbrettd7655fr4n2y
semver: 7.3.8
swr: 1.3.0_react@18.2.0
zustand: 4.1.1_react@18.2.0
devDependencies:
'@commitlint/cli': 17.1.2
'@commitlint/config-conventional': 17.2.0
'@fullhuman/postcss-purgecss': 4.1.3_postcss@8.4.16
'@testing-library/dom': 8.18.1
'@testing-library/jest-dom': 4.2.4
@@ -1423,13 +1421,6 @@ packages:
- '@swc/wasm'
dev: true
/@commitlint/config-conventional/17.2.0:
resolution: {integrity: sha512-g5hQqRa80f++SYS233dbDSg16YdyounMTAhVcmqtInNeY/GF3aA4st9SVtJxpeGrGmueMrU4L+BBb+6Vs5wrcg==}
engines: {node: '>=v14'}
dependencies:
conventional-changelog-conventionalcommits: 5.0.0
dev: true
/@commitlint/config-validator/17.1.0:
resolution: {integrity: sha512-Q1rRRSU09ngrTgeTXHq6ePJs2KrI+axPTgkNYDWSJIuS1Op4w3J30vUfSXjwn5YEJHklK3fSqWNHmBhmTR7Vdg==}
engines: {node: '>=v14'}
@@ -1491,7 +1482,7 @@ packages:
cosmiconfig-typescript-loader: 4.1.0_3owiowz3ujipd4k6pbqn3n7oui
lodash: 4.17.21
resolve-from: 5.0.0
ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra
ts-node: 10.9.1_ck2axrxkiif44rdbzjywaqjysa
typescript: 4.8.3
transitivePeerDependencies:
- '@swc/core'
@@ -2678,7 +2669,7 @@ packages:
eslint: 8.23.1
ignore: 5.2.0
regexpp: 3.2.0
semver: 7.3.7
semver: 7.3.8
tsutils: 3.21.0_typescript@4.8.3
typescript: 4.8.3
transitivePeerDependencies:
@@ -2759,7 +2750,7 @@ packages:
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
semver: 7.3.7
semver: 7.3.8
tsutils: 3.21.0_typescript@4.8.3
typescript: 4.8.3
transitivePeerDependencies:
@@ -3483,7 +3474,7 @@ packages:
/builtins/5.0.1:
resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==}
dependencies:
semver: 7.3.7
semver: 7.3.8
dev: true
/bytes/3.0.0:
@@ -3774,15 +3765,6 @@ packages:
q: 1.5.1
dev: true
/conventional-changelog-conventionalcommits/5.0.0:
resolution: {integrity: sha512-lCDbA+ZqVFQGUj7h9QBKoIpLhl8iihkO0nCTyRNzuXtcd7ubODpYB04IFy31JloiJgG0Uovu8ot8oxRzn7Nwtw==}
engines: {node: '>=10'}
dependencies:
compare-func: 2.0.0
lodash: 4.17.21
q: 1.5.1
dev: true
/conventional-commits-parser/3.2.4:
resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==}
engines: {node: '>=10'}
@@ -3841,7 +3823,7 @@ packages:
dependencies:
'@types/node': 14.18.29
cosmiconfig: 7.0.1
ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra
ts-node: 10.9.1_ck2axrxkiif44rdbzjywaqjysa
typescript: 4.8.3
dev: true
@@ -3925,7 +3907,7 @@ packages:
postcss-modules-scope: 3.0.0_postcss@8.4.16
postcss-modules-values: 4.0.0_postcss@8.4.16
postcss-value-parser: 4.2.0
semver: 7.3.7
semver: 7.3.8
webpack: 5.74.0
/css-minimizer-webpack-plugin/3.4.1_webpack@5.74.0:
@@ -4762,11 +4744,6 @@ packages:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
/diff/5.1.0:
resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
engines: {node: '>=0.3.1'}
dev: false
/dir-glob/3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@@ -5297,7 +5274,7 @@ packages:
is-core-module: 2.10.0
minimatch: 3.1.2
resolve: 1.22.1
semver: 7.3.7
semver: 7.3.8
dev: true
/eslint-plugin-prettier/4.2.1_cabrci5exjdaojcvd6xoxgeowu:
@@ -5761,7 +5738,7 @@ packages:
memfs: 3.4.7
minimatch: 3.1.2
schema-utils: 2.7.0
semver: 7.3.7
semver: 7.3.8
tapable: 1.1.3
typescript: 4.8.3
webpack: 5.74.0
@@ -6933,7 +6910,7 @@ packages:
jest-util: 27.5.1
natural-compare: 1.4.0
pretty-format: 27.5.1
semver: 7.3.7
semver: 7.3.8
transitivePeerDependencies:
- supports-color
@@ -7640,7 +7617,7 @@ packages:
dependencies:
hosted-git-info: 4.1.0
is-core-module: 2.10.0
semver: 7.3.7
semver: 7.3.8
validate-npm-package-license: 3.0.4
dev: true
@@ -8244,7 +8221,7 @@ packages:
cosmiconfig: 7.0.1
klona: 2.0.5
postcss: 8.4.16
semver: 7.3.7
semver: 7.3.8
webpack: 5.74.0
/postcss-logical/5.0.4_postcss@8.4.16:
@@ -9069,7 +9046,7 @@ packages:
resolve: 1.22.1
resolve-url-loader: 4.0.0
sass-loader: 12.6.0_sass@1.54.9+webpack@5.74.0
semver: 7.3.7
semver: 7.3.8
source-map-loader: 3.0.1_webpack@5.74.0
style-loader: 3.3.1_webpack@5.74.0
tailwindcss: 3.1.8_57znarxsqwmnneadci5z5fd5gu
@@ -9516,6 +9493,14 @@ packages:
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: true
/semver/7.3.8:
resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
/send/0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
@@ -10211,6 +10196,37 @@ packages:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
/ts-node/10.9.1_ck2axrxkiif44rdbzjywaqjysa:
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.9
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.3
'@types/node': 14.18.29
acorn: 8.8.0
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.8.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/tsconfig-paths/3.14.1:
resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==}
dependencies:
-9
View File
@@ -557,12 +557,3 @@ export const TIMEZONES = [
},
];
export const DEFAULT_TIMEZONE = 'UTC+0';
export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
'undeleted',
'deleted',
'downvote',
'upvote',
'reopened',
'closed',
];
+1 -66
View File
@@ -124,7 +124,6 @@ export interface UserInfoRes extends UserInfoBase {
*/
mail_status: number;
language: string;
is_admin: boolean;
e_mail?: string;
[prop: string]: any;
}
@@ -157,13 +156,6 @@ export interface SetNoticeReq {
notice_switch: boolean;
}
export interface NotificationStatus {
inbox: number;
achievement: number;
revision: number;
can_revision: boolean;
}
export interface QuestionDetailRes {
id: string;
title: string;
@@ -199,6 +191,7 @@ export interface AnswerItem {
create_time: string;
update_time: string;
user_info: UserInfoBase;
[prop: string]: any;
}
@@ -380,61 +373,3 @@ export interface AdminDashboard {
};
};
}
export interface TimelineReq {
show_vote: boolean;
object_id: string;
}
export interface TimelineItem {
activity_id: number;
revision_id: number;
created_at: number;
activity_type: string;
username: string;
user_display_name: string;
comment: string;
object_id: string;
object_type: string;
cancelled: boolean;
cancelled_at: any;
}
export interface TimelineObject {
title: string;
object_type: string;
question_id: string;
answer_id: string;
main_tag_slug_name?: string;
}
export interface TimelineRes {
object_info: TimelineObject;
timeline: TimelineItem[];
}
export interface ReviewItem {
type: 'question' | 'answer' | 'tag';
info: {
object_id: string;
title: string;
content: string;
html: string;
tags: Tag[];
};
unreviewed_info: {
id: string;
use_id: string;
object_id: string;
title: string;
status: 0 | 1;
create_at: number;
user_info: UserInfoBase;
reason: string;
content: Tag | QuestionDetailRes | AnswerItem;
};
}
export interface ReviewResp {
count: number;
list: ReviewItem[];
}
+3 -7
View File
@@ -8,7 +8,6 @@ interface Props {
data: any;
showAvatar?: boolean;
avatarSize?: string;
showReputation?: boolean;
avatarSearchStr?: string;
className?: string;
}
@@ -19,7 +18,6 @@ const Index: FC<Props> = ({
avatarSize = '20px',
className = 'fs-14',
avatarSearchStr = 's=48',
showReputation = true,
}) => {
return (
<div className={`text-secondary ${className}`}>
@@ -49,11 +47,9 @@ const Index: FC<Props> = ({
</>
)}
{showReputation && (
<span className="fw-bold" title="Reputation">
{formatCount(data?.rank)}
</span>
)}
<span className="fw-bold" title="Reputation">
{formatCount(data?.rank)}
</span>
</div>
);
};
-111
View File
@@ -1,111 +0,0 @@
import { FC, memo } from 'react';
import { Tag } from '@/components';
import { diffText } from '@/utils';
interface Props {
objectType: string | 'question' | 'answer' | 'tag';
newData: Record<string, any>;
oldData?: Record<string, any>;
className?: string;
opts?: Partial<{
showTitle: boolean;
showTagUrlSlug: boolean;
}>;
}
const Index: FC<Props> = ({
objectType,
newData,
oldData,
className = '',
opts = {
showTitle: true,
showTagUrlSlug: true,
},
}) => {
if (!newData?.original_text) return null;
let tag = newData.tags;
if (objectType === 'question' && oldData?.tags) {
const addTags = newData.tags.filter(
(c) => !oldData?.tags?.find((p) => p.slug_name === c.slug_name),
);
let deleteTags = oldData?.tags
.filter((c) => !newData?.tags.find((p) => p.slug_name === c.slug_name))
.map((v) => ({ ...v, state: 'delete' }));
deleteTags = deleteTags?.map((v) => {
const index = oldData?.tags?.findIndex(
(c) => c.slug_name === v.slug_name,
);
return {
...v,
pre_index: index,
};
});
tag = newData.tags.map((item) => {
const find = addTags.find((c) => c.slug_name === item.slug_name);
if (find) {
return {
...find,
state: 'add',
};
}
return item;
});
deleteTags.forEach((v) => {
tag.splice(v.pre_index, 0, v);
});
}
return (
<div className={className}>
{objectType !== 'answer' && opts?.showTitle && (
<h5
dangerouslySetInnerHTML={{
__html: diffText(newData.title, oldData?.title),
}}
className="mb-3"
/>
)}
{objectType === 'question' && (
<div className="mb-4">
{tag.map((item) => {
return (
<Tag
key={item.slug_name}
className="me-1"
data={item}
textClassName={`d-inline-block review-text-${item.state}`}
/>
);
})}
</div>
)}
{objectType === 'tag' && opts?.showTagUrlSlug && (
<div className="mb-4">
{`/tags/${
newData?.main_tag_slug_name
? diffText(
newData.main_tag_slug_name,
oldData?.main_tag_slug_name,
)
: diffText(newData.slug_name, oldData?.slug_name)
}`}
</div>
)}
<div
dangerouslySetInnerHTML={{
__html: diffText(newData.original_text, oldData?.original_text),
}}
className="pre-line text-break font-monospace"
/>
</div>
);
};
export default memo(Index);
+6 -8
View File
@@ -1,15 +1,13 @@
import { FC, memo, ReactNode } from 'react';
import { FC, memo } from 'react';
import { Trans } from 'react-i18next';
const Index: FC<{ children?: ReactNode }> = ({ children }) => {
const Index: FC = () => {
return (
<div className="text-center py-5">
{children || (
<Trans i18nKey="personal.list_empty">
We couldn't find anything. <br /> Try different or less specific
keywords.
</Trans>
)}
<Trans i18nKey="personal.list_empty">
We couldn't find anything. <br /> Try different or less specific
keywords.
</Trans>
</div>
);
};
+1 -4
View File
@@ -9,10 +9,7 @@ const Index = () => {
<p className="text-center mb-0 fs-14 text-secondary">
<Trans i18nKey="footer.build_on">
Built on
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a href="https://answer.dev/" target="_blank">
Answer
</a>
<a href="/"> Answer </a>
- the open-source software that powers Q&A communities.
<br />
Made with love. © 2022 Answer .
@@ -3,11 +3,10 @@ import { Nav, Dropdown } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link, NavLink } from 'react-router-dom';
import type * as Type from '@/common/interface';
import { Avatar, Icon } from '@/components';
interface Props {
redDot: Type.NotificationStatus | undefined;
redDot;
userInfo;
logOut: () => void;
}
@@ -57,16 +56,6 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
{userInfo?.is_admin ? (
<Dropdown.Item href="/admin">{t('header.nav.admin')}</Dropdown.Item>
) : null}
{redDot?.can_revision ? (
<Dropdown.Item href="/review" className="position-relative">
{t('header.nav.review')}
{redDot?.revision > 0 && (
<span className="position-absolute top-50 translate-middle-y end-0 me-3 p-2 bg-danger border border-light rounded-circle">
<span className="visually-hidden">New Review</span>
</span>
)}
</Dropdown.Item>
) : null}
<Dropdown.Divider />
<Dropdown.Item onClick={logOut}>
{t('header.nav.logout')}
+3
View File
@@ -122,6 +122,9 @@ const Header: FC = () => {
<NavLink className="nav-link" to="/tags">
{t('header.nav.tag')}
</NavLink>
<NavLink className="nav-link d-none" to="/users">
{t('header.nav.user')}
</NavLink>
</Nav>
</Col>
<hr className="hr lg-none mt-2" />
+1 -11
View File
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
import { Modal } from '@/components';
import { useReportModal, useToast } from '@/hooks';
import Share from '../Share';
import { deleteQuestion, deleteAnswer, editCheck } from '@/services';
import { deleteQuestion, deleteAnswer } from '@/services';
import { tryNormalLogged } from '@/utils/guard';
interface IProps {
@@ -96,15 +96,6 @@ const Index: FC<IProps> = ({
});
}
};
const handleEdit = async (evt) => {
let checkObjectId = qid;
if (type === 'answer') {
checkObjectId = aid;
}
editCheck(checkObjectId).catch(() => {
evt.preventDefault();
});
};
const handleAction = (action) => {
if (!tryNormalLogged(true)) {
@@ -133,7 +124,6 @@ const Index: FC<IProps> = ({
key={item.action}
to={editUrl}
className="link-secondary p-0 fs-14 me-3"
onClick={handleEdit}
style={{ lineHeight: '23px' }}>
{item.name}
</Link>
+26 -11
View File
@@ -14,6 +14,7 @@ export interface JSONSchema {
[key: string]: {
type: 'string' | 'boolean';
title: string;
label?: string;
description?: string;
enum?: Array<string | boolean>;
enumNames?: string[];
@@ -108,6 +109,17 @@ const SchemaForm: FC<IProps> = ({
}
};
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = e.target;
const data = {
...formData,
[name]: { ...formData[name], value, isInvalid: false },
};
if (onChange instanceof Function) {
onChange(data);
}
};
const handleSwitchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = e.target;
const data = {
@@ -228,7 +240,7 @@ const SchemaForm: FC<IProps> = ({
return (
<Form noValidate onSubmit={handleSubmit}>
{keys.map((key) => {
const { title, description } = properties[key];
const { title, description, label } = properties[key];
const { 'ui:widget': widget = 'input', 'ui:options': options = {} } =
uiSchema[key] || {};
if (widget === 'select') {
@@ -237,6 +249,9 @@ const SchemaForm: FC<IProps> = ({
<Form.Label>{title}</Form.Label>
<Form.Select
aria-label={description}
name={key}
value={formData[key]?.value}
onChange={handleSelectChange}
isInvalid={formData[key].isInvalid}>
{properties[key].enum?.map((item, index) => {
return (
@@ -249,7 +264,7 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text className="text-muted">{description}</Form.Text>
<Form.Text>{description}</Form.Text>
</Form.Group>
);
}
@@ -279,13 +294,12 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text className="text-muted">{description}</Form.Text>
<Form.Text>{description}</Form.Text>
</Form.Group>
);
}
if (widget === 'switch') {
console.log(formData[key]?.value, 'switch=====');
return (
<Form.Group key={title} className="mb-3" controlId={key}>
<Form.Label>{title}</Form.Label>
@@ -294,7 +308,7 @@ const SchemaForm: FC<IProps> = ({
id={title}
name={key}
type="switch"
label={title}
label={label}
checked={formData[key]?.value}
feedback={formData[key]?.errorMsg}
feedbackType="invalid"
@@ -304,7 +318,7 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text className="text-muted">{description}</Form.Text>
<Form.Text>{description}</Form.Text>
</Form.Group>
);
}
@@ -314,7 +328,8 @@ const SchemaForm: FC<IProps> = ({
<Form.Label>{title}</Form.Label>
<TimeZonePicker
value={formData[key]?.value}
onChange={handleInputChange}
name={key}
onChange={handleSelectChange}
/>
<Form.Control
name={key}
@@ -324,7 +339,7 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text className="text-muted">{description}</Form.Text>
<Form.Text>{description}</Form.Text>
</Form.Group>
);
}
@@ -347,7 +362,7 @@ const SchemaForm: FC<IProps> = ({
<Form.Control.Feedback type="invalid">
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text className="text-muted">{description}</Form.Text>
<Form.Text>{description}</Form.Text>
</Form.Group>
);
}
@@ -370,7 +385,7 @@ const SchemaForm: FC<IProps> = ({
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text className="text-muted">{description}</Form.Text>
<Form.Text>{description}</Form.Text>
</Form.Group>
);
}
@@ -390,7 +405,7 @@ const SchemaForm: FC<IProps> = ({
{formData[key]?.errorMsg}
</Form.Control.Feedback>
<Form.Text className="text-muted">{description}</Form.Text>
<Form.Text>{description}</Form.Text>
</Form.Group>
);
})}
+5 -10
View File
@@ -3,22 +3,17 @@ import React, { memo, FC } from 'react';
import classNames from 'classnames';
import { Tag } from '@/common/interface';
import { pathFactory } from '@/router/pathFactory';
interface IProps {
data: Tag;
href?: string;
className?: string;
textClassName?: string;
}
const Index: FC<IProps> = ({
data,
href,
className = '',
textClassName = '',
}) => {
href ||= pathFactory.tagLanding(data);
const Index: FC<IProps> = ({ className = '', href, data }) => {
href ||= `/tags/${encodeURIComponent(
data.main_tag_slug_name || data.slug_name,
)}`.toLowerCase();
return (
<a
@@ -29,7 +24,7 @@ const Index: FC<IProps> = ({
data.recommend && 'badge-tag-required',
className,
)}>
<span className={textClassName}>{data.slug_name}</span>
{data.slug_name}
</a>
);
};
+1 -6
View File
@@ -229,12 +229,7 @@ const TagSelector: FC<IProps> = ({
{showRequiredTagText &&
tags &&
tags.filter((v) => v.recommend)?.length > 0 && (
<Dropdown.Item
disabled
style={{ fontWeight: 500 }}
className="text-secondary">
{t('tag_required_text')}
</Dropdown.Item>
<h6 className="dropdown-header">{t('tag_required_text')}</h6>
)}
{tags?.map((item, index) => {
+2 -22
View File
@@ -10,19 +10,10 @@ interface Props {
data: any;
time: number;
preFix: string;
isLogged: boolean;
timelinePath: string;
className?: string;
}
const Index: FC<Props> = ({
data,
time,
preFix,
isLogged,
timelinePath,
className = '',
}) => {
const Index: FC<Props> = ({ data, time, preFix, className = '' }) => {
return (
<div className={classnames('d-flex', className)}>
{data?.status !== 'deleted' ? (
@@ -71,18 +62,7 @@ const Index: FC<Props> = ({
{formatCount(data?.rank)}
</span>
</div>
{time &&
(isLogged ? (
<Link to={timelinePath}>
<FormatTime
time={time}
preFix={preFix}
className="link-secondary"
/>
</Link>
) : (
<FormatTime time={time} preFix={preFix} />
))}
{time && <FormatTime time={time} preFix={preFix} />}
</div>
</div>
);

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