From fe0f0810dce16323764d536e8f977e0c943ec700 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Mon, 24 Oct 2022 14:48:52 +0800 Subject: [PATCH 0001/3337] docs(ui): update link in doc --- ui/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/README.md b/ui/README.md index 50bb1baa9..fcf0cb4ce 100644 --- a/ui/README.md +++ b/ui/README.md @@ -2,7 +2,7 @@ `Answer` is a modern Q&A community application ✨ -To learn more about the philosophy and goals of the project, visit [Answer](https://answer.dev.segmentfault.com). +To learn more about the philosophy and goals of the project, visit [Answer](https://answer.dev). ### 📦 Prerequisites @@ -53,7 +53,7 @@ when cloning repo, and run `pnpm install` to init dependencies. you can use proj | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | last 2 versions | last 2 versions | last 2 versions | last 2 versions | -## Build with +## 🧱 Build with - [React.js](https://reactjs.org/) - Our front end is a React.js app. - [Bootstrap](https://getbootstrap.com/) - UI library. From 4fc3caebe679b44ac36b39b46a5bff86e638e128 Mon Sep 17 00:00:00 2001 From: ppchart Date: Tue, 25 Oct 2022 11:00:46 +0800 Subject: [PATCH 0002/3337] Fix RouteNode type error --- ui/src/router/index.tsx | 3 ++- ui/src/router/route-config.ts | 7 +------ ui/src/router/types.ts | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 ui/src/router/types.ts diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index b9dbb2de8..aa279cf36 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -1,8 +1,9 @@ import React, { Suspense, lazy } from 'react'; import { RouteObject, createBrowserRouter } from 'react-router-dom'; -import routeConfig, { RouteNode } from '@/router/route-config'; +import routeConfig from '@/router/route-config'; import RouteRules from '@/router/route-rules'; +import { RouteNode } from './types'; const routes: RouteObject[] = []; diff --git a/ui/src/router/route-config.ts b/ui/src/router/route-config.ts index 6267482ef..c347922e3 100644 --- a/ui/src/router/route-config.ts +++ b/ui/src/router/route-config.ts @@ -1,10 +1,5 @@ -import { RouteObject } from 'react-router-dom'; +import { RouteNode } from './types'; -export interface RouteNode extends RouteObject { - page: string; - children?: RouteNode[]; - rules?: string[]; -} const routeConfig: RouteNode[] = [ { path: '/', diff --git a/ui/src/router/types.ts b/ui/src/router/types.ts new file mode 100644 index 000000000..f0d13c875 --- /dev/null +++ b/ui/src/router/types.ts @@ -0,0 +1,14 @@ +import { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom'; + +type CustomRouteObject = { + page: string; + rules?: string[]; +}; + +type IndexRouteNode = IndexRouteObject & CustomRouteObject; + +interface NonIndexRouteNode extends NonIndexRouteObject, CustomRouteObject { + children?: (IndexRouteNode | NonIndexRouteNode)[]; +} + +export type RouteNode = IndexRouteNode | NonIndexRouteNode; From c75ba7b5bdf4913164552e01abea8d6cc2b2844d Mon Sep 17 00:00:00 2001 From: ppchart Date: Tue, 25 Oct 2022 11:26:24 +0800 Subject: [PATCH 0003/3337] Fix RouteNode type error --- ui/src/router/index.tsx | 2 +- ui/src/router/route-config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 1b78d19b9..7f2f0d3e3 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -5,7 +5,7 @@ import Layout from '@answer/pages/Layout'; import routeConfig from '@/router/route-config'; import RouteRules from '@/router/route-rules'; -import { RouteNode } from './types'; +import { RouteNode } from '@/router/types'; const routes: RouteObject[] = []; diff --git a/ui/src/router/route-config.ts b/ui/src/router/route-config.ts index aba498711..869068aed 100644 --- a/ui/src/router/route-config.ts +++ b/ui/src/router/route-config.ts @@ -1,4 +1,4 @@ -import { RouteNode } from './types'; +import { RouteNode } from '@/router/types'; const routeConfig: RouteNode[] = [ { From e4522f280d2559119b098b6b9c814155fa72433f Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Tue, 25 Oct 2022 11:14:51 +0800 Subject: [PATCH 0004/3337] feat: add repo unit test --- go.mod | 21 +++ go.sum | 76 +++++++++ internal/repo/repo_test/comment_repo_test.go | 81 ++++++++++ internal/repo/repo_test/repo_main_test.go | 155 +++++++++++++++++++ internal/service/comment/comment_service.go | 1 + 5 files changed, 334 insertions(+) create mode 100644 internal/repo/repo_test/comment_repo_test.go create mode 100644 internal/repo/repo_test/repo_main_test.go diff --git a/go.mod b/go.mod index 7fe0bfd9e..c37aecedf 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/lib/pq v1.10.2 github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mojocn/base64Captcha v1.3.5 + github.com/ory/dockertest/v3 v3.9.1 github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347 @@ -39,18 +40,30 @@ require ( ) require ( + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/andybalholm/brotli v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/containerd/continuity v0.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v20.10.14+incompatible // indirect + github.com/docker/docker v20.10.7+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/spec v0.20.7 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -61,14 +74,19 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nicksnyder/go-i18n/v2 v2.2.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.2 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -77,6 +95,9 @@ require ( github.com/subosito/gotenv v1.4.1 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/ugorji/go/codec v1.2.7 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect diff --git a/go.sum b/go.sum index 238004c45..683ad715d 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -49,6 +51,10 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -82,11 +88,15 @@ github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgIS github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -95,20 +105,35 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/cli v20.10.14+incompatible h1:dSBKJOVesDgHo7rbxlYjYsXe7gPzrTT+/cKQgpDAazg= +github.com/docker/cli v20.10.14+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= +github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -125,6 +150,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= @@ -180,12 +206,16 @@ github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -249,6 +279,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -294,6 +326,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= @@ -366,6 +400,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -435,6 +470,9 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -444,6 +482,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0= github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= @@ -464,6 +503,14 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.2 h1:2VSZwLx5k/BfsBxMMipG/LYUnmqOD/BPkIVgQUcTlLw= +github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -472,6 +519,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= +github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= @@ -533,6 +582,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/segmentfault/pacman v1.0.1 h1:GFdvPtNxvVVjnDM4ty02D/+4unHwG9PmjcOZSc2wRXE= github.com/segmentfault/pacman v1.0.1/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 h1:0xWBBXHHuemzMY61KYJXh7F5FW/4K8g98RYKNXodTCc= @@ -552,6 +602,9 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -566,6 +619,7 @@ github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJ github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= @@ -595,6 +649,7 @@ github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89 github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg= github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -606,6 +661,14 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -785,12 +848,14 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -808,6 +873,7 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -815,6 +881,7 @@ golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -824,7 +891,10 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -862,6 +932,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -892,6 +963,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -901,6 +973,7 @@ golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= @@ -1010,6 +1083,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -1048,6 +1122,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/repo/repo_test/comment_repo_test.go b/internal/repo/repo_test/comment_repo_test.go new file mode 100644 index 000000000..2b94b628e --- /dev/null +++ b/internal/repo/repo_test/comment_repo_test.go @@ -0,0 +1,81 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/base/pager" + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/comment" + "github.com/answerdev/answer/internal/repo/unique" + commentService "github.com/answerdev/answer/internal/service/comment" + "github.com/stretchr/testify/assert" +) + +func buildCommentEntity() *entity.Comment { + return &entity.Comment{ + UserID: "1", + ObjectID: "1", + QuestionID: "1", + VoteCount: 1, + Status: entity.CommentStatusAvailable, + OriginalText: "# title", + ParsedText: "

Title

", + } +} + +func Test_commentRepo_AddComment(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(dataSource) + commentRepo := comment.NewCommentRepo(dataSource, uniqueIDRepo) + testCommentEntity := buildCommentEntity() + err := commentRepo.AddComment(context.TODO(), testCommentEntity) + assert.NoError(t, err) + + err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) + return +} + +func Test_commentRepo_GetCommentPage(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(dataSource) + commentRepo := comment.NewCommentRepo(dataSource, uniqueIDRepo) + testCommentEntity := buildCommentEntity() + err := commentRepo.AddComment(context.TODO(), testCommentEntity) + assert.NoError(t, err) + + resp, total, err := commentRepo.GetCommentPage(context.TODO(), &commentService.CommentQuery{ + PageCond: pager.PageCond{ + Page: 1, + PageSize: 10, + }, + }) + assert.NoError(t, err) + assert.Equal(t, total, int64(1)) + assert.Equal(t, resp[0].ID, testCommentEntity.ID) + + err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) + return +} + +func Test_commentRepo_UpdateComment(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(dataSource) + commentRepo := comment.NewCommentRepo(dataSource, uniqueIDRepo) + commonCommentRepo := comment.NewCommentCommonRepo(dataSource, uniqueIDRepo) + testCommentEntity := buildCommentEntity() + err := commentRepo.AddComment(context.TODO(), testCommentEntity) + assert.NoError(t, err) + + testCommentEntity.ParsedText = "test" + err = commentRepo.UpdateComment(context.TODO(), testCommentEntity) + assert.NoError(t, err) + + newComment, exist, err := commonCommentRepo.GetComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, testCommentEntity.ParsedText, newComment.ParsedText) + + err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID) + assert.NoError(t, err) + return +} diff --git a/internal/repo/repo_test/repo_main_test.go b/internal/repo/repo_test/repo_main_test.go new file mode 100644 index 000000000..4c476c56e --- /dev/null +++ b/internal/repo/repo_test/repo_main_test.go @@ -0,0 +1,155 @@ +package repo_test + +import ( + "database/sql" + "fmt" + "os" + "testing" + "time" + + "github.com/answerdev/answer/internal/base/data" + "github.com/answerdev/answer/internal/migrations" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/segmentfault/pacman/log" + "xorm.io/xorm/schemas" +) + +var ( + mysqlDBSetting = TestDBSetting{ + Driver: string(schemas.MYSQL), + ImageName: "mariadb", + ImageVersion: "10.4.7", + ENV: []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=answer", "MYSQL_ROOT_HOST=%"}, + PortID: "3306/tcp", + Connection: "root:root@(localhost:%s)/answer?parseTime=true", // port is not fixed, it will be got by port id + } + postgresDBSetting = TestDBSetting{ + Driver: string(schemas.POSTGRES), + ImageName: "postgres", + ImageVersion: "14", + ENV: []string{"POSTGRES_USER=root", "POSTGRES_PASSWORD=root", "POSTGRES_DB=answer", "LISTEN_ADDRESSES='*'"}, + PortID: "5432/tcp", + Connection: "host=localhost port=%s user=root password=root dbname=answer sslmode=disable", + } + sqlite3DBSetting = TestDBSetting{ + Driver: string(schemas.SQLITE), + Connection: os.TempDir() + "answer-test-data.db", + } + dbSettingMapping = map[string]TestDBSetting{ + mysqlDBSetting.Driver: mysqlDBSetting, + sqlite3DBSetting.Driver: sqlite3DBSetting, + postgresDBSetting.Driver: postgresDBSetting, + } + // after all test down will execute tearDown function to clean-up + tearDown func() + // dataSource used for repo testing + dataSource *data.Data +) + +func TestMain(t *testing.M) { + dbSetting, ok := dbSettingMapping[os.Getenv("TEST_DB_DRIVER")] + if !ok { + dbSetting = dbSettingMapping[string(schemas.MYSQL)] + } + defer func() { + if tearDown != nil { + tearDown() + } + }() + if err := initTestDB(dbSetting); err != nil { + panic(err) + } + log.Info("init test database successfully") + + if ret := t.Run(); ret != 0 { + panic(ret) + } +} + +type TestDBSetting struct { + Driver string + ImageName string + ImageVersion string + ENV []string + PortID string + Connection string +} + +func initTestDB(dbSetting TestDBSetting) error { + connection, imageCleanUp, err := initDatabaseImage(dbSetting) + if err != nil { + return err + } + dbSetting.Connection = connection + ds, dbCleanUp, err := initDatabase(dbSetting) + if err != nil { + return err + } + dataSource = ds + tearDown = func() { + dbCleanUp() + log.Info("cleanup test database successfully") + imageCleanUp() + log.Info("cleanup test database image successfully") + } + return nil +} + +func initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func(), err error) { + // sqlite3 don't need to set up image + if dbSetting.Driver == string(schemas.SQLITE) { + return dbSetting.Connection, func() { + log.Info("remove database", dbSetting.Connection) + _ = os.Remove(dbSetting.Connection) + }, nil + } + pool, err := dockertest.NewPool("") + pool.MaxWait = time.Minute * 5 + if err != nil { + return "", nil, fmt.Errorf("could not connect to docker: %s", err) + } + + //resource, err := pool.Run(dbSetting.ImageName, dbSetting.ImageVersion, dbSetting.ENV) + resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: dbSetting.ImageName, + Tag: dbSetting.ImageVersion, + Env: dbSetting.ENV, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + if err != nil { + return "", nil, fmt.Errorf("could not pull resource: %s", err) + } + + connection = fmt.Sprintf(dbSetting.Connection, resource.GetPort(dbSetting.PortID)) + if err := pool.Retry(func() error { + db, err := sql.Open(dbSetting.Driver, connection) + if err != nil { + return err + } + return db.Ping() + }); err != nil { + return "", nil, fmt.Errorf("could not connect to database: %s", err) + } + return connection, func() { _ = pool.Purge(resource) }, nil +} + +func initDatabase(dbSetting TestDBSetting) (dataSource *data.Data, cleanup func(), err error) { + dataConf := &data.Database{Driver: dbSetting.Driver, Connection: dbSetting.Connection} + db, err := data.NewDB(true, dataConf) + if err != nil { + return nil, nil, fmt.Errorf("connection to database failed: %s", err) + } + err = migrations.InitDB(dataConf) + if err != nil { + return nil, nil, fmt.Errorf("migrations init database failed: %s", err) + } + + dataSource, dbCleanUp, err := data.NewData(db, nil) + if err != nil { + return nil, nil, fmt.Errorf("new data failed: %s", err) + } + return dataSource, dbCleanUp, nil +} diff --git a/internal/service/comment/comment_service.go b/internal/service/comment/comment_service.go index 749f0becc..a7156c76f 100644 --- a/internal/service/comment/comment_service.go +++ b/internal/service/comment/comment_service.go @@ -24,6 +24,7 @@ type CommentRepo interface { AddComment(ctx context.Context, comment *entity.Comment) (err error) RemoveComment(ctx context.Context, commentID string) (err error) UpdateComment(ctx context.Context, comment *entity.Comment) (err error) + GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) GetCommentPage(ctx context.Context, commentQuery *CommentQuery) ( comments []*entity.Comment, total int64, err error) } From dd16b0db747d3367d74b9267344b6d05ab322b16 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 26 Oct 2022 16:24:58 +0800 Subject: [PATCH 0005/3337] feat: add email repo unit test --- internal/repo/export/email_repo.go | 2 + internal/repo/repo_test/email_repo_test.go | 20 ++++++++++ internal/repo/repo_test/repo_main_test.go | 45 +++++++++++++++------- 3 files changed, 53 insertions(+), 14 deletions(-) create mode 100644 internal/repo/repo_test/email_repo_test.go diff --git a/internal/repo/export/email_repo.go b/internal/repo/export/email_repo.go index 85f0b2495..826eff29c 100644 --- a/internal/repo/export/email_repo.go +++ b/internal/repo/export/email_repo.go @@ -22,6 +22,7 @@ func NewEmailRepo(data *data.Data) export.EmailRepo { } } +// SetCode The email code is used to verify that the link in the message is out of date func (e *emailRepo) SetCode(ctx context.Context, code, content string) error { err := e.data.Cache.SetString(ctx, code, content, 10*time.Minute) if err != nil { @@ -30,6 +31,7 @@ func (e *emailRepo) SetCode(ctx context.Context, code, content string) error { return nil } +// VerifyCode verify the code if out of date func (e *emailRepo) VerifyCode(ctx context.Context, code string) (content string, err error) { content, err = e.data.Cache.GetString(ctx, code) if err != nil { diff --git a/internal/repo/repo_test/email_repo_test.go b/internal/repo/repo_test/email_repo_test.go new file mode 100644 index 000000000..2a6bd6307 --- /dev/null +++ b/internal/repo/repo_test/email_repo_test.go @@ -0,0 +1,20 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/repo/export" + "github.com/stretchr/testify/assert" +) + +func Test_emailRepo_VerifyCode(t *testing.T) { + emailRepo := export.NewEmailRepo(dataSource) + code, content := "1111", "test" + err := emailRepo.SetCode(context.TODO(), code, content) + assert.NoError(t, err) + + verifyContent, err := emailRepo.VerifyCode(context.TODO(), code) + assert.NoError(t, err) + assert.Equal(t, content, verifyContent) +} diff --git a/internal/repo/repo_test/repo_main_test.go b/internal/repo/repo_test/repo_main_test.go index 4c476c56e..8ccfb8793 100644 --- a/internal/repo/repo_test/repo_main_test.go +++ b/internal/repo/repo_test/repo_main_test.go @@ -11,7 +11,9 @@ import ( "github.com/answerdev/answer/internal/migrations" "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" + "github.com/segmentfault/pacman/cache" "github.com/segmentfault/pacman/log" + "xorm.io/xorm" "xorm.io/xorm/schemas" ) @@ -57,7 +59,7 @@ func TestMain(t *testing.M) { tearDown() } }() - if err := initTestDB(dbSetting); err != nil { + if err := initTestDataSource(dbSetting); err != nil { panic(err) } log.Info("init test database successfully") @@ -76,17 +78,29 @@ type TestDBSetting struct { Connection string } -func initTestDB(dbSetting TestDBSetting) error { +func initTestDataSource(dbSetting TestDBSetting) error { connection, imageCleanUp, err := initDatabaseImage(dbSetting) if err != nil { return err } dbSetting.Connection = connection - ds, dbCleanUp, err := initDatabase(dbSetting) + + dbEngine, err := initDatabase(dbSetting) if err != nil { return err } - dataSource = ds + + newCache, err := initCache() + if err != nil { + return err + } + + newData, dbCleanUp, err := data.NewData(dbEngine, newCache) + if err != nil { + return err + } + dataSource = newData + tearDown = func() { dbCleanUp() log.Info("cleanup test database successfully") @@ -101,7 +115,10 @@ func initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func if dbSetting.Driver == string(schemas.SQLITE) { return dbSetting.Connection, func() { log.Info("remove database", dbSetting.Connection) - _ = os.Remove(dbSetting.Connection) + err = os.Remove(dbSetting.Connection) + if err != nil { + log.Error(err) + } }, nil } pool, err := dockertest.NewPool("") @@ -136,20 +153,20 @@ func initDatabaseImage(dbSetting TestDBSetting) (connection string, cleanup func return connection, func() { _ = pool.Purge(resource) }, nil } -func initDatabase(dbSetting TestDBSetting) (dataSource *data.Data, cleanup func(), err error) { +func initDatabase(dbSetting TestDBSetting) (dbEngine *xorm.Engine, err error) { dataConf := &data.Database{Driver: dbSetting.Driver, Connection: dbSetting.Connection} - db, err := data.NewDB(true, dataConf) + dbEngine, err = data.NewDB(true, dataConf) if err != nil { - return nil, nil, fmt.Errorf("connection to database failed: %s", err) + return nil, fmt.Errorf("connection to database failed: %s", err) } err = migrations.InitDB(dataConf) if err != nil { - return nil, nil, fmt.Errorf("migrations init database failed: %s", err) + return nil, fmt.Errorf("migrations init database failed: %s", err) } + return dbEngine, nil +} - dataSource, dbCleanUp, err := data.NewData(db, nil) - if err != nil { - return nil, nil, fmt.Errorf("new data failed: %s", err) - } - return dataSource, dbCleanUp, nil +func initCache() (newCache cache.Cache, err error) { + newCache, _, err = data.NewCache(&data.CacheConf{}) + return newCache, err } From 1b206b42609472dce8415d640d62fee8e1af4b95 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 26 Oct 2022 16:29:21 +0800 Subject: [PATCH 0006/3337] feat: tag change unique id generate func --- internal/repo/tag/tag_repo.go | 4 +--- internal/repo/unique/uniqid_repo.go | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 5243402e5..bb0f3ad87 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -2,7 +2,6 @@ package tag import ( "context" - "fmt" "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/pager" @@ -34,12 +33,11 @@ func NewTagRepo( // AddTagList add tag func (tr *tagRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) { for _, item := range tagList { - ID, err := tr.uniqueIDRepo.GenUniqueID(ctx, item.TableName()) + item.ID, err = tr.uniqueIDRepo.GenUniqueIDStr(ctx, item.TableName()) if err != nil { return err } item.RevisionID = "0" - item.ID = fmt.Sprintf("%d", ID) } _, err = tr.data.DB.Insert(tagList) if err != nil { diff --git a/internal/repo/unique/uniqid_repo.go b/internal/repo/unique/uniqid_repo.go index 44a9c0faf..5af8e0286 100644 --- a/internal/repo/unique/uniqid_repo.go +++ b/internal/repo/unique/uniqid_repo.go @@ -40,6 +40,7 @@ func (ur *uniqueIDRepo) GenUniqueID(ctx context.Context, key string) (uniqueID i } // GenUniqueIDStr generate unique id string +// 1 + 00x(objectType) + 000000000000x(id) func (ur *uniqueIDRepo) GenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error) { objectType := constant.ObjectTypeStrMapping[key] bean := &entity.Uniqid{UniqidType: objectType} From c59f48e69440c57af4c75bf9979e7d42d4335250 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 26 Oct 2022 16:37:55 +0800 Subject: [PATCH 0007/3337] style: rename backyard auth function name --- internal/repo/auth/auth.go | 27 +++++++++++++++------------ internal/service/auth/auth.go | 12 ++++++------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/internal/repo/auth/auth.go b/internal/repo/auth/auth.go index 462029804..61bd00c7b 100644 --- a/internal/repo/auth/auth.go +++ b/internal/repo/auth/auth.go @@ -17,6 +17,7 @@ type authRepo struct { data *data.Data } +// GetUserCacheInfo get user cache info func (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { userInfoCache, err := ar.data.Cache.GetString(ctx, constant.UserTokenCacheKey+accessToken) if err != nil { @@ -30,6 +31,7 @@ func (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (u return userInfo, nil } +// GetUserStatus get user status func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) { userInfoCache, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID) if err != nil { @@ -43,6 +45,7 @@ func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo return userInfo, nil } +// RemoveUserStatus remove user status func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err error) { err = ar.data.Cache.Del(ctx, constant.UserStatusChangedCacheKey+userID) if err != nil { @@ -51,6 +54,7 @@ func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err er return nil } +// SetUserCacheInfo set user cache info func (ar *authRepo) SetUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { userInfoCache, err := json.Marshal(userInfo) if err != nil { @@ -64,16 +68,17 @@ func (ar *authRepo) SetUserCacheInfo(ctx context.Context, accessToken string, us return nil } +// RemoveUserCacheInfo remove user cache info func (ar *authRepo) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) { err = ar.data.Cache.Del(ctx, constant.UserTokenCacheKey+accessToken) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return err + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -func (ar *authRepo) GetCmsUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { +// GetBackyardUserCacheInfo get backyard user cache info +func (ar *authRepo) GetBackyardUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { userInfoCache, err := ar.data.Cache.GetString(ctx, constant.AdminTokenCacheKey+accessToken) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -87,7 +92,8 @@ func (ar *authRepo) GetCmsUserCacheInfo(ctx context.Context, accessToken string) return userInfo, nil } -func (ar *authRepo) SetCmsUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { +// SetBackyardUserCacheInfo set backyard user cache info +func (ar *authRepo) SetBackyardUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { userInfoCache, err := json.Marshal(userInfo) if err != nil { return err @@ -96,25 +102,22 @@ func (ar *authRepo) SetCmsUserCacheInfo(ctx context.Context, accessToken string, err = ar.data.Cache.SetString(ctx, constant.AdminTokenCacheKey+accessToken, string(userInfoCache), constant.AdminTokenCacheTime) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return err + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -func (ar *authRepo) RemoveCmsUserCacheInfo(ctx context.Context, accessToken string) (err error) { +// RemoveBackyardUserCacheInfo remove backyard user cache info +func (ar *authRepo) RemoveBackyardUserCacheInfo(ctx context.Context, accessToken string) (err error) { err = ar.data.Cache.Del(ctx, constant.AdminTokenCacheKey+accessToken) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return err + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } // NewAuthRepo new repository -func NewAuthRepo( - data *data.Data, -) auth.AuthRepo { +func NewAuthRepo(data *data.Data) auth.AuthRepo { return &authRepo{ data: data, } diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go index c7f4e7125..e4fd91821 100644 --- a/internal/service/auth/auth.go +++ b/internal/service/auth/auth.go @@ -15,9 +15,9 @@ type AuthRepo interface { RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) RemoveUserStatus(ctx context.Context, userID string) (err error) - GetCmsUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) - SetCmsUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error - RemoveCmsUserCacheInfo(ctx context.Context, accessToken string) (err error) + GetBackyardUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) + SetBackyardUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error + RemoveBackyardUserCacheInfo(ctx context.Context, accessToken string) (err error) } // AuthService kit service @@ -75,14 +75,14 @@ func (as *AuthService) RemoveUserCacheInfo(ctx context.Context, accessToken stri //cms func (as *AuthService) GetCmsUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) { - return as.authRepo.GetCmsUserCacheInfo(ctx, accessToken) + return as.authRepo.GetBackyardUserCacheInfo(ctx, accessToken) } func (as *AuthService) SetCmsUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { - err = as.authRepo.SetCmsUserCacheInfo(ctx, accessToken, userInfo) + err = as.authRepo.SetBackyardUserCacheInfo(ctx, accessToken, userInfo) return err } func (as *AuthService) RemoveCmsUserCacheInfo(ctx context.Context, accessToken string) (err error) { - return as.authRepo.RemoveCmsUserCacheInfo(ctx, accessToken) + return as.authRepo.RemoveBackyardUserCacheInfo(ctx, accessToken) } From 6f1e8b6aa929e9542061ab67af301f08c16851cc Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 26 Oct 2022 16:59:33 +0800 Subject: [PATCH 0008/3337] feat: add auth repo unit test --- cmd/answer/wire_gen.go | 2 +- internal/repo/auth/auth.go | 52 +++++++----- internal/repo/repo_test/auth_test.go | 86 ++++++++++++++++++++ internal/repo/repo_test/comment_repo_test.go | 14 ++-- internal/repo/repo_test/email_repo_test.go | 2 +- internal/repo/repo_test/repo_main_test.go | 6 +- internal/repo/user/user_backyard_repo.go | 13 +-- internal/service/auth/auth.go | 1 + 8 files changed, 139 insertions(+), 37 deletions(-) create mode 100644 internal/repo/repo_test/auth_test.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index b034797f6..eef3585e2 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -155,7 +155,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reportHandle := report_handle_backyard.NewReportHandle(questionCommon, commentRepo, configRepo) reportBackyardService := report_backyard.NewReportBackyardService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo) controller_backyardReportController := controller_backyard.NewReportController(reportBackyardService) - userBackyardRepo := user.NewUserBackyardRepo(dataData) + userBackyardRepo := user.NewUserBackyardRepo(dataData, authRepo) userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo) userBackyardController := controller_backyard.NewUserBackyardController(userBackyardService) reasonRepo := reason.NewReasonRepo(configRepo) diff --git a/internal/repo/auth/auth.go b/internal/repo/auth/auth.go index 61bd00c7b..9f5a1813c 100644 --- a/internal/repo/auth/auth.go +++ b/internal/repo/auth/auth.go @@ -12,7 +12,7 @@ import ( "github.com/segmentfault/pacman/errors" ) -// authRepo activity repository +// authRepo auth repository type authRepo struct { data *data.Data } @@ -31,46 +31,60 @@ func (ar *authRepo) GetUserCacheInfo(ctx context.Context, accessToken string) (u return userInfo, nil } -// GetUserStatus get user status -func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) { - userInfoCache, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID) +// SetUserCacheInfo set user cache info +func (ar *authRepo) SetUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { + userInfoCache, err := json.Marshal(userInfo) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return err } - userInfo = &entity.UserCacheInfo{} - err = json.Unmarshal([]byte(userInfoCache), userInfo) + err = ar.data.Cache.SetString(ctx, constant.UserTokenCacheKey+accessToken, + string(userInfoCache), constant.UserTokenCacheTime) if err != nil { - return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - return userInfo, nil + return nil } -// RemoveUserStatus remove user status -func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err error) { - err = ar.data.Cache.Del(ctx, constant.UserStatusChangedCacheKey+userID) +// RemoveUserCacheInfo remove user cache info +func (ar *authRepo) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) { + err = ar.data.Cache.Del(ctx, constant.UserTokenCacheKey+accessToken) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -// SetUserCacheInfo set user cache info -func (ar *authRepo) SetUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) (err error) { +// SetUserStatus set user status +func (ar *authRepo) SetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error) { userInfoCache, err := json.Marshal(userInfo) if err != nil { return err } - err = ar.data.Cache.SetString(ctx, constant.UserTokenCacheKey+accessToken, - string(userInfoCache), constant.UserTokenCacheTime) + err = ar.data.Cache.SetString(ctx, constant.UserStatusChangedCacheKey+userID, + string(userInfoCache), constant.UserStatusChangedCacheTime) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -// RemoveUserCacheInfo remove user cache info -func (ar *authRepo) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) { - err = ar.data.Cache.Del(ctx, constant.UserTokenCacheKey+accessToken) +// GetUserStatus get user status +func (ar *authRepo) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) { + userInfoCache, err := ar.data.Cache.GetString(ctx, constant.UserStatusChangedCacheKey+userID) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + userInfo = &entity.UserCacheInfo{} + err = json.Unmarshal([]byte(userInfoCache), userInfo) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return userInfo, nil +} + +// RemoveUserStatus remove user status +func (ar *authRepo) RemoveUserStatus(ctx context.Context, userID string) (err error) { + err = ar.data.Cache.Del(ctx, constant.UserStatusChangedCacheKey+userID) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/repo/repo_test/auth_test.go b/internal/repo/repo_test/auth_test.go new file mode 100644 index 000000000..7682b3db4 --- /dev/null +++ b/internal/repo/repo_test/auth_test.go @@ -0,0 +1,86 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/auth" + "github.com/stretchr/testify/assert" +) + +var ( + token = "token" + userID = "1" +) + +func Test_authRepo_SetUserCacheInfo(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetUserCacheInfo(context.TODO(), token, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + cacheInfo, err := authRepo.GetUserCacheInfo(context.TODO(), token) + assert.NoError(t, err) + assert.Equal(t, userID, cacheInfo.UserID) +} + +func Test_authRepo_RemoveUserCacheInfo(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetUserCacheInfo(context.TODO(), token, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + err = authRepo.RemoveUserCacheInfo(context.TODO(), token) + assert.NoError(t, err) + + _, err = authRepo.GetUserCacheInfo(context.TODO(), token) + assert.Error(t, err) +} + +func Test_authRepo_SetUserStatus(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + cacheInfo, err := authRepo.GetUserStatus(context.TODO(), userID) + assert.NoError(t, err) + assert.Equal(t, userID, cacheInfo.UserID) +} +func Test_authRepo_RemoveUserStatus(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetUserStatus(context.TODO(), userID, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + err = authRepo.RemoveUserStatus(context.TODO(), userID) + assert.NoError(t, err) + + _, err = authRepo.GetUserStatus(context.TODO(), userID) + assert.Error(t, err) +} + +func Test_authRepo_SetBackyardUserCacheInfo(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetBackyardUserCacheInfo(context.TODO(), token, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + cacheInfo, err := authRepo.GetBackyardUserCacheInfo(context.TODO(), token) + assert.NoError(t, err) + assert.Equal(t, userID, cacheInfo.UserID) +} + +func Test_authRepo_RemoveBackyardUserCacheInfo(t *testing.T) { + authRepo := auth.NewAuthRepo(testDataSource) + + err := authRepo.SetBackyardUserCacheInfo(context.TODO(), token, &entity.UserCacheInfo{UserID: userID}) + assert.NoError(t, err) + + err = authRepo.RemoveBackyardUserCacheInfo(context.TODO(), token) + assert.NoError(t, err) + + _, err = authRepo.GetBackyardUserCacheInfo(context.TODO(), token) + assert.Error(t, err) +} diff --git a/internal/repo/repo_test/comment_repo_test.go b/internal/repo/repo_test/comment_repo_test.go index 2b94b628e..c7ad1d41c 100644 --- a/internal/repo/repo_test/comment_repo_test.go +++ b/internal/repo/repo_test/comment_repo_test.go @@ -25,8 +25,8 @@ func buildCommentEntity() *entity.Comment { } func Test_commentRepo_AddComment(t *testing.T) { - uniqueIDRepo := unique.NewUniqueIDRepo(dataSource) - commentRepo := comment.NewCommentRepo(dataSource, uniqueIDRepo) + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) testCommentEntity := buildCommentEntity() err := commentRepo.AddComment(context.TODO(), testCommentEntity) assert.NoError(t, err) @@ -37,8 +37,8 @@ func Test_commentRepo_AddComment(t *testing.T) { } func Test_commentRepo_GetCommentPage(t *testing.T) { - uniqueIDRepo := unique.NewUniqueIDRepo(dataSource) - commentRepo := comment.NewCommentRepo(dataSource, uniqueIDRepo) + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) testCommentEntity := buildCommentEntity() err := commentRepo.AddComment(context.TODO(), testCommentEntity) assert.NoError(t, err) @@ -59,9 +59,9 @@ func Test_commentRepo_GetCommentPage(t *testing.T) { } func Test_commentRepo_UpdateComment(t *testing.T) { - uniqueIDRepo := unique.NewUniqueIDRepo(dataSource) - commentRepo := comment.NewCommentRepo(dataSource, uniqueIDRepo) - commonCommentRepo := comment.NewCommentCommonRepo(dataSource, uniqueIDRepo) + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + commentRepo := comment.NewCommentRepo(testDataSource, uniqueIDRepo) + commonCommentRepo := comment.NewCommentCommonRepo(testDataSource, uniqueIDRepo) testCommentEntity := buildCommentEntity() err := commentRepo.AddComment(context.TODO(), testCommentEntity) assert.NoError(t, err) diff --git a/internal/repo/repo_test/email_repo_test.go b/internal/repo/repo_test/email_repo_test.go index 2a6bd6307..2da709448 100644 --- a/internal/repo/repo_test/email_repo_test.go +++ b/internal/repo/repo_test/email_repo_test.go @@ -9,7 +9,7 @@ import ( ) func Test_emailRepo_VerifyCode(t *testing.T) { - emailRepo := export.NewEmailRepo(dataSource) + emailRepo := export.NewEmailRepo(testDataSource) code, content := "1111", "test" err := emailRepo.SetCode(context.TODO(), code, content) assert.NoError(t, err) diff --git a/internal/repo/repo_test/repo_main_test.go b/internal/repo/repo_test/repo_main_test.go index 8ccfb8793..e67223ff4 100644 --- a/internal/repo/repo_test/repo_main_test.go +++ b/internal/repo/repo_test/repo_main_test.go @@ -45,8 +45,8 @@ var ( } // after all test down will execute tearDown function to clean-up tearDown func() - // dataSource used for repo testing - dataSource *data.Data + // testDataSource used for repo testing + testDataSource *data.Data ) func TestMain(t *testing.M) { @@ -99,7 +99,7 @@ func initTestDataSource(dbSetting TestDBSetting) error { if err != nil { return err } - dataSource = newData + testDataSource = newData tearDown = func() { dbCleanUp() diff --git a/internal/repo/user/user_backyard_repo.go b/internal/repo/user/user_backyard_repo.go index 06ce7d2f0..82c95c38a 100644 --- a/internal/repo/user/user_backyard_repo.go +++ b/internal/repo/user/user_backyard_repo.go @@ -5,11 +5,11 @@ import ( "encoding/json" "time" - "github.com/answerdev/answer/internal/base/constant" "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" + "github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/user_backyard" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" @@ -17,13 +17,15 @@ import ( // userBackyardRepo user repository type userBackyardRepo struct { - data *data.Data + data *data.Data + authRepo auth.AuthRepo } // NewUserBackyardRepo new repository -func NewUserBackyardRepo(data *data.Data) user_backyard.UserBackyardRepo { +func NewUserBackyardRepo(data *data.Data, authRepo auth.AuthRepo) user_backyard.UserBackyardRepo { return &userBackyardRepo{ - data: data, + data: data, + authRepo: authRepo, } } @@ -49,8 +51,7 @@ func (ur *userBackyardRepo) UpdateUserStatus(ctx context.Context, userID string, } t, _ := json.Marshal(userCacheInfo) log.Infof("user change status: %s", string(t)) - err = ur.data.Cache.SetString(ctx, constant.UserStatusChangedCacheKey+userID, string(t), - constant.UserStatusChangedCacheTime) + err = ur.authRepo.SetUserStatus(ctx, userID, userCacheInfo) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go index e4fd91821..95604a147 100644 --- a/internal/service/auth/auth.go +++ b/internal/service/auth/auth.go @@ -13,6 +13,7 @@ type AuthRepo interface { GetUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) SetUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) + SetUserStatus(ctx context.Context, userID string, userInfo *entity.UserCacheInfo) (err error) GetUserStatus(ctx context.Context, userID string) (userInfo *entity.UserCacheInfo, err error) RemoveUserStatus(ctx context.Context, userID string) (err error) GetBackyardUserCacheInfo(ctx context.Context, accessToken string) (userInfo *entity.UserCacheInfo, err error) From 7af6db7c7fb532b0d93d68cc8ddd1cb7f3022bc5 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 26 Oct 2022 17:14:36 +0800 Subject: [PATCH 0009/3337] feat: add captcha repo unit test --- internal/repo/repo_test/captcha_test.go | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 internal/repo/repo_test/captcha_test.go diff --git a/internal/repo/repo_test/captcha_test.go b/internal/repo/repo_test/captcha_test.go new file mode 100644 index 000000000..74353a8e2 --- /dev/null +++ b/internal/repo/repo_test/captcha_test.go @@ -0,0 +1,39 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/repo/captcha" + "github.com/stretchr/testify/assert" +) + +var ( + ip = "127.0.0.1" + actionType = "actionType" + amount = 1 +) + +func Test_captchaRepo_DelActionType(t *testing.T) { + captchaRepo := captcha.NewCaptchaRepo(testDataSource) + err := captchaRepo.SetActionType(context.TODO(), ip, actionType, amount) + assert.NoError(t, err) + + gotAmount, err := captchaRepo.GetActionType(context.TODO(), ip, actionType) + assert.NoError(t, err) + assert.Equal(t, amount, gotAmount) + + err = captchaRepo.DelActionType(context.TODO(), ip, actionType) + assert.NoError(t, err) +} + +func Test_captchaRepo_SetCaptcha(t *testing.T) { + captchaRepo := captcha.NewCaptchaRepo(testDataSource) + key, capt := "key", "1234" + err := captchaRepo.SetCaptcha(context.TODO(), key, capt) + assert.NoError(t, err) + + gotCaptcha, err := captchaRepo.GetCaptcha(context.TODO(), key) + assert.NoError(t, err) + assert.Equal(t, capt, gotCaptcha) +} From 2874fde1c9ee8996648faca8cd731c9be27896d5 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 26 Oct 2022 17:26:34 +0800 Subject: [PATCH 0010/3337] feat: add meta repo unit test --- internal/repo/meta/meta_repo.go | 2 +- internal/repo/repo_test/meta_repo_test.go | 86 +++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 internal/repo/repo_test/meta_repo_test.go diff --git a/internal/repo/meta/meta_repo.go b/internal/repo/meta/meta_repo.go index e98c8020c..8a3ea7a49 100644 --- a/internal/repo/meta/meta_repo.go +++ b/internal/repo/meta/meta_repo.go @@ -65,7 +65,7 @@ func (mr *metaRepo) GetMetaByObjectIdAndKey(ctx context.Context, objectID, key s // GetMetaList get meta list all func (mr *metaRepo) GetMetaList(ctx context.Context, meta *entity.Meta) (metaList []*entity.Meta, err error) { metaList = make([]*entity.Meta, 0) - err = mr.data.DB.Find(metaList, meta) + err = mr.data.DB.Find(&metaList, meta) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } diff --git a/internal/repo/repo_test/meta_repo_test.go b/internal/repo/repo_test/meta_repo_test.go new file mode 100644 index 000000000..6dcd85453 --- /dev/null +++ b/internal/repo/repo_test/meta_repo_test.go @@ -0,0 +1,86 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/meta" + "github.com/stretchr/testify/assert" +) + +func buildMetaEntity() *entity.Meta { + return &entity.Meta{ + ObjectID: "1", + Key: "1", + Value: "1", + } +} + +func Test_metaRepo_GetMetaByObjectIdAndKey(t *testing.T) { + metaRepo := meta.NewMetaRepo(testDataSource) + metaEnt := buildMetaEntity() + + err := metaRepo.AddMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + gotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, metaEnt.ID, gotMeta.ID) + + err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) + assert.NoError(t, err) +} + +func Test_metaRepo_GetMetaList(t *testing.T) { + metaRepo := meta.NewMetaRepo(testDataSource) + metaEnt := buildMetaEntity() + + err := metaRepo.AddMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + gotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt) + assert.NoError(t, err) + assert.Equal(t, len(gotMetaList), 1) + assert.Equal(t, gotMetaList[0].ID, metaEnt.ID) + + err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) + assert.NoError(t, err) +} + +func Test_metaRepo_GetMetaPage(t *testing.T) { + metaRepo := meta.NewMetaRepo(testDataSource) + metaEnt := buildMetaEntity() + + err := metaRepo.AddMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + gotMetaList, err := metaRepo.GetMetaList(context.TODO(), metaEnt) + assert.NoError(t, err) + assert.Equal(t, len(gotMetaList), 1) + assert.Equal(t, gotMetaList[0].ID, metaEnt.ID) + + err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) + assert.NoError(t, err) +} + +func Test_metaRepo_UpdateMeta(t *testing.T) { + metaRepo := meta.NewMetaRepo(testDataSource) + metaEnt := buildMetaEntity() + + err := metaRepo.AddMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + metaEnt.Value = "testing" + err = metaRepo.UpdateMeta(context.TODO(), metaEnt) + assert.NoError(t, err) + + gotMeta, exist, err := metaRepo.GetMetaByObjectIdAndKey(context.TODO(), metaEnt.ObjectID, metaEnt.Key) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, gotMeta.Value, metaEnt.Value) + + err = metaRepo.RemoveMeta(context.TODO(), metaEnt.ID) + assert.NoError(t, err) +} From 863a9d91ca14bcf140d57841f0c4f235928afc99 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 26 Oct 2022 17:37:11 +0800 Subject: [PATCH 0011/3337] feat: refactor notification page list --- .../controller/notification_controller.go | 2 +- .../repo/notification/notification_repo.go | 50 +++++++------------ .../notification/notification_service.go | 10 ++-- .../notification_common/notification.go | 2 +- 4 files changed, 25 insertions(+), 39 deletions(-) diff --git a/internal/controller/notification_controller.go b/internal/controller/notification_controller.go index aa173be3e..7c3afdaec 100644 --- a/internal/controller/notification_controller.go +++ b/internal/controller/notification_controller.go @@ -111,6 +111,6 @@ func (nc *NotificationController) GetList(ctx *gin.Context) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) - resp, err := nc.notificationService.GetList(ctx, req) + resp, err := nc.notificationService.GetNotificationPage(ctx, req) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/repo/notification/notification_repo.go b/internal/repo/notification/notification_repo.go index deaf431aa..37c9337ae 100644 --- a/internal/repo/notification/notification_repo.go +++ b/internal/repo/notification/notification_repo.go @@ -4,8 +4,8 @@ import ( "context" "time" - "github.com/answerdev/answer/internal/base/constant" "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" "github.com/answerdev/answer/internal/schema" @@ -29,8 +29,7 @@ func NewNotificationRepo(data *data.Data) notficationcommon.NotificationRepo { func (nr *notificationRepo) AddNotification(ctx context.Context, notification *entity.Notification) (err error) { _, err = nr.data.DB.Insert(notification) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } @@ -40,8 +39,7 @@ func (nr *notificationRepo) UpdateNotificationContent(ctx context.Context, notif notification.UpdatedAt = now _, err = nr.data.DB.Where("id =?", notification.ID).Cols("content", "updated_at").Update(notification) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } @@ -51,8 +49,7 @@ func (nr *notificationRepo) ClearUnRead(ctx context.Context, userID string, noti info.IsRead = schema.NotificationRead _, err = nr.data.DB.Where("user_id =?", userID).And("type =?", notificationType).Cols("is_read").Update(info) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } @@ -62,8 +59,7 @@ func (nr *notificationRepo) ClearIDUnRead(ctx context.Context, userID string, id info.IsRead = schema.NotificationRead _, err = nr.data.DB.Where("user_id =?", userID).And("id =?", id).Cols("is_read").Update(info) if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return } @@ -88,32 +84,22 @@ func (nr *notificationRepo) GetByUserIdObjectIdTypeId(ctx context.Context, userI return info, exist, nil } -func (nr *notificationRepo) SearchList(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error) { - var count int64 - var err error - - rows := make([]*entity.Notification, 0) - if search.UserID == "" { - return rows, 0, nil - } - if search.Page > 0 { - search.Page = search.Page - 1 - } else { - search.Page = 0 +func (nr *notificationRepo) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) ( + notificationList []*entity.Notification, total int64, err error) { + notificationList = make([]*entity.Notification, 0) + if searchCond.UserID == "" { + return notificationList, 0, nil } - if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize + + session := nr.data.DB.NewSession() + session = session.Desc("updated_at") + cond := &entity.Notification{ + UserID: searchCond.UserID, + Type: searchCond.Type, } - offset := search.Page * search.PageSize - session := nr.data.DB.Where("") - session = session.And("user_id = ?", search.UserID) - session = session.And("type = ?", search.Type) - session = session.OrderBy("updated_at desc") - session = session.Limit(search.PageSize, offset) - count, err = session.FindAndCount(&rows) + total, err = pager.Help(searchCond.Page, searchCond.PageSize, ¬ificationList, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return rows, count, err } - return rows, count, nil + return } diff --git a/internal/service/notification/notification_service.go b/internal/service/notification/notification_service.go index b8a099764..f0a9cc1f4 100644 --- a/internal/service/notification/notification_service.go +++ b/internal/service/notification/notification_service.go @@ -95,15 +95,15 @@ func (ns *NotificationService) ClearIDUnRead(ctx context.Context, userID string, return nil } -func (ns *NotificationService) GetList(ctx context.Context, search *schema.NotificationSearch) ( +func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCond *schema.NotificationSearch) ( pageModel *pager.PageModel, err error) { resp := make([]*schema.NotificationContent, 0) - searchType, ok := schema.NotificationType[search.TypeStr] + searchType, ok := schema.NotificationType[searchCond.TypeStr] if !ok { return pager.NewPageModel(0, resp), nil } - search.Type = searchType - notifications, count, err := ns.notificationRepo.SearchList(ctx, search) + searchCond.Type = searchType + notifications, total, err := ns.notificationRepo.GetNotificationPage(ctx, searchCond) if err != nil { return nil, err } @@ -123,5 +123,5 @@ func (ns *NotificationService) GetList(ctx context.Context, search *schema.Notif } resp = append(resp, item) } - return pager.NewPageModel(count, resp), nil + return pager.NewPageModel(total, resp), nil } diff --git a/internal/service/notification_common/notification.go b/internal/service/notification_common/notification.go index 3513d0c77..bf5ac725b 100644 --- a/internal/service/notification_common/notification.go +++ b/internal/service/notification_common/notification.go @@ -22,7 +22,7 @@ import ( type NotificationRepo interface { AddNotification(ctx context.Context, notification *entity.Notification) (err error) - SearchList(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error) + GetNotificationPage(ctx context.Context, search *schema.NotificationSearch) ([]*entity.Notification, int64, error) ClearUnRead(ctx context.Context, userID string, notificationType int) (err error) ClearIDUnRead(ctx context.Context, userID string, id string) (err error) GetByUserIdObjectIdTypeId(ctx context.Context, userID, objectID string, notificationType int) (*entity.Notification, bool, error) From f74c46802183484239a2f327c9ab86477911cd0a Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 26 Oct 2022 18:01:01 +0800 Subject: [PATCH 0012/3337] feat: add notification repo unit test --- internal/repo/meta/meta_repo.go | 11 -- .../repo/repo_test/notification_repo_test.go | 104 ++++++++++++++++++ internal/repo/unique/uniqid_repo.go | 15 --- internal/service/unique/uniqid_service.go | 1 - 4 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 internal/repo/repo_test/notification_repo_test.go diff --git a/internal/repo/meta/meta_repo.go b/internal/repo/meta/meta_repo.go index 8a3ea7a49..415ad4286 100644 --- a/internal/repo/meta/meta_repo.go +++ b/internal/repo/meta/meta_repo.go @@ -4,7 +4,6 @@ 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" "github.com/answerdev/answer/internal/service/meta" @@ -71,13 +70,3 @@ func (mr *metaRepo) GetMetaList(ctx context.Context, meta *entity.Meta) (metaLis } return } - -// GetMetaPage get meta page -func (mr *metaRepo) GetMetaPage(ctx context.Context, page, pageSize int, meta *entity.Meta) (metaList []*entity.Meta, total int64, err error) { - metaList = make([]*entity.Meta, 0) - total, err = pager.Help(page, pageSize, metaList, meta, mr.data.DB.NewSession()) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} diff --git a/internal/repo/repo_test/notification_repo_test.go b/internal/repo/repo_test/notification_repo_test.go new file mode 100644 index 000000000..e07255a50 --- /dev/null +++ b/internal/repo/repo_test/notification_repo_test.go @@ -0,0 +1,104 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/notification" + "github.com/answerdev/answer/internal/schema" + "github.com/stretchr/testify/assert" +) + +func buildNotificationEntity() *entity.Notification { + return &entity.Notification{ + UserID: "1", + ObjectID: "1", + Content: "1", + Type: schema.NotificationTypeInbox, + IsRead: schema.NotificationNotRead, + Status: schema.NotificationStatusNormal, + } +} + +func Test_notificationRepo_ClearIDUnRead(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + err = notificationRepo.ClearIDUnRead(context.TODO(), ent.UserID, ent.ID) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, schema.NotificationRead, got.IsRead) +} + +func Test_notificationRepo_ClearUnRead(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + err = notificationRepo.ClearUnRead(context.TODO(), ent.UserID, ent.Type) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, schema.NotificationRead, got.IsRead) +} + +func Test_notificationRepo_GetById(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, got.ID, ent.ID) +} + +func Test_notificationRepo_GetByUserIdObjectIdTypeId(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetByUserIdObjectIdTypeId(context.TODO(), ent.UserID, ent.ObjectID, ent.Type) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, got.ObjectID, ent.ObjectID) +} + +func Test_notificationRepo_GetNotificationPage(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + notificationPage, total, err := notificationRepo.GetNotificationPage(context.TODO(), &schema.NotificationSearch{UserID: userID}) + assert.NoError(t, err) + assert.True(t, total > 0) + assert.Equal(t, notificationPage[0].UserID, ent.UserID) +} + +func Test_notificationRepo_UpdateNotificationContent(t *testing.T) { + notificationRepo := notification.NewNotificationRepo(testDataSource) + ent := buildNotificationEntity() + err := notificationRepo.AddNotification(context.TODO(), ent) + assert.NoError(t, err) + + ent.Content = "test" + err = notificationRepo.UpdateNotificationContent(context.TODO(), ent) + assert.NoError(t, err) + + got, exists, err := notificationRepo.GetById(context.TODO(), ent.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.Equal(t, got.Content, ent.Content) +} diff --git a/internal/repo/unique/uniqid_repo.go b/internal/repo/unique/uniqid_repo.go index 5af8e0286..c460d904d 100644 --- a/internal/repo/unique/uniqid_repo.go +++ b/internal/repo/unique/uniqid_repo.go @@ -3,7 +3,6 @@ package unique import ( "context" "fmt" - "strconv" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/data" @@ -25,20 +24,6 @@ func NewUniqueIDRepo(data *data.Data) unique.UniqueIDRepo { } } -// GenUniqueID generate unique id -// 1 + 00x(objectType) + 000000000000x(id) -func (ur *uniqueIDRepo) GenUniqueID(ctx context.Context, key string) (uniqueID int64, err error) { - idStr, err := ur.GenUniqueIDStr(ctx, key) - if err != nil { - return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - uniqueID, err = strconv.ParseInt(idStr, 10, 64) - if err != nil { - return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return uniqueID, nil -} - // GenUniqueIDStr generate unique id string // 1 + 00x(objectType) + 000000000000x(id) func (ur *uniqueIDRepo) GenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error) { diff --git a/internal/service/unique/uniqid_service.go b/internal/service/unique/uniqid_service.go index a8bdc02c9..bca5e209b 100644 --- a/internal/service/unique/uniqid_service.go +++ b/internal/service/unique/uniqid_service.go @@ -6,6 +6,5 @@ import ( // UniqueIDRepo unique id repository type UniqueIDRepo interface { - GenUniqueID(ctx context.Context, key string) (uniqueID int64, err error) GenUniqueIDStr(ctx context.Context, key string) (uniqueID string, err error) } From 4bbe327e9687fa014a3d14932a6b1a3a146bf2d3 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 27 Oct 2022 10:22:43 +0800 Subject: [PATCH 0013/3337] update github actions --- .github/workflows/build_dockerhub_img.yml | 4 ++-- .github/workflows/build_github_img.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_dockerhub_img.yml b/.github/workflows/build_dockerhub_img.yml index 52e402058..b92958921 100644 --- a/.github/workflows/build_dockerhub_img.yml +++ b/.github/workflows/build_dockerhub_img.yml @@ -7,8 +7,8 @@ on: - 2.* - 1.* - 0.* - pull_request: - branches: [ "main" ] + # pull_request: + # branches: [ "main" ] jobs: build: diff --git a/.github/workflows/build_github_img.yml b/.github/workflows/build_github_img.yml index 6a6744755..358a296b3 100644 --- a/.github/workflows/build_github_img.yml +++ b/.github/workflows/build_github_img.yml @@ -7,8 +7,8 @@ on: - 2.* - 1.* - 0.* - pull_request: - branches: [ "main" ] + # pull_request: + # branches: [ "main" ] env: From c3b096edc59d4a932dca4f90d26eb8ca9c2176aa Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 27 Oct 2022 10:41:57 +0800 Subject: [PATCH 0014/3337] fix question tags search --- docs/docs.go | 9 +++------ docs/swagger.json | 9 +++------ docs/swagger.yaml | 9 ++++----- internal/schema/question_schema.go | 13 +++++++------ internal/service/question_service.go | 13 ++++++++----- 5 files changed, 25 insertions(+), 28 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 51596b919..e7df3ce07 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4961,12 +4961,9 @@ const docTemplate = `{ "description": "Search page size", "type": "integer" }, - "tags": { - "description": "Search tag", - "type": "array", - "items": { - "type": "string" - } + "tag": { + "description": "Tags []string ` + "`" + `json:\"tags\" form:\"tags\"` + "`" + ` //Search tag", + "type": "string" }, "username": { "description": "Search username", diff --git a/docs/swagger.json b/docs/swagger.json index bd08e5547..1063944b7 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4949,12 +4949,9 @@ "description": "Search page size", "type": "integer" }, - "tags": { - "description": "Search tag", - "type": "array", - "items": { - "type": "string" - } + "tag": { + "description": "Tags []string `json:\"tags\" form:\"tags\"` //Search tag", + "type": "string" }, "username": { "description": "Search username", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index cf6bcd49b..bde9a77d6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -762,11 +762,10 @@ definitions: page_size: description: Search page size type: integer - tags: - description: Search tag - items: - type: string - type: array + tag: + description: Tags []string `json:"tags" form:"tags"` //Search + tag + type: string username: description: Search username type: string diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index a619c4544..5bd3b4c18 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -156,12 +156,13 @@ type UserQuestionInfo struct { } type QuestionSearch struct { - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size - Order string `json:"order" form:"order"` //Search order by - Tags []string `json:"tags" form:"tags"` //Search tag - TagIDs []string `json:"-" form:"-"` //Search tag - UserName string `json:"username" form:"username"` //Search username + Page int `json:"page" form:"page"` //Query number of pages + PageSize int `json:"page_size" form:"page_size"` //Search page size + Order string `json:"order" form:"order"` //Search order by + //Tags []string `json:"tags" form:"tags"` //Search tag + Tag string `json:"tag" form:"tag"` //Search tag + TagIDs []string `json:"-" form:"-"` //Search tag + UserName string `json:"username" form:"username"` //Search username UserID string `json:"-" form:"-"` } diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 881786543..8aa598980 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -481,20 +481,23 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin search.Order = "frequent" search.Page = 0 search.PageSize = 6 - search.Tags = tagNames + if len(tagNames) > 0 { + search.Tag = tagNames[0] + } return qs.SearchList(ctx, search, loginUserID) } // SearchList func (qs *QuestionService) SearchList(ctx context.Context, req *schema.QuestionSearch, loginUserID string) ([]*schema.QuestionInfo, int64, error) { - if len(req.Tags) > 0 { - taginfo, err := qs.tagCommon.GetTagListByNames(ctx, req.Tags) + if len(req.Tag) > 0 { + taginfo, has, err := qs.tagCommon.GetTagListByName(ctx, req.Tag) if err != nil { log.Error("tagCommon.GetTagListByNames error", err) } - for _, tag := range taginfo { - req.TagIDs = append(req.TagIDs, tag.ID) + if has { + req.TagIDs = append(req.TagIDs, taginfo.ID) } + } list := make([]*schema.QuestionInfo, 0) if req.UserName != "" { From 413700ece70b5995f52acaf1f5c8d87fe7ef71cf Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 27 Oct 2022 10:47:08 +0800 Subject: [PATCH 0015/3337] feat: init admin set rank --- internal/migrations/init.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 0601ffdd7..2e17f5e6d 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -77,6 +77,7 @@ func initAdminUser(engine *xorm.Engine) error { MailStatus: 1, NoticeStatus: 1, Status: 1, + Rank: 1, DisplayName: "admin", IsAdmin: true, }) From d985024877811301c74cedb9179ab48fb6007466 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 27 Oct 2022 10:50:10 +0800 Subject: [PATCH 0016/3337] fix: #27 Fixed tag question list parameter --- ui/src/common/interface.ts | 2 +- ui/src/components/QuestionList/index.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 6558a97cc..4f39ecbe1 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -218,7 +218,7 @@ export type QuestionOrderBy = export interface QueryQuestionsReq extends Paging { order: QuestionOrderBy; - tags?: string[]; + tag?: string; } export type AdminQuestionStatus = 'available' | 'closed' | 'deleted'; diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 295b90346..2e487a2d1 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -92,11 +92,11 @@ const QuestionList: FC = ({ source }) => { page_size: pageSize, page: curPage, order: curOrder as Type.QuestionOrderBy, - tags: [tagName], + tag: tagName, }; if (source === 'questions') { - delete reqParams.tags; + delete reqParams.tag; } const { data: listData, isLoading } = useQuestionList(reqParams); const count = listData?.count || 0; From 2e13736ee72853592b61f83d8253845eabab4e97 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 27 Oct 2022 11:23:16 +0800 Subject: [PATCH 0017/3337] feat: add siteinfo repo unit test --- cmd/answer/wire_gen.go | 3 +- internal/repo/provider.go | 3 +- internal/repo/repo_test/siteinfo_repo_test.go | 33 +++++++++++++++++++ .../repo/{ => site_info}/siteinfo_repo.go | 2 +- 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 internal/repo/repo_test/siteinfo_repo_test.go rename internal/repo/{ => site_info}/siteinfo_repo.go (98%) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index eef3585e2..36ba55de6 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -30,6 +30,7 @@ import ( "github.com/answerdev/answer/internal/repo/reason" "github.com/answerdev/answer/internal/repo/report" "github.com/answerdev/answer/internal/repo/revision" + "github.com/answerdev/answer/internal/repo/site_info" "github.com/answerdev/answer/internal/repo/tag" "github.com/answerdev/answer/internal/repo/unique" "github.com/answerdev/answer/internal/repo/user" @@ -162,7 +163,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_backyard.NewThemeController() - siteInfoRepo := repo.NewSiteInfo(dataData) + siteInfoRepo := site_info.NewSiteInfo(dataData) siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) siteinfoController := controller.NewSiteinfoController(siteInfoService) diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 16c751ec0..7966d797a 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -17,6 +17,7 @@ import ( "github.com/answerdev/answer/internal/repo/reason" "github.com/answerdev/answer/internal/repo/report" "github.com/answerdev/answer/internal/repo/revision" + "github.com/answerdev/answer/internal/repo/site_info" "github.com/answerdev/answer/internal/repo/tag" "github.com/answerdev/answer/internal/repo/unique" "github.com/answerdev/answer/internal/repo/user" @@ -58,6 +59,6 @@ var ProviderSetRepo = wire.NewSet( meta.NewMetaRepo, export.NewEmailRepo, reason.NewReasonRepo, - NewSiteInfo, + site_info.NewSiteInfo, notification.NewNotificationRepo, ) diff --git a/internal/repo/repo_test/siteinfo_repo_test.go b/internal/repo/repo_test/siteinfo_repo_test.go new file mode 100644 index 000000000..d96f14c19 --- /dev/null +++ b/internal/repo/repo_test/siteinfo_repo_test.go @@ -0,0 +1,33 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/site_info" + "github.com/stretchr/testify/assert" +) + +func Test_siteInfoRepo_SaveByType(t *testing.T) { + siteInfoRepo := site_info.NewSiteInfo(testDataSource) + + data := &entity.SiteInfo{Content: "site_info", Type: "test"} + + err := siteInfoRepo.SaveByType(context.TODO(), data.Type, data) + assert.NoError(t, err) + + got, exist, err := siteInfoRepo.GetByType(context.TODO(), data.Type) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, data.Content, got.Content) + + data.Content = "new site_info" + err = siteInfoRepo.SaveByType(context.TODO(), data.Type, data) + assert.NoError(t, err) + + got, exist, err = siteInfoRepo.GetByType(context.TODO(), data.Type) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, data.Content, got.Content) +} diff --git a/internal/repo/siteinfo_repo.go b/internal/repo/site_info/siteinfo_repo.go similarity index 98% rename from internal/repo/siteinfo_repo.go rename to internal/repo/site_info/siteinfo_repo.go index 579d9bb15..1063e94f1 100644 --- a/internal/repo/siteinfo_repo.go +++ b/internal/repo/site_info/siteinfo_repo.go @@ -1,4 +1,4 @@ -package repo +package site_info import ( "context" From 3c8bdcde29b67b98f86d46e9623312671c837b11 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 27 Oct 2022 12:00:03 +0800 Subject: [PATCH 0018/3337] feat: add config repo unit test --- internal/repo/config/config_repo.go | 47 ++++++++------ internal/repo/repo_test/config_repo_test.go | 62 +++++++++++++++++++ internal/service/config/config_service.go | 2 +- internal/service/question_common/question.go | 2 +- .../report_backyard/report_backyard.go | 4 +- 5 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 internal/repo/repo_test/config_repo_test.go diff --git a/internal/repo/config/config_repo.go b/internal/repo/config/config_repo.go index 4f97ad684..329e2b652 100644 --- a/internal/repo/config/config_repo.go +++ b/internal/repo/config/config_repo.go @@ -65,10 +65,14 @@ func (cr *configRepo) Get(key string) (interface{}, error) { // key string func (cr *configRepo) GetString(key string) (string, error) { value, err := cr.Get(key) - if value != nil { - return value.(string), err + if err != nil { + return "", err + } + str, ok := value.(string) + if !ok { + return "", errors.InternalServer(reason.DatabaseError).WithMsg(fmt.Sprintf("config value is wrong type: %v", key)) } - return "", err + return str, nil } // GetInt method for getting the config value to int64 @@ -77,9 +81,8 @@ func (cr *configRepo) GetInt(key string) (int, error) { value, err := cr.GetString(key) if err != nil { return 0, err - } else { - return converter.StringToInt(value), nil } + return converter.StringToInt(value), nil } // GetArrayString method for getting the config value to string array @@ -96,31 +99,35 @@ func (cr *configRepo) GetArrayString(key string) ([]string, error) { // GetConfigType method for getting the config type func (cr *configRepo) GetConfigType(key string) (int, error) { value, ok := Key2IDMapping[key] - if ok { - return value, nil - } else { + if !ok { return 0, errors.InternalServer(reason.DatabaseError).WithMsg(fmt.Sprintf("no such config type: %v", key)) } + return value, nil } -// GetConfigById get config key from config id -func (cr *configRepo) GetConfigById(id int, value any) (err error) { - var ( - ok = true - key string - conf interface{} - ) - key, ok = ID2KeyMapping[id] +// GetJsonConfigByIDAndSetToObject get config key from config id +func (cr *configRepo) GetJsonConfigByIDAndSetToObject(id int, object any) (err error) { + key, ok := ID2KeyMapping[id] if !ok { - err = errors.InternalServer(reason.DatabaseError).WithMsg(fmt.Sprintf("no such config id: %v", id)) - return + return errors.InternalServer(reason.DatabaseError).WithMsg(fmt.Sprintf("no such config id: %v", id)) } - conf, err = cr.Get(key) - value = json.Unmarshal([]byte(conf.(string)), value) + conf, err := cr.Get(key) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err) + } + str, ok := conf.(string) + if !ok { + return errors.InternalServer(reason.DatabaseError).WithMsg(fmt.Sprintf("no such config id: %v", id)) + } + err = json.Unmarshal([]byte(str), object) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithMsg(fmt.Sprintf("no such config id: %v", id)) + } return } +// SetConfig set config func (cr *configRepo) SetConfig(key, value string) (err error) { id := Key2IDMapping[key] _, err = cr.data.DB.ID(id).Update(&entity.Config{Value: value}) diff --git a/internal/repo/repo_test/config_repo_test.go b/internal/repo/repo_test/config_repo_test.go new file mode 100644 index 000000000..5f76ddefd --- /dev/null +++ b/internal/repo/repo_test/config_repo_test.go @@ -0,0 +1,62 @@ +package repo_test + +import ( + "testing" + + "github.com/answerdev/answer/internal/repo/config" + "github.com/answerdev/answer/internal/schema" + "github.com/stretchr/testify/assert" +) + +func Test_configRepo_Get(t *testing.T) { + configRepo := config.NewConfigRepo(testDataSource) + _, err := configRepo.Get("email.config") + assert.NoError(t, err) +} + +func Test_configRepo_GetArrayString(t *testing.T) { + configRepo := config.NewConfigRepo(testDataSource) + got, err := configRepo.GetArrayString("daily_rank_limit.exclude") + assert.NoError(t, err) + assert.Equal(t, 1, len(got)) + assert.Equal(t, "answer.accepted", got[0]) +} + +func Test_configRepo_GetConfigById(t *testing.T) { + configRepo := config.NewConfigRepo(testDataSource) + + closeInfo := &schema.GetReportTypeResp{} + err := configRepo.GetJsonConfigByIDAndSetToObject(74, closeInfo) + + assert.NoError(t, err) + assert.Equal(t, "needs close", closeInfo.Name) +} + +func Test_configRepo_GetConfigType(t *testing.T) { + configRepo := config.NewConfigRepo(testDataSource) + configType, err := configRepo.GetConfigType("answer.accepted") + assert.NoError(t, err) + assert.Equal(t, 1, configType) +} + +func Test_configRepo_GetInt(t *testing.T) { + configRepo := config.NewConfigRepo(testDataSource) + got, err := configRepo.GetInt("answer.accepted") + assert.NoError(t, err) + assert.Equal(t, 15, got) +} + +func Test_configRepo_GetString(t *testing.T) { + configRepo := config.NewConfigRepo(testDataSource) + _, err := configRepo.GetString("email.config") + assert.NoError(t, err) +} + +func Test_configRepo_SetConfig(t *testing.T) { + configRepo := config.NewConfigRepo(testDataSource) + got, err := configRepo.GetString("email.config") + assert.NoError(t, err) + + err = configRepo.SetConfig("email.config", got) + assert.NoError(t, err) +} diff --git a/internal/service/config/config_service.go b/internal/service/config/config_service.go index 818b7f28b..882b8fa5e 100644 --- a/internal/service/config/config_service.go +++ b/internal/service/config/config_service.go @@ -7,7 +7,7 @@ type ConfigRepo interface { GetInt(key string) (int, error) GetArrayString(key string) ([]string, error) GetConfigType(key string) (int, error) - GetConfigById(id int, value any) (err error) + GetJsonConfigByIDAndSetToObject(id int, value any) (err error) SetConfig(key, value string) (err error) } diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index c9c105c8a..98b9db8ad 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -156,7 +156,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUser log.Error("json.Unmarshal CloseQuestionMeta error", err.Error()) } else { closeinfo := &schema.GetReportTypeResp{} - err = qs.configRepo.GetConfigById(closemsg.CloseType, closeinfo) + err = qs.configRepo.GetJsonConfigByIDAndSetToObject(closemsg.CloseType, closeinfo) if err != nil { log.Error("json.Unmarshal QuestionCloseJson error", err.Error()) } else { diff --git a/internal/service/report_backyard/report_backyard.go b/internal/service/report_backyard/report_backyard.go index 20249fced..947e71972 100644 --- a/internal/service/report_backyard/report_backyard.go +++ b/internal/service/report_backyard/report_backyard.go @@ -201,13 +201,13 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem r.Reason = &schema.ReasonItem{ ReasonType: r.ReportType, } - err = rs.configRepo.GetConfigById(r.ReportType, r.Reason) + err = rs.configRepo.GetJsonConfigByIDAndSetToObject(r.ReportType, r.Reason) } if r.FlaggedType > 0 { r.FlaggedReason = &schema.ReasonItem{ ReasonType: r.FlaggedType, } - _ = rs.configRepo.GetConfigById(r.FlaggedType, r.FlaggedReason) + _ = rs.configRepo.GetJsonConfigByIDAndSetToObject(r.FlaggedType, r.FlaggedReason) } res[i] = r From 792ef23218b225383d6218c9e82b9bae70860016 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 27 Oct 2022 15:40:11 +0800 Subject: [PATCH 0019/3337] style: repo change directories --- cmd/answer/wire_gen.go | 12 +++++++----- internal/repo/{ => activity_common}/activity_repo.go | 2 +- internal/repo/{ => answer}/answer_repo.go | 2 +- internal/repo/provider.go | 11 +++++++---- internal/repo/{ => question}/question_repo.go | 2 +- internal/repo/{ => search_common}/search_repo.go | 2 +- 6 files changed, 18 insertions(+), 13 deletions(-) rename internal/repo/{ => activity_common}/activity_repo.go (99%) rename internal/repo/{ => answer}/answer_repo.go (99%) rename internal/repo/{ => question}/question_repo.go (99%) rename internal/repo/{ => search_common}/search_repo.go (99%) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 36ba55de6..d2d0a5db7 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -14,9 +14,9 @@ import ( "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/controller" "github.com/answerdev/answer/internal/controller_backyard" - "github.com/answerdev/answer/internal/repo" "github.com/answerdev/answer/internal/repo/activity" "github.com/answerdev/answer/internal/repo/activity_common" + "github.com/answerdev/answer/internal/repo/answer" "github.com/answerdev/answer/internal/repo/auth" "github.com/answerdev/answer/internal/repo/captcha" "github.com/answerdev/answer/internal/repo/collection" @@ -26,10 +26,12 @@ import ( "github.com/answerdev/answer/internal/repo/export" "github.com/answerdev/answer/internal/repo/meta" "github.com/answerdev/answer/internal/repo/notification" + "github.com/answerdev/answer/internal/repo/question" "github.com/answerdev/answer/internal/repo/rank" "github.com/answerdev/answer/internal/repo/reason" "github.com/answerdev/answer/internal/repo/report" "github.com/answerdev/answer/internal/repo/revision" + "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/unique" @@ -93,7 +95,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, configRepo := config.NewConfigRepo(dataData) userRepo := user.NewUserRepo(dataData, configRepo) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) - activityRepo := repo.NewActivityRepo(dataData, uniqueIDRepo, configRepo) + activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo) userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) emailRepo := export.NewEmailRepo(dataData) @@ -106,8 +108,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) userCommon := usercommon.NewUserCommon(userRepo) - answerRepo := repo.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) - questionRepo := repo.NewQuestionRepo(dataData, uniqueIDRepo) + 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) voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) @@ -146,7 +148,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) answerController := controller.NewAnswerController(answerService, rankService) - searchRepo := repo.NewSearchRepo(dataData, uniqueIDRepo, userCommon) + searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) searchController := controller.NewSearchController(searchService) serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService) diff --git a/internal/repo/activity_repo.go b/internal/repo/activity_common/activity_repo.go similarity index 99% rename from internal/repo/activity_repo.go rename to internal/repo/activity_common/activity_repo.go index df14ae019..9abc3e6ff 100644 --- a/internal/repo/activity_repo.go +++ b/internal/repo/activity_common/activity_repo.go @@ -1,4 +1,4 @@ -package repo +package activity_common import ( "context" diff --git a/internal/repo/answer_repo.go b/internal/repo/answer/answer_repo.go similarity index 99% rename from internal/repo/answer_repo.go rename to internal/repo/answer/answer_repo.go index dd4753e87..95a989397 100644 --- a/internal/repo/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -1,4 +1,4 @@ -package repo +package answer import ( "context" diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 7966d797a..6dcbaa6ac 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -4,6 +4,7 @@ import ( "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/repo/activity" "github.com/answerdev/answer/internal/repo/activity_common" + "github.com/answerdev/answer/internal/repo/answer" "github.com/answerdev/answer/internal/repo/auth" "github.com/answerdev/answer/internal/repo/captcha" "github.com/answerdev/answer/internal/repo/collection" @@ -13,10 +14,12 @@ import ( "github.com/answerdev/answer/internal/repo/export" "github.com/answerdev/answer/internal/repo/meta" "github.com/answerdev/answer/internal/repo/notification" + "github.com/answerdev/answer/internal/repo/question" "github.com/answerdev/answer/internal/repo/rank" "github.com/answerdev/answer/internal/repo/reason" "github.com/answerdev/answer/internal/repo/report" "github.com/answerdev/answer/internal/repo/revision" + "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/unique" @@ -41,9 +44,9 @@ var ProviderSetRepo = wire.NewSet( user.NewUserRepo, user.NewUserBackyardRepo, rank.NewUserRankRepo, - NewQuestionRepo, - NewAnswerRepo, - NewActivityRepo, + question.NewQuestionRepo, + answer.NewAnswerRepo, + activity_common.NewActivityRepo, activity.NewVoteRepo, activity.NewFollowRepo, activity.NewAnswerActivityRepo, @@ -55,7 +58,7 @@ var ProviderSetRepo = wire.NewSet( collection.NewCollectionGroupRepo, auth.NewAuthRepo, revision.NewRevisionRepo, - NewSearchRepo, + search_common.NewSearchRepo, meta.NewMetaRepo, export.NewEmailRepo, reason.NewReasonRepo, diff --git a/internal/repo/question_repo.go b/internal/repo/question/question_repo.go similarity index 99% rename from internal/repo/question_repo.go rename to internal/repo/question/question_repo.go index 8554fea18..75ed710de 100644 --- a/internal/repo/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -1,4 +1,4 @@ -package repo +package question import ( "context" diff --git a/internal/repo/search_repo.go b/internal/repo/search_common/search_repo.go similarity index 99% rename from internal/repo/search_repo.go rename to internal/repo/search_common/search_repo.go index abf533606..94c69f626 100644 --- a/internal/repo/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -1,4 +1,4 @@ -package repo +package search_common import ( "context" From 95deb1b0d8a6aa72fdb4182044019a640d75c379 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 27 Oct 2022 15:42:25 +0800 Subject: [PATCH 0020/3337] fix: #1087 jittering of internal text when search box is focused --- ui/src/components/Header/index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/Header/index.scss b/ui/src/components/Header/index.scss index 64a3d67f5..6b4ed9283 100644 --- a/ui/src/components/Header/index.scss +++ b/ui/src/components/Header/index.scss @@ -20,7 +20,7 @@ } .placeholder-search { background-color: rgba(255, 255, 255, .2); - border: 0; + border: $border-width $border-style transparent; &:focus { border: $border-width $border-style $border-color; } From ae462618cbaf5218ef42ffe51de4e6dcd8040ec6 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 27 Oct 2022 15:45:59 +0800 Subject: [PATCH 0021/3337] fix: navbar search input border color changed --- ui/src/components/Header/index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/Header/index.scss b/ui/src/components/Header/index.scss index 6b4ed9283..348632a37 100644 --- a/ui/src/components/Header/index.scss +++ b/ui/src/components/Header/index.scss @@ -20,7 +20,7 @@ } .placeholder-search { background-color: rgba(255, 255, 255, .2); - border: $border-width $border-style transparent; + border: $border-width $border-style rgba(255, 255, 255, .2); &:focus { border: $border-width $border-style $border-color; } From 3f48de2fe96a625677057c940d4e20fc014ccd59 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 27 Oct 2022 18:09:27 +0800 Subject: [PATCH 0022/3337] add avatar middleware --- .gitignore | 1 + cmd/answer/wire_gen.go | 3 +- go.mod | 1 + go.sum | 3 ++ internal/base/middleware/avatar.go | 41 ++++++++++++++++++++++++++ internal/base/middleware/provider.go | 1 + internal/base/server/http.go | 8 +++-- internal/controller/user_controller.go | 4 ++- internal/service/uploader/upload.go | 34 +++++++++++++++++++++ 9 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 internal/base/middleware/avatar.go diff --git a/.gitignore b/.gitignore index c0aed5e72..239e76e67 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /.fleet /.vscode/*.log /cmd/answer/*.sh +/cmd/answer/upfiles/* /cmd/logs /configs/config-dev.yaml /go.work* diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index cc83240d0..2a9e9f5f5 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -174,7 +174,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter() authUserMiddleware := middleware.NewAuthUserMiddleware(authService) - ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware) + avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf) + ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware) application := newApplication(serverConf, ginEngine) return application, func() { cleanup2() diff --git a/go.mod b/go.mod index 7fe0bfd9e..976db1a03 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Chain-Zhang/pinyin v0.1.3 github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267 github.com/bwmarrin/snowflake v0.3.0 + github.com/disintegration/imaging v1.6.2 github.com/gin-gonic/gin v1.8.1 github.com/go-playground/locales v0.14.0 github.com/go-playground/universal-translator v0.18.0 diff --git a/go.sum b/go.sum index 238004c45..99b56caeb 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -678,6 +680,7 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go new file mode 100644 index 000000000..73b539ae4 --- /dev/null +++ b/internal/base/middleware/avatar.go @@ -0,0 +1,41 @@ +package middleware + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + "github.com/answerdev/answer/internal/service/service_config" + "github.com/gin-gonic/gin" +) + +type AvatarMiddleware struct { + serviceConfig *service_config.ServiceConfig +} + +// NewAvatarMiddleware new auth user middleware +func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig) *AvatarMiddleware { + return &AvatarMiddleware{ + serviceConfig: serviceConfig, + } +} + +func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { + return func(ctx *gin.Context) { + url := ctx.Request.RequestURI + if strings.Contains(url, "/uploads/avatar/") { + _, fileName := filepath.Split(url) + filepath := fmt.Sprintf("%s/avatar/%s", am.serviceConfig.UploadPath, fileName) + f, err := ioutil.ReadFile(filepath) + if err != nil { + ctx.Next() + return + } + ctx.Writer.WriteString(string(f)) + ctx.Abort() + return + } + ctx.Next() + } +} diff --git a/internal/base/middleware/provider.go b/internal/base/middleware/provider.go index a07318d92..db89854fa 100644 --- a/internal/base/middleware/provider.go +++ b/internal/base/middleware/provider.go @@ -7,4 +7,5 @@ import ( // ProviderSetMiddleware is providers. var ProviderSetMiddleware = wire.NewSet( NewAuthUserMiddleware, + NewAvatarMiddleware, ) diff --git a/internal/base/server/http.go b/internal/base/server/http.go index c1eb86a66..712201628 100644 --- a/internal/base/server/http.go +++ b/internal/base/server/http.go @@ -13,7 +13,9 @@ func NewHTTPServer(debug bool, answerRouter *router.AnswerAPIRouter, swaggerRouter *router.SwaggerRouter, viewRouter *router.UIRouter, - authUserMiddleware *middleware.AuthUserMiddleware) *gin.Engine { + authUserMiddleware *middleware.AuthUserMiddleware, + avatarMiddleware *middleware.AvatarMiddleware, +) *gin.Engine { if debug { gin.SetMode(gin.DebugMode) @@ -28,7 +30,9 @@ func NewHTTPServer(debug bool, rootGroup := r.Group("") swaggerRouter.Register(rootGroup) - staticRouter.RegisterStaticRouter(rootGroup) + static := r.Group("") + static.Use(avatarMiddleware.AvatarThumb()) + staticRouter.RegisterStaticRouter(static) // register api that no need to login unAuthV1 := r.Group("/answer/api/v1") diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 390b72ced..4c3460357 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -376,7 +376,9 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) { // @Router /answer/api/v1/user/avatar/upload [post] func (uc *UserController) UploadUserAvatar(ctx *gin.Context) { // max size - ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024) + 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()) diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 029a8b6a4..917eb47a5 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -1,8 +1,11 @@ package uploader import ( + "bytes" "fmt" + "io" "mime/multipart" + "os" "path" "path/filepath" @@ -10,6 +13,7 @@ import ( "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/uid" + "github.com/disintegration/imaging" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) @@ -46,6 +50,36 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.Fi return us.uploadFile(ctx, file, avatarFilePath) } +func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, header *multipart.FileHeader, file multipart.File, fileExt string) ( + url string, err error) { + + img, err := imaging.Decode(file) + if err != nil { + return "", err + } + formatImg := imaging.Resize(img, 1024, 0, imaging.Linear) + var buf bytes.Buffer + err = imaging.Encode(&buf, formatImg, imaging.JPEG) + if err != nil { + return "", err + } + reader := bytes.NewReader(buf.Bytes()) + newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) + avatarFilePath := path.Join(avatarSubPath, newFilename) + filePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) + out, err := os.Create(filePath) + if err != nil { + return "", err + } + defer out.Close() + _, err = io.Copy(out, reader) + if err != nil { + return "", err + } + url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, avatarFilePath) + return url, nil +} + func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( url string, err error) { newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) From dcf14df50a371c53d6a172ad25c9ebbcf8fbe81e Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 27 Oct 2022 19:21:49 +0800 Subject: [PATCH 0023/3337] feat: add tag repo unit test --- cmd/answer/wire_gen.go | 2 +- internal/repo/provider.go | 2 +- internal/repo/repo_test/tag_rel_repo_test.go | 32 ++++ internal/repo/repo_test/tag_repo_test.go | 173 +++++++++++++++++++ internal/repo/tag/tag_rel_repo.go | 33 ++-- internal/service/tag_common/tag_common.go | 1 - 6 files changed, 219 insertions(+), 24 deletions(-) create mode 100644 internal/repo/repo_test/tag_rel_repo_test.go create mode 100644 internal/repo/repo_test/tag_repo_test.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index d2d0a5db7..7d5331f20 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -132,7 +132,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, followController := controller.NewFollowController(followService) collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo) collectionGroupRepo := collection.NewCollectionGroupRepo(dataData) - tagRelRepo := tag.NewTagListRepo(dataData) + tagRelRepo := tag.NewTagRelRepo(dataData) tagCommonService := tagcommon.NewTagCommonService(tagRepo, tagRelRepo, revisionService) collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) answerCommon := answercommon.NewAnswerCommon(answerRepo) diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 6dcbaa6ac..54d43c100 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -53,7 +53,7 @@ var ProviderSetRepo = wire.NewSet( activity.NewQuestionActivityRepo, activity.NewUserActiveActivityRepo, tag.NewTagRepo, - tag.NewTagListRepo, + tag.NewTagRelRepo, collection.NewCollectionRepo, collection.NewCollectionGroupRepo, auth.NewAuthRepo, diff --git a/internal/repo/repo_test/tag_rel_repo_test.go b/internal/repo/repo_test/tag_rel_repo_test.go new file mode 100644 index 000000000..4df8138a8 --- /dev/null +++ b/internal/repo/repo_test/tag_rel_repo_test.go @@ -0,0 +1,32 @@ +package repo_test + +import ( + "testing" +) + +func Test_tagListRepo_AddTagRelList(t *testing.T) { +} + +func Test_tagListRepo_BatchGetObjectTagRelList(t *testing.T) { + +} + +func Test_tagListRepo_CountTagRelByTagID(t *testing.T) { + +} + +func Test_tagListRepo_EnableTagRelByIDs(t *testing.T) { + +} + +func Test_tagListRepo_GetObjectTagRelList(t *testing.T) { + +} + +func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) { + +} + +func Test_tagListRepo_RemoveTagRelListByIDs(t *testing.T) { + +} diff --git a/internal/repo/repo_test/tag_repo_test.go b/internal/repo/repo_test/tag_repo_test.go new file mode 100644 index 000000000..9c2e012a6 --- /dev/null +++ b/internal/repo/repo_test/tag_repo_test.go @@ -0,0 +1,173 @@ +package repo_test + +import ( + "context" + "fmt" + "sync" + "testing" + + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/tag" + "github.com/answerdev/answer/internal/repo/unique" + "github.com/answerdev/answer/pkg/converter" + "github.com/stretchr/testify/assert" +) + +var ( + tagOnce sync.Once + testTagList = []*entity.Tag{ + { + SlugName: "go", + DisplayName: "Golang", + OriginalText: "golang", + ParsedText: "

golang

", + Status: entity.TagStatusAvailable, + }, + { + SlugName: "js", + DisplayName: "javascript", + OriginalText: "javascript", + ParsedText: "

javascript

", + Status: entity.TagStatusAvailable, + }, + { + SlugName: "go2", + DisplayName: "Golang2", + OriginalText: "golang2", + ParsedText: "

golang2

", + Status: entity.TagStatusAvailable, + }, + } +) + +func addTagList() { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) + err := tagRepo.AddTagList(context.TODO(), testTagList) + if err != nil { + panic(err) + } +} + +func Test_tagRepo_GetTagByID(t *testing.T) { + tagOnce.Do(addTagList) + tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName) +} + +func Test_tagRepo_GetTagBySlugName(t *testing.T) { + tagOnce.Do(addTagList) + tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTag, exist, err := tagRepo.GetTagBySlugName(context.TODO(), testTagList[0].SlugName) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName) +} + +func Test_tagRepo_GetTagList(t *testing.T) { + tagOnce.Do(addTagList) + tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTags, err := tagRepo.GetTagList(context.TODO(), &entity.Tag{ID: testTagList[0].ID}) + assert.NoError(t, err) + assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) +} + +func Test_tagRepo_GetTagListByIDs(t *testing.T) { + tagOnce.Do(addTagList) + tagRepo := tag.NewTagRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource)) + + gotTags, err := tagRepo.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)) + + gotTags, err := tagRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, 1) + 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)) + + gotTags, err := tagRepo.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)) + + gotTags, _, err := tagRepo.GetTagPage(context.TODO(), 1, 1, &entity.Tag{SlugName: testTagList[0].SlugName}, "") + assert.NoError(t, err) + assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName) +} + +func Test_tagRepo_RemoveTag(t *testing.T) { + tagOnce.Do(addTagList) + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) + err := tagRepo.RemoveTag(context.TODO(), testTagList[1].ID) + assert.NoError(t, err) + + _, exist, err := tagRepo.GetTagBySlugName(context.TODO(), testTagList[1].SlugName) + assert.NoError(t, err) + assert.False(t, exist) +} + +func Test_tagRepo_UpdateTag(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) + + testTagList[0].DisplayName = "golang" + err := tagRepo.UpdateTag(context.TODO(), testTagList[0]) + assert.NoError(t, err) + + gotTag, exist, err := tagRepo.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) + + testTagList[0].DisplayName = "golang" + err := tagRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100) + assert.NoError(t, err) + + gotTag, exist, err := tagRepo.GetTagByID(context.TODO(), testTagList[0].ID) + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, 100, gotTag.QuestionCount) +} + +func Test_tagRepo_UpdateTagSynonym(t *testing.T) { + uniqueIDRepo := unique.NewUniqueIDRepo(testDataSource) + tagRepo := tag.NewTagRepo(testDataSource, uniqueIDRepo) + + testTagList[0].DisplayName = "golang" + err := tagRepo.UpdateTag(context.TODO(), testTagList[0]) + assert.NoError(t, err) + + err = tagRepo.UpdateTagSynonym(context.TODO(), []string{testTagList[2].SlugName}, + converter.StringToInt64(testTagList[0].ID), testTagList[0].SlugName) + assert.NoError(t, err) + + gotTag, exist, err := tagRepo.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)) +} diff --git a/internal/repo/tag/tag_rel_repo.go b/internal/repo/tag/tag_rel_repo.go index c54bf8c0d..0996f7f62 100644 --- a/internal/repo/tag/tag_rel_repo.go +++ b/internal/repo/tag/tag_rel_repo.go @@ -10,20 +10,20 @@ import ( "github.com/segmentfault/pacman/errors" ) -// tagListRepo tagList repository -type tagListRepo struct { +// tagRelRepo tag rel repository +type tagRelRepo struct { data *data.Data } -// NewTagListRepo new repository -func NewTagListRepo(data *data.Data) tagcommon.TagRelRepo { - return &tagListRepo{ +// NewTagRelRepo new repository +func NewTagRelRepo(data *data.Data) tagcommon.TagRelRepo { + return &tagRelRepo{ data: data, } } // AddTagRelList add tag list -func (tr *tagListRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) { +func (tr *tagRelRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) { _, err = tr.data.DB.Insert(tagList) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -31,17 +31,8 @@ func (tr *tagListRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagR return } -// RemoveTagRelListByObjectID delete tag list -func (tr *tagListRepo) RemoveTagRelListByObjectID(ctx context.Context, objectId string) (err error) { - _, err = tr.data.DB.Where("object_id = ?", objectId).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - // RemoveTagRelListByIDs delete tag list -func (tr *tagListRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) { +func (tr *tagRelRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) { _, err = tr.data.DB.In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -50,7 +41,7 @@ func (tr *tagListRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) ( } // GetObjectTagRelWithoutStatus get object tag relation no matter status -func (tr *tagListRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) ( +func (tr *tagRelRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) ( tagRel *entity.TagRel, exist bool, err error) { tagRel = &entity.TagRel{} session := tr.data.DB.Where("object_id = ?", objectId).And("tag_id = ?", tagID) @@ -62,7 +53,7 @@ func (tr *tagListRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectI } // EnableTagRelByIDs update tag status to available -func (tr *tagListRepo) EnableTagRelByIDs(ctx context.Context, ids []int64) (err error) { +func (tr *tagRelRepo) EnableTagRelByIDs(ctx context.Context, ids []int64) (err error) { _, err = tr.data.DB.In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -71,7 +62,7 @@ func (tr *tagListRepo) EnableTagRelByIDs(ctx context.Context, ids []int64) (err } // GetObjectTagRelList get object tag relation list all -func (tr *tagListRepo) GetObjectTagRelList(ctx context.Context, objectId string) (tagListList []*entity.TagRel, err error) { +func (tr *tagRelRepo) GetObjectTagRelList(ctx context.Context, objectId string) (tagListList []*entity.TagRel, err error) { tagListList = make([]*entity.TagRel, 0) session := tr.data.DB.Where("object_id = ?", objectId) session.Where("status = ?", entity.TagRelStatusAvailable) @@ -83,7 +74,7 @@ func (tr *tagListRepo) GetObjectTagRelList(ctx context.Context, objectId string) } // BatchGetObjectTagRelList get object tag relation list all -func (tr *tagListRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) { +func (tr *tagRelRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error) { tagListList = make([]*entity.TagRel, 0) session := tr.data.DB.In("object_id", objectIds) session.Where("status = ?", entity.TagRelStatusAvailable) @@ -95,7 +86,7 @@ func (tr *tagListRepo) BatchGetObjectTagRelList(ctx context.Context, objectIds [ } // CountTagRelByTagID count tag relation -func (tr *tagListRepo) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) { +func (tr *tagRelRepo) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) { count, err = tr.data.DB.Count(&entity.TagRel{TagID: tagID, Status: entity.AnswerStatusAvailable}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index dd8353eba..14e438ffc 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -30,7 +30,6 @@ type TagRepo interface { type TagRelRepo interface { AddTagRelList(ctx context.Context, tagList []*entity.TagRel) (err error) RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error) - RemoveTagRelListByObjectID(ctx context.Context, objectId string) (err error) EnableTagRelByIDs(ctx context.Context, ids []int64) (err error) GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) (tagRel *entity.TagRel, exist bool, err error) GetObjectTagRelList(ctx context.Context, objectId string) (tagListList []*entity.TagRel, err error) From b98783a2f02edb5ec195d394af57d450801a58c1 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 27 Oct 2022 20:27:34 +0800 Subject: [PATCH 0024/3337] avatar --- cmd/answer/wire_gen.go | 2 +- internal/base/middleware/avatar.go | 46 ++++++++++++++++----- internal/service/uploader/upload.go | 63 ++++++++++++++++++----------- 3 files changed, 77 insertions(+), 34 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 2a9e9f5f5..051564d4e 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -174,7 +174,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter() authUserMiddleware := middleware.NewAuthUserMiddleware(authService) - avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf) + avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService) ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware) application := newApplication(serverConf, ginEngine) return application, func() { diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go index 73b539ae4..8201c28e9 100644 --- a/internal/base/middleware/avatar.go +++ b/internal/base/middleware/avatar.go @@ -7,34 +7,60 @@ import ( "strings" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/internal/service/uploader" + "github.com/answerdev/answer/pkg/converter" + "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" ) type AvatarMiddleware struct { - serviceConfig *service_config.ServiceConfig + serviceConfig *service_config.ServiceConfig + uploaderService *uploader.UploaderService } // NewAvatarMiddleware new auth user middleware -func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig) *AvatarMiddleware { +func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig, + uploaderService *uploader.UploaderService, +) *AvatarMiddleware { return &AvatarMiddleware{ - serviceConfig: serviceConfig, + serviceConfig: serviceConfig, + uploaderService: uploaderService, } } func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { return func(ctx *gin.Context) { + //?width=100&height=100 + wstr := ctx.Query("width") + hstr := ctx.Query("height") + w := converter.StringToInt(wstr) + h := converter.StringToInt(hstr) url := ctx.Request.RequestURI if strings.Contains(url, "/uploads/avatar/") { _, fileName := filepath.Split(url) - filepath := fmt.Sprintf("%s/avatar/%s", am.serviceConfig.UploadPath, fileName) - f, err := ioutil.ReadFile(filepath) - if err != nil { - ctx.Next() + uploadPath := am.serviceConfig.UploadPath + filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) + if w == 0 && h == 0 { + avatarfile, err := ioutil.ReadFile(filePath) + if err != nil { + ctx.Next() + return + } + ctx.Writer.WriteString(string(avatarfile)) + ctx.Abort() + return + } else { + spew.Dump(w, h, fileName) + avatarfile, err := am.uploaderService.AvatarThumbFile(ctx, uploadPath, fileName, w, h) + if err != nil { + ctx.Next() + return + } + ctx.Writer.WriteString(string(avatarfile)) + ctx.Abort() return } - ctx.Writer.WriteString(string(f)) - ctx.Abort() - return + } ctx.Next() } diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 917eb47a5..222b6fa30 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -3,9 +3,8 @@ package uploader import ( "bytes" "fmt" - "io" + "io/ioutil" "mime/multipart" - "os" "path" "path/filepath" @@ -13,6 +12,7 @@ import ( "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/uid" + "github.com/davecgh/go-spew/spew" "github.com/disintegration/imaging" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" @@ -50,34 +50,51 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.Fi return us.uploadFile(ctx, file, avatarFilePath) } -func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, header *multipart.FileHeader, file multipart.File, fileExt string) ( - url string, err error) { +func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, w, h int) ( + avatarfile []byte, err error) { - img, err := imaging.Decode(file) + filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) + avatarfile, err = ioutil.ReadFile(filePath) + spew.Dump("ioutil.ReadFile", err) if err != nil { - return "", err + return avatarfile, err } - formatImg := imaging.Resize(img, 1024, 0, imaging.Linear) - var buf bytes.Buffer - err = imaging.Encode(&buf, formatImg, imaging.JPEG) + reader := bytes.NewReader(avatarfile) + img, err := imaging.Decode(reader) + spew.Dump("imaging.Decode", err) if err != nil { - return "", err + return avatarfile, err } - reader := bytes.NewReader(buf.Bytes()) - newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) - avatarFilePath := path.Join(avatarSubPath, newFilename) - filePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) - out, err := os.Create(filePath) - if err != nil { - return "", err - } - defer out.Close() - _, err = io.Copy(out, reader) + new_image := imaging.Resize(img, w, h, imaging.Linear) + var buf bytes.Buffer + err = imaging.Encode(&buf, new_image, imaging.JPEG) + spew.Dump(buf) if err != nil { - return "", err + return avatarfile, err } - url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, avatarFilePath) - return url, nil + return buf.Bytes(), nil + + // formatImg := imaging.Resize(img, 1024, 0, imaging.Linear) + // var buf bytes.Buffer + // err = imaging.Encode(&buf, formatImg, imaging.JPEG) + // if err != nil { + // return "", err + // } + // reader := bytes.NewReader(buf.Bytes()) + // newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) + // avatarFilePath := path.Join(avatarSubPath, newFilename) + // filePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) + // out, err := os.Create(filePath) + // if err != nil { + // return "", err + // } + // defer out.Close() + // _, err = io.Copy(out, reader) + // if err != nil { + // return "", err + // } + // url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, avatarFilePath) + // return url, nil } func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( From 2563e499448c2e0fe42223ff75fb4340a8399184 Mon Sep 17 00:00:00 2001 From: aichy Date: Thu, 27 Oct 2022 21:44:07 +0800 Subject: [PATCH 0025/3337] fix --- internal/base/middleware/avatar.go | 18 ++++-- internal/service/uploader/upload.go | 99 ++++++++++++++++------------- 2 files changed, 67 insertions(+), 50 deletions(-) diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go index 8201c28e9..f2bee0b43 100644 --- a/internal/base/middleware/avatar.go +++ b/internal/base/middleware/avatar.go @@ -3,13 +3,13 @@ package middleware import ( "fmt" "io/ioutil" + "net/url" "path/filepath" "strings" "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/uploader" "github.com/answerdev/answer/pkg/converter" - "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" ) @@ -35,11 +35,16 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { hstr := ctx.Query("height") w := converter.StringToInt(wstr) h := converter.StringToInt(hstr) - url := ctx.Request.RequestURI - if strings.Contains(url, "/uploads/avatar/") { - _, fileName := filepath.Split(url) + u := ctx.Request.RequestURI + if strings.Contains(u, "/uploads/avatar/") { + uUrl, err := url.Parse(u) + if err != nil { + ctx.Next() + return + } + _, urlfileName := filepath.Split(uUrl.Path) uploadPath := am.serviceConfig.UploadPath - filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) + filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, urlfileName) if w == 0 && h == 0 { avatarfile, err := ioutil.ReadFile(filePath) if err != nil { @@ -50,8 +55,7 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { ctx.Abort() return } else { - spew.Dump(w, h, fileName) - avatarfile, err := am.uploaderService.AvatarThumbFile(ctx, uploadPath, fileName, w, h) + avatarfile, err := am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, w, h) if err != nil { ctx.Next() return diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 222b6fa30..3fdd2646c 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -3,8 +3,10 @@ package uploader import ( "bytes" "fmt" + "io" "io/ioutil" "mime/multipart" + "os" "path" "path/filepath" @@ -12,15 +14,15 @@ import ( "github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/uid" - "github.com/davecgh/go-spew/spew" "github.com/disintegration/imaging" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) const ( - avatarSubPath = "avatar" - postSubPath = "post" + avatarSubPath = "avatar" + avatarThumbSubPath = "avatar_thumb" + postSubPath = "post" ) // UploaderService user service @@ -52,49 +54,60 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.Fi func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, w, h int) ( avatarfile []byte, err error) { - - filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) - avatarfile, err = ioutil.ReadFile(filePath) - spew.Dump("ioutil.ReadFile", err) - if err != nil { - return avatarfile, err - } - reader := bytes.NewReader(avatarfile) - img, err := imaging.Decode(reader) - spew.Dump("imaging.Decode", err) + thumbFileName := fmt.Sprintf("%d_%d@%s", w, h, fileName) + thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName) + avatarfile, err = ioutil.ReadFile(thumbfilePath) if err != nil { - return avatarfile, err - } - new_image := imaging.Resize(img, w, h, imaging.Linear) - var buf bytes.Buffer - err = imaging.Encode(&buf, new_image, imaging.JPEG) - spew.Dump(buf) - if err != nil { - return avatarfile, err + filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) + avatarfile, err = ioutil.ReadFile(filePath) + if err != nil { + return avatarfile, err + } + reader := bytes.NewReader(avatarfile) + img, err := imaging.Decode(reader) + if err != nil { + return avatarfile, err + } + new_image := imaging.Fill(img, w, h, imaging.Center, imaging.Linear) + var buf bytes.Buffer + fileSuffix := path.Ext(fileName) + 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, + } + _, ok := formatExts[fileSuffix] + + if !ok { + return avatarfile, fmt.Errorf("img extension not exist") + } + err = imaging.Encode(&buf, new_image, formatExts[fileSuffix]) + + if err != nil { + return avatarfile, err + } + thumbReader := bytes.NewReader(buf.Bytes()) + dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath)) + avatarFilePath := path.Join(avatarThumbSubPath, thumbFileName) + savefilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) + out, err := os.Create(savefilePath) + if err != nil { + return avatarfile, err + } + defer out.Close() + _, err = io.Copy(out, thumbReader) + if err != nil { + return avatarfile, err + } + return buf.Bytes(), nil } - return buf.Bytes(), nil + // thumb file exist + return avatarfile, nil - // formatImg := imaging.Resize(img, 1024, 0, imaging.Linear) - // var buf bytes.Buffer - // err = imaging.Encode(&buf, formatImg, imaging.JPEG) - // if err != nil { - // return "", err - // } - // reader := bytes.NewReader(buf.Bytes()) - // newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) - // avatarFilePath := path.Join(avatarSubPath, newFilename) - // filePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) - // out, err := os.Create(filePath) - // if err != nil { - // return "", err - // } - // defer out.Close() - // _, err = io.Copy(out, reader) - // if err != nil { - // return "", err - // } - // url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, avatarFilePath) - // return url, nil } func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( From a4c751728068fcf502e3378fc2f1fe3756e49a1a Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 27 Oct 2022 20:27:34 +0800 Subject: [PATCH 0026/3337] add avatar Thumbnail image add avatar Thumbnail image --- cmd/answer/wire_gen.go | 2 +- internal/base/middleware/avatar.go | 52 +++++++++++++---- internal/service/uploader/upload.go | 86 +++++++++++++++++++---------- 3 files changed, 100 insertions(+), 40 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 2a9e9f5f5..051564d4e 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -174,7 +174,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter() authUserMiddleware := middleware.NewAuthUserMiddleware(authService) - avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf) + avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService) ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware) application := newApplication(serverConf, ginEngine) return application, func() { diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go index 73b539ae4..f2bee0b43 100644 --- a/internal/base/middleware/avatar.go +++ b/internal/base/middleware/avatar.go @@ -3,38 +3,68 @@ package middleware import ( "fmt" "io/ioutil" + "net/url" "path/filepath" "strings" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/internal/service/uploader" + "github.com/answerdev/answer/pkg/converter" "github.com/gin-gonic/gin" ) type AvatarMiddleware struct { - serviceConfig *service_config.ServiceConfig + serviceConfig *service_config.ServiceConfig + uploaderService *uploader.UploaderService } // NewAvatarMiddleware new auth user middleware -func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig) *AvatarMiddleware { +func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig, + uploaderService *uploader.UploaderService, +) *AvatarMiddleware { return &AvatarMiddleware{ - serviceConfig: serviceConfig, + serviceConfig: serviceConfig, + uploaderService: uploaderService, } } func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { return func(ctx *gin.Context) { - url := ctx.Request.RequestURI - if strings.Contains(url, "/uploads/avatar/") { - _, fileName := filepath.Split(url) - filepath := fmt.Sprintf("%s/avatar/%s", am.serviceConfig.UploadPath, fileName) - f, err := ioutil.ReadFile(filepath) + //?width=100&height=100 + wstr := ctx.Query("width") + hstr := ctx.Query("height") + w := converter.StringToInt(wstr) + h := converter.StringToInt(hstr) + u := ctx.Request.RequestURI + if strings.Contains(u, "/uploads/avatar/") { + uUrl, err := url.Parse(u) if err != nil { ctx.Next() return } - ctx.Writer.WriteString(string(f)) - ctx.Abort() - return + _, urlfileName := filepath.Split(uUrl.Path) + uploadPath := am.serviceConfig.UploadPath + filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, urlfileName) + if w == 0 && h == 0 { + avatarfile, err := ioutil.ReadFile(filePath) + if err != nil { + ctx.Next() + return + } + ctx.Writer.WriteString(string(avatarfile)) + ctx.Abort() + return + } else { + avatarfile, err := am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, w, h) + if err != nil { + ctx.Next() + return + } + ctx.Writer.WriteString(string(avatarfile)) + ctx.Abort() + return + } + } ctx.Next() } diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 917eb47a5..3fdd2646c 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "mime/multipart" "os" "path" @@ -19,8 +20,9 @@ import ( ) const ( - avatarSubPath = "avatar" - postSubPath = "post" + avatarSubPath = "avatar" + avatarThumbSubPath = "avatar_thumb" + postSubPath = "post" ) // UploaderService user service @@ -50,34 +52,62 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.Fi return us.uploadFile(ctx, file, avatarFilePath) } -func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, header *multipart.FileHeader, file multipart.File, fileExt string) ( - url string, err error) { - - img, err := imaging.Decode(file) - if err != nil { - return "", err - } - formatImg := imaging.Resize(img, 1024, 0, imaging.Linear) - var buf bytes.Buffer - err = imaging.Encode(&buf, formatImg, imaging.JPEG) - if err != nil { - return "", err - } - reader := bytes.NewReader(buf.Bytes()) - newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt) - avatarFilePath := path.Join(avatarSubPath, newFilename) - filePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) - out, err := os.Create(filePath) - if err != nil { - return "", err - } - defer out.Close() - _, err = io.Copy(out, reader) +func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, w, h int) ( + avatarfile []byte, err error) { + thumbFileName := fmt.Sprintf("%d_%d@%s", w, h, fileName) + thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName) + avatarfile, err = ioutil.ReadFile(thumbfilePath) if err != nil { - return "", err + filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) + avatarfile, err = ioutil.ReadFile(filePath) + if err != nil { + return avatarfile, err + } + reader := bytes.NewReader(avatarfile) + img, err := imaging.Decode(reader) + if err != nil { + return avatarfile, err + } + new_image := imaging.Fill(img, w, h, imaging.Center, imaging.Linear) + var buf bytes.Buffer + fileSuffix := path.Ext(fileName) + 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, + } + _, ok := formatExts[fileSuffix] + + if !ok { + return avatarfile, fmt.Errorf("img extension not exist") + } + err = imaging.Encode(&buf, new_image, formatExts[fileSuffix]) + + if err != nil { + return avatarfile, err + } + thumbReader := bytes.NewReader(buf.Bytes()) + dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath)) + avatarFilePath := path.Join(avatarThumbSubPath, thumbFileName) + savefilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) + out, err := os.Create(savefilePath) + if err != nil { + return avatarfile, err + } + defer out.Close() + _, err = io.Copy(out, thumbReader) + if err != nil { + return avatarfile, err + } + return buf.Bytes(), nil } - url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, avatarFilePath) - return url, nil + // thumb file exist + return avatarfile, nil + } func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( From bab71a28eb59574d556cd9cc5e6a74a31fdbaec1 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 09:52:10 +0800 Subject: [PATCH 0027/3337] fix AvatarThumbFile --- internal/service/uploader/upload.go | 92 ++++++++++++++--------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 3fdd2646c..3582a76fb 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -57,57 +57,55 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam thumbFileName := fmt.Sprintf("%d_%d@%s", w, h, fileName) thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName) avatarfile, err = ioutil.ReadFile(thumbfilePath) + if err == nil { + return avatarfile, nil + } + filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) + avatarfile, err = ioutil.ReadFile(filePath) if err != nil { - filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) - avatarfile, err = ioutil.ReadFile(filePath) - if err != nil { - return avatarfile, err - } - reader := bytes.NewReader(avatarfile) - img, err := imaging.Decode(reader) - if err != nil { - return avatarfile, err - } - new_image := imaging.Fill(img, w, h, imaging.Center, imaging.Linear) - var buf bytes.Buffer - fileSuffix := path.Ext(fileName) - 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, - } - _, ok := formatExts[fileSuffix] - - if !ok { - return avatarfile, fmt.Errorf("img extension not exist") - } - err = imaging.Encode(&buf, new_image, formatExts[fileSuffix]) + return avatarfile, err + } + reader := bytes.NewReader(avatarfile) + img, err := imaging.Decode(reader) + if err != nil { + return avatarfile, err + } + new_image := imaging.Fill(img, w, h, imaging.Center, imaging.Linear) + var buf bytes.Buffer + fileSuffix := path.Ext(fileName) + 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, + } + _, ok := formatExts[fileSuffix] - if err != nil { - return avatarfile, err - } - thumbReader := bytes.NewReader(buf.Bytes()) - dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath)) - avatarFilePath := path.Join(avatarThumbSubPath, thumbFileName) - savefilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) - out, err := os.Create(savefilePath) - if err != nil { - return avatarfile, err - } - defer out.Close() - _, err = io.Copy(out, thumbReader) - if err != nil { - return avatarfile, err - } - return buf.Bytes(), nil + if !ok { + return avatarfile, fmt.Errorf("img extension not exist") } - // thumb file exist - return avatarfile, nil + err = imaging.Encode(&buf, new_image, formatExts[fileSuffix]) + if err != nil { + return avatarfile, err + } + thumbReader := bytes.NewReader(buf.Bytes()) + dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath)) + avatarFilePath := path.Join(avatarThumbSubPath, thumbFileName) + savefilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) + out, err := os.Create(savefilePath) + if err != nil { + return avatarfile, err + } + defer out.Close() + _, err = io.Copy(out, thumbReader) + if err != nil { + return avatarfile, err + } + return buf.Bytes(), nil } func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.FileHeader, fileExt string) ( From 8b399dbe66a019d33a55889624f074a93f4f9332 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:29:26 +0800 Subject: [PATCH 0028/3337] update img upload --- internal/base/middleware/avatar.go | 33 ++++++++++++----------------- internal/service/uploader/upload.go | 32 +++++++++++++++------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go index f2bee0b43..402ded09c 100644 --- a/internal/base/middleware/avatar.go +++ b/internal/base/middleware/avatar.go @@ -30,13 +30,12 @@ func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig, func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { return func(ctx *gin.Context) { - //?width=100&height=100 - wstr := ctx.Query("width") - hstr := ctx.Query("height") - w := converter.StringToInt(wstr) - h := converter.StringToInt(hstr) u := ctx.Request.RequestURI if strings.Contains(u, "/uploads/avatar/") { + wstr := ctx.Query("width") + hstr := ctx.Query("height") + w := converter.StringToInt(wstr) + h := converter.StringToInt(hstr) uUrl, err := url.Parse(u) if err != nil { ctx.Next() @@ -45,25 +44,19 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { _, urlfileName := filepath.Split(uUrl.Path) uploadPath := am.serviceConfig.UploadPath filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, urlfileName) + var avatarfile []byte if w == 0 && h == 0 { - avatarfile, err := ioutil.ReadFile(filePath) - if err != nil { - ctx.Next() - return - } - ctx.Writer.WriteString(string(avatarfile)) - ctx.Abort() - return + avatarfile, err = ioutil.ReadFile(filePath) } else { - avatarfile, err := am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, w, h) - if err != nil { - ctx.Next() - return - } - ctx.Writer.WriteString(string(avatarfile)) - ctx.Abort() + avatarfile, err = am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, w, h) + } + if err != nil { + ctx.Next() return } + ctx.Writer.WriteString(string(avatarfile)) + ctx.Abort() + return } ctx.Next() diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 3582a76fb..1fc094d67 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -52,6 +52,16 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context, file *multipart.Fi 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, w, h int) ( avatarfile []byte, err error) { thumbFileName := fmt.Sprintf("%d_%d@%s", w, h, fileName) @@ -63,26 +73,18 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, fileName) avatarfile, err = ioutil.ReadFile(filePath) if err != nil { - return avatarfile, err + return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } reader := bytes.NewReader(avatarfile) img, err := imaging.Decode(reader) if err != nil { - return avatarfile, err + return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } new_image := imaging.Fill(img, w, h, imaging.Center, imaging.Linear) var buf bytes.Buffer fileSuffix := path.Ext(fileName) - 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, - } - _, ok := formatExts[fileSuffix] + + _, ok := FormatExts[fileSuffix] if !ok { return avatarfile, fmt.Errorf("img extension not exist") @@ -90,7 +92,7 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam err = imaging.Encode(&buf, new_image, formatExts[fileSuffix]) if err != nil { - return avatarfile, err + return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } thumbReader := bytes.NewReader(buf.Bytes()) dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath)) @@ -98,12 +100,12 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam savefilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath) out, err := os.Create(savefilePath) if err != nil { - return avatarfile, err + return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } defer out.Close() _, err = io.Copy(out, thumbReader) if err != nil { - return avatarfile, err + return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } return buf.Bytes(), nil } From 7496897855d1f313d137253d024af6281e7e5f4e Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:43:53 +0800 Subject: [PATCH 0029/3337] fix img upload --- internal/service/uploader/upload.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index 1fc094d67..ca8ab3a02 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -89,7 +89,7 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam if !ok { return avatarfile, fmt.Errorf("img extension not exist") } - err = imaging.Encode(&buf, new_image, formatExts[fileSuffix]) + err = imaging.Encode(&buf, new_image, FormatExts[fileSuffix]) if err != nil { return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() From 97ab26663fed4e10e871d3fc16ac12c1945a83dd Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 28 Oct 2022 10:50:36 +0800 Subject: [PATCH 0030/3337] feat: support gravatar avatar --- pkg/gravatar/gravatar.go | 34 +++++++++++++++++ pkg/gravatar/gravatar_test.go | 71 +++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 pkg/gravatar/gravatar.go create mode 100644 pkg/gravatar/gravatar_test.go diff --git a/pkg/gravatar/gravatar.go b/pkg/gravatar/gravatar.go new file mode 100644 index 000000000..a1d02ed49 --- /dev/null +++ b/pkg/gravatar/gravatar.go @@ -0,0 +1,34 @@ +package gravatar + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "net/url" +) + +const ( + defaultURLPrefix = "https://www.gravatar.com/avatar/" +) + +// GetAvatarURL get avatar url from gravatar by email +func GetAvatarURL(email string) string { + h := md5.New() + h.Write([]byte(email)) + return defaultURLPrefix + hex.EncodeToString(h.Sum(nil)) +} + +// Resize resize avatar by pixel +func Resize(originalAvatarURL string, sizePixel int) (resizedAvatarURL string) { + if len(originalAvatarURL) == 0 { + return + } + originalURL, err := url.Parse(originalAvatarURL) + if err != nil { + return originalAvatarURL + } + query := originalURL.Query() + query.Set("p", fmt.Sprintf("%d", sizePixel)) + originalURL.RawQuery = query.Encode() + return originalURL.String() +} diff --git a/pkg/gravatar/gravatar_test.go b/pkg/gravatar/gravatar_test.go new file mode 100644 index 000000000..98928b1a0 --- /dev/null +++ b/pkg/gravatar/gravatar_test.go @@ -0,0 +1,71 @@ +package gravatar + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetAvatarURL(t *testing.T) { + type args struct { + email string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "answer@answer.com", + args: args{email: "answer@answer.com"}, + want: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, GetAvatarURL(tt.args.email)) + }) + } +} + +func TestResize(t *testing.T) { + type args struct { + originalAvatarURL string + sizePixel int + } + tests := []struct { + name string + args args + wantResizedAvatarURL string + }{ + { + name: "original url", + args: args{ + originalAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7", + sizePixel: 128, + }, + wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?p=128", + }, + { + name: "already resized url", + args: args{ + originalAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?p=128", + sizePixel: 64, + }, + wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?p=64", + }, + { + name: "empty url", + args: args{ + originalAvatarURL: "", + sizePixel: 64, + }, + wantResizedAvatarURL: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.wantResizedAvatarURL, Resize(tt.args.originalAvatarURL, tt.args.sizePixel), "Resize(%v, %v)", tt.args.originalAvatarURL, tt.args.sizePixel) + }) + } +} From 5a16859e6b8ebd6e139f2bd0912acb051ab112a1 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 28 Oct 2022 11:01:42 +0800 Subject: [PATCH 0031/3337] fix: set gravatar size parameter is s --- pkg/gravatar/gravatar.go | 2 +- pkg/gravatar/gravatar_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/gravatar/gravatar.go b/pkg/gravatar/gravatar.go index a1d02ed49..6166af5e3 100644 --- a/pkg/gravatar/gravatar.go +++ b/pkg/gravatar/gravatar.go @@ -28,7 +28,7 @@ func Resize(originalAvatarURL string, sizePixel int) (resizedAvatarURL string) { return originalAvatarURL } query := originalURL.Query() - query.Set("p", fmt.Sprintf("%d", sizePixel)) + query.Set("s", fmt.Sprintf("%d", sizePixel)) originalURL.RawQuery = query.Encode() return originalURL.String() } diff --git a/pkg/gravatar/gravatar_test.go b/pkg/gravatar/gravatar_test.go index 98928b1a0..927988aad 100644 --- a/pkg/gravatar/gravatar_test.go +++ b/pkg/gravatar/gravatar_test.go @@ -44,15 +44,15 @@ func TestResize(t *testing.T) { originalAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7", sizePixel: 128, }, - wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?p=128", + wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=128", }, { name: "already resized url", args: args{ - originalAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?p=128", + originalAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=128", sizePixel: 64, }, - wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?p=64", + wantResizedAvatarURL: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7?s=64", }, { name: "empty url", From 1563c6b583fd9c5d81f77012f88ba31e91c647a4 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 11:15:46 +0800 Subject: [PATCH 0032/3337] update avatar img size --- internal/base/middleware/avatar.go | 10 ++++------ internal/service/uploader/upload.go | 9 ++++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/internal/base/middleware/avatar.go b/internal/base/middleware/avatar.go index 402ded09c..3a6ce2da6 100644 --- a/internal/base/middleware/avatar.go +++ b/internal/base/middleware/avatar.go @@ -32,10 +32,8 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { return func(ctx *gin.Context) { u := ctx.Request.RequestURI if strings.Contains(u, "/uploads/avatar/") { - wstr := ctx.Query("width") - hstr := ctx.Query("height") - w := converter.StringToInt(wstr) - h := converter.StringToInt(hstr) + sizeStr := ctx.Query("s") + size := converter.StringToInt(sizeStr) uUrl, err := url.Parse(u) if err != nil { ctx.Next() @@ -45,10 +43,10 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc { uploadPath := am.serviceConfig.UploadPath filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, urlfileName) var avatarfile []byte - if w == 0 && h == 0 { + if size == 0 { avatarfile, err = ioutil.ReadFile(filePath) } else { - avatarfile, err = am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, w, h) + avatarfile, err = am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, size) } if err != nil { ctx.Next() diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index ca8ab3a02..fab199e36 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -62,9 +62,12 @@ var FormatExts = map[string]imaging.Format{ ".bmp": imaging.BMP, } -func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, w, h int) ( +func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) ( avatarfile []byte, err error) { - thumbFileName := fmt.Sprintf("%d_%d@%s", w, h, fileName) + if size > 1024 { + size = 1024 + } + thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName) thumbfilePath := fmt.Sprintf("%s/%s/%s", uploadPath, avatarThumbSubPath, thumbFileName) avatarfile, err = ioutil.ReadFile(thumbfilePath) if err == nil { @@ -80,7 +83,7 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam if err != nil { return avatarfile, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } - new_image := imaging.Fill(img, w, h, imaging.Center, imaging.Linear) + new_image := imaging.Fill(img, size, size, imaging.Center, imaging.Linear) var buf bytes.Buffer fileSuffix := path.Ext(fileName) From bb18db9e13cfd915e4a619378ba5da3fba3518c0 Mon Sep 17 00:00:00 2001 From: mingcheng Date: Fri, 28 Oct 2022 13:34:23 +0800 Subject: [PATCH 0033/3337] ci: update dockerfile and makefile for sqlite embed issue --- Dockerfile | 54 ++++++++++++++++++++++++++++++++++-------------------- Makefile | 2 +- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4c46698bb..8b55d58f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,40 +6,54 @@ COPY . /answer WORKDIR /answer RUN make install-ui-packages ui && mv ui/build /tmp -FROM golang:1.18 AS golang-builder -LABEL maintainer="aichy" +# stage2 build the main binary within static resource +FROM golang:1.19-alpine AS golang-builder +LABEL maintainer="aichy@sf.com" + +ARG GOPROXY +ENV GOPROXY ${GOPROXY:-direct} ENV GOPATH /go ENV GOROOT /usr/local/go ENV PACKAGE github.com/answerdev/answer -ENV GOPROXY https://goproxy.cn,direct ENV BUILD_DIR ${GOPATH}/src/${PACKAGE} -# Build + +ARG TAGS="sqlite sqlite_unlock_notify" +ENV TAGS "bindata timetzdata $TAGS" +ARG CGO_EXTRA_CFLAGS + COPY . ${BUILD_DIR} WORKDIR ${BUILD_DIR} COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build -RUN make clean build && \ - cp answer /usr/bin/answer && \ - mkdir -p /data/upfiles && chmod 777 /data/upfiles && \ - mkdir -p /data/i18n && chmod 777 /data/i18n && cp -r i18n/*.yaml /data/i18n +RUN apk --no-cache add build-base git \ + && make clean build \ + && cp answer /usr/bin/answer -FROM debian:bullseye -ENV TZ "Asia/Shanghai" -RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list \ - && sed -i 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list \ - && echo "Asia/Shanghai" > /etc/timezone \ - && apt -y update \ - && apt -y upgrade \ - && apt -y install ca-certificates openssl tzdata curl netcat dumb-init \ - && apt -y autoremove \ - && mkdir -p /tmp/cache +RUN mkdir -p /data/upfiles && chmod 777 /data/upfiles \ + && mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n -COPY --from=golang-builder /data /data -VOLUME /data +# stage3 copy the binary and resource files into fresh container +FROM alpine +LABEL maintainer="maintainers@sf.com" + +ENV TZ "Asia/Shanghai" +RUN apk update \ + && apk --no-cache add \ + bash \ + ca-certificates \ + curl \ + dumb-init \ + gettext \ + openssh \ + sqlite \ + gnupg \ + && echo "Asia/Shanghai" > /etc/timezone COPY --from=golang-builder /usr/bin/answer /usr/bin/answer +COPY --from=golang-builder /data /data COPY /script/entrypoint.sh /entrypoint.sh RUN chmod 755 /entrypoint.sh +VOLUME /data EXPOSE 80 ENTRYPOINT ["/entrypoint.sh"] diff --git a/Makefile b/Makefile index 76ae07d8e..9feb089d0 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ DOCKER_CMD=docker #GO_ENV=CGO_ENABLED=0 Revision=$(shell git rev-parse --short HEAD) -GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X main.Revision=$(Revision) -X 'main.Time=`date`' -extldflags -static" +GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X 'main.Revision=$(Revision)' -X 'main.Time=`date`' -extldflags -static" GO=$(GO_ENV) $(shell which go) build: From 10a06573d5fa9461bdec598620ec0a223d05b380 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 28 Oct 2022 13:43:25 +0800 Subject: [PATCH 0034/3337] fix: degradation sqlite3 pkg --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 976db1a03..e489a8b25 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/jinzhu/copier v0.3.5 github.com/jinzhu/now v1.1.5 github.com/lib/pq v1.10.2 - github.com/mattn/go-sqlite3 v2.0.3+incompatible + github.com/mattn/go-sqlite3 v1.14.15 github.com/mojocn/base64Captcha v1.3.5 github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 diff --git a/go.sum b/go.sum index 99b56caeb..db884bf40 100644 --- a/go.sum +++ b/go.sum @@ -424,8 +424,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= From 7f66bb32623ffed6552ef27c11b09bc0e3b108d3 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 14:14:20 +0800 Subject: [PATCH 0035/3337] fix user api ErrorResponse --- internal/controller/user_controller.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 4c3460357..42a65792b 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -323,7 +323,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { } if !oldPassVerification { resp := schema.UserVerifyEmailErrorResponse{ - Key: "captcha_code", + Key: "old_pass", Value: "error.object.old_password_verification_failed", } resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) @@ -333,7 +333,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { if req.OldPass == req.Pass { resp := schema.UserVerifyEmailErrorResponse{ - Key: "captcha_code", + Key: "pass", Value: "error.object.new_password_same_as_previous_setting", } resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) From b162fcbee01f7426d42698210c1d52a6e3f9f467 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 28 Oct 2022 14:42:27 +0800 Subject: [PATCH 0036/3337] feat: user can change email when email hasn't been verified. --- internal/controller/user_controller.go | 6 ++++++ internal/router/answer_api_router.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 42a65792b..07ad63cde 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -480,6 +480,12 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { return } req.UserID = middleware.GetLoginUserIDFromContext(ctx) + // If the user is not logged in, the api cannot be used. + // If the user email is not verified, that also can use this api to modify the email. + if len(req.UserID) == 0 { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + return + } err := uc.userService.UserChangeEmailSendCode(ctx, req) handler.HandleResponse(ctx, err, nil) diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index e227a10fe..781f92d25 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -98,6 +98,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.POST("/user/email/verification/send", a.userController.UserVerifyEmailSend) r.GET("/user/logout", a.userController.UserLogout) r.PUT("/user/email", a.userController.UserChangeEmailVerify) + r.POST("/user/email/change/code", a.userController.UserChangeEmailSendCode) //answer r.GET("/answer/info", a.answerController.Get) @@ -179,7 +180,6 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { r.POST("/user/avatar/upload", a.userController.UploadUserAvatar) r.POST("/user/post/file", a.userController.UploadUserPostFile) r.POST("/user/notice/set", a.userController.UserNoticeSet) - r.POST("/user/email/change/code", a.userController.UserChangeEmailSendCode) // vote r.GET("/personal/vote/page", a.voteController.UserVotes) From 90deaf027bb11968cd8af927364df17f87f2e3a5 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 14:53:44 +0800 Subject: [PATCH 0037/3337] user avatar save --- docs/docs.go | 20 +++++++++++++++++-- docs/swagger.json | 20 +++++++++++++++++-- docs/swagger.yaml | 15 ++++++++++++-- internal/schema/user_schema.go | 30 +++++++++++++++++++++++++++- internal/service/user_common/user.go | 4 +++- internal/service/user_service.go | 8 ++++++-- 6 files changed, 87 insertions(+), 10 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index e7df3ce07..8ba38fcb5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4108,6 +4108,23 @@ const docTemplate = `{ } } }, + "schema.AvatarInfo": { + "type": "object", + "properties": { + "custom": { + "type": "string", + "maxLength": 200 + }, + "gravatar": { + "type": "string", + "maxLength": 200 + }, + "type": { + "type": "string", + "maxLength": 100 + } + } + }, "schema.CloseQuestionReq": { "type": "object", "required": [ @@ -5318,8 +5335,7 @@ const docTemplate = `{ "properties": { "avatar": { "description": "avatar", - "type": "string", - "maxLength": 500 + "$ref": "#/definitions/schema.AvatarInfo" }, "bio": { "description": "bio", diff --git a/docs/swagger.json b/docs/swagger.json index 1063944b7..3d1859d3a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4096,6 +4096,23 @@ } } }, + "schema.AvatarInfo": { + "type": "object", + "properties": { + "custom": { + "type": "string", + "maxLength": 200 + }, + "gravatar": { + "type": "string", + "maxLength": 200 + }, + "type": { + "type": "string", + "maxLength": 100 + } + } + }, "schema.CloseQuestionReq": { "type": "object", "required": [ @@ -5306,8 +5323,7 @@ "properties": { "avatar": { "description": "avatar", - "type": "string", - "maxLength": 500 + "$ref": "#/definitions/schema.AvatarInfo" }, "bio": { "description": "bio", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index bde9a77d6..aaa1a77dc 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -139,6 +139,18 @@ definitions: description: title type: string type: object + schema.AvatarInfo: + properties: + custom: + maxLength: 200 + type: string + gravatar: + maxLength: 200 + type: string + type: + maxLength: 100 + type: string + type: object schema.CloseQuestionReq: properties: close_msg: @@ -1013,9 +1025,8 @@ definitions: schema.UpdateInfoRequest: properties: avatar: + $ref: '#/definitions/schema.AvatarInfo' description: avatar - maxLength: 500 - type: string bio: description: bio maxLength: 4096 diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 757926c79..aff28858e 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -10,6 +10,7 @@ import ( "github.com/answerdev/answer/pkg/checker" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" ) // UserVerifyEmailReq user verify email request @@ -72,6 +73,7 @@ type GetUserResp struct { func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) { _ = copier.Copy(r, userInfo) + r.Avatar = r.AvatarInfo(userInfo.Avatar) r.CreatedAt = userInfo.CreatedAt.Unix() r.LastLoginDate = userInfo.LastLoginDate.Unix() statusShow, ok := UserStatusShow[userInfo.Status] @@ -80,6 +82,26 @@ func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) { } } +func (us *GetUserResp) AvatarInfo(AvatarJson string) string { + if AvatarJson == "" { + return "" + } + AvatarInfo := &AvatarInfo{} + err := json.Unmarshal([]byte(AvatarJson), AvatarInfo) + if err != nil { + log.Error("AvatarInfo json.Unmarshal Error", err) + return "" + } + switch AvatarInfo.Type { + case "gravatar": + return AvatarInfo.Gravatar + case "custom": + return AvatarInfo.Custom + default: + return "" + } +} + // GetUserStatusResp get user status info type GetUserStatusResp struct { // user status @@ -230,7 +252,7 @@ type UpdateInfoRequest struct { // username Username string `validate:"omitempty,gt=0,lte=30" json:"username"` // avatar - Avatar string `validate:"omitempty,gt=0,lte=500" json:"avatar"` + Avatar AvatarInfo `json:"avatar"` // bio Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"` // bio @@ -243,6 +265,12 @@ type UpdateInfoRequest struct { UserId string `json:"-" ` } +type AvatarInfo struct { + Type string `validate:"omitempty,gt=0,lte=100" json:"type"` + Gravatar string `validate:"omitempty,gt=0,lte=200" json:"gravatar"` + Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"` +} + func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error) { if len(u.Username) > 0 { re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`) diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 0297ae477..adb87317f 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -76,11 +76,13 @@ func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, IDs []string) // UserBasicInfoFormat func (us *UserCommon) UserBasicInfoFormat(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo { userBasicInfo := &schema.UserBasicInfo{} + uinfo := &schema.GetUserResp{} + uinfo.AvatarInfo(userInfo.Avatar) userBasicInfo.ID = userInfo.ID userBasicInfo.Username = userInfo.Username userBasicInfo.Rank = userInfo.Rank userBasicInfo.DisplayName = userInfo.DisplayName - userBasicInfo.Avatar = userInfo.Avatar + userBasicInfo.Avatar = uinfo.Avatar userBasicInfo.Website = userInfo.Website userBasicInfo.Location = userInfo.Location userBasicInfo.IpInfo = userInfo.IPInfo diff --git a/internal/service/user_service.go b/internal/service/user_service.go index af2d4d6fc..580d8bda0 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -3,6 +3,7 @@ package service import ( "context" "encoding/hex" + "encoding/json" "fmt" "math/rand" "regexp" @@ -255,10 +256,13 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq return errors.BadRequest(reason.UsernameDuplicate) } } - + Avatar, err := json.Marshal(req.Avatar) + if err != nil { + return err + } userInfo := entity.User{} userInfo.ID = req.UserId - userInfo.Avatar = req.Avatar + userInfo.Avatar = string(Avatar) userInfo.DisplayName = req.DisplayName userInfo.Bio = req.Bio userInfo.BioHtml = req.BioHtml From d41fd7a46771a5157ce84965abe87f71b81b1325 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:07:19 +0800 Subject: [PATCH 0038/3337] add Captcha --- internal/controller/user_controller.go | 15 ++++++++++++++- internal/schema/user_schema.go | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 07ad63cde..bebf7188f 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -482,11 +482,23 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { req.UserID = middleware.GetLoginUserIDFromContext(ctx) // If the user is not logged in, the api cannot be used. // If the user email is not verified, that also can use this api to modify the email. + + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Email, 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) + return + } + if len(req.UserID) == 0 { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) return } - + _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) err := uc.userService.UserChangeEmailSendCode(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -514,5 +526,6 @@ func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) { } err := uc.userService.UserChangeEmailVerify(ctx, req.Content) + uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) handler.HandleResponse(ctx, err, nil) } diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 757926c79..f6d88e669 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -325,6 +325,7 @@ type GetOtherUserInfoResp struct { } type UserChangeEmailSendCodeReq struct { + UserVerifyEmailSendReq Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"` UserID string `json:"-"` } From 9b6a35d88bc13561d2de598190e064f3184b071b Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:21:23 +0800 Subject: [PATCH 0039/3337] update set user avatar --- i18n/en_US.yaml | 2 ++ i18n/zh_CN.yaml | 2 ++ internal/base/reason/reason.go | 1 + internal/schema/user_schema.go | 6 +++--- internal/service/user_service.go | 3 ++- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 2d08b02ac..cc863635b 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -86,6 +86,8 @@ error: other: "username is invalid" username_duplicate: other: "username is already in use" + set_avatar: + other: "avatar set failure" report: spam: diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 15c951268..7d76cc061 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -86,6 +86,8 @@ error: other: "用户名无效" username_duplicate: other: "用户名已被使用" + set_avatar: + other: "头像设置错误" report: spam: diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index fb64cd781..f9fd7a661 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -26,6 +26,7 @@ const ( 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" diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index aff28858e..951e18b11 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -82,12 +82,12 @@ func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) { } } -func (us *GetUserResp) AvatarInfo(AvatarJson string) string { - if AvatarJson == "" { +func (us *GetUserResp) AvatarInfo(avatarJson string) string { + if avatarJson == "" { return "" } AvatarInfo := &AvatarInfo{} - err := json.Unmarshal([]byte(AvatarJson), AvatarInfo) + err := json.Unmarshal([]byte(avatarJson), AvatarInfo) if err != nil { log.Error("AvatarInfo json.Unmarshal Error", err) return "" diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 580d8bda0..7af14d55f 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -258,6 +258,7 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq } Avatar, err := json.Marshal(req.Avatar) if err != nil { + err = errors.BadRequest(reason.UserSetAvatar).WithError(err).WithStack() return err } userInfo := entity.User{} @@ -548,7 +549,7 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string } err = us.userRepo.UpdateEmail(ctx, data.UserID, data.Email) if err != nil { - return err + return errors.BadRequest(reason.UserNotFound) } return nil } From 5ecac4dda3ef43aa53bf672db6dd813b63da892c Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:43:37 +0800 Subject: [PATCH 0040/3337] update userinfo show user set avatar --- internal/schema/user_schema.go | 21 +++++++++++++++++++++ internal/service/user_service.go | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 951e18b11..db1e42f6d 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -82,6 +82,27 @@ func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) { } } +type GetUserToSetShowResp struct { + *GetUserResp + Avatar *AvatarInfo `json:"avatar"` +} + +func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) { + _ = copier.Copy(r, userInfo) + r.CreatedAt = userInfo.CreatedAt.Unix() + r.LastLoginDate = userInfo.LastLoginDate.Unix() + statusShow, ok := UserStatusShow[userInfo.Status] + if ok { + r.Status = statusShow + } + AvatarInfo := &AvatarInfo{} + err := json.Unmarshal([]byte(userInfo.Avatar), AvatarInfo) + if err != nil { + log.Error("AvatarInfo json.Unmarshal Error", err) + } + r.Avatar = AvatarInfo +} + func (us *GetUserResp) AvatarInfo(avatarJson string) string { if avatarJson == "" { return "" diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 7af14d55f..bb5f105f7 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -51,7 +51,7 @@ func NewUserService(userRepo usercommon.UserRepo, } // GetUserInfoByUserID get user info by user id -func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (resp *schema.GetUserResp, err error) { +func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (resp *schema.GetUserToSetShowResp, err error) { userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID) if err != nil { return nil, err @@ -59,7 +59,7 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st if !exist { return nil, errors.BadRequest(reason.UserNotFound) } - resp = &schema.GetUserResp{} + resp = &schema.GetUserToSetShowResp{} resp.GetFromUserEntity(userInfo) resp.AccessToken = token return resp, nil From 8fb4474052949ce445b66e98962b332c7d8c72ea Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 15:46:05 +0800 Subject: [PATCH 0041/3337] update swagger --- docs/docs.go | 98 +++++++++++++++++++++++++- docs/swagger.json | 98 +++++++++++++++++++++++++- docs/swagger.yaml | 73 ++++++++++++++++++- internal/controller/user_controller.go | 2 +- 4 files changed, 267 insertions(+), 4 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 8ba38fcb5..af029e38e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3326,7 +3326,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetUserResp" + "$ref": "#/definitions/schema.GetUserToSetShowResp" } } } @@ -4853,6 +4853,102 @@ const docTemplate = `{ } } }, + "schema.GetUserToSetShowResp": { + "type": "object", + "properties": { + "access_token": { + "description": "access token", + "type": "string" + }, + "answer_count": { + "description": "answer count", + "type": "integer" + }, + "authority_group": { + "description": "authority group", + "type": "integer" + }, + "avatar": { + "$ref": "#/definitions/schema.AvatarInfo" + }, + "bio": { + "description": "bio markdown", + "type": "string" + }, + "bio_html": { + "description": "bio html", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "e_mail": { + "description": "email", + "type": "string" + }, + "follow_count": { + "description": "follow count", + "type": "integer" + }, + "id": { + "description": "user id", + "type": "string" + }, + "ip_info": { + "description": "ip info", + "type": "string" + }, + "is_admin": { + "description": "is admin", + "type": "boolean" + }, + "last_login_date": { + "description": "last login date", + "type": "integer" + }, + "location": { + "description": "location", + "type": "string" + }, + "mail_status": { + "description": "mail status(1 pass 2 to be verified)", + "type": "integer" + }, + "mobile": { + "description": "mobile", + "type": "string" + }, + "notice_status": { + "description": "notice status(1 on 2off)", + "type": "integer" + }, + "question_count": { + "description": "question count", + "type": "integer" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "status": { + "description": "user status", + "type": "string" + }, + "username": { + "description": "username", + "type": "string" + }, + "website": { + "description": "website", + "type": "string" + } + } + }, "schema.GetVoteWithPageResp": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 3d1859d3a..d2273f9e1 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3314,7 +3314,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.GetUserResp" + "$ref": "#/definitions/schema.GetUserToSetShowResp" } } } @@ -4841,6 +4841,102 @@ } } }, + "schema.GetUserToSetShowResp": { + "type": "object", + "properties": { + "access_token": { + "description": "access token", + "type": "string" + }, + "answer_count": { + "description": "answer count", + "type": "integer" + }, + "authority_group": { + "description": "authority group", + "type": "integer" + }, + "avatar": { + "$ref": "#/definitions/schema.AvatarInfo" + }, + "bio": { + "description": "bio markdown", + "type": "string" + }, + "bio_html": { + "description": "bio html", + "type": "string" + }, + "created_at": { + "description": "create time", + "type": "integer" + }, + "display_name": { + "description": "display name", + "type": "string" + }, + "e_mail": { + "description": "email", + "type": "string" + }, + "follow_count": { + "description": "follow count", + "type": "integer" + }, + "id": { + "description": "user id", + "type": "string" + }, + "ip_info": { + "description": "ip info", + "type": "string" + }, + "is_admin": { + "description": "is admin", + "type": "boolean" + }, + "last_login_date": { + "description": "last login date", + "type": "integer" + }, + "location": { + "description": "location", + "type": "string" + }, + "mail_status": { + "description": "mail status(1 pass 2 to be verified)", + "type": "integer" + }, + "mobile": { + "description": "mobile", + "type": "string" + }, + "notice_status": { + "description": "notice status(1 on 2off)", + "type": "integer" + }, + "question_count": { + "description": "question count", + "type": "integer" + }, + "rank": { + "description": "rank", + "type": "integer" + }, + "status": { + "description": "user status", + "type": "string" + }, + "username": { + "description": "username", + "type": "string" + }, + "website": { + "description": "website", + "type": "string" + } + } + }, "schema.GetVoteWithPageResp": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index aaa1a77dc..66991ad22 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -683,6 +683,77 @@ definitions: description: website type: string type: object + schema.GetUserToSetShowResp: + properties: + access_token: + description: access token + type: string + answer_count: + description: answer count + type: integer + authority_group: + description: authority group + type: integer + avatar: + $ref: '#/definitions/schema.AvatarInfo' + bio: + description: bio markdown + type: string + bio_html: + description: bio html + type: string + created_at: + description: create time + type: integer + display_name: + description: display name + type: string + e_mail: + description: email + type: string + follow_count: + description: follow count + type: integer + id: + description: user id + type: string + ip_info: + description: ip info + type: string + is_admin: + description: is admin + type: boolean + last_login_date: + description: last login date + type: integer + location: + description: location + type: string + mail_status: + description: mail status(1 pass 2 to be verified) + type: integer + mobile: + description: mobile + type: string + notice_status: + description: notice status(1 on 2off) + type: integer + question_count: + description: question count + type: integer + rank: + description: rank + type: integer + status: + description: user status + type: string + username: + description: username + type: string + website: + description: website + type: string + type: object schema.GetVoteWithPageResp: properties: answer_id: @@ -3305,7 +3376,7 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.GetUserResp' + $ref: '#/definitions/schema.GetUserToSetShowResp' type: object security: - ApiKeyAuth: [] diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 4c3460357..19bbc6aef 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -52,7 +52,7 @@ func NewUserController( // @Accept json // @Produce json // @Security ApiKeyAuth -// @Success 200 {object} handler.RespBody{data=schema.GetUserResp} +// @Success 200 {object} handler.RespBody{data=schema.GetUserToSetShowResp} // @Router /answer/api/v1/user/info [get] func (uc *UserController) GetUserInfoByUserID(ctx *gin.Context) { userID := middleware.GetLoginUserIDFromContext(ctx) From a445504c916a4121e815919f3d91dec226ec191b Mon Sep 17 00:00:00 2001 From: fen Date: Fri, 28 Oct 2022 15:47:46 +0800 Subject: [PATCH 0042/3337] fix: typo --- i18n/en_US.yaml | 76 ++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 2d08b02ac..55de83d91 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1,91 +1,91 @@ base: success: - other: "success" + other: "Success." unknown: - other: "unknown error" + other: "Unknown error." request_format_error: - other: "request format is not valid" + other: "Request format is not valid." unauthorized_error: - other: "unauthorized" + other: "Unauthorized." database_error: - other: "data server error" + other: "Data server error." email: - other: "email" + other: "Email" password: - other: "password" + other: "Password" email_or_password_wrong_error: &email_or_password_wrong - other: "email or password wrong" + other: "Email and password do not match." error: admin: email_or_password_wrong: *email_or_password_wrong answer: not_found: - other: "answer not found" + other: "Answer do not found." comment: edit_without_permission: - other: "comment not allowed to edit" + other: "Comment are not allowed to edit." not_found: - other: "comment not found" + other: "Comment not found." email: duplicate: - other: "email already exists" + other: "Email already exists." need_to_be_verified: - other: "email should be verified" + other: "Email should be verified." verify_url_expired: - other: "email verified url is expired, please resend the email" + other: "Email verified URL has expired, please resend the email." lang: not_found: - other: "language not found" + other: "Language file not found." object: captcha_verification_failed: - other: "captcha wrong" + other: "Captcha wrong." disallow_follow: - other: "You are not allowed to follow" + other: "You are not allowed to follow." disallow_vote: - other: "You are not allowed to vote" + other: "You are not allowed to vote." disallow_vote_your_self: - other: "You can't vote for your own post!" + other: "You can't vote for your own post." not_found: - other: "object not found" + other: "Object not found." verification_failed: - other: "verification failed" + other: "Verification failed." email_or_password_incorrect: - other: "email or password incorrect" + other: "Email and password do not match." old_password_verification_failed: - other: "the old password verification failed" + other: "The old password verification failed" new_password_same_as_previous_setting: - other: "The new password is the same as the previous setting" + other: "The new password is the same as the previous one." question: not_found: - other: "question not found" + other: "Question not found." rank: fail_to_meet_the_condition: - other: "rank fail to meet the condition" + other: "Rank fail to meet the condition." report: handle_failed: - other: "report handle failed" + other: "Report handle failed." not_found: - other: "report not found" + other: "Report not found." tag: not_found: - other: "tag not found" + other: "Tag not found." theme: not_found: - other: "theme not found" + other: "Theme not found." user: email_or_password_wrong: other: *email_or_password_wrong not_found: - other: "user not found" + other: "User not found." suspended: - other: "user is suspended" + other: "User has been suspended." username_invalid: - other: "username is invalid" + other: "Username is invalid." username_duplicate: - other: "username is already in use" + other: "Username is already in use." report: spam: @@ -161,10 +161,10 @@ notification: mention_you: other: "mentioned you" your_question_is_closed: - other: "your question has been closed" + other: "Your question has been closed" your_question_was_deleted: - other: "your question has been deleted" + other: "Your question has been deleted" your_answer_was_deleted: - other: "your answer has been deleted" + other: "Your answer has been deleted" your_comment_was_deleted: - other: "your comment has been deleted" + other: "Your comment has been deleted" From f8f35a39b1d3757f6c61cf7b43e35dcec0703dcb Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:07:14 +0800 Subject: [PATCH 0043/3337] ChangeEmail add email status --- internal/service/user_service.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index af2d4d6fc..7284228f1 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -546,5 +546,9 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string if err != nil { return err } + us.userRepo.UpdateEmailStatus(ctx, data.UserID, entity.EmailStatusAvailable) + if err != nil { + return err + } return nil } From 9f2799996a723d92720055f6268f4e061508466a Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 28 Oct 2022 16:21:09 +0800 Subject: [PATCH 0044/3337] ChangeEmail add email status --- internal/service/user_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 7284228f1..8e4d8273e 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -546,7 +546,7 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string if err != nil { return err } - us.userRepo.UpdateEmailStatus(ctx, data.UserID, entity.EmailStatusAvailable) + err = us.userRepo.UpdateEmailStatus(ctx, data.UserID, entity.EmailStatusAvailable) if err != nil { return err } From ccbf42c685c0c55dc7d514077fdc85df26303ef2 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 28 Oct 2022 16:36:08 +0800 Subject: [PATCH 0045/3337] feat: add tag-rel repo unit test --- internal/repo/repo_test/tag_rel_repo_test.go | 77 +++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/internal/repo/repo_test/tag_rel_repo_test.go b/internal/repo/repo_test/tag_rel_repo_test.go index 4df8138a8..3cb1ffeba 100644 --- a/internal/repo/repo_test/tag_rel_repo_test.go +++ b/internal/repo/repo_test/tag_rel_repo_test.go @@ -1,32 +1,91 @@ package repo_test import ( + "context" + "sync" "testing" + + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/tag" + "github.com/stretchr/testify/assert" +) + +var ( + tagRelOnce sync.Once + testTagRelList = []*entity.TagRel{ + { + ObjectID: "1", + TagID: "1", + Status: entity.TagRelStatusAvailable, + }, + { + ObjectID: "2", + TagID: "2", + Status: entity.TagRelStatusAvailable, + }, + } ) -func Test_tagListRepo_AddTagRelList(t *testing.T) { +func addTagRelList() { + tagRelRepo := tag.NewTagRelRepo(testDataSource) + err := tagRelRepo.AddTagRelList(context.TODO(), testTagRelList) + if err != nil { + panic(err) + } } func Test_tagListRepo_BatchGetObjectTagRelList(t *testing.T) { - + tagRelOnce.Do(addTagRelList) + tagRelRepo := tag.NewTagRelRepo(testDataSource) + relList, err := + tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID}) + assert.NoError(t, err) + assert.Equal(t, 2, len(relList)) } func Test_tagListRepo_CountTagRelByTagID(t *testing.T) { - -} - -func Test_tagListRepo_EnableTagRelByIDs(t *testing.T) { - + tagRelOnce.Do(addTagRelList) + tagRelRepo := tag.NewTagRelRepo(testDataSource) + count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "1") + assert.NoError(t, err) + assert.Equal(t, int64(1), count) } func Test_tagListRepo_GetObjectTagRelList(t *testing.T) { + tagRelOnce.Do(addTagRelList) + tagRelRepo := tag.NewTagRelRepo(testDataSource) + relList, err := + tagRelRepo.GetObjectTagRelList(context.TODO(), testTagRelList[0].ObjectID) + assert.NoError(t, err) + assert.Equal(t, 1, len(relList)) } func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) { + tagRelOnce.Do(addTagRelList) + tagRelRepo := tag.NewTagRelRepo(testDataSource) -} + relList, err := + tagRelRepo.BatchGetObjectTagRelList(context.TODO(), []string{testTagRelList[0].ObjectID, testTagRelList[1].ObjectID}) + assert.NoError(t, err) + assert.Equal(t, 2, len(relList)) + + ids := []int64{relList[0].ID, relList[1].ID} + err = tagRelRepo.RemoveTagRelListByIDs(context.TODO(), ids) + assert.NoError(t, err) + + count, err := tagRelRepo.CountTagRelByTagID(context.TODO(), "1") + assert.NoError(t, err) + assert.Equal(t, int64(0), count) + + _, exist, err := tagRelRepo.GetObjectTagRelWithoutStatus(context.TODO(), relList[0].ObjectID, relList[0].TagID) + assert.NoError(t, err) + assert.True(t, exist) -func Test_tagListRepo_RemoveTagRelListByIDs(t *testing.T) { + err = tagRelRepo.EnableTagRelByIDs(context.TODO(), ids) + assert.NoError(t, err) + count, err = tagRelRepo.CountTagRelByTagID(context.TODO(), "1") + assert.NoError(t, err) + assert.Equal(t, int64(1), count) } From 915f3d1c94a797498ab6350044c10aa7b3477f45 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 28 Oct 2022 17:02:24 +0800 Subject: [PATCH 0046/3337] feat: add reason repo unit test --- internal/repo/reason/reason_repo.go | 4 ++-- internal/repo/repo_test/reason_repo_test.go | 18 ++++++++++++++++++ internal/service/reason/reason_service.go | 3 ++- internal/service/reason_common/reason.go | 3 ++- 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 internal/repo/repo_test/reason_repo_test.go diff --git a/internal/repo/reason/reason_repo.go b/internal/repo/reason/reason_repo.go index 1576df6e9..171b4d85c 100644 --- a/internal/repo/reason/reason_repo.go +++ b/internal/repo/reason/reason_repo.go @@ -21,9 +21,9 @@ func NewReasonRepo(configRepo config.ConfigRepo) reason_common.ReasonRepo { } } -func (rr *reasonRepo) ListReasons(ctx context.Context, req schema.ReasonReq) (resp []schema.ReasonItem, err error) { +func (rr *reasonRepo) ListReasons(ctx context.Context, objectType, action string) (resp []schema.ReasonItem, err error) { var ( - reasonAction = fmt.Sprintf("%s.%s.reasons", req.ObjectType, req.Action) + reasonAction = fmt.Sprintf("%s.%s.reasons", objectType, action) reasonKeys []string cfgValue string ) diff --git a/internal/repo/repo_test/reason_repo_test.go b/internal/repo/repo_test/reason_repo_test.go new file mode 100644 index 000000000..0e3e91e91 --- /dev/null +++ b/internal/repo/repo_test/reason_repo_test.go @@ -0,0 +1,18 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/repo/config" + "github.com/answerdev/answer/internal/repo/reason" + "github.com/stretchr/testify/assert" +) + +func Test_reasonRepo_ListReasons(t *testing.T) { + configRepo := config.NewConfigRepo(testDataSource) + reasonRepo := reason.NewReasonRepo(configRepo) + reasonItems, err := reasonRepo.ListReasons(context.TODO(), "question", "close") + assert.NoError(t, err) + assert.Equal(t, 4, len(reasonItems)) +} diff --git a/internal/service/reason/reason_service.go b/internal/service/reason/reason_service.go index c3d8b2c99..daa9cb54b 100644 --- a/internal/service/reason/reason_service.go +++ b/internal/service/reason/reason_service.go @@ -2,6 +2,7 @@ package reason import ( "context" + "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/reason_common" ) @@ -17,5 +18,5 @@ func NewReasonService(reasonRepo reason_common.ReasonRepo) *ReasonService { } func (rs ReasonService) GetReasons(ctx context.Context, req schema.ReasonReq) (resp []schema.ReasonItem, err error) { - return rs.reasonRepo.ListReasons(ctx, req) + return rs.reasonRepo.ListReasons(ctx, req.ObjectType, req.Action) } diff --git a/internal/service/reason_common/reason.go b/internal/service/reason_common/reason.go index e4918ee17..870f9ee9c 100644 --- a/internal/service/reason_common/reason.go +++ b/internal/service/reason_common/reason.go @@ -2,9 +2,10 @@ package reason_common import ( "context" + "github.com/answerdev/answer/internal/schema" ) type ReasonRepo interface { - ListReasons(ctx context.Context, req schema.ReasonReq) (resp []schema.ReasonItem, err error) + ListReasons(ctx context.Context, objectType, action string) (resp []schema.ReasonItem, err error) } From ddb612fac79ad588ca737b469a790d54cd01767f Mon Sep 17 00:00:00 2001 From: kumfo Date: Fri, 28 Oct 2022 17:12:22 +0800 Subject: [PATCH 0047/3337] feat: revision repo test --- internal/entity/revision_entity.go | 3 +- internal/repo/repo_test/revision_repo_test.go | 99 +++++++++++++++++++ internal/repo/revision/revision_repo.go | 11 --- internal/service/revision/revision.go | 1 - internal/service/revision_service.go | 27 ----- 5 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 internal/repo/repo_test/revision_repo_test.go diff --git a/internal/entity/revision_entity.go b/internal/entity/revision_entity.go index daf59a370..48740ef29 100644 --- a/internal/entity/revision_entity.go +++ b/internal/entity/revision_entity.go @@ -13,7 +13,8 @@ type Revision struct { Title string `xorm:"not null default '' VARCHAR(255) title"` Content string `xorm:"not null TEXT content"` Log string `xorm:"VARCHAR(255) log"` - Status int `xorm:"not null default 1 INT(11) status"` + // Status todo: this field is not used, will be removed in the future + Status int `xorm:"not null default 1 INT(11) status"` } // TableName revision table name diff --git a/internal/repo/repo_test/revision_repo_test.go b/internal/repo/repo_test/revision_repo_test.go new file mode 100644 index 000000000..fb6e0561b --- /dev/null +++ b/internal/repo/repo_test/revision_repo_test.go @@ -0,0 +1,99 @@ +package repo_test + +import ( + "context" + "encoding/json" + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/question" + "github.com/answerdev/answer/internal/repo/revision" + "github.com/answerdev/answer/internal/repo/unique" + "github.com/stretchr/testify/assert" + "testing" +) + +var q = &entity.Question{ + ID: "", + UserID: "1", + Title: "test", + OriginalText: "test", + ParsedText: "test", + Status: entity.QuestionStatusAvailable, + ViewCount: 0, + UniqueViewCount: 0, + VoteCount: 0, + AnswerCount: 0, + CollectionCount: 0, + FollowCount: 0, + AcceptedAnswerID: "", + LastAnswerID: "", + RevisionID: "0", +} + +func getRev(objID, title, content string) *entity.Revision { + return &entity.Revision{ + ID: "", + UserID: "1", + ObjectID: objID, + Title: title, + Content: content, + Log: "add rev", + } +} + +func Test_revisionRepo_AddRevision(t *testing.T) { + var ( + uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) + revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo) + questionRepo = question.NewQuestionRepo(testDataSource, uniqueIDRepo) + ) + + // create question + err := questionRepo.AddQuestion(context.TODO(), q) + assert.NoError(t, err) + assert.NotEqual(t, "", q.ID) + + content, err := json.Marshal(q) + // auto update false + rev := getRev(q.ID, q.Title, string(content)) + err = revisionRepo.AddRevision(context.TODO(), rev, false) + assert.NoError(t, err) + qr, _, _ := questionRepo.GetQuestion(context.TODO(), q.ID) + assert.NotEqual(t, rev.ID, qr.RevisionID) + + // auto update false + rev = getRev(q.ID, q.Title, string(content)) + err = revisionRepo.AddRevision(context.TODO(), rev, true) + assert.NoError(t, err) + qr, _, _ = questionRepo.GetQuestion(context.TODO(), q.ID) + assert.Equal(t, rev.ID, qr.RevisionID) + + // recovery + t.Cleanup(func() { + err = questionRepo.RemoveQuestion(context.TODO(), q.ID) + assert.NoError(t, err) + }) +} + +func Test_revisionRepo_GetLastRevisionByObjectID(t *testing.T) { + var ( + uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) + revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo) + ) + + Test_revisionRepo_AddRevision(t) + rev, exists, err := revisionRepo.GetLastRevisionByObjectID(context.TODO(), q.ID) + assert.NoError(t, err) + assert.True(t, exists) + assert.NotNil(t, rev) +} + +func Test_revisionRepo_GetRevisionList(t *testing.T) { + var ( + uniqueIDRepo = unique.NewUniqueIDRepo(testDataSource) + revisionRepo = revision.NewRevisionRepo(testDataSource, uniqueIDRepo) + ) + Test_revisionRepo_AddRevision(t) + revs, err := revisionRepo.GetRevisionList(context.TODO(), &entity.Revision{ObjectID: q.ID}) + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(revs), 1) +} diff --git a/internal/repo/revision/revision_repo.go b/internal/repo/revision/revision_repo.go index 4529caedc..f5b97dd04 100644 --- a/internal/repo/revision/revision_repo.go +++ b/internal/repo/revision/revision_repo.go @@ -79,17 +79,6 @@ func (rr *revisionRepo) UpdateObjectRevisionId(ctx context.Context, revision *en return nil } -// GetRevision get revision one -func (rr *revisionRepo) GetRevision(ctx context.Context, id string) ( - revision *entity.Revision, exist bool, err error) { - revision = &entity.Revision{} - exist, err = rr.data.DB.ID(id).Get(revision) - if err != nil { - return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - return -} - // GetLastRevisionByObjectID get object's last revision by object TagID func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID string) ( revision *entity.Revision, exist bool, err error) { diff --git a/internal/service/revision/revision.go b/internal/service/revision/revision.go index 333d83797..1d5d999cf 100644 --- a/internal/service/revision/revision.go +++ b/internal/service/revision/revision.go @@ -9,7 +9,6 @@ import ( // RevisionRepo revision repository type RevisionRepo interface { AddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error) - GetRevision(ctx context.Context, id string) (revision *entity.Revision, exist bool, err error) GetLastRevisionByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error) GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error) UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error) diff --git a/internal/service/revision_service.go b/internal/service/revision_service.go index 1e9b21d60..bb5443a31 100644 --- a/internal/service/revision_service.go +++ b/internal/service/revision_service.go @@ -5,14 +5,12 @@ import ( "encoding/json" "github.com/answerdev/answer/internal/base/constant" - "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/revision" usercommon "github.com/answerdev/answer/internal/service/user_common" "github.com/jinzhu/copier" - "github.com/segmentfault/pacman/errors" ) // RevisionService user service @@ -36,31 +34,6 @@ func NewRevisionService( } } -// GetRevision get revision one -func (rs *RevisionService) GetRevision(ctx context.Context, id string) (resp schema.GetRevisionResp, err error) { - var ( - rev *entity.Revision - exists bool - ) - - resp = schema.GetRevisionResp{} - - rev, exists, err = rs.revisionRepo.GetRevision(ctx, id) - if err != nil { - return - } - - if !exists { - err = errors.BadRequest(reason.ObjectNotFound) - return - } - - _ = copier.Copy(&resp, rev) - rs.parseItem(ctx, &resp) - - return -} - // GetRevisionList get revision list all func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetRevisionListReq) (resp []schema.GetRevisionResp, err error) { var ( From 9c1df37e82c7988eabd567c7b93943f03f5abb46 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 28 Oct 2022 17:13:49 +0800 Subject: [PATCH 0048/3337] feat: user info api allow no login user use and return empty data --- docs/docs.go | 10 +++++++++- docs/swagger.json | 10 +++++++++- docs/swagger.yaml | 9 ++++++++- internal/controller/user_controller.go | 11 +++++++++-- internal/router/answer_api_router.go | 2 +- internal/schema/user_schema.go | 6 +++--- 6 files changed, 39 insertions(+), 9 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index af029e38e..81ccb1a7b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3303,7 +3303,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "GetUserInfoByUserID", + "description": "get user info, if user no login response http code is 200, but user info is null", "consumes": [ "application/json" ], @@ -5628,6 +5628,14 @@ const docTemplate = `{ "e_mail" ], "properties": { + "captcha_code": { + "type": "string", + "maxLength": 500 + }, + "captcha_id": { + "type": "string", + "maxLength": 500 + }, "e_mail": { "type": "string", "maxLength": 500 diff --git a/docs/swagger.json b/docs/swagger.json index d2273f9e1..c92a6b1e5 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3291,7 +3291,7 @@ "ApiKeyAuth": [] } ], - "description": "GetUserInfoByUserID", + "description": "get user info, if user no login response http code is 200, but user info is null", "consumes": [ "application/json" ], @@ -5616,6 +5616,14 @@ "e_mail" ], "properties": { + "captcha_code": { + "type": "string", + "maxLength": 500 + }, + "captcha_id": { + "type": "string", + "maxLength": 500 + }, "e_mail": { "type": "string", "maxLength": 500 diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 66991ad22..5a261821e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1241,6 +1241,12 @@ definitions: type: object schema.UserChangeEmailSendCodeReq: properties: + captcha_code: + maxLength: 500 + type: string + captcha_id: + maxLength: 500 + type: string e_mail: maxLength: 500 type: string @@ -3365,7 +3371,8 @@ paths: get: consumes: - application/json - description: GetUserInfoByUserID + description: get user info, if user no login response http code is 200, but + user info is null produces: - application/json responses: diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 80d040f2e..6fd2abf09 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -45,9 +45,9 @@ func NewUserController( } } -// GetUserInfoByUserID godoc +// GetUserInfoByUserID get user info, if user no login response http code is 200, but user info is null // @Summary GetUserInfoByUserID -// @Description GetUserInfoByUserID +// @Description get user info, if user no login response http code is 200, but user info is null // @Tags User // @Accept json // @Produce json @@ -57,6 +57,13 @@ func NewUserController( func (uc *UserController) GetUserInfoByUserID(ctx *gin.Context) { userID := middleware.GetLoginUserIDFromContext(ctx) token := middleware.ExtractToken(ctx) + + // if user is no login return null in data + if len(token) == 0 || len(userID) == 0 { + handler.HandleResponse(ctx, nil, nil) + return + } + resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userID) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 781f92d25..56a972a8e 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -87,6 +87,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/comment", a.commentController.GetComment) // user + r.GET("/user/info", a.userController.GetUserInfoByUserID) r.GET("/user/status", a.userController.GetUserStatus) r.GET("/user/action/record", a.userController.ActionRecord) r.POST("/user/login/email", a.userController.UserEmailLogin) @@ -174,7 +175,6 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { r.DELETE("/answer", a.answerController.RemoveAnswer) // user - r.GET("/user/info", a.userController.GetUserInfoByUserID) r.PUT("/user/password", a.userController.UserModifyPassWord) r.PUT("/user/info", a.userController.UserUpdateInfo) r.POST("/user/avatar/upload", a.userController.UploadUserAvatar) diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 56702ba0f..ea37582f7 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -95,12 +95,12 @@ func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) { if ok { r.Status = statusShow } - AvatarInfo := &AvatarInfo{} - err := json.Unmarshal([]byte(userInfo.Avatar), AvatarInfo) + avatarInfo := &AvatarInfo{} + err := json.Unmarshal([]byte(userInfo.Avatar), avatarInfo) if err != nil { log.Error("AvatarInfo json.Unmarshal Error", err) } - r.Avatar = AvatarInfo + r.Avatar = avatarInfo } func (us *GetUserResp) AvatarInfo(avatarJson string) string { From 95cb136e3f5ae8972b1d261bf316bad62e1fbe8e Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 28 Oct 2022 19:28:08 +0800 Subject: [PATCH 0049/3337] feat: add user repo unit test --- internal/controller/user_controller.go | 22 ++-- .../repo/repo_test/user_backyard_repo_test.go | 53 ++++++++ internal/repo/repo_test/user_repo_test.go | 121 ++++++++++++++++++ internal/repo/user/user_repo.go | 10 +- internal/schema/user_schema.go | 14 +- internal/service/user_common/user.go | 2 +- internal/service/user_service.go | 18 ++- 7 files changed, 202 insertions(+), 38 deletions(-) create mode 100644 internal/repo/repo_test/user_backyard_repo_test.go create mode 100644 internal/repo/repo_test/user_repo_test.go diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 390b72ced..8c0b8364c 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -112,7 +112,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) { return } - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { resp := schema.UserVerifyEmailErrorResponse{ Key: "captcha_code", @@ -125,7 +125,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) { resp, err := uc.userService.EmailLogin(ctx, req) if err != nil { - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP()) + _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) resp := schema.UserVerifyEmailErrorResponse{ Key: "e_mail", Value: "error.object.email_or_password_incorrect", @@ -134,7 +134,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) return } - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) handler.HandleResponse(ctx, nil, resp) } @@ -152,7 +152,7 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { resp := schema.UserVerifyEmailErrorResponse{ Key: "captcha_code", @@ -162,7 +162,7 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) return } - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP()) + _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP()) code, err := uc.userService.RetrievePassWord(ctx, req) handler.HandleResponse(ctx, err, code) } @@ -189,8 +189,8 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) { return } - resp, err := uc.userService.UseRePassWord(ctx, req) - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP()) + resp, err := uc.userService.UseRePassword(ctx, req) + uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP()) handler.HandleResponse(ctx, err, resp) } @@ -256,7 +256,7 @@ func (uc *UserController) UserVerifyEmail(ctx *gin.Context) { return } - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) handler.HandleResponse(ctx, err, resp) } @@ -282,7 +282,7 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) { return } - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP(), + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { resp := schema.UserVerifyEmailErrorResponse{ @@ -294,7 +294,7 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) { return } - uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) + uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) err := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID) handler.HandleResponse(ctx, err, nil) } @@ -340,7 +340,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) return } - err = uc.userService.UserModifyPassWord(ctx, req) + err = uc.userService.UserModifyPassword(ctx, req) handler.HandleResponse(ctx, err, nil) } diff --git a/internal/repo/repo_test/user_backyard_repo_test.go b/internal/repo/repo_test/user_backyard_repo_test.go new file mode 100644 index 000000000..2d67cd79d --- /dev/null +++ b/internal/repo/repo_test/user_backyard_repo_test.go @@ -0,0 +1,53 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/auth" + "github.com/answerdev/answer/internal/repo/user" + "github.com/stretchr/testify/assert" +) + +func Test_userBackyardRepo_GetUserInfo(t *testing.T) { + userBackyardRepo := user.NewUserBackyardRepo(testDataSource, auth.NewAuthRepo(testDataSource)) + got, exist, err := userBackyardRepo.GetUserInfo(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "1", got.ID) +} + +func Test_userBackyardRepo_GetUserPage(t *testing.T) { + userBackyardRepo := user.NewUserBackyardRepo(testDataSource, auth.NewAuthRepo(testDataSource)) + got, total, err := userBackyardRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}) + assert.NoError(t, err) + assert.Equal(t, int64(1), total) + assert.Equal(t, "1", got[0].ID) +} + +func Test_userBackyardRepo_UpdateUserStatus(t *testing.T) { + userBackyardRepo := user.NewUserBackyardRepo(testDataSource, auth.NewAuthRepo(testDataSource)) + got, exist, err := userBackyardRepo.GetUserInfo(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, entity.UserStatusAvailable, got.Status) + + err = userBackyardRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusSuspended, entity.EmailStatusAvailable, + "admin@admin.com") + assert.NoError(t, err) + + got, exist, err = userBackyardRepo.GetUserInfo(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, entity.UserStatusSuspended, got.Status) + + err = userBackyardRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusAvailable, entity.EmailStatusAvailable, + "admin@admin.com") + assert.NoError(t, err) + + got, exist, err = userBackyardRepo.GetUserInfo(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, entity.UserStatusAvailable, got.Status) +} diff --git a/internal/repo/repo_test/user_repo_test.go b/internal/repo/repo_test/user_repo_test.go new file mode 100644 index 000000000..7b6f24ac1 --- /dev/null +++ b/internal/repo/repo_test/user_repo_test.go @@ -0,0 +1,121 @@ +package repo_test + +import ( + "context" + "testing" + + "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/repo/config" + "github.com/answerdev/answer/internal/repo/user" + "github.com/stretchr/testify/assert" +) + +func Test_userRepo_AddUser(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + userInfo := &entity.User{ + Username: "answer", + Pass: "answer", + EMail: "answer@example.com", + MailStatus: entity.EmailStatusAvailable, + Status: entity.UserStatusAvailable, + DisplayName: "answer", + IsAdmin: false, + } + err := userRepo.AddUser(context.TODO(), userInfo) + assert.NoError(t, err) +} + +func Test_userRepo_BatchGetByID(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + got, err := userRepo.BatchGetByID(context.TODO(), []string{"1"}) + assert.NoError(t, err) + assert.Equal(t, 1, len(got)) + assert.Equal(t, "admin", got[0].Username) +} + +func Test_userRepo_GetByEmail(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + got, exist, err := userRepo.GetByEmail(context.TODO(), "admin@admin.com") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "admin", got.Username) +} + +func Test_userRepo_GetByUserID(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + got, exist, err := userRepo.GetByUserID(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "admin", got.Username) +} + +func Test_userRepo_GetByUsername(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + got, exist, err := userRepo.GetByUsername(context.TODO(), "admin") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "admin", got.Username) +} + +func Test_userRepo_IncreaseAnswerCount(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + err := userRepo.IncreaseAnswerCount(context.TODO(), "1", 1) + assert.NoError(t, err) + + got, exist, err := userRepo.GetByUserID(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, 1, got.AnswerCount) +} + +func Test_userRepo_IncreaseQuestionCount(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + err := userRepo.IncreaseQuestionCount(context.TODO(), "1", 1) + assert.NoError(t, err) + + got, exist, err := userRepo.GetByUserID(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, 1, got.AnswerCount) +} + +func Test_userRepo_UpdateEmail(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + err := userRepo.UpdateEmail(context.TODO(), "1", "admin@admin.com") + assert.NoError(t, err) +} + +func Test_userRepo_UpdateEmailStatus(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + err := userRepo.UpdateEmailStatus(context.TODO(), "1", entity.EmailStatusToBeVerified) + assert.NoError(t, err) +} + +func Test_userRepo_UpdateInfo(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + err := userRepo.UpdateInfo(context.TODO(), &entity.User{ID: "1", Bio: "test"}) + assert.NoError(t, err) + + got, exist, err := userRepo.GetByUserID(context.TODO(), "1") + assert.NoError(t, err) + assert.True(t, exist) + assert.Equal(t, "test", got.Bio) +} + +func Test_userRepo_UpdateLastLoginDate(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + err := userRepo.UpdateLastLoginDate(context.TODO(), "1") + assert.NoError(t, err) +} + +func Test_userRepo_UpdateNoticeStatus(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + err := userRepo.UpdateNoticeStatus(context.TODO(), "1", 1) + assert.NoError(t, err) +} + +func Test_userRepo_UpdatePass(t *testing.T) { + userRepo := user.NewUserRepo(testDataSource, config.NewConfigRepo(testDataSource)) + err := userRepo.UpdatePass(context.TODO(), "1", "admin") + assert.NoError(t, err) +} diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index 119bab410..db1679ed2 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -2,7 +2,6 @@ package user import ( "context" - "fmt" "time" "github.com/answerdev/answer/internal/base/data" @@ -86,13 +85,10 @@ func (ur *userRepo) UpdateNoticeStatus(ctx context.Context, userID string, notic return nil } -func (ur *userRepo) UpdatePass(ctx context.Context, Data *entity.User) error { - if Data.ID == "" { - return fmt.Errorf("input error") - } - _, err := ur.data.DB.Where("id = ?", Data.ID).Cols("pass").Update(Data) +func (ur *userRepo) UpdatePass(ctx context.Context, userID, pass string) error { + _, err := ur.data.DB.Where("id = ?", userID).Cols("pass").Update(&entity.User{Pass: pass}) if err != nil { - return err + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 757926c79..82b67f188 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -150,16 +150,12 @@ func (r *GetOtherUserInfoByUsernameResp) GetFromUserEntity(userInfo *entity.User } const ( - Mail_State_Pass = 1 - Mail_State_Verifi = 2 + NoticeStatusOn = 1 + NoticeStatusOff = 2 - Notice_Status_On = 1 - Notice_Status_Off = 2 - - //ActionRecord ReportType - ActionRecord_Type_Login = "login" - ActionRecord_Type_Email = "e_mail" - ActionRecord_Type_Find_Pass = "find_pass" + ActionRecordTypeLogin = "login" + ActionRecordTypeEmail = "e_mail" + ActionRecordTypeFindPass = "find_pass" ) var UserStatusShow = map[int]string{ diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 0297ae477..5b888bac9 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -15,7 +15,7 @@ type UserRepo interface { UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error UpdateEmail(ctx context.Context, userID, email string) error - UpdatePass(ctx context.Context, Data *entity.User) error + UpdatePass(ctx context.Context, userID, pass string) error UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 0b5ad76f4..548e50650 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -174,8 +174,8 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet return code, nil } -// UseRePassWord -func (us *UserService) UseRePassWord(ctx context.Context, req *schema.UserRePassWordRequest) (resp *schema.GetUserResp, err error) { +// UseRePassword +func (us *UserService) UseRePassword(ctx context.Context, req *schema.UserRePassWordRequest) (resp *schema.GetUserResp, err error) { data := &schema.EmailCodeContent{} err = data.FromJSONString(req.Content) if err != nil { @@ -193,8 +193,7 @@ func (us *UserService) UseRePassWord(ctx context.Context, req *schema.UserRePass if err != nil { return nil, err } - userInfo.Pass = enpass - err = us.userRepo.UpdatePass(ctx, userInfo) + err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass) if err != nil { return nil, err } @@ -219,8 +218,8 @@ func (us *UserService) UserModifyPassWordVerification(ctx context.Context, reque return true, nil } -// UserModifyPassWord -func (us *UserService) UserModifyPassWord(ctx context.Context, request *schema.UserModifyPassWordRequest) error { +// UserModifyPassword user modify password +func (us *UserService) UserModifyPassword(ctx context.Context, request *schema.UserModifyPassWordRequest) error { enpass, err := us.encryptPassword(ctx, request.Pass) if err != nil { return err @@ -236,8 +235,7 @@ func (us *UserService) UserModifyPassWord(ctx context.Context, request *schema.U if !isPass { return fmt.Errorf("the old password verification failed") } - userInfo.Pass = enpass - err = us.userRepo.UpdatePass(ctx, userInfo) + err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass) if err != nil { return err } @@ -377,9 +375,9 @@ func (us *UserService) UserNoticeSet(ctx context.Context, userId string, noticeS return nil, errors.BadRequest(reason.UserNotFound) } if noticeSwitch { - userInfo.NoticeStatus = schema.Notice_Status_On + userInfo.NoticeStatus = schema.NoticeStatusOn } else { - userInfo.NoticeStatus = schema.Notice_Status_Off + userInfo.NoticeStatus = schema.NoticeStatusOff } err = us.userRepo.UpdateNoticeStatus(ctx, userInfo.ID, userInfo.NoticeStatus) return &schema.UserNoticeSetResp{NoticeSwitch: noticeSwitch}, err From f587d29c126c6da13eeb9867b1322812d698ac5f Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 28 Oct 2022 20:01:00 +0800 Subject: [PATCH 0050/3337] feat: add profile avatar setting --- ui/config-overrides.js | 3 +- ui/package.json | 2 + ui/pnpm-lock.yaml | 66 ++++++- ui/src/common/interface.ts | 5 +- ui/src/components/Avatar/index.tsx | 16 +- ui/src/components/Unactivate/index.tsx | 4 + ui/src/i18n/locales/en.json | 23 ++- .../ChangeEmail/components/sendEmail.tsx | 176 ++++++++++++++++++ ui/src/pages/Users/ChangeEmail/index.tsx | 25 +++ ui/src/pages/Users/ConfirmNewEmail/index.tsx | 9 +- ui/src/pages/Users/Settings/Profile/index.tsx | 172 +++++++++++++---- ui/src/router/route-config.ts | 4 + 12 files changed, 452 insertions(+), 53 deletions(-) create mode 100644 ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx create mode 100644 ui/src/pages/Users/ChangeEmail/index.tsx diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 438c1c29b..06464361b 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -25,8 +25,7 @@ module.exports = { const config = configFunction(proxy, allowedHost); config.proxy = { '/answer': { - target: "http://10.0.20.84:8080", - // target: 'http://10.0.10.98:2060', + target: 'http://10.0.10.98:2060', changeOrigin: true, secure: false, }, diff --git a/ui/package.json b/ui/package.json index 40d0bd4a3..541cd9714 100644 --- a/ui/package.json +++ b/ui/package.json @@ -40,6 +40,8 @@ "katex": "^0.16.2", "lodash": "^4.17.21", "marked": "^4.0.19", + "md5": "^2.3.0", + "md5.js": "^1.3.5", "mermaid": "^9.1.7", "next-share": "^0.18.1", "qs": "^6.11.0", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 22d75f0e8..cd1bfe161 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -57,6 +57,8 @@ specifiers: lint-staged: ^13.0.3 lodash: ^4.17.21 marked: ^4.0.19 + md5: ^2.3.0 + md5.js: ^1.3.5 mermaid: ^9.1.7 next-share: ^0.18.1 postcss: ^8.0.0 @@ -96,6 +98,8 @@ dependencies: katex: 0.16.2 lodash: 4.17.21 marked: 4.1.0 + md5: 2.3.0 + md5.js: 1.3.5 mermaid: 9.1.7 next-share: 0.18.1_lbqamd2wfmenkveygahn4wdfcq qs: 6.11.0 @@ -152,7 +156,7 @@ devDependencies: prettier: 2.7.1 purgecss-webpack-plugin: 4.1.3 react-app-rewired: 2.2.1_react-scripts@5.0.1 - react-scripts: 5.0.1_r727nmttzgvwuocpb6eyxi2m5i + react-scripts: 5.0.1_vcopaw66ubzwqe5wj7m4edgwnq sass: 1.54.9 tsconfig-paths-webpack-plugin: 4.0.0 typescript: 4.8.3 @@ -3660,6 +3664,10 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /charenc/0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + /check-types/11.1.2: resolution: {integrity: sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==} @@ -4060,8 +4068,8 @@ packages: engines: {node: '>=10'} hasBin: true dependencies: - is-text-path: 1.0.1 JSONStream: 1.3.5 + is-text-path: 1.0.1 lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 @@ -4157,6 +4165,10 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /crypt/0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + /crypto-random-string/2.0.0: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} @@ -6449,6 +6461,15 @@ packages: dependencies: function-bind: 1.1.1 + /hash-base/3.1.0: + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.0 + safe-buffer: 5.2.1 + dev: false + /he/1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -6807,6 +6828,10 @@ packages: call-bind: 1.0.2 has-tostringtag: 1.0.0 + /is-buffer/1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + /is-callable/1.2.6: resolution: {integrity: sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q==} engines: {node: '>= 0.4'} @@ -7966,6 +7991,22 @@ packages: hasBin: true dev: false + /md5.js/1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: false + + /md5/2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + /mdn-data/2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} @@ -8167,7 +8208,7 @@ packages: jsonp: 0.2.1 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 - react-scripts: 5.0.1_r727nmttzgvwuocpb6eyxi2m5i + react-scripts: 5.0.1_vcopaw66ubzwqe5wj7m4edgwnq transitivePeerDependencies: - supports-color dev: false @@ -9497,7 +9538,7 @@ packages: peerDependencies: react-scripts: '>=2.1.3' dependencies: - react-scripts: 5.0.1_r727nmttzgvwuocpb6eyxi2m5i + react-scripts: 5.0.1_vcopaw66ubzwqe5wj7m4edgwnq semver: 5.7.1 dev: true @@ -9531,6 +9572,12 @@ packages: /react-dev-utils/12.0.1_npfwkgbcmgrbevrxnqgustqabe: resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} engines: {node: '>=14'} + peerDependencies: + typescript: '>=2.7' + webpack: '>=4' + peerDependenciesMeta: + typescript: + optional: true dependencies: '@babel/code-frame': 7.18.6 address: 1.2.1 @@ -9561,9 +9608,7 @@ packages: transitivePeerDependencies: - eslint - supports-color - - typescript - vue-template-compiler - - webpack /react-dom/18.2.0_react@18.2.0: resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} @@ -9655,11 +9700,12 @@ packages: react: 18.2.0 dev: false - /react-scripts/5.0.1_r727nmttzgvwuocpb6eyxi2m5i: + /react-scripts/5.0.1_vcopaw66ubzwqe5wj7m4edgwnq: resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} engines: {node: '>=14.0.0'} hasBin: true peerDependencies: + eslint: '*' react: '>= 16' typescript: ^3.2.1 || ^4 peerDependenciesMeta: @@ -9708,7 +9754,7 @@ packages: semver: 7.3.7 source-map-loader: 3.0.1_webpack@5.74.0 style-loader: 3.3.1_webpack@5.74.0 - tailwindcss: 3.1.8 + tailwindcss: 3.1.8_postcss@8.4.16 terser-webpack-plugin: 5.3.6_webpack@5.74.0 typescript: 4.8.3 webpack: 5.74.0 @@ -10686,10 +10732,12 @@ packages: /symbol-tree/3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - /tailwindcss/3.1.8: + /tailwindcss/3.1.8_postcss@8.4.16: resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==} engines: {node: '>=12.13.0'} hasBin: true + peerDependencies: + postcss: ^8.0.9 dependencies: arg: 5.0.2 chokidar: 3.5.3 diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 4f39ecbe1..2319f1a60 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -2,6 +2,7 @@ export interface FormValue { value: T; isInvalid: boolean; errorMsg: string; + [prop: string]: any; } export interface FormDataType { @@ -89,7 +90,7 @@ export interface ModifyPasswordReq { export interface ModifyUserReq { display_name: string; username?: string; - avatar: string; + avatar: any; bio: string; bio_html?: string; location: string; @@ -97,7 +98,7 @@ export interface ModifyUserReq { } export interface UserInfoBase { - avatar: string; + avatar: any; username: string; display_name: string; rank: number; diff --git a/ui/src/components/Avatar/index.tsx b/ui/src/components/Avatar/index.tsx index 08242715c..db3effbbd 100644 --- a/ui/src/components/Avatar/index.tsx +++ b/ui/src/components/Avatar/index.tsx @@ -6,15 +6,25 @@ import DefaultAvatar from '@/assets/images/default-avatar.svg'; interface IProps { /** avatar url */ - avatar: string; + avatar: string | { type: string; gravatar: string; custom: string }; size: string; + searchStr?: string; className?: string; } -const Index: FC = ({ avatar, size, className }) => { +const Index: FC = ({ avatar, size, className, searchStr = '' }) => { + let url = ''; + if (typeof avatar === 'string') { + if (avatar.length > 1) { + url = `${avatar}?${searchStr}`; + } + } else if (avatar?.type !== 'default') { + url = `${avatar[avatar.type]}?${searchStr}`; + } + return ( = ({ visible = false }) => { + + {t('change_btn_name')} + )} diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 81e349264..94277ee9e 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -27,7 +27,8 @@ "account_activation": "Account Activation", "confirm_email": "Confirm Email", "account_suspended": "Account Suspended", - "admin": "Admin" + "admin": "Admin", + "change_email": "Modify Email" }, "notifications": { "title": "Notifications", @@ -421,6 +422,7 @@ "info": "If it doesn't arrive, check your spam folder.", "another": "We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder.", "btn_name": "Resend activation email", + "change_btn_name": "Change email", "msg": { "empty": "Cannot be empty." } @@ -462,6 +464,18 @@ } } }, + "change_email": { + "page_title": "Welcome to Answer", + "btn_cancel": "Cancel", + "btn_update": "Update email address", + "send_success": "If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly.", + "email": { + "label": "New Email", + "msg": { + "empty": "Email cannot be empty." + } + } + }, "password_reset": { "page_title": "Password Reset", "btn_name": "Reset my password", @@ -504,7 +518,12 @@ }, "avatar": { "label": "Profile Image", - "text": "You can upload your image or <1>reset it to" + "gravatar": "Gravatar", + "gravatar_text": "You can change image on <1>gravatar.com", + "custom": "Custom", + "btn_refresh": "Refresh", + "custom_text": "You can upload your image.", + "default": "Default" }, "bio": { "label": "About Me (optional)" diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx new file mode 100644 index 000000000..28de25c50 --- /dev/null +++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx @@ -0,0 +1,176 @@ +import { FC, memo, useEffect, useState } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { changeEmail, checkImgCode } from '@answer/api'; +import type { + ImgCodeRes, + PasswordResetReq, + FormDataType, +} from '@answer/common/interface'; +import { userInfoStore } from '@answer/stores'; + +import { PicAuthCodeModal } from '@/components/Modal'; + +const Index: FC = () => { + const { t } = useTranslation('translation', { keyPrefix: 'change_email' }); + const [formData, setFormData] = useState({ + e_mail: { + value: '', + isInvalid: false, + errorMsg: '', + }, + captcha_code: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }); + const [imgCode, setImgCode] = useState({ + captcha_id: '', + captcha_img: '', + verify: false, + }); + const [showModal, setModalState] = useState(false); + const navigate = useNavigate(); + const { user: userInfo, update: updateUser } = userInfoStore(); + + const getImgCode = () => { + checkImgCode({ + action: 'e_mail', + }).then((res) => { + setImgCode(res); + }); + }; + + const handleChange = (params: FormDataType) => { + setFormData({ ...formData, ...params }); + }; + + const checkValidated = (): boolean => { + let bol = true; + + if (!formData.e_mail.value) { + bol = false; + formData.e_mail = { + value: '', + isInvalid: true, + errorMsg: t('email.msg.empty'), + }; + } + setFormData({ + ...formData, + }); + return bol; + }; + + const sendEmail = (e?: any) => { + if (e) { + e.preventDefault(); + } + const params: PasswordResetReq = { + e_mail: formData.e_mail.value, + }; + if (imgCode.verify) { + params.captcha_code = formData.captcha_code.value; + params.captcha_id = imgCode.captcha_id; + } + + changeEmail(params) + .then(() => { + userInfo.e_mail = formData.e_mail.value; + updateUser(userInfo); + navigate('/users/login', { replace: true }); + setModalState(false); + }) + .catch((err) => { + if (err.isError && err.key) { + formData[err.key].isInvalid = true; + formData[err.key].errorMsg = err.value; + if (err.key.indexOf('captcha') < 0) { + setModalState(false); + } + } + setFormData({ ...formData }); + }) + .finally(() => { + getImgCode(); + }); + }; + + const handleSubmit = async (event: any) => { + event.preventDefault(); + event.stopPropagation(); + + if (!checkValidated()) { + return; + } + + if (imgCode.verify) { + setModalState(true); + return; + } + + sendEmail(); + }; + + const goBack = () => { + navigate('/users/login?status=inactive', { replace: true }); + }; + + useEffect(() => { + getImgCode(); + }, []); + + return ( + <> +
+ + {t('email.label')} + { + handleChange({ + e_mail: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {formData.e_mail.errorMsg} + + + +
+ + +
+
+ + setModalState(false)} + /> + + ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Users/ChangeEmail/index.tsx b/ui/src/pages/Users/ChangeEmail/index.tsx new file mode 100644 index 000000000..cbb743a5d --- /dev/null +++ b/ui/src/pages/Users/ChangeEmail/index.tsx @@ -0,0 +1,25 @@ +import { FC, memo } from 'react'; +import { Container, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import SendEmail from './components/sendEmail'; + +import { PageTitle } from '@/components'; + +const Index: FC = () => { + const { t } = useTranslation('translation', { keyPrefix: 'change_email' }); + + return ( + <> + + +

{t('page_title')}

+ + + +
+ + ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Users/ConfirmNewEmail/index.tsx b/ui/src/pages/Users/ConfirmNewEmail/index.tsx index cbd9ad706..83c48a66d 100644 --- a/ui/src/pages/Users/ConfirmNewEmail/index.tsx +++ b/ui/src/pages/Users/ConfirmNewEmail/index.tsx @@ -3,7 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap'; import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { changeEmailVerify } from '@answer/api'; +import { changeEmailVerify, getUserInfo } from '@answer/api'; +import { userInfoStore } from '@answer/stores'; import { PageTitle } from '@/components'; @@ -12,6 +13,8 @@ const Index: FC = () => { const [searchParams] = useSearchParams(); const [step, setStep] = useState('loading'); + const updateUser = userInfoStore((state) => state.update); + useEffect(() => { const code = searchParams.get('code'); if (code) { @@ -19,6 +22,10 @@ const Index: FC = () => { changeEmailVerify({ code }) .then(() => { setStep('success'); + getUserInfo().then((res) => { + // update user info + updateUser(res); + }); }) .catch(() => { setStep('invalid'); diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 49266cebd..92272f6fc 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -3,6 +3,7 @@ import { Form, Button } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; import { marked } from 'marked'; +import MD5 from 'md5'; import { modifyUserInfo, uploadAvatar, getUserInfo } from '@answer/api'; import type { FormDataType } from '@answer/common/interface'; @@ -16,6 +17,9 @@ const Index: React.FC = () => { }); const toast = useToast(); const { user, update } = userInfoStore(); + const [mailHash, setMailHash] = useState(''); + const [count, setCount] = useState(0); + const [formData, setFormData] = useState({ display_name: { value: '', @@ -28,6 +32,9 @@ const Index: React.FC = () => { errorMsg: '', }, avatar: { + type: 'default', + gravatar: '', + custom: '', value: '', isInvalid: false, errorMsg: '', @@ -59,7 +66,9 @@ const Index: React.FC = () => { setFormData({ ...formData, avatar: { - value: res, + ...formData.avatar, + type: 'custom', + custom: res, isInvalid: false, errorMsg: '', }, @@ -136,7 +145,11 @@ const Index: React.FC = () => { const params = { display_name: formData.display_name.value, username: formData.username.value, - avatar: formData.avatar.value, + avatar: { + type: formData.avatar.type, + gravatar: formData.avatar.gravatar, + custom: formData.avatar.custom, + }, bio: formData.bio.value, website: formData.website.value, location: formData.location.value, @@ -168,13 +181,25 @@ const Index: React.FC = () => { formData.display_name.value = res.display_name; formData.username.value = res.username; formData.bio.value = res.bio; - formData.avatar.value = res.avatar; + formData.avatar.type = res.avatar.type || 'default'; + formData.avatar.gravatar = res.avatar.gravatar; + formData.avatar.custom = res.avatar.custom; formData.location.value = res.location; formData.website.value = res.website; setFormData({ ...formData }); + if (res.e_mail) { + const str = res.e_mail.toLowerCase().trim(); + const hash = MD5(str); + console.log(str, hash, mailHash); + setMailHash(hash); + } }); }; + const refreshGravatar = () => { + setCount((pre) => pre + 1); + }; + useEffect(() => { getProfile(); }, []); @@ -227,39 +252,118 @@ const Index: React.FC = () => { {t('avatar.label')} -
- + + handleChange({ + avatar: { + ...formData.avatar, + type: 'gravatar', + gravatar: `https://www.gravatar.com/avatar/${mailHash}`, + isInvalid: false, + errorMsg: '', + }, + }) + } /> + + handleChange({ + avatar: { + ...formData.avatar, + type: 'custom', + isInvalid: false, + errorMsg: '', + }, + }) + } + /> + + handleChange({ + avatar: { + ...formData.avatar, + type: 'default', + isInvalid: false, + errorMsg: '', + }, + }) + } + /> +
+
+ {formData.avatar.type === 'gravatar' && ( + <> + +
+ +
+ + + You can change your image on{' '} + + gravatar.com + + + +
+
+ + )} - + {formData.avatar.type === 'custom' && ( + <> + +
+ +
+ + + You can upload your image. + + +
+
+ + )} + {formData.avatar.type === 'default' && ( + + )}
diff --git a/ui/src/router/route-config.ts b/ui/src/router/route-config.ts index b942464fb..2e46ffbe2 100644 --- a/ui/src/router/route-config.ts +++ b/ui/src/router/route-config.ts @@ -114,6 +114,10 @@ const routeConfig: RouteNode[] = [ path: 'users/account-recovery', page: 'pages/Users/AccountForgot', }, + { + path: 'users/change-email', + page: 'pages/Users/ChangeEmail', + }, { path: 'users/password-reset', page: 'pages/Users/PasswordReset', From 1af9de79fc8f3b5882531bd8b1c66d7d88fe12ed Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 28 Oct 2022 20:02:59 +0800 Subject: [PATCH 0051/3337] fix: ptimization of avatar taking values --- ui/src/components/BaseUserCard/index.tsx | 16 ++++++++++++++-- .../Header/components/NavItems/index.tsx | 2 +- ui/src/components/UserCard/index.tsx | 9 ++++++++- ui/src/pages/Admin/Users/index.tsx | 1 + .../Users/Personal/components/UserInfo/index.tsx | 4 ++-- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/ui/src/components/BaseUserCard/index.tsx b/ui/src/components/BaseUserCard/index.tsx index 81d5a3970..fa524bda9 100644 --- a/ui/src/components/BaseUserCard/index.tsx +++ b/ui/src/components/BaseUserCard/index.tsx @@ -9,6 +9,7 @@ interface Props { data: any; showAvatar?: boolean; avatarSize?: string; + avatarSearchStr?: string; className?: string; } @@ -17,20 +18,31 @@ const Index: FC = ({ showAvatar = true, avatarSize = '20px', className = 'fs-14', + avatarSearchStr = 's=48', }) => { return (
{data?.status !== 'deleted' ? ( {showAvatar && ( - + )} {data?.display_name} ) : ( <> {showAvatar && ( - + )} {data?.display_name} diff --git a/ui/src/components/Header/components/NavItems/index.tsx b/ui/src/components/Header/components/NavItems/index.tsx index ec2009ec0..67112758e 100644 --- a/ui/src/components/Header/components/NavItems/index.tsx +++ b/ui/src/components/Header/components/NavItems/index.tsx @@ -43,7 +43,7 @@ const Index: FC = ({ redDot, userInfo, logOut }) => { id="dropdown-basic" as="a" className="no-toggle pointer"> - + diff --git a/ui/src/components/UserCard/index.tsx b/ui/src/components/UserCard/index.tsx index 259ab8c5f..0110c82a0 100644 --- a/ui/src/components/UserCard/index.tsx +++ b/ui/src/components/UserCard/index.tsx @@ -23,16 +23,23 @@ const Index: FC = ({ data, time, preFix, className = '' }) => { avatar={data?.avatar} size="40px" className="me-2 d-none d-md-block" + searchStr="s=48" /> ) : ( - + )}
diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index 6662b11f7..f15db7b5d 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -107,6 +107,7 @@ const Users: FC = () => { data={user} className="fs-6" avatarSize="24px" + avatarSearchStr="s=48" /> {user.rank} diff --git a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx index 02b39b685..97caca3e1 100644 --- a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx +++ b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx @@ -19,10 +19,10 @@ const Index: FC = ({ data }) => {
{data?.status !== 'deleted' ? ( - + ) : ( - + )}
From 1cd83f4e879db5f016a51ee5d4300976aef46c00 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 28 Oct 2022 20:24:04 +0800 Subject: [PATCH 0052/3337] feat: change default cache dir to data, create dir if not exist. --- configs/config.yaml | 2 +- internal/base/data/data.go | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/configs/config.yaml b/configs/config.yaml index 3e7cfd84b..0ec1a601c 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -6,7 +6,7 @@ data: driver: "mysql" connection: root:root@tcp(db:3306)/answer cache: - file_path: "/tmp/cache/cache.db" + file_path: "/data/cache/cache.db" i18n: bundle_dir: "/data/i18n" swaggerui: diff --git a/internal/base/data/data.go b/internal/base/data/data.go index cb50f7e0b..b6878181b 100644 --- a/internal/base/data/data.go +++ b/internal/base/data/data.go @@ -1,8 +1,10 @@ package data import ( + "path/filepath" "time" + "github.com/answerdev/answer/pkg/dir" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" @@ -69,6 +71,12 @@ func NewCache(c *CacheConf) (cache.Cache, func(), error) { memCache := memory.NewCache() if len(c.FilePath) > 0 { + cacheFileDir := filepath.Dir(c.FilePath) + log.Debugf("try to create cache directory %s", cacheFileDir) + err := dir.CreateDirIfNotExist(cacheFileDir) + if err != nil { + log.Errorf("create cache dir failed: %s", err) + } log.Infof("try to load cache file from %s", c.FilePath) if err := memory.Load(memCache, c.FilePath); err != nil { log.Warn(err) From a801ff6cda4aca2221ca26717b81a7add5042d76 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Sat, 29 Oct 2022 20:43:52 +0800 Subject: [PATCH 0053/3337] feat(navigation): guard route done --- ui/commitlint.config.js | 2 +- ui/src/App.tsx | 3 +- ui/src/common/constants.ts | 12 +- ui/src/common/interface.ts | 2 +- ui/src/components/Actions/index.tsx | 13 +- ui/src/components/Comment/index.tsx | 18 +- ui/src/components/Editor/ToolBars/image.tsx | 3 +- ui/src/components/FollowingTags/index.tsx | 7 +- ui/src/components/Header/index.tsx | 16 +- ui/src/components/HotQuestions/index.tsx | 3 +- ui/src/components/Modal/PicAuthCodeModal.tsx | 5 +- ui/src/components/Operate/index.tsx | 7 +- ui/src/components/QuestionList/index.tsx | 2 +- ui/src/components/Share/index.tsx | 4 +- ui/src/components/TagSelector/index.tsx | 2 +- ui/src/components/Unactivate/index.tsx | 11 +- ui/src/hooks/useChangeModal/index.tsx | 2 +- ui/src/hooks/useReportModal/index.tsx | 3 +- ui/src/i18n/init.ts | 4 +- ui/src/index.tsx | 22 ++- ui/src/pages/Admin/Answers/index.tsx | 2 +- ui/src/pages/Admin/Flags/index.tsx | 2 +- ui/src/pages/Admin/General/index.tsx | 2 +- ui/src/pages/Admin/Interface/index.tsx | 2 +- ui/src/pages/Admin/Questions/index.tsx | 2 +- ui/src/pages/Admin/Users/index.tsx | 2 +- ui/src/pages/Layout/index.tsx | 43 ++--- ui/src/pages/Questions/Ask/index.tsx | 2 +- .../Detail/components/Answer/index.tsx | 2 +- .../Detail/components/Question/index.tsx | 2 +- .../components/RelatedQuestions/index.tsx | 6 +- .../Detail/components/WriteAnswer/index.tsx | 2 +- ui/src/pages/Questions/Detail/index.tsx | 6 +- ui/src/pages/Questions/EditAnswer/index.tsx | 2 +- ui/src/pages/Search/components/Head/index.tsx | 6 +- ui/src/pages/Search/index.tsx | 2 +- ui/src/pages/Tags/Detail/index.tsx | 2 +- ui/src/pages/Tags/Edit/index.tsx | 6 +- ui/src/pages/Tags/Info/index.tsx | 2 +- ui/src/pages/Tags/index.tsx | 2 +- .../AccountForgot/components/sendEmail.tsx | 2 +- ui/src/pages/Users/AccountForgot/index.tsx | 5 +- ui/src/pages/Users/ActiveEmail/index.tsx | 6 +- ui/src/pages/Users/ConfirmNewEmail/index.tsx | 2 +- ui/src/pages/Users/Login/index.tsx | 31 +-- ui/src/pages/Users/Notifications/index.tsx | 2 +- ui/src/pages/Users/PasswordReset/index.tsx | 13 +- ui/src/pages/Users/Personal/index.tsx | 6 +- .../Register/components/SignUpForm/index.tsx | 2 +- ui/src/pages/Users/Register/index.tsx | 5 +- .../Account/components/ModifyEmail/index.tsx | 4 +- .../Account/components/ModifyPass/index.tsx | 2 +- .../pages/Users/Settings/Interface/index.tsx | 10 +- .../Users/Settings/Notification/index.tsx | 4 +- ui/src/pages/Users/Settings/Profile/index.tsx | 8 +- ui/src/pages/Users/Settings/index.tsx | 4 +- ui/src/pages/Users/Suspended/index.tsx | 4 +- ui/src/router/alias.ts | 8 + ui/src/router/guarder.ts | 42 ++++ ui/src/router/index.tsx | 48 +++-- ui/src/router/route-rules.ts | 9 - ui/src/router/{route-config.ts => routes.ts} | 19 +- ui/src/services/client/index.ts | 1 - ui/src/services/client/notification.ts | 5 +- ui/src/services/client/tag.ts | 5 +- ui/src/services/client/user.ts | 17 -- ui/src/services/common.ts | 2 +- ui/src/services/{api.ts => index.ts} | 0 ui/src/stores/index.ts | 4 +- ui/src/stores/userInfo.ts | 19 +- ui/src/utils/common.ts | 80 ++++++++ ui/src/utils/floppyNavigation.ts | 40 ++++ ui/src/utils/guards.ts | 182 ++++++++++++++++++ ui/src/utils/index.ts | 118 +----------- ui/src/utils/request.ts | 88 +++++---- ui/src/utils/storage.ts | 5 +- ui/tsconfig.json | 1 - 77 files changed, 643 insertions(+), 398 deletions(-) create mode 100644 ui/src/router/alias.ts create mode 100644 ui/src/router/guarder.ts delete mode 100644 ui/src/router/route-rules.ts rename ui/src/router/{route-config.ts => routes.ts} (90%) delete mode 100644 ui/src/services/client/user.ts rename ui/src/services/{api.ts => index.ts} (100%) create mode 100644 ui/src/utils/common.ts create mode 100644 ui/src/utils/floppyNavigation.ts create mode 100644 ui/src/utils/guards.ts diff --git a/ui/commitlint.config.js b/ui/commitlint.config.js index 84dcb122a..4944db0ea 100644 --- a/ui/commitlint.config.js +++ b/ui/commitlint.config.js @@ -1,3 +1,3 @@ module.exports = { - extends: ['@commitlint/config-conventional'], + extends: ['@commitlint/routes-conventional'], }; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 5d4f69259..878ca1ab1 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,8 +1,9 @@ import { RouterProvider } from 'react-router-dom'; -import router from '@/router'; +import { routes, createBrowserRouter } from '@/router'; function App() { + const router = createBrowserRouter(routes); return ; } diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 9f7f6f5e6..364c6bfef 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -1,9 +1,9 @@ -export const LOGIN_NEED_BACK = [ - '/users/login', - '/users/register', - '/users/account-recovery', - '/users/password-reset', -]; +export const DEFAULT_LANG = 'en_US'; +export const CURRENT_LANG_STORAGE_KEY = '_a_lang__'; +export const LOGGED_USER_STORAGE_KEY = '_a_lui_'; +export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; +export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; +export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_'; export const ADMIN_LIST_STATUS = { // normal; diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 73eac5d43..0b0a733ef 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -108,7 +108,7 @@ export interface UserInfoBase { */ status?: string; /** roles */ - is_admin?: true; + is_admin?: boolean; } export interface UserInfoRes extends UserInfoBase { diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx index 87c35cef2..25402ab2d 100644 --- a/ui/src/components/Actions/index.tsx +++ b/ui/src/components/Actions/index.tsx @@ -5,11 +5,12 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import { Icon } from '@answer/components'; -import { bookmark, postVote } from '@answer/api'; -import { isLogin } from '@answer/utils'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { useToast } from '@answer/hooks'; +import { tryNormalLogged } from '@/utils/guards'; +import { bookmark, postVote } from '@/services'; + interface Props { className?: string; data: { @@ -32,7 +33,7 @@ const Index: FC = ({ className, data }) => { state: data?.collected, count: data?.collectCount, }); - const { username = '' } = userInfoStore((state) => state.user); + const { username = '' } = loggedUserInfoStore((state) => state.user); const toast = useToast(); const { t } = useTranslation(); useEffect(() => { @@ -48,7 +49,7 @@ const Index: FC = ({ className, data }) => { }, []); const handleVote = (type: 'up' | 'down') => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } @@ -84,7 +85,7 @@ const Index: FC = ({ className, data }) => { }; const handleBookmark = () => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } bookmark({ diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index e7fd3bff3..a6dcdc98c 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -8,18 +8,20 @@ import { unionBy } from 'lodash'; import { marked } from 'marked'; import * as Types from '@answer/common/interface'; +import { Modal } from '@answer/components'; +import { usePageUsers, useReportModal } from '@answer/hooks'; +import { matchedUsers, parseUserInfo } from '@answer/utils'; + +import { Form, ActionBar, Reply } from './components'; + +import { tryNormalLogged } from '@/utils/guards'; import { useQueryComments, addComment, deleteComment, updateComment, postVote, -} from '@answer/api'; -import { Modal } from '@answer/components'; -import { usePageUsers, useReportModal } from '@answer/hooks'; -import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils'; - -import { Form, ActionBar, Reply } from './components'; +} from '@/services'; import './index.scss'; @@ -163,7 +165,7 @@ const Comment = ({ objectId, mode }) => { }; const handleVote = (id, is_cancel) => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } @@ -189,7 +191,7 @@ const Comment = ({ objectId, mode }) => { }; const handleAction = ({ action }, item) => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } if (action === 'report') { diff --git a/ui/src/components/Editor/ToolBars/image.tsx b/ui/src/components/Editor/ToolBars/image.tsx index 5f4475a23..ead20eee9 100644 --- a/ui/src/components/Editor/ToolBars/image.tsx +++ b/ui/src/components/Editor/ToolBars/image.tsx @@ -3,10 +3,11 @@ import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Modal as AnswerModal } from '@answer/components'; -import { uploadImage } from '@answer/api'; import ToolItem from '../toolItem'; import { IEditorContext } from '../types'; +import { uploadImage } from '@/services'; + const Image: FC = ({ editor }) => { const { t } = useTranslation('translation', { keyPrefix: 'editor' }); diff --git a/ui/src/components/FollowingTags/index.tsx b/ui/src/components/FollowingTags/index.tsx index 7f93a2052..55e9bc762 100644 --- a/ui/src/components/FollowingTags/index.tsx +++ b/ui/src/components/FollowingTags/index.tsx @@ -4,8 +4,9 @@ import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; import { TagSelector, Tag } from '@answer/components'; -import { isLogin } from '@answer/utils'; -import { useFollowingTags, followTags } from '@answer/api'; + +import { tryNormalLogged } from '@/utils/guards'; +import { useFollowingTags, followTags } from '@/services'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'question' }); @@ -32,7 +33,7 @@ const Index: FC = () => { }); }; - if (!isLogin()) { + if (!tryNormalLogged()) { return null; } diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 54d6cee74..b4cd4e164 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -17,17 +17,22 @@ import { useLocation, } from 'react-router-dom'; -import { userInfoStore, siteInfoStore, interfaceStore } from '@answer/stores'; -import { logout, useQueryNotificationStatus } from '@answer/api'; -import Storage from '@answer/utils/storage'; +import { + loggedUserInfoStore, + siteInfoStore, + interfaceStore, +} from '@answer/stores'; import NavItems from './components/NavItems'; +import { logout, useQueryNotificationStatus } from '@/services'; +import { RouteAlias } from '@/router/alias'; + import './index.scss'; const Header: FC = () => { const navigate = useNavigate(); - const { user, clear } = userInfoStore(); + const { user, clear } = loggedUserInfoStore(); const { t } = useTranslation(); const [urlSearch] = useSearchParams(); const q = urlSearch.get('q'); @@ -42,9 +47,8 @@ const Header: FC = () => { const handleLogout = async () => { await logout(); - Storage.remove('token'); clear(); - navigate('/'); + navigate(RouteAlias.home); }; useEffect(() => { diff --git a/ui/src/components/HotQuestions/index.tsx b/ui/src/components/HotQuestions/index.tsx index d398d172a..920bdce22 100644 --- a/ui/src/components/HotQuestions/index.tsx +++ b/ui/src/components/HotQuestions/index.tsx @@ -3,9 +3,10 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useHotQuestions } from '@answer/api'; import { Icon } from '@answer/components'; +import { useHotQuestions } from '@/services'; + const HotQuestions: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'question' }); const [questions, setQuestions] = useState([]); diff --git a/ui/src/components/Modal/PicAuthCodeModal.tsx b/ui/src/components/Modal/PicAuthCodeModal.tsx index f0fe20914..3a081a0cf 100644 --- a/ui/src/components/Modal/PicAuthCodeModal.tsx +++ b/ui/src/components/Modal/PicAuthCodeModal.tsx @@ -9,6 +9,9 @@ import type { ImgCodeRes, } from '@answer/common/interface'; +import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; +import Storage from '@/utils/storage'; + interface IProps { /** control visible */ visible: boolean; @@ -55,7 +58,7 @@ const Index: React.FC = ({ placeholder={t('placeholder')} isInvalid={captcha.isInvalid} onChange={(e) => { - localStorage.setItem('captchaCode', e.target.value); + Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value); handleCaptcha({ captcha_code: { value: e.target.value, diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index 6d76af2ad..201774c6c 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -5,10 +5,11 @@ import { useTranslation } from 'react-i18next'; import { Modal } from '@answer/components'; import { useReportModal, useToast } from '@answer/hooks'; -import { deleteQuestion, deleteAnswer } from '@answer/api'; -import { isLogin } from '@answer/utils'; import Share from '../Share'; +import { deleteQuestion, deleteAnswer } from '@/services'; +import { tryNormalLogged } from '@/utils/guards'; + interface IProps { type: 'answer' | 'question'; qid: string; @@ -98,7 +99,7 @@ const Index: FC = ({ }; const handleAction = (action) => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } if (action === 'delete') { diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 295b90346..69c6ad9d9 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -3,7 +3,7 @@ import { Row, Col, ListGroup } from 'react-bootstrap'; import { NavLink, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQuestionList } from '@answer/api'; +import { useQuestionList } from '@/services'; import type * as Type from '@answer/common/interface'; import { Icon, diff --git a/ui/src/components/Share/index.tsx b/ui/src/components/Share/index.tsx index d5866d980..74b3de34c 100644 --- a/ui/src/components/Share/index.tsx +++ b/ui/src/components/Share/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { FacebookShareButton, TwitterShareButton } from 'next-share'; import copy from 'copy-to-clipboard'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; interface IProps { type: 'answer' | 'question'; @@ -15,7 +15,7 @@ interface IProps { } const Index: FC = ({ type, qid, aid, title }) => { - const user = userInfoStore((state) => state.user); + const user = loggedUserInfoStore((state) => state.user); const [show, setShow] = useState(false); const [showTip, setShowTip] = useState(false); const [canSystemShare, setSystemShareState] = useState(false); diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index 790b82422..f939b6749 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -6,7 +6,7 @@ import { marked } from 'marked'; import classNames from 'classnames'; import { useTagModal } from '@answer/hooks'; -import { queryTags } from '@answer/api'; +import { queryTags } from '@/services'; import type * as Type from '@answer/common/interface'; import './index.scss'; diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index 3fc262009..dd44f68fa 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -2,14 +2,17 @@ import React, { useState, useEffect } from 'react'; import { Button, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { resendEmail, checkImgCode } from '@answer/api'; +import { resendEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@answer/components/Modal'; import type { ImgCodeRes, ImgCodeReq, FormDataType, } from '@answer/common/interface'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; + +import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; +import Storage from '@/utils/storage'; interface IProps { visible: boolean; @@ -19,7 +22,7 @@ const Index: React.FC = ({ visible = false }) => { const { t } = useTranslation('translation', { keyPrefix: 'inactive' }); const [isSuccess, setSuccess] = useState(false); const [showModal, setModalState] = useState(false); - const { e_mail } = userInfoStore((state) => state.user); + const { e_mail } = loggedUserInfoStore((state) => state.user); const [formData, setFormData] = useState({ captcha_code: { value: '', @@ -47,7 +50,7 @@ const Index: React.FC = ({ visible = false }) => { } let obj: ImgCodeReq = {}; if (imgCode.verify) { - const code = localStorage.getItem('captchaCode') || ''; + const code = Storage.get(CAPTCHA_CODE_STORAGE_KEY) || ''; obj = { captcha_code: code, captcha_id: imgCode.captcha_id, diff --git a/ui/src/hooks/useChangeModal/index.tsx b/ui/src/hooks/useChangeModal/index.tsx index b46aa80da..76b9f2ead 100644 --- a/ui/src/hooks/useChangeModal/index.tsx +++ b/ui/src/hooks/useChangeModal/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { changeUserStatus } from '@answer/api'; +import { changeUserStatus } from '@/services'; import { Modal as AnswerModal } from '@answer/components'; const div = document.createElement('div'); diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx index 2566ccd60..2f7c8f184 100644 --- a/ui/src/hooks/useReportModal/index.tsx +++ b/ui/src/hooks/useReportModal/index.tsx @@ -4,10 +4,11 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { reportList, postReport, closeQuestion, putReport } from '@answer/api'; import { useToast } from '@answer/hooks'; import type * as Type from '@answer/common/interface'; +import { reportList, postReport, closeQuestion, putReport } from '@/services'; + interface Params { isBackend?: boolean; type: Type.ReportType; diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index 495ecc1dd..deecc0e23 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -6,6 +6,8 @@ import Backend from 'i18next-http-backend'; import en from './locales/en.json'; import zh from './locales/zh_CN.json'; +import { DEFAULT_LANG } from '@/common/constants'; + i18next // load translation using http .use(Backend) @@ -21,7 +23,7 @@ i18next }, }, // debug: process.env.NODE_ENV === 'development', - fallbackLng: process.env.REACT_APP_LANG || 'en_US', + fallbackLng: process.env.REACT_APP_LANG || DEFAULT_LANG, interpolation: { escapeValue: false, }, diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 8553972ac..1943b61a3 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -3,14 +3,26 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; + +import { pullLoggedUser } from '@/utils/guards'; + import './i18n/init'; import './index.scss'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement, ); -root.render( - - - , -); + +async function bootstrapApp() { + /** + * NOTICE: must pre init logged user info for router + */ + await pullLoggedUser(); + root.render( + + + , + ); +} + +bootstrapApp(); diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index f9a9de278..ead61b578 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -14,7 +14,7 @@ import { } from '@answer/components'; import { ADMIN_LIST_STATUS } from '@answer/common/constants'; import { useEditStatusModal } from '@answer/hooks'; -import { useAnswerSearch, changeAnswerStatus } from '@answer/api'; +import { useAnswerSearch, changeAnswerStatus } from '@/services'; import * as Type from '@answer/common/interface'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Flags/index.tsx b/ui/src/pages/Admin/Flags/index.tsx index 5eb14ec4f..ed10eaa55 100644 --- a/ui/src/pages/Admin/Flags/index.tsx +++ b/ui/src/pages/Admin/Flags/index.tsx @@ -12,7 +12,7 @@ import { } from '@answer/components'; import { useReportModal } from '@answer/hooks'; import * as Type from '@answer/common/interface'; -import { useFlagSearch } from '@answer/api'; +import { useFlagSearch } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 87e473d34..9caf81e82 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; import { useToast } from '@answer/hooks'; import { siteInfoStore } from '@answer/stores'; -import { useGeneralSetting, updateGeneralSetting } from '@answer/api'; +import { useGeneralSetting, updateGeneralSetting } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 66f7ef4c9..e22d1046d 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -14,7 +14,7 @@ import { updateInterfaceSetting, useInterfaceSetting, useThemeOptions, -} from '@answer/api'; +} from '@/services'; import { interfaceStore } from '@answer/stores'; import { UploadImg } from '@answer/components'; diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 373faf947..3235a819d 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -18,7 +18,7 @@ import { useQuestionSearch, changeQuestionStatus, deleteQuestion, -} from '@answer/api'; +} from '@/services'; import * as Type from '@answer/common/interface'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index 6662b11f7..b4efe613d 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -3,7 +3,7 @@ import { Button, Form, Table, Badge } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQueryUsers } from '@answer/api'; +import { useQueryUsers } from '@/services'; import { Pagination, FormatTime, diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 69c5195df..bad408bfe 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -1,60 +1,43 @@ -import { FC, useEffect } from 'react'; +import { FC, useEffect, memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { - userInfoStore, - siteInfoStore, - interfaceStore, - toastStore, -} from '@answer/stores'; +import { siteInfoStore, interfaceStore, toastStore } from '@answer/stores'; import { Header, AdminHeader, Footer, Toast } from '@answer/components'; -import { useSiteSettings, useCheckUserStatus } from '@answer/api'; +import { useSiteSettings } from '@/services'; import Storage from '@/utils/storage'; +import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; let isMounted = false; const Layout: FC = () => { const { siteInfo, update: siteStoreUpdate } = siteInfoStore(); const { update: interfaceStoreUpdate } = interfaceStore(); const { data: siteSettings } = useSiteSettings(); - const { data: userStatus } = useCheckUserStatus(); - useEffect(() => { - if (siteSettings) { - siteStoreUpdate(siteSettings.general); - interfaceStoreUpdate(siteSettings.interface); - } - }, [siteSettings]); - const updateUser = userInfoStore((state) => state.update); const { msg: toastMsg, variant, clear: toastClear } = toastStore(); const { i18n } = useTranslation(); const closeToast = () => { toastClear(); }; + + useEffect(() => { + if (siteSettings) { + siteStoreUpdate(siteSettings.general); + interfaceStoreUpdate(siteSettings.interface); + } + }, [siteSettings]); if (!isMounted) { isMounted = true; - const lang = Storage.get('LANG'); - const user = Storage.get('userInfo'); - if (user) { - updateUser(user); - } + const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); if (lang) { i18n.changeLanguage(lang); } } - if (userStatus?.status) { - const user = Storage.get('userInfo'); - if (userStatus.status !== user.status) { - user.status = userStatus?.status; - updateUser(user); - } - } - return ( @@ -76,4 +59,4 @@ const Layout: FC = () => { ); }; -export default Layout; +export default memo(Layout); diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index abcc86f62..f202e0dc1 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -14,7 +14,7 @@ import { useQueryRevisions, postAnswer, useQueryQuestionByTitle, -} from '@answer/api'; +} from '@/services'; import type * as Type from '@answer/common/interface'; import SearchQuestion from './components/SearchQuestion'; diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index b7bd25d88..4ffed114a 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -11,7 +11,7 @@ import { FormatTime, htmlRender, } from '@answer/components'; -import { acceptanceAnswer } from '@answer/api'; +import { acceptanceAnswer } from '@/services'; import { scrollTop } from '@answer/utils'; import { AnswerItem } from '@answer/common/interface'; diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index 8085b1720..10c18a473 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -13,7 +13,7 @@ import { htmlRender, } from '@answer/components'; import { formatCount } from '@answer/utils'; -import { following } from '@answer/api'; +import { following } from '@/services'; interface Props { data: any; diff --git a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx index bada82078..c6cf29d71 100644 --- a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx @@ -3,16 +3,16 @@ import { Card, ListGroup } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useSimilarQuestion } from '@answer/api'; +import { useSimilarQuestion } from '@/services'; import { Icon } from '@answer/components'; -import { userInfoStore } from '@/stores'; +import { loggedUserInfoStore } from '@/stores'; interface Props { id: string; } const Index: FC = ({ id }) => { - const { user } = userInfoStore(); + const { user } = loggedUserInfoStore(); const { t } = useTranslation('translation', { keyPrefix: 'related_question', }); diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx index 6343f7efb..d18eb4a81 100644 --- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx @@ -6,7 +6,7 @@ import { marked } from 'marked'; import classNames from 'classnames'; import { Editor, Modal } from '@answer/components'; -import { postAnswer } from '@answer/api'; +import { postAnswer } from '@/services'; import { FormDataType } from '@answer/common/interface'; interface Props { diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 2098b6a66..0eb6a15f3 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -2,9 +2,9 @@ import { useEffect, useState } from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; -import { questionDetail, getAnswers } from '@answer/api'; +import { questionDetail, getAnswers } from '@/services'; import { Pagination, PageTitle } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { scrollTop } from '@answer/utils'; import { usePageUsers } from '@answer/hooks'; import type { @@ -37,7 +37,7 @@ const Index = () => { list: [], }); const { setUsers } = usePageUsers(); - const userInfo = userInfoStore((state) => state.user); + const userInfo = loggedUserInfoStore((state) => state.user); const isAuthor = userInfo?.username === question?.user_info?.username; const requestAnswers = async () => { const res = await getAnswers({ diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index 118fd33cf..6a3d5127e 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -11,7 +11,7 @@ import { useQueryAnswerInfo, modifyAnswer, useQueryRevisions, -} from '@answer/api'; +} from '@/services'; import type * as Type from '@answer/common/interface'; import './index.scss'; diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index c44f67bd5..1558bc35d 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -3,8 +3,8 @@ import { useSearchParams, Link } from 'react-router-dom'; import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { following } from '@answer/api'; -import { isLogin } from '@answer/utils'; +import { following } from '@/services'; +import { tryNormalLogged } from '@/utils/guards'; interface Props { data; @@ -20,7 +20,7 @@ const Index: FC = ({ data }) => { const [followed, setFollowed] = useState(data?.is_follower); const follow = () => { - if (!isLogin(true)) { + if (!tryNormalLogged(true)) { return; } following({ diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index d08968f4e..0582684d9 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { Pagination, PageTitle } from '@answer/components'; -import { useSearch } from '@answer/api'; +import { useSearch } from '@/services'; import { Head, SearchHead, SearchItem, Tips, Empty } from './components'; diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index cb736c920..3098108c6 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import * as Type from '@answer/common/interface'; import { PageTitle, FollowingTags } from '@answer/components'; -import { useTagInfo, useFollow } from '@answer/api'; +import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; diff --git a/ui/src/pages/Tags/Edit/index.tsx b/ui/src/pages/Tags/Edit/index.tsx index 2b0541e1d..12019642e 100644 --- a/ui/src/pages/Tags/Edit/index.tsx +++ b/ui/src/pages/Tags/Edit/index.tsx @@ -7,8 +7,8 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { Editor, EditorRef, PageTitle } from '@answer/components'; -import { useTagInfo, modifyTag, useQueryRevisions } from '@answer/api'; -import { userInfoStore } from '@answer/stores'; +import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; +import { loggedUserInfoStore } from '@answer/stores'; import type * as Type from '@answer/common/interface'; interface FormDataItem { @@ -40,7 +40,7 @@ const initFormData = { }, }; const Ask = () => { - const { is_admin = false } = userInfoStore((state) => state.user); + const { is_admin = false } = loggedUserInfoStore((state) => state.user); const { tagId } = useParams(); const navigate = useNavigate(); diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 96cd814af..31c8ac291 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -17,7 +17,7 @@ import { useQuerySynonymsTags, saveSynonymsTags, deleteTag, -} from '@answer/api'; +} from '@/services'; const TagIntroduction = () => { const [isEdit, setEditState] = useState(false); diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index 2f9719f69..63fc7276d 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -3,7 +3,7 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQueryTags, following } from '@answer/api'; +import { useQueryTags, following } from '@/services'; import { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components'; import { formatCount } from '@answer/utils'; diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index 9ae6d1952..e9619ced9 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -2,7 +2,7 @@ import { FC, memo, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { resetPassword, checkImgCode } from '@answer/api'; +import { resetPassword, checkImgCode } from '@/services'; import type { ImgCodeRes, PasswordResetReq, diff --git a/ui/src/pages/Users/AccountForgot/index.tsx b/ui/src/pages/Users/AccountForgot/index.tsx index b6a346103..5065e9b61 100644 --- a/ui/src/pages/Users/AccountForgot/index.tsx +++ b/ui/src/pages/Users/AccountForgot/index.tsx @@ -2,10 +2,9 @@ import React, { useState, useEffect } from 'react'; import { Container, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { isLogin } from '@answer/utils'; - import SendEmail from './components/sendEmail'; +import { tryNormalLogged } from '@/utils/guards'; import { PageTitle } from '@/components'; const Index: React.FC = () => { @@ -19,7 +18,7 @@ const Index: React.FC = () => { }; useEffect(() => { - isLogin(); + tryNormalLogged(); }, []); return ( diff --git a/ui/src/pages/Users/ActiveEmail/index.tsx b/ui/src/pages/Users/ActiveEmail/index.tsx index dee3fcb73..e2b3491ef 100644 --- a/ui/src/pages/Users/ActiveEmail/index.tsx +++ b/ui/src/pages/Users/ActiveEmail/index.tsx @@ -1,15 +1,15 @@ import { FC, memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { activateAccount } from '@answer/api'; -import { userInfoStore } from '@answer/stores'; +import { activateAccount } from '@/services'; +import { loggedUserInfoStore } from '@answer/stores'; import { getQueryString } from '@answer/utils'; import { PageTitle } from '@/components'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'page_title' }); - const updateUser = userInfoStore((state) => state.update); + const updateUser = loggedUserInfoStore((state) => state.update); useEffect(() => { const code = getQueryString('code'); diff --git a/ui/src/pages/Users/ConfirmNewEmail/index.tsx b/ui/src/pages/Users/ConfirmNewEmail/index.tsx index cbd9ad706..9adaf80eb 100644 --- a/ui/src/pages/Users/ConfirmNewEmail/index.tsx +++ b/ui/src/pages/Users/ConfirmNewEmail/index.tsx @@ -3,7 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap'; import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { changeEmailVerify } from '@answer/api'; +import { changeEmailVerify } from '@/services'; import { PageTitle } from '@/components'; diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index 8d18a5001..6d9af5c2d 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -1,26 +1,30 @@ import React, { FormEvent, useState, useEffect } from 'react'; import { Container, Form, Button, Col } from 'react-bootstrap'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { login, checkImgCode } from '@answer/api'; import type { LoginReqParams, ImgCodeRes, FormDataType, } from '@answer/common/interface'; import { PageTitle, Unactivate } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; -import { isLogin, getQueryString } from '@answer/utils'; +import { loggedUserInfoStore } from '@answer/stores'; +import { getQueryString } from '@answer/utils'; +import { login, checkImgCode } from '@/services'; +import { deriveUserStat, tryNormalLogged } from '@/utils/guards'; +import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; +import { RouteAlias } from '@/router/alias'; import { PicAuthCodeModal } from '@/components/Modal'; import Storage from '@/utils/storage'; const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'login' }); + const navigate = useNavigate(); const [refresh, setRefresh] = useState(0); - const updateUser = userInfoStore((state) => state.update); - const storeUser = userInfoStore((state) => state.user); + const updateUser = loggedUserInfoStore((state) => state.update); + const storeUser = loggedUserInfoStore((state) => state.user); const [formData, setFormData] = useState({ e_mail: { value: '', @@ -102,15 +106,16 @@ const Index: React.FC = () => { login(params) .then((res) => { updateUser(res); - if (res.mail_status === 2) { + const userStat = deriveUserStat(); + if (!userStat.isActivated) { // inactive setStep(2); setRefresh((pre) => pre + 1); - } - if (res.mail_status === 1) { - const path = Storage.get('ANSWER_PATH') || '/'; - Storage.remove('ANSWER_PATH'); - window.location.replace(path); + } else { + const path = + Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home; + Storage.remove(REDIRECT_PATH_STORAGE_KEY); + navigate(path, { replace: true }); } setModalState(false); @@ -154,7 +159,7 @@ const Index: React.FC = () => { if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { setStep(2); } else { - isLogin(); + tryNormalLogged(); } }, []); diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index aef9be457..687e77738 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -8,7 +8,7 @@ import { clearUnreadNotification, clearNotificationStatus, readNotification, -} from '@answer/api'; +} from '@/services'; import { PageTitle } from '@answer/components'; import Inbox from './components/Inbox'; diff --git a/ui/src/pages/Users/PasswordReset/index.tsx b/ui/src/pages/Users/PasswordReset/index.tsx index abffc6bae..af97001f7 100644 --- a/ui/src/pages/Users/PasswordReset/index.tsx +++ b/ui/src/pages/Users/PasswordReset/index.tsx @@ -3,19 +3,19 @@ import { Container, Col, Form, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { replacementPassword } from '@answer/api'; -import { userInfoStore } from '@answer/stores'; -import { getQueryString, isLogin } from '@answer/utils'; +import { loggedUserInfoStore } from '@answer/stores'; +import { getQueryString } from '@answer/utils'; import type { FormDataType } from '@answer/common/interface'; -import Storage from '@/utils/storage'; +import { replacementPassword } from '@/services'; +import { tryNormalLogged } from '@/utils/guards'; import { PageTitle } from '@/components'; const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'password_reset' }); const [step, setStep] = useState(1); - const clearUser = userInfoStore((state) => state.clear); + const clearUser = loggedUserInfoStore((state) => state.clear); const [formData, setFormData] = useState({ pass: { value: '', @@ -105,7 +105,6 @@ const Index: React.FC = () => { .then(() => { // clear login information then to login page clearUser(); - Storage.remove('token'); setStep(2); }) .catch((err) => { @@ -118,7 +117,7 @@ const Index: React.FC = () => { }; useEffect(() => { - isLogin(); + tryNormalLogged(); }, []); return ( <> diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index 7c099a4bf..fa8c2200a 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -4,12 +4,12 @@ import { useTranslation } from 'react-i18next'; import { useParams, useSearchParams } from 'react-router-dom'; import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { usePersonalInfoByName, usePersonalTop, usePersonalListByTabName, -} from '@answer/api'; +} from '@/services'; import { UserInfo, @@ -30,7 +30,7 @@ const Personal: FC = () => { const page = searchParams.get('page') || 1; const order = searchParams.get('order') || 'newest'; const { t } = useTranslation('translation', { keyPrefix: 'personal' }); - const sessionUser = userInfoStore((state) => state.user); + const sessionUser = loggedUserInfoStore((state) => state.user); const isSelf = sessionUser?.username === username; const { data: userInfo } = usePersonalInfoByName(username); diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx index 2f067962a..08afacf28 100644 --- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx +++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx @@ -3,7 +3,7 @@ import { Form, Button, Col } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { register } from '@answer/api'; +import { register } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import userStore from '@/stores/userInfo'; diff --git a/ui/src/pages/Users/Register/index.tsx b/ui/src/pages/Users/Register/index.tsx index c50c353c4..5f8370a04 100644 --- a/ui/src/pages/Users/Register/index.tsx +++ b/ui/src/pages/Users/Register/index.tsx @@ -3,10 +3,11 @@ import { Container } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { PageTitle, Unactivate } from '@answer/components'; -import { isLogin } from '@answer/utils'; import SignUpForm from './components/SignUpForm'; +import { tryNormalLogged } from '@/utils/guards'; + const Index: React.FC = () => { const [showForm, setShowForm] = useState(true); const { t } = useTranslation('translation', { keyPrefix: 'login' }); @@ -16,7 +17,7 @@ const Index: React.FC = () => { }; useEffect(() => { - isLogin(); + tryNormalLogged(); }, []); return ( diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index 84182a536..495fd1d43 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -3,7 +3,7 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; -import { getUserInfo, changeEmail } from '@answer/api'; +import { getLoggedUserInfo, changeEmail } from '@/services'; import { useToast } from '@answer/hooks'; const reg = /(?<=.{2}).+(?=@)/gi; @@ -23,7 +23,7 @@ const Index: FC = () => { const [userInfo, setUserInfo] = useState(); const toast = useToast(); useEffect(() => { - getUserInfo().then((resp) => { + getLoggedUserInfo().then((resp) => { setUserInfo(resp); }); }, []); diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx index 232657637..eac17839b 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx @@ -2,7 +2,7 @@ import React, { FC, FormEvent, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { modifyPassword } from '@answer/api'; +import { modifyPassword } from '@/services'; import { useToast } from '@answer/hooks'; import type { FormDataType } from '@answer/common/interface'; diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index ca7ce38d0..a256a65f1 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -6,10 +6,11 @@ import dayjs from 'dayjs'; import en from 'dayjs/locale/en'; import zh from 'dayjs/locale/zh-cn'; -import { languages } from '@answer/api'; +import { languages } from '@/services'; import type { LangsType, FormDataType } from '@answer/common/interface'; import { useToast } from '@answer/hooks'; +import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; const Index = () => { @@ -34,8 +35,8 @@ const Index = () => { const handleSubmit = (event: FormEvent) => { event.preventDefault(); - Storage.set('LANG', formData.lang.value); - dayjs.locale(formData.lang.value === 'en_US' ? en : zh); + Storage.set(CURRENT_LANG_STORAGE_KEY, formData.lang.value); + dayjs.locale(formData.lang.value === DEFAULT_LANG ? en : zh); i18n.changeLanguage(formData.lang.value); toast.onShow({ msg: t('update', { keyPrefix: 'toast' }), @@ -45,7 +46,7 @@ const Index = () => { useEffect(() => { getLangs(); - const lang = Storage.get('LANG'); + const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); if (lang) { setFormData({ lang: { @@ -60,7 +61,6 @@ const Index = () => {
{t('lang.label')} - { @@ -20,7 +20,7 @@ const Index = () => { }); const getProfile = () => { - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { setFormData({ notice_switch: { value: res.notice_status === 1, diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 49266cebd..ecfd3449f 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -4,10 +4,10 @@ import { Trans, useTranslation } from 'react-i18next'; import { marked } from 'marked'; -import { modifyUserInfo, uploadAvatar, getUserInfo } from '@answer/api'; +import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import { UploadImg, Avatar } from '@answer/components'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { useToast } from '@answer/hooks'; const Index: React.FC = () => { @@ -15,7 +15,7 @@ const Index: React.FC = () => { keyPrefix: 'settings.profile', }); const toast = useToast(); - const { user, update } = userInfoStore(); + const { user, update } = loggedUserInfoStore(); const [formData, setFormData] = useState({ display_name: { value: '', @@ -164,7 +164,7 @@ const Index: React.FC = () => { }; const getProfile = () => { - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { formData.display_name.value = res.display_name; formData.username.value = res.username; formData.bio.value = res.bio; diff --git a/ui/src/pages/Users/Settings/index.tsx b/ui/src/pages/Users/Settings/index.tsx index 45f3080c8..8967cd4e0 100644 --- a/ui/src/pages/Users/Settings/index.tsx +++ b/ui/src/pages/Users/Settings/index.tsx @@ -3,7 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { getUserInfo } from '@answer/api'; +import { getLoggedUserInfo } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import Nav from './components/Nav'; @@ -43,7 +43,7 @@ const Index: React.FC = () => { }, }); const getProfile = () => { - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { formData.display_name.value = res.display_name; formData.bio.value = res.bio; formData.avatar.value = res.avatar; diff --git a/ui/src/pages/Users/Suspended/index.tsx b/ui/src/pages/Users/Suspended/index.tsx index 293603d05..4c381d449 100644 --- a/ui/src/pages/Users/Suspended/index.tsx +++ b/ui/src/pages/Users/Suspended/index.tsx @@ -1,12 +1,12 @@ import { useTranslation } from 'react-i18next'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; import { PageTitle } from '@/components'; const Suspended = () => { const { t } = useTranslation('translation', { keyPrefix: 'suspended' }); - const userInfo = userInfoStore((state) => state.user); + const userInfo = loggedUserInfoStore((state) => state.user); if (userInfo.status !== 'forbidden') { window.location.replace('/'); diff --git a/ui/src/router/alias.ts b/ui/src/router/alias.ts new file mode 100644 index 000000000..f6959ed3c --- /dev/null +++ b/ui/src/router/alias.ts @@ -0,0 +1,8 @@ +export const RouteAlias = { + home: '/', + login: '/users/login', + register: '/users/register', + activation: '/users/login?status=inactive', + activationFailed: '/users/account-activation/failed', + suspended: '/users/account-suspended', +}; diff --git a/ui/src/router/guarder.ts b/ui/src/router/guarder.ts new file mode 100644 index 000000000..8fd8a69ba --- /dev/null +++ b/ui/src/router/guarder.ts @@ -0,0 +1,42 @@ +import { + pullLoggedUser, + isLoggedAndNormal, + isAdminLogged, + isLogged, + isNotLogged, + isNotLoggedOrNormal, + isLoggedAndInactive, + isLoggedAndSuspended, + isNotLoggedOrInactive, +} from '@/utils/guards'; + +const RouteGuarder = { + base: async () => { + return isNotLoggedOrNormal(); + }, + logged: async () => { + return isLogged(); + }, + notLogged: async () => { + return isNotLogged(); + }, + notLoggedOrInactive: async () => { + return isNotLoggedOrInactive(); + }, + loggedAndNormal: async () => { + await pullLoggedUser(true); + return isLoggedAndNormal(); + }, + loggedAndInactive: async () => { + return isLoggedAndInactive(); + }, + loggedAndSuspended: async () => { + return isLoggedAndSuspended(); + }, + adminLogged: async () => { + await pullLoggedUser(true); + return isAdminLogged(); + }, +}; + +export default RouteGuarder; diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 99d447233..a7452ff85 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -1,14 +1,13 @@ import React, { Suspense, lazy } from 'react'; -import { RouteObject, createBrowserRouter } from 'react-router-dom'; +import { RouteObject, createBrowserRouter, redirect } from 'react-router-dom'; -import Layout from '@answer/pages/Layout'; - -import routeConfig, { RouteNode } from '@/router/route-config'; -import RouteRules from '@/router/route-rules'; +import Layout from '@/pages/Layout'; +import baseRoutes, { RouteNode } from '@/router/routes'; +import { floppyNavigation } from '@/utils'; const routes: RouteObject[] = []; -const routeGen = (routeNodes: RouteNode[], root: RouteObject[]) => { +const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => { routeNodes.forEach((rn) => { if (rn.path === '/') { rn.element = ; @@ -18,40 +17,37 @@ const routeGen = (routeNodes: RouteNode[], root: RouteObject[]) => { * ref: https://webpack.js.org/api/module-methods/#import-1 */ rn.page = rn.page.replace('pages/', ''); - const Control = lazy(() => import(`@/pages/${rn.page}`)); + const Ctrl = lazy(() => import(`@/pages/${rn.page}`)); rn.element = ( - + ); } root.push(rn); - if (Array.isArray(rn.rules)) { - const ruleFunc: Function[] = []; - if (typeof rn.loader === 'function') { - ruleFunc.push(rn.loader); - } - rn.rules.forEach((ruleKey) => { - const func = RouteRules[ruleKey]; - if (typeof func === 'function') { - ruleFunc.push(func); + if (rn.guard) { + const { guard } = rn; + const loaderRef = rn.loader; + rn.loader = async (args) => { + const gr = await guard(args); + if (gr?.redirect && floppyNavigation.differentCurrent(gr.redirect)) { + return redirect(gr.redirect); + } + let ret; + if (typeof loaderRef === 'function') { + ret = await loaderRef(args); } - }); - rn.loader = ({ params }) => { - ruleFunc.forEach((func) => { - func(params); - }); + return ret; }; } const children = Array.isArray(rn.children) ? rn.children : null; if (children) { rn.children = []; - routeGen(children, rn.children); + routeWrapper(children, rn.children); } }); }; -routeGen(routeConfig, routes); +routeWrapper(baseRoutes, routes); -const router = createBrowserRouter(routes); -export default router; +export { routes, createBrowserRouter }; diff --git a/ui/src/router/route-rules.ts b/ui/src/router/route-rules.ts deleted file mode 100644 index e7c2b83c2..000000000 --- a/ui/src/router/route-rules.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { isLogin } from '@answer/utils'; - -const RouteRules = { - isLoginAndNormal: () => { - return isLogin(true); - }, -}; - -export default RouteRules; diff --git a/ui/src/router/route-config.ts b/ui/src/router/routes.ts similarity index 90% rename from ui/src/router/route-config.ts rename to ui/src/router/routes.ts index 6267482ef..ad964c389 100644 --- a/ui/src/router/route-config.ts +++ b/ui/src/router/routes.ts @@ -1,14 +1,18 @@ import { RouteObject } from 'react-router-dom'; +import RouteGuarder from '@/router/guarder'; + export interface RouteNode extends RouteObject { page: string; children?: RouteNode[]; - rules?: string[]; + guard?: Function; } -const routeConfig: RouteNode[] = [ + +const routes: RouteNode[] = [ { path: '/', page: 'pages/Layout', + guard: RouteGuarder.base, children: [ // question and answer { @@ -31,12 +35,12 @@ const routeConfig: RouteNode[] = [ { path: 'questions/ask', page: 'pages/Questions/Ask', - rules: ['isLoginAndNormal'], + guard: RouteGuarder.loggedAndNormal, }, { path: 'posts/:qid/edit', page: 'pages/Questions/Ask', - rules: ['isLoginAndNormal'], + guard: RouteGuarder.loggedAndNormal, }, { path: 'posts/:qid/:aid/edit', @@ -105,18 +109,22 @@ const routeConfig: RouteNode[] = [ { path: 'users/login', page: 'pages/Users/Login', + guard: RouteGuarder.notLoggedOrInactive, }, { path: 'users/register', page: 'pages/Users/Register', + guard: RouteGuarder.notLogged, }, { path: 'users/account-recovery', page: 'pages/Users/AccountForgot', + guard: RouteGuarder.loggedAndNormal, }, { path: 'users/password-reset', page: 'pages/Users/PasswordReset', + guard: RouteGuarder.loggedAndNormal, }, { path: 'users/account-activation', @@ -142,6 +150,7 @@ const routeConfig: RouteNode[] = [ { path: 'admin', page: 'pages/Admin', + guard: RouteGuarder.adminLogged, children: [ { index: true, @@ -192,4 +201,4 @@ const routeConfig: RouteNode[] = [ ], }, ]; -export default routeConfig; +export default routes; diff --git a/ui/src/services/client/index.ts b/ui/src/services/client/index.ts index dbea39bfa..1f1af66aa 100644 --- a/ui/src/services/client/index.ts +++ b/ui/src/services/client/index.ts @@ -1,6 +1,5 @@ export * from './activity'; export * from './personal'; -export * from './user'; export * from './notification'; export * from './question'; export * from './search'; diff --git a/ui/src/services/client/notification.ts b/ui/src/services/client/notification.ts index 18b733436..dd9d880e9 100644 --- a/ui/src/services/client/notification.ts +++ b/ui/src/services/client/notification.ts @@ -2,9 +2,10 @@ import useSWR from 'swr'; import qs from 'qs'; import request from '@answer/utils/request'; -import { isLogin } from '@answer/utils'; import type * as Type from '@answer/common/interface'; +import { tryNormalLogged } from '@/utils/guards'; + export const useQueryNotifications = (params) => { const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, { skipNulls: true, @@ -33,7 +34,7 @@ export const useQueryNotificationStatus = () => { const apiUrl = '/answer/api/v1/notification/status'; return useSWR<{ inbox: number; achievement: number }>( - isLogin() ? apiUrl : null, + tryNormalLogged() ? apiUrl : null, request.instance.get, { refreshInterval: 3000, diff --git a/ui/src/services/client/tag.ts b/ui/src/services/client/tag.ts index 87e467436..634b44729 100644 --- a/ui/src/services/client/tag.ts +++ b/ui/src/services/client/tag.ts @@ -1,9 +1,10 @@ import useSWR from 'swr'; import request from '@answer/utils/request'; -import { isLogin } from '@answer/utils'; import type * as Type from '@answer/common/interface'; +import { tryNormalLogged } from '@/utils/guards'; + export const deleteTag = (id) => { return request.delete('/answer/api/v1/tag', { tag_id: id, @@ -24,7 +25,7 @@ export const saveSynonymsTags = (params) => { export const useFollowingTags = () => { let apiUrl = ''; - if (isLogin()) { + if (tryNormalLogged()) { apiUrl = '/answer/api/v1/tags/following'; } const { data, error, mutate } = useSWR(apiUrl, request.instance.get); diff --git a/ui/src/services/client/user.ts b/ui/src/services/client/user.ts deleted file mode 100644 index abe9ee3a9..000000000 --- a/ui/src/services/client/user.ts +++ /dev/null @@ -1,17 +0,0 @@ -import useSWR from 'swr'; - -import request from '@answer/utils/request'; - -export const useCheckUserStatus = () => { - const apiUrl = '/answer/api/v1/user/status'; - const hasToken = localStorage.getItem('token'); - const { data, error } = useSWR<{ status: string }, Error>( - hasToken ? apiUrl : null, - request.instance.get, - ); - return { - data, - isLoading: !data && !error, - error, - }; -}; diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 6cef9f199..6f2cf83aa 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -115,7 +115,7 @@ export const resendEmail = (params?: Type.ImgCodeReq) => { * @description get login userinfo * @returns {UserInfo} */ -export const getUserInfo = () => { +export const getLoggedUserInfo = () => { return request.get('/answer/api/v1/user/info'); }; diff --git a/ui/src/services/api.ts b/ui/src/services/index.ts similarity index 100% rename from ui/src/services/api.ts rename to ui/src/services/index.ts diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts index 0962911e5..6bc6377e9 100644 --- a/ui/src/stores/index.ts +++ b/ui/src/stores/index.ts @@ -1,12 +1,12 @@ import toastStore from './toast'; -import userInfoStore from './userInfo'; +import loggedUserInfoStore from './userInfo'; import globalStore from './global'; import siteInfoStore from './siteInfo'; import interfaceStore from './interface'; export { toastStore, - userInfoStore, + loggedUserInfoStore, globalStore, siteInfoStore, interfaceStore, diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index bbfb640e6..9aa540b55 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -3,6 +3,11 @@ import create from 'zustand'; import type { UserInfoRes } from '@answer/common/interface'; import Storage from '@answer/utils/storage'; +import { + LOGGED_USER_STORAGE_KEY, + LOGGED_TOKEN_STORAGE_KEY, +} from '@/common/constants'; + interface UserInfoStore { user: UserInfoRes; update: (params: UserInfoRes) => void; @@ -19,23 +24,23 @@ const initUser: UserInfoRes = { location: '', website: '', status: '', - mail_status: 0, + mail_status: 1, }; -const userInfoStore = create((set) => ({ +const loggedUserInfoStore = create((set) => ({ user: initUser, update: (params) => set(() => { - Storage.set('token', params.access_token); - Storage.set('userInfo', params); + Storage.set(LOGGED_TOKEN_STORAGE_KEY, params.access_token); + Storage.set(LOGGED_USER_STORAGE_KEY, params); return { user: params }; }), clear: () => set(() => { - // Storage.remove('token'); - Storage.remove('userInfo'); + Storage.remove(LOGGED_TOKEN_STORAGE_KEY); + Storage.remove(LOGGED_USER_STORAGE_KEY); return { user: initUser }; }), })); -export default userInfoStore; +export default loggedUserInfoStore; diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts new file mode 100644 index 000000000..169dc1ad0 --- /dev/null +++ b/ui/src/utils/common.ts @@ -0,0 +1,80 @@ +function getQueryString(name: string): string { + const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`); + const r = window.location.search.substr(1).match(reg); + if (r != null) return unescape(r[2]); + return ''; +} + +function thousandthDivision(num) { + const reg = /\d{1,3}(?=(\d{3})+$)/g; + return `${num}`.replace(reg, '$&,'); +} + +function formatCount($num: number): string { + let res = String($num); + if (!Number.isFinite($num)) { + res = '0'; + } else if ($num < 10000) { + res = thousandthDivision($num); + } else if ($num < 1000000) { + res = `${Math.round($num / 100) / 10}k`; + } else if ($num >= 1000000) { + res = `${Math.round($num / 100000) / 10}m`; + } + return res; +} + +function scrollTop(element) { + if (!element) { + return; + } + const offset = 120; + const bodyRect = document.body.getBoundingClientRect().top; + const elementRect = element.getBoundingClientRect().top; + const elementPosition = elementRect - bodyRect; + const offsetPosition = elementPosition - offset; + + window.scrollTo({ + top: offsetPosition, + }); +} + +/** + * Extract user info from markdown + * @param markdown string + * @returns Array<{displayName: string, userName: string}> + */ +function matchedUsers(markdown) { + const globalReg = /\B@([\w|]+)/g; + const reg = /\B@([\w\\_\\.]+)/; + + const users = markdown.match(globalReg); + if (!users) { + return []; + } + return users.map((user) => { + const matched = user.match(reg); + return { + userName: matched[1], + }; + }); +} + +/** + * Identify user information from markdown + * @param markdown string + * @returns string + */ +function parseUserInfo(markdown) { + const globalReg = /\B@([\w\\_\\.\\-]+)/g; + return markdown.replace(globalReg, '[@$1](/u/$1)'); +} + +export { + getQueryString, + thousandthDivision, + formatCount, + scrollTop, + matchedUsers, + parseUserInfo, +}; diff --git a/ui/src/utils/floppyNavigation.ts b/ui/src/utils/floppyNavigation.ts new file mode 100644 index 000000000..7edbbaa0f --- /dev/null +++ b/ui/src/utils/floppyNavigation.ts @@ -0,0 +1,40 @@ +import { RouteAlias } from '@/router/alias'; +import Storage from '@/utils/storage'; +import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; + +const differentCurrent = (target: string, base?: string) => { + base ||= window.location.origin; + const targetUrl = new URL(target, base); + return targetUrl.toString() !== window.location.href; +}; + +/** + * only navigate if not same as current url + * @param pathname + * @param callback + */ +const navigate = (pathname: string, callback: Function) => { + if (differentCurrent(pathname)) { + callback(); + } +}; + +/** + * auto navigate to login page with redirect info + */ +const navigateToLogin = () => { + const { pathname } = window.location; + if (pathname !== RouteAlias.login && pathname !== RouteAlias.register) { + const redirectUrl = window.location.href; + Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl); + } + navigate(RouteAlias.login, () => { + window.location.replace(RouteAlias.login); + }); +}; + +export const floppyNavigation = { + differentCurrent, + navigate, + navigateToLogin, +}; diff --git a/ui/src/utils/guards.ts b/ui/src/utils/guards.ts new file mode 100644 index 000000000..f0faf55b7 --- /dev/null +++ b/ui/src/utils/guards.ts @@ -0,0 +1,182 @@ +import { getLoggedUserInfo } from '@/services'; +import { loggedUserInfoStore } from '@/stores'; +import { RouteAlias } from '@/router/alias'; +import Storage from '@/utils/storage'; +import { LOGGED_USER_STORAGE_KEY } from '@/common/constants'; +import { floppyNavigation } from '@/utils/floppyNavigation'; + +type UserStat = { + isLogged: boolean; + isActivated: boolean; + isSuspended: boolean; + isNormal: boolean; + isAdmin: boolean; +}; +export const deriveUserStat = (): UserStat => { + const stat: UserStat = { + isLogged: false, + isActivated: false, + isSuspended: false, + isNormal: false, + isAdmin: false, + }; + const { user } = loggedUserInfoStore.getState(); + if (user.id && user.username) { + stat.isLogged = true; + } + if (stat.isLogged && user.mail_status === 1) { + stat.isActivated = true; + } + if (stat.isLogged && user.status === 'forbidden') { + stat.isSuspended = true; + } + if (stat.isLogged && stat.isActivated && !stat.isSuspended) { + stat.isNormal = true; + } + if (stat.isNormal && user.is_admin === true) { + stat.isAdmin = true; + } + + return stat; +}; + +type GuardResult = { + ok: boolean; + redirect?: string; +}; +let pullLock = false; +let dedupeTimestamp = 0; +export const pullLoggedUser = async (forceRePull = false) => { + // only pull once if not force re-pull + if (pullLock && !forceRePull) { + return; + } + // dedupe pull requests in this time span in 10 seconds + if (Date.now() - dedupeTimestamp < 1000 * 10) { + return; + } + dedupeTimestamp = Date.now(); + const loggedUserInfo = await getLoggedUserInfo().catch((ex) => { + dedupeTimestamp = 0; + if (!deriveUserStat().isLogged) { + // load fallback userInfo from local storage + const storageLoggedUserInfo = Storage.get(LOGGED_USER_STORAGE_KEY); + if (storageLoggedUserInfo) { + loggedUserInfoStore.getState().update(storageLoggedUserInfo); + } + } + console.error(ex); + }); + if (loggedUserInfo) { + pullLock = true; + loggedUserInfoStore.getState().update(loggedUserInfo); + } +}; + +export const isLogged = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + if (!userStat.isLogged) { + ret.ok = false; + ret.redirect = RouteAlias.login; + } + return ret; +}; + +export const isNotLogged = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + if (userStat.isLogged) { + ret.ok = false; + ret.redirect = RouteAlias.home; + } + return ret; +}; + +export const isLoggedAndInactive = () => { + const ret: GuardResult = { ok: false, redirect: undefined }; + const userStat = deriveUserStat(); + if (!userStat.isActivated) { + ret.ok = true; + ret.redirect = RouteAlias.activation; + } + return ret; +}; + +export const isLoggedAndSuspended = () => { + const ret: GuardResult = { ok: false, redirect: undefined }; + const userStat = deriveUserStat(); + if (userStat.isSuspended) { + ret.redirect = RouteAlias.suspended; + ret.ok = true; + } + return ret; +}; + +export const isLoggedAndNormal = () => { + const ret: GuardResult = { ok: false, redirect: undefined }; + const userStat = deriveUserStat(); + if (userStat.isNormal) { + ret.ok = true; + } else if (!userStat.isActivated) { + ret.redirect = RouteAlias.activation; + } else if (!userStat.isSuspended) { + ret.redirect = RouteAlias.suspended; + } else if (!userStat.isLogged) { + ret.redirect = RouteAlias.login; + } + return ret; +}; + +export const isNotLoggedOrNormal = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + const gr = isLoggedAndNormal(); + if (!gr.ok && userStat.isLogged) { + ret.ok = false; + ret.redirect = gr.redirect; + } + return ret; +}; + +export const isNotLoggedOrInactive = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + if (userStat.isLogged || userStat.isActivated) { + ret.ok = false; + ret.redirect = RouteAlias.home; + } + return ret; +}; + +export const isAdminLogged = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + if (!userStat.isAdmin) { + ret.redirect = RouteAlias.home; + ret.ok = false; + } + return ret; +}; + +/** + * try user was logged and all state ok + * @param autoLogin + */ +export const tryNormalLogged = (autoLogin: boolean = false) => { + const gr = isLoggedAndNormal(); + if (gr.ok) { + return true; + } + + if (gr.redirect === RouteAlias.login && autoLogin) { + floppyNavigation.navigateToLogin(); + } else if (gr.redirect) { + floppyNavigation.navigate(gr.redirect, () => { + // @ts-ignore + window.location.replace(gr.redirect); + }); + } + + return false; +}; diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts index 20cde2935..a1eaf02c6 100644 --- a/ui/src/utils/index.ts +++ b/ui/src/utils/index.ts @@ -1,114 +1,6 @@ -import { LOGIN_NEED_BACK } from '@answer/common/constants'; +export * from './common'; +export * as guards from './guards'; -import Storage from './storage'; - -function getQueryString(name: string): string { - const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`); - const r = window.location.search.substr(1).match(reg); - if (r != null) return unescape(r[2]); - return ''; -} - -function thousandthDivision(num) { - const reg = /\d{1,3}(?=(\d{3})+$)/g; - return `${num}`.replace(reg, '$&,'); -} - -function formatCount($num: number): string { - let res = String($num); - if (!Number.isFinite($num)) { - res = '0'; - } else if ($num < 10000) { - res = thousandthDivision($num); - } else if ($num < 1000000) { - res = `${Math.round($num / 100) / 10}k`; - } else if ($num >= 1000000) { - res = `${Math.round($num / 100000) / 10}m`; - } - return res; -} - -function isLogin(needToLogin?: boolean): boolean { - const user = Storage.get('userInfo'); - const path = window.location.pathname; - - // User deleted or suspended - if (user.username && user.status === 'forbidden') { - if (path !== '/users/account-suspended') { - window.location.pathname = '/users/account-suspended'; - } - return false; - } - - // login and active - if (user.username && user.mail_status === 1) { - if (LOGIN_NEED_BACK.includes(path)) { - window.location.replace('/'); - } - return true; - } - - // un login or inactivated - if ((!user.username || user.mail_status === 2) && needToLogin) { - Storage.set('ANSWER_PATH', path); - window.location.href = '/users/login'; - } - - return false; -} - -function scrollTop(element) { - if (!element) { - return; - } - const offset = 120; - const bodyRect = document.body.getBoundingClientRect().top; - const elementRect = element.getBoundingClientRect().top; - const elementPosition = elementRect - bodyRect; - const offsetPosition = elementPosition - offset; - - window.scrollTo({ - top: offsetPosition, - }); -} - -/** - * Extract user info from markdown - * @param markdown string - * @returns Array<{displayName: string, userName: string}> - */ -function matchedUsers(markdown) { - const globalReg = /\B@([\w|]+)/g; - const reg = /\B@([\w\\_\\.]+)/; - - const users = markdown.match(globalReg); - if (!users) { - return []; - } - return users.map((user) => { - const matched = user.match(reg); - return { - userName: matched[1], - }; - }); -} - -/** - * Identify user infromation from markdown - * @param markdown string - * @returns string - */ -function parseUserInfo(markdown) { - const globalReg = /\B@([\w\\_\\.\\-]+)/g; - return markdown.replace(globalReg, '[@$1](/u/$1)'); -} - -export { - getQueryString, - thousandthDivision, - formatCount, - isLogin, - scrollTop, - matchedUsers, - parseUserInfo, -}; +export { default as request } from './request'; +export { default as Storage } from './storage'; +export { floppyNavigation } from './floppyNavigation'; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index d532d707a..c9f9e67fa 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -2,9 +2,17 @@ import axios, { AxiosResponse } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import { Modal } from '@answer/components'; -import { userInfoStore, toastStore } from '@answer/stores'; +import { loggedUserInfoStore, toastStore } from '@answer/stores'; import Storage from './storage'; +import { floppyNavigation } from './floppyNavigation'; + +import { + LOGGED_TOKEN_STORAGE_KEY, + CURRENT_LANG_STORAGE_KEY, + DEFAULT_LANG, +} from '@/common/constants'; +import { RouteAlias } from '@/router/alias'; const API = { development: '', @@ -25,12 +33,11 @@ class Request { constructor(config: AxiosRequestConfig) { this.instance = axios.create(config); - this.instance.interceptors.request.use( (requestConfig: AxiosRequestConfig) => { - const token = Storage.get('token') || ''; + const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || ''; // default lang en_US - const lang = Storage.get('LANG') || 'en_US'; + const lang = Storage.get(CURRENT_LANG_STORAGE_KEY) || DEFAULT_LANG; requestConfig.headers = { Authorization: token, 'Accept-Language': lang, @@ -54,23 +61,23 @@ class Request { return data; }, (error) => { - const { status, data, msg } = error.response; - const { data: realData, msg: realMsg = '' } = data; + const { status, data: respData, msg: respMsg } = error.response; + const { data, msg = '' } = respData; if (status === 400) { // show error message - if (realData instanceof Object && realData.err_type) { - if (realData.err_type === 'toast') { + if (data instanceof Object && data.err_type) { + if (data.err_type === 'toast') { // toast error message toastStore.getState().show({ - msg: realMsg, + msg, variant: 'danger', }); } - if (realData.type === 'modal') { + if (data.type === 'modal') { // modal error message Modal.confirm({ - content: realMsg, + content: msg, }); } @@ -78,63 +85,56 @@ class Request { } if ( - realData instanceof Object && - Object.keys(realData).length > 0 && - realData.key + data instanceof Object && + Object.keys(data).length > 0 && + data.key ) { // handle form error - return Promise.reject({ ...realData, isError: true }); + return Promise.reject({ ...data, isError: true }); } - if (!realData || Object.keys(realData).length <= 0) { + if (!data || Object.keys(data).length <= 0) { // default error msg will show modal Modal.confirm({ - content: realMsg, + content: msg, }); return Promise.reject(false); } } - + // 401: Re-login required if (status === 401) { - // clear userinfo; - Storage.remove('token'); - userInfoStore.getState().clear(); - // need login - const { pathname } = window.location; - if (pathname !== '/users/login' && pathname !== '/users/register') { - Storage.set('ANSWER_PATH', window.location.pathname); - } - window.location.href = '/users/login'; - + // clear userinfo + loggedUserInfoStore.getState().clear(); + floppyNavigation.navigateToLogin(); return Promise.reject(false); } - if (status === 403) { // Permission interception - - if (realData?.type === 'inactive') { - // inactivated - window.location.href = '/users/login?status=inactive'; + if (data?.type === 'url_expired') { + // url expired + floppyNavigation.navigate(RouteAlias.activationFailed, () => { + window.location.replace(RouteAlias.activationFailed); + }); return Promise.reject(false); } - - if (realData?.type === 'url_expired') { - // url expired - window.location.href = '/users/account-activation/failed'; + if (data?.type === 'inactive') { + // inactivated + floppyNavigation.navigate(RouteAlias.activation, () => { + window.location.href = RouteAlias.activation; + }); return Promise.reject(false); } - if (realData?.type === 'suspended') { - if (window.location.pathname !== '/users/account-suspended') { - window.location.href = '/users/account-suspended'; - } - + if (data?.type === 'suspended') { + floppyNavigation.navigate(RouteAlias.suspended, () => { + window.location.replace(RouteAlias.suspended); + }); return Promise.reject(false); } } toastStore.getState().show({ - msg: `statusCode: ${status}; ${msg || ''}`, + msg: `statusCode: ${status}; ${respMsg || ''}`, variant: 'danger', }); return Promise.reject(false); @@ -178,6 +178,4 @@ class Request { } } -// export const Request; - export default new Request(baseConfig); diff --git a/ui/src/utils/storage.ts b/ui/src/utils/storage.ts index bf14d85ea..0ab3b1158 100644 --- a/ui/src/utils/storage.ts +++ b/ui/src/utils/storage.ts @@ -3,13 +3,12 @@ const Storage = { const value = localStorage.getItem(key); if (value) { try { - const v = JSON.parse(value); - return v; + return JSON.parse(value); } catch { return value; } } - return false; + return undefined; }, set: (key: string, value: any): void => { if (typeof value === 'string') { diff --git a/ui/tsconfig.json b/ui/tsconfig.json index c38047479..d7c5decfa 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -26,7 +26,6 @@ "@answer/components/*": ["src/components/*"], "@answer/stores": ["src/stores"], "@answer/stores/*": ["src/stores/*"], - "@answer/api": ["src/services/api.ts"], "@answer/services/*": ["src/services/*"], "@answer/hooks": ["src/hooks"], "@answer/common": ["src/common"], From e6f8e62d782b7c541e09f79929e25060632c23e9 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 31 Oct 2022 10:24:09 +0800 Subject: [PATCH 0054/3337] fix: unread quantity style adjustment --- ui/src/index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/index.scss b/ui/src/index.scss index 09807a9ed..e81a900ca 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -53,7 +53,7 @@ a { height: 18px; border-radius: 50%; position: absolute; - left: 20px; + left: 15px; top: 0; border: 1px solid #fff; } From e2cf6d2b3d8fd87dd6eb9bbfdcd78598aff5ec7b Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Mon, 31 Oct 2022 10:34:51 +0800 Subject: [PATCH 0055/3337] feat(router-guard): set route guard for activation/* and confirm-new-email --- ui/src/router/guarder.ts | 4 ---- ui/src/router/routes.ts | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/src/router/guarder.ts b/ui/src/router/guarder.ts index 8fd8a69ba..8c5bf7c2b 100644 --- a/ui/src/router/guarder.ts +++ b/ui/src/router/guarder.ts @@ -2,7 +2,6 @@ import { pullLoggedUser, isLoggedAndNormal, isAdminLogged, - isLogged, isNotLogged, isNotLoggedOrNormal, isLoggedAndInactive, @@ -14,9 +13,6 @@ const RouteGuarder = { base: async () => { return isNotLoggedOrNormal(); }, - logged: async () => { - return isLogged(); - }, notLogged: async () => { return isNotLogged(); }, diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index ad964c389..5abf7e4b4 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -126,9 +126,11 @@ const routes: RouteNode[] = [ page: 'pages/Users/PasswordReset', guard: RouteGuarder.loggedAndNormal, }, + // TODO: guard '/account-activation/*', '/users/confirm-new-email' { path: 'users/account-activation', page: 'pages/Users/ActiveEmail', + guard: RouteGuarder.loggedAndInactive, }, { path: 'users/account-activation/success', @@ -145,6 +147,7 @@ const routes: RouteNode[] = [ { path: '/users/account-suspended', page: 'pages/Users/Suspended', + guard: RouteGuarder.loggedAndSuspended, }, // for admin { From 0ec198bad016d1f9a9806fab39356e526b3d6b19 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 31 Oct 2022 11:04:51 +0800 Subject: [PATCH 0056/3337] update AvatarInfo to show img --- internal/schema/user_schema.go | 7 +++++-- internal/service/user_common/user.go | 5 ++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index ea37582f7..2362cf034 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -73,7 +73,7 @@ type GetUserResp struct { func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) { _ = copier.Copy(r, userInfo) - r.Avatar = r.AvatarInfo(userInfo.Avatar) + r.Avatar = FormatAvatarInfo(userInfo.Avatar) r.CreatedAt = userInfo.CreatedAt.Unix() r.LastLoginDate = userInfo.LastLoginDate.Unix() statusShow, ok := UserStatusShow[userInfo.Status] @@ -103,7 +103,7 @@ func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) { r.Avatar = avatarInfo } -func (us *GetUserResp) AvatarInfo(avatarJson string) string { +func FormatAvatarInfo(avatarJson string) string { if avatarJson == "" { return "" } @@ -172,6 +172,9 @@ type GetOtherUserInfoByUsernameResp struct { func (r *GetOtherUserInfoByUsernameResp) GetFromUserEntity(userInfo *entity.User) { _ = copier.Copy(r, userInfo) + Avatar := FormatAvatarInfo(userInfo.Avatar) + r.Avatar = Avatar + r.CreatedAt = userInfo.CreatedAt.Unix() r.LastLoginDate = userInfo.LastLoginDate.Unix() statusShow, ok := UserStatusShow[userInfo.Status] diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index adb87317f..37363166c 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -76,13 +76,12 @@ func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, IDs []string) // UserBasicInfoFormat func (us *UserCommon) UserBasicInfoFormat(ctx context.Context, userInfo *entity.User) *schema.UserBasicInfo { userBasicInfo := &schema.UserBasicInfo{} - uinfo := &schema.GetUserResp{} - uinfo.AvatarInfo(userInfo.Avatar) + Avatar := schema.FormatAvatarInfo(userInfo.Avatar) userBasicInfo.ID = userInfo.ID userBasicInfo.Username = userInfo.Username userBasicInfo.Rank = userInfo.Rank userBasicInfo.DisplayName = userInfo.DisplayName - userBasicInfo.Avatar = uinfo.Avatar + userBasicInfo.Avatar = Avatar userBasicInfo.Website = userInfo.Website userBasicInfo.Location = userInfo.Location userBasicInfo.IpInfo = userInfo.IPInfo From d3cd9a94dd733310aafb8365d53969b8fcf9ef07 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Mon, 31 Oct 2022 11:39:33 +0800 Subject: [PATCH 0057/3337] refactor(import): fix import order --- ui/src/components/QuestionList/index.tsx | 3 ++- ui/src/components/TagSelector/index.tsx | 3 ++- ui/src/components/Unactivate/index.tsx | 2 +- ui/src/hooks/useChangeModal/index.tsx | 3 ++- ui/src/pages/Admin/Answers/index.tsx | 3 ++- ui/src/pages/Admin/Flags/index.tsx | 1 + ui/src/pages/Admin/General/index.tsx | 1 + ui/src/pages/Admin/Interface/index.tsx | 5 +++-- ui/src/pages/Admin/Questions/index.tsx | 3 ++- ui/src/pages/Admin/Users/index.tsx | 3 ++- ui/src/pages/Layout/index.tsx | 2 +- ui/src/pages/Questions/Ask/index.tsx | 7 ++++--- .../Questions/Detail/components/Answer/index.tsx | 3 ++- .../Questions/Detail/components/Question/index.tsx | 1 + .../Detail/components/RelatedQuestions/index.tsx | 2 +- .../Questions/Detail/components/WriteAnswer/index.tsx | 3 ++- ui/src/pages/Questions/Detail/index.tsx | 3 ++- ui/src/pages/Questions/EditAnswer/index.tsx | 3 ++- ui/src/pages/Search/index.tsx | 3 ++- ui/src/pages/Tags/Detail/index.tsx | 2 +- ui/src/pages/Tags/Edit/index.tsx | 3 ++- ui/src/pages/Tags/Info/index.tsx | 1 + ui/src/pages/Tags/index.tsx | 3 ++- .../Users/AccountForgot/components/sendEmail.tsx | 2 +- ui/src/pages/Users/ActiveEmail/index.tsx | 2 +- ui/src/pages/Users/ConfirmNewEmail/index.tsx | 1 - ui/src/pages/Users/Notifications/index.tsx | 9 +++++---- ui/src/pages/Users/Personal/index.tsx | 11 ++++++----- .../Users/Register/components/SignUpForm/index.tsx | 2 +- .../Settings/Account/components/ModifyEmail/index.tsx | 3 ++- .../Settings/Account/components/ModifyPass/index.tsx | 3 ++- ui/src/pages/Users/Settings/Interface/index.tsx | 2 +- ui/src/pages/Users/Settings/Notification/index.tsx | 3 ++- ui/src/pages/Users/Settings/Profile/index.tsx | 3 ++- ui/src/pages/Users/Settings/index.tsx | 2 +- 35 files changed, 65 insertions(+), 41 deletions(-) diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 69c6ad9d9..1be1f8474 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -3,7 +3,6 @@ import { Row, Col, ListGroup } from 'react-bootstrap'; import { NavLink, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQuestionList } from '@/services'; import type * as Type from '@answer/common/interface'; import { Icon, @@ -15,6 +14,8 @@ import { QueryGroup, } from '@answer/components'; +import { useQuestionList } from '@/services'; + const QuestionOrderKeys: Type.QuestionOrderBy[] = [ 'newest', 'active', diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index f939b6749..cedb85a3c 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -6,9 +6,10 @@ import { marked } from 'marked'; import classNames from 'classnames'; import { useTagModal } from '@answer/hooks'; -import { queryTags } from '@/services'; import type * as Type from '@answer/common/interface'; +import { queryTags } from '@/services'; + import './index.scss'; interface IProps { diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index dd44f68fa..eeb3a1af8 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import { Button, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { resendEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@answer/components/Modal'; import type { ImgCodeRes, @@ -11,6 +10,7 @@ import type { } from '@answer/common/interface'; import { loggedUserInfoStore } from '@answer/stores'; +import { resendEmail, checkImgCode } from '@/services'; import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/hooks/useChangeModal/index.tsx b/ui/src/hooks/useChangeModal/index.tsx index 76b9f2ead..f484331cb 100644 --- a/ui/src/hooks/useChangeModal/index.tsx +++ b/ui/src/hooks/useChangeModal/index.tsx @@ -4,9 +4,10 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { changeUserStatus } from '@/services'; import { Modal as AnswerModal } from '@answer/components'; +import { changeUserStatus } from '@/services'; + const div = document.createElement('div'); const root = ReactDOM.createRoot(div); diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index ead61b578..2a25f53cb 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -14,9 +14,10 @@ import { } from '@answer/components'; import { ADMIN_LIST_STATUS } from '@answer/common/constants'; import { useEditStatusModal } from '@answer/hooks'; -import { useAnswerSearch, changeAnswerStatus } from '@/services'; import * as Type from '@answer/common/interface'; +import { useAnswerSearch, changeAnswerStatus } from '@/services'; + import '../index.scss'; const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted']; diff --git a/ui/src/pages/Admin/Flags/index.tsx b/ui/src/pages/Admin/Flags/index.tsx index ed10eaa55..a7842429f 100644 --- a/ui/src/pages/Admin/Flags/index.tsx +++ b/ui/src/pages/Admin/Flags/index.tsx @@ -12,6 +12,7 @@ import { } from '@answer/components'; import { useReportModal } from '@answer/hooks'; import * as Type from '@answer/common/interface'; + import { useFlagSearch } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 9caf81e82..3154caecf 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; import { useToast } from '@answer/hooks'; import { siteInfoStore } from '@answer/stores'; + import { useGeneralSetting, updateGeneralSetting } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index e22d1046d..7c83e10e1 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -8,6 +8,9 @@ import { FormDataType, AdminSettingsInterface, } from '@answer/common/interface'; +import { interfaceStore } from '@answer/stores'; +import { UploadImg } from '@answer/components'; + import { languages, uploadAvatar, @@ -15,8 +18,6 @@ import { useInterfaceSetting, useThemeOptions, } from '@/services'; -import { interfaceStore } from '@answer/stores'; -import { UploadImg } from '@answer/components'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 3235a819d..1a1885129 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -14,12 +14,13 @@ import { } from '@answer/components'; import { ADMIN_LIST_STATUS } from '@answer/common/constants'; import { useEditStatusModal, useReportModal } from '@answer/hooks'; +import * as Type from '@answer/common/interface'; + import { useQuestionSearch, changeQuestionStatus, deleteQuestion, } from '@/services'; -import * as Type from '@answer/common/interface'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index b4efe613d..825e7f238 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -3,7 +3,6 @@ import { Button, Form, Table, Badge } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQueryUsers } from '@/services'; import { Pagination, FormatTime, @@ -14,6 +13,8 @@ import { import * as Type from '@answer/common/interface'; import { useChangeModal } from '@answer/hooks'; +import { useQueryUsers } from '@/services'; + import '../index.scss'; const UserFilterKeys: Type.UserFilterBy[] = [ diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index bad408bfe..7affe2f03 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -7,8 +7,8 @@ import { SWRConfig } from 'swr'; import { siteInfoStore, interfaceStore, toastStore } from '@answer/stores'; import { Header, AdminHeader, Footer, Toast } from '@answer/components'; -import { useSiteSettings } from '@/services'; +import { useSiteSettings } from '@/services'; import Storage from '@/utils/storage'; import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index f202e0dc1..f233150e2 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -7,6 +7,10 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { Editor, EditorRef, TagSelector, PageTitle } from '@answer/components'; +import type * as Type from '@answer/common/interface'; + +import SearchQuestion from './components/SearchQuestion'; + import { saveQuestion, questionDetail, @@ -15,9 +19,6 @@ import { postAnswer, useQueryQuestionByTitle, } from '@/services'; -import type * as Type from '@answer/common/interface'; - -import SearchQuestion from './components/SearchQuestion'; interface FormDataItem { title: Type.FormValue; diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index 4ffed114a..e97238cff 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -11,10 +11,11 @@ import { FormatTime, htmlRender, } from '@answer/components'; -import { acceptanceAnswer } from '@/services'; import { scrollTop } from '@answer/utils'; import { AnswerItem } from '@answer/common/interface'; +import { acceptanceAnswer } from '@/services'; + interface Props { data: AnswerItem; /** router answer id */ diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index 10c18a473..aa3a38ec0 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -13,6 +13,7 @@ import { htmlRender, } from '@answer/components'; import { formatCount } from '@answer/utils'; + import { following } from '@/services'; interface Props { diff --git a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx index c6cf29d71..8493408ce 100644 --- a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx @@ -3,9 +3,9 @@ import { Card, ListGroup } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useSimilarQuestion } from '@/services'; import { Icon } from '@answer/components'; +import { useSimilarQuestion } from '@/services'; import { loggedUserInfoStore } from '@/stores'; interface Props { diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx index d18eb4a81..9659b2862 100644 --- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx @@ -6,9 +6,10 @@ import { marked } from 'marked'; import classNames from 'classnames'; import { Editor, Modal } from '@answer/components'; -import { postAnswer } from '@/services'; import { FormDataType } from '@answer/common/interface'; +import { postAnswer } from '@/services'; + interface Props { visible?: boolean; data: { diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 0eb6a15f3..2f469415b 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; -import { questionDetail, getAnswers } from '@/services'; import { Pagination, PageTitle } from '@answer/components'; import { loggedUserInfoStore } from '@answer/stores'; import { scrollTop } from '@answer/utils'; @@ -22,6 +21,8 @@ import { Alert, } from './components'; +import { questionDetail, getAnswers } from '@/services'; + import './index.scss'; const Index = () => { diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index 6a3d5127e..c87733f4d 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -7,12 +7,13 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { Editor, EditorRef, Icon, PageTitle } from '@answer/components'; +import type * as Type from '@answer/common/interface'; + import { useQueryAnswerInfo, modifyAnswer, useQueryRevisions, } from '@/services'; -import type * as Type from '@answer/common/interface'; import './index.scss'; diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index 0582684d9..914565ae4 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -4,10 +4,11 @@ import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { Pagination, PageTitle } from '@answer/components'; -import { useSearch } from '@/services'; import { Head, SearchHead, SearchItem, Tips, Empty } from './components'; +import { useSearch } from '@/services'; + const Index = () => { const { t } = useTranslation('translation'); const [searchParams] = useSearchParams(); diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index 3098108c6..e37ab0abf 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next'; import * as Type from '@answer/common/interface'; import { PageTitle, FollowingTags } from '@answer/components'; -import { useTagInfo, useFollow } from '@/services'; +import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; diff --git a/ui/src/pages/Tags/Edit/index.tsx b/ui/src/pages/Tags/Edit/index.tsx index 12019642e..58e7d59c9 100644 --- a/ui/src/pages/Tags/Edit/index.tsx +++ b/ui/src/pages/Tags/Edit/index.tsx @@ -7,10 +7,11 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { Editor, EditorRef, PageTitle } from '@answer/components'; -import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; import { loggedUserInfoStore } from '@answer/stores'; import type * as Type from '@answer/common/interface'; +import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; + interface FormDataItem { displayName: Type.FormValue; slugName: Type.FormValue; diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 31c8ac291..d8c4ad756 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -12,6 +12,7 @@ import { Modal, PageTitle, } from '@answer/components'; + import { useTagInfo, useQuerySynonymsTags, diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index 63fc7276d..80256d943 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -3,10 +3,11 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { useQueryTags, following } from '@/services'; import { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components'; import { formatCount } from '@answer/utils'; +import { useQueryTags, following } from '@/services'; + const sortBtns = ['popular', 'name', 'newest']; const Tags = () => { diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index e9619ced9..e95b13545 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -2,13 +2,13 @@ import { FC, memo, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { resetPassword, checkImgCode } from '@/services'; import type { ImgCodeRes, PasswordResetReq, FormDataType, } from '@answer/common/interface'; +import { resetPassword, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; interface IProps { diff --git a/ui/src/pages/Users/ActiveEmail/index.tsx b/ui/src/pages/Users/ActiveEmail/index.tsx index e2b3491ef..f50fbc3e0 100644 --- a/ui/src/pages/Users/ActiveEmail/index.tsx +++ b/ui/src/pages/Users/ActiveEmail/index.tsx @@ -1,10 +1,10 @@ import { FC, memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { activateAccount } from '@/services'; import { loggedUserInfoStore } from '@answer/stores'; import { getQueryString } from '@answer/utils'; +import { activateAccount } from '@/services'; import { PageTitle } from '@/components'; const Index: FC = () => { diff --git a/ui/src/pages/Users/ConfirmNewEmail/index.tsx b/ui/src/pages/Users/ConfirmNewEmail/index.tsx index 9adaf80eb..d849ffa5f 100644 --- a/ui/src/pages/Users/ConfirmNewEmail/index.tsx +++ b/ui/src/pages/Users/ConfirmNewEmail/index.tsx @@ -4,7 +4,6 @@ import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { changeEmailVerify } from '@/services'; - import { PageTitle } from '@/components'; const Index: FC = () => { diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index 687e77738..47e69a041 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -3,16 +3,17 @@ import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useParams, useNavigate } from 'react-router-dom'; +import { PageTitle } from '@answer/components'; + +import Inbox from './components/Inbox'; +import Achievements from './components/Achievements'; + import { useQueryNotifications, clearUnreadNotification, clearNotificationStatus, readNotification, } from '@/services'; -import { PageTitle } from '@answer/components'; - -import Inbox from './components/Inbox'; -import Achievements from './components/Achievements'; const PAGE_SIZE = 10; diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index fa8c2200a..fcab49ff6 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -5,11 +5,6 @@ import { useParams, useSearchParams } from 'react-router-dom'; import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components'; import { loggedUserInfoStore } from '@answer/stores'; -import { - usePersonalInfoByName, - usePersonalTop, - usePersonalListByTabName, -} from '@/services'; import { UserInfo, @@ -24,6 +19,12 @@ import { Votes, } from './components'; +import { + usePersonalInfoByName, + usePersonalTop, + usePersonalListByTabName, +} from '@/services'; + const Personal: FC = () => { const { tabName = 'overview', username = '' } = useParams(); const [searchParams] = useSearchParams(); diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx index 08afacf28..4f072fe7b 100644 --- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx +++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx @@ -3,9 +3,9 @@ import { Form, Button, Col } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { register } from '@/services'; import type { FormDataType } from '@answer/common/interface'; +import { register } from '@/services'; import userStore from '@/stores/userInfo'; interface Props { diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index 495fd1d43..4ed6b0ff9 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -3,9 +3,10 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; -import { getLoggedUserInfo, changeEmail } from '@/services'; import { useToast } from '@answer/hooks'; +import { getLoggedUserInfo, changeEmail } from '@/services'; + const reg = /(?<=.{2}).+(?=@)/gi; const Index: FC = () => { diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx index eac17839b..2c7ed9ef7 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx @@ -2,10 +2,11 @@ import React, { FC, FormEvent, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { modifyPassword } from '@/services'; import { useToast } from '@answer/hooks'; import type { FormDataType } from '@answer/common/interface'; +import { modifyPassword } from '@/services'; + const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.account', diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index a256a65f1..f38f92e87 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -6,10 +6,10 @@ import dayjs from 'dayjs'; import en from 'dayjs/locale/en'; import zh from 'dayjs/locale/zh-cn'; -import { languages } from '@/services'; import type { LangsType, FormDataType } from '@answer/common/interface'; import { useToast } from '@answer/hooks'; +import { languages } from '@/services'; import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/pages/Users/Settings/Notification/index.tsx b/ui/src/pages/Users/Settings/Notification/index.tsx index 8015901af..6d0cab613 100644 --- a/ui/src/pages/Users/Settings/Notification/index.tsx +++ b/ui/src/pages/Users/Settings/Notification/index.tsx @@ -3,9 +3,10 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import type { FormDataType } from '@answer/common/interface'; -import { setNotice, getLoggedUserInfo } from '@/services'; import { useToast } from '@answer/hooks'; +import { setNotice, getLoggedUserInfo } from '@/services'; + const Index = () => { const toast = useToast(); const { t } = useTranslation('translation', { diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index ecfd3449f..661c222a9 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -4,12 +4,13 @@ import { Trans, useTranslation } from 'react-i18next'; import { marked } from 'marked'; -import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import { UploadImg, Avatar } from '@answer/components'; import { loggedUserInfoStore } from '@answer/stores'; import { useToast } from '@answer/hooks'; +import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services'; + const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.profile', diff --git a/ui/src/pages/Users/Settings/index.tsx b/ui/src/pages/Users/Settings/index.tsx index 8967cd4e0..671707feb 100644 --- a/ui/src/pages/Users/Settings/index.tsx +++ b/ui/src/pages/Users/Settings/index.tsx @@ -3,11 +3,11 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { getLoggedUserInfo } from '@/services'; import type { FormDataType } from '@answer/common/interface'; import Nav from './components/Nav'; +import { getLoggedUserInfo } from '@/services'; import { PageTitle } from '@/components'; const Index: React.FC = () => { From fce82bdda17ebb4b2581426fe3343cad37d21f79 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 31 Oct 2022 11:58:04 +0800 Subject: [PATCH 0058/3337] fix: gravatar add default d=identicon --- ui/package.json | 1 - ui/pnpm-lock.yaml | 19 ------------------- ui/src/components/Avatar/index.tsx | 6 ++++-- ui/src/pages/Users/Settings/Profile/index.tsx | 4 ++-- 4 files changed, 6 insertions(+), 24 deletions(-) diff --git a/ui/package.json b/ui/package.json index 541cd9714..069456ef6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -41,7 +41,6 @@ "lodash": "^4.17.21", "marked": "^4.0.19", "md5": "^2.3.0", - "md5.js": "^1.3.5", "mermaid": "^9.1.7", "next-share": "^0.18.1", "qs": "^6.11.0", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index cd1bfe161..505ff7383 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -58,7 +58,6 @@ specifiers: lodash: ^4.17.21 marked: ^4.0.19 md5: ^2.3.0 - md5.js: ^1.3.5 mermaid: ^9.1.7 next-share: ^0.18.1 postcss: ^8.0.0 @@ -99,7 +98,6 @@ dependencies: lodash: 4.17.21 marked: 4.1.0 md5: 2.3.0 - md5.js: 1.3.5 mermaid: 9.1.7 next-share: 0.18.1_lbqamd2wfmenkveygahn4wdfcq qs: 6.11.0 @@ -6461,15 +6459,6 @@ packages: dependencies: function-bind: 1.1.1 - /hash-base/3.1.0: - resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} - engines: {node: '>=4'} - dependencies: - inherits: 2.0.4 - readable-stream: 3.6.0 - safe-buffer: 5.2.1 - dev: false - /he/1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -7991,14 +7980,6 @@ packages: hasBin: true dev: false - /md5.js/1.3.5: - resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} - dependencies: - hash-base: 3.1.0 - inherits: 2.0.4 - safe-buffer: 5.2.1 - dev: false - /md5/2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} dependencies: diff --git a/ui/src/components/Avatar/index.tsx b/ui/src/components/Avatar/index.tsx index db3effbbd..97a01b998 100644 --- a/ui/src/components/Avatar/index.tsx +++ b/ui/src/components/Avatar/index.tsx @@ -18,8 +18,10 @@ const Index: FC = ({ avatar, size, className, searchStr = '' }) => { if (avatar.length > 1) { url = `${avatar}?${searchStr}`; } - } else if (avatar?.type !== 'default') { - url = `${avatar[avatar.type]}?${searchStr}`; + } else if (avatar?.type === 'gravatar') { + url = `${avatar.gravatar}?${searchStr}&d=identicon`; + } else if (avatar?.type === 'custom') { + url = `${avatar.custom}?${searchStr}`; } return ( diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 92272f6fc..d2e9c4aec 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -312,8 +312,8 @@ const Index: React.FC = () => { 0 ? `&t=${new Date().valueOf()}` : '' }`} className="me-3 rounded" /> From c039760f334766212194f08d514685ae6b3c3ff7 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 31 Oct 2022 14:20:48 +0800 Subject: [PATCH 0059/3337] fix: add id attribute for avatar setting --- ui/src/pages/Users/Settings/Profile/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index d2e9c4aec..92c1cbfc7 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -256,6 +256,7 @@ const Index: React.FC = () => { { inline type="radio" label={t('avatar.custom')} + id="custom" className="mb-0" checked={formData.avatar.type === 'custom'} onChange={() => @@ -291,6 +293,7 @@ const Index: React.FC = () => { Date: Mon, 31 Oct 2022 14:36:42 +0800 Subject: [PATCH 0060/3337] fix: avatar sizes --- ui/src/components/Avatar/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/components/Avatar/index.tsx b/ui/src/components/Avatar/index.tsx index 97a01b998..0436f12ac 100644 --- a/ui/src/components/Avatar/index.tsx +++ b/ui/src/components/Avatar/index.tsx @@ -7,6 +7,7 @@ import DefaultAvatar from '@/assets/images/default-avatar.svg'; interface IProps { /** avatar url */ avatar: string | { type: string; gravatar: string; custom: string }; + /** size 48 96 128 256 */ size: string; searchStr?: string; className?: string; From a2dd4f2f35352bd37339f21234b8754b0fa4123e Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 31 Oct 2022 14:52:56 +0800 Subject: [PATCH 0061/3337] fix: question list style adjustment --- ui/src/components/QuestionList/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 2e487a2d1..c513f795a 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -41,7 +41,7 @@ const QuestionLastUpdate = ({ q }) => { •
@@ -60,7 +60,7 @@ const QuestionLastUpdate = ({ q }) => { •
@@ -75,7 +75,7 @@ const QuestionLastUpdate = ({ q }) => {
); @@ -136,7 +136,7 @@ const QuestionList: FC = ({ source }) => {
- {li.vote_count} + {li.vote_count} = ({ source }) => { : 'chat-square-text-fill' } /> - {li.answer_count} + {li.answer_count} - {li.view_count} + {li.view_count}
From 04b1b46ef25975f6cbfeea40d8fe4d6a88fad610 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Mon, 31 Oct 2022 16:10:12 +0800 Subject: [PATCH 0062/3337] refactor(services): update some service import issue --- ui/src/pages/Admin/Smtp/index.tsx | 2 +- ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx | 6 +++--- ui/src/pages/Users/ConfirmNewEmail/index.tsx | 9 ++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx index f67142550..17abd481d 100644 --- a/ui/src/pages/Admin/Smtp/index.tsx +++ b/ui/src/pages/Admin/Smtp/index.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import type * as Type from '@answer/common/interface'; import { useToast } from '@answer/hooks'; -import { useSmtpSetting, updateSmtpSetting } from '@answer/api'; +import { useSmtpSetting, updateSmtpSetting } from '@/services'; import pattern from '@/common/pattern'; const Smtp: FC = () => { diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx index 28de25c50..4d419091a 100644 --- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx +++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx @@ -3,14 +3,14 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { changeEmail, checkImgCode } from '@answer/api'; import type { ImgCodeRes, PasswordResetReq, FormDataType, } from '@answer/common/interface'; -import { userInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@answer/stores'; +import { changeEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; const Index: FC = () => { @@ -34,7 +34,7 @@ const Index: FC = () => { }); const [showModal, setModalState] = useState(false); const navigate = useNavigate(); - const { user: userInfo, update: updateUser } = userInfoStore(); + const { user: userInfo, update: updateUser } = loggedUserInfoStore(); const getImgCode = () => { checkImgCode({ diff --git a/ui/src/pages/Users/ConfirmNewEmail/index.tsx b/ui/src/pages/Users/ConfirmNewEmail/index.tsx index 41695a89f..79b55ee7b 100644 --- a/ui/src/pages/Users/ConfirmNewEmail/index.tsx +++ b/ui/src/pages/Users/ConfirmNewEmail/index.tsx @@ -3,9 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap'; import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { changeEmailVerify, getUserInfo } from '@/services'; -import { userInfoStore } from '@answer/stores'; - +import { loggedUserInfoStore } from '@/stores'; +import { changeEmailVerify, getLoggedUserInfo } from '@/services'; import { PageTitle } from '@/components'; const Index: FC = () => { @@ -13,7 +12,7 @@ const Index: FC = () => { const [searchParams] = useSearchParams(); const [step, setStep] = useState('loading'); - const updateUser = userInfoStore((state) => state.update); + const updateUser = loggedUserInfoStore((state) => state.update); useEffect(() => { const code = searchParams.get('code'); @@ -22,7 +21,7 @@ const Index: FC = () => { changeEmailVerify({ code }) .then(() => { setStep('success'); - getUserInfo().then((res) => { + getLoggedUserInfo().then((res) => { // update user info updateUser(res); }); From beed798aa6b52cd81923db460e567bccd8f89276 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 31 Oct 2022 16:13:01 +0800 Subject: [PATCH 0063/3337] fix userlist update avatar --- internal/service/user_backyard/user_backyard.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/service/user_backyard/user_backyard.go b/internal/service/user_backyard/user_backyard.go index c82ce1ae2..4dfe84338 100644 --- a/internal/service/user_backyard/user_backyard.go +++ b/internal/service/user_backyard/user_backyard.go @@ -98,6 +98,7 @@ func (us *UserBackyardService) GetUserPage(ctx context.Context, req *schema.GetU resp := make([]*schema.GetUserPageResp, 0) for _, u := range users { + avatar := schema.FormatAvatarInfo(u.Avatar) t := &schema.GetUserPageResp{ UserID: u.ID, CreatedAt: u.CreatedAt.Unix(), @@ -105,7 +106,7 @@ func (us *UserBackyardService) GetUserPage(ctx context.Context, req *schema.GetU EMail: u.EMail, Rank: u.Rank, DisplayName: u.DisplayName, - Avatar: u.Avatar, + Avatar: avatar, } if u.Status == entity.UserStatusDeleted { t.Status = schema.UserDeleted From 05986a324983c17d1e7211efb0f3e0cd5719de87 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 31 Oct 2022 16:20:58 +0800 Subject: [PATCH 0064/3337] feat: use sqlite for default initialization database --- README.md | 6 ++---- README_CN.md | 6 ++---- configs/config.yaml | 4 ++-- internal/base/data/data.go | 7 +++++++ 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5bd089ac5..17c6a2e39 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,10 @@ To learn more about the project, visit [answer.dev](https://answer.dev). ## Quick start -### Running with docker-compose +### Running with docker ```bash -mkdir answer && cd answer -wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml -docker-compose up +docker run -d -p 9080:80 -v $PWD/answer-data/data:/data --name answer answerdev/answer:latest ``` For more information you can see [INSTALL.md](./INSTALL.md) diff --git a/README_CN.md b/README_CN.md index 56032f02e..bb3ec814f 100644 --- a/README_CN.md +++ b/README_CN.md @@ -18,12 +18,10 @@ ## 快速开始 -### 使用 docker-compose 快速搭建 +### 使用 docker 快速搭建 ```bash -mkdir answer && cd answer -wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml -docker-compose up +docker run -d -p 9080:80 -v $PWD/answer-data/data:/data --name answer answerdev/answer:latest ``` 其他安装配置细节请参考 [INSTALL.md](./INSTALL.md) diff --git a/configs/config.yaml b/configs/config.yaml index 0ec1a601c..6830048f2 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -3,8 +3,8 @@ server: addr: 0.0.0.0:80 data: database: - driver: "mysql" - connection: root:root@tcp(db:3306)/answer + driver: "sqlite3" + connection: "/data/sqlite3/answer.db" cache: file_path: "/data/cache/cache.db" i18n: diff --git a/internal/base/data/data.go b/internal/base/data/data.go index b6878181b..356c71570 100644 --- a/internal/base/data/data.go +++ b/internal/base/data/data.go @@ -37,6 +37,13 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) { if dataConf.Driver == "" { dataConf.Driver = string(schemas.MYSQL) } + if dataConf.Driver == string(schemas.SQLITE) { + dbFileDir := filepath.Dir(dataConf.Connection) + log.Debugf("try to create database directory %s", dbFileDir) + if err := dir.CreateDirIfNotExist(dbFileDir); err != nil { + log.Errorf("create database dir failed: %s", err) + } + } engine, err := xorm.NewEngine(dataConf.Driver, dataConf.Connection) if err != nil { return nil, err From 302150aa6e00981d69165ee1de4fa00f0d8f88ae Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 31 Oct 2022 16:50:38 +0800 Subject: [PATCH 0065/3337] update user change email --- internal/service/user_service.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 071bbe885..10befdab7 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -492,7 +492,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) error { - _, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) + userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { return err } @@ -513,10 +513,15 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. UserID: req.UserID, } code := uuid.NewString() + var title, body string verifyEmailUrl := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.serviceConfig.WebHost, code) - title, body, err := us.emailService.ChangeEmailTemplate(ctx, verifyEmailUrl) - if err != nil { - return err + if userInfo.MailStatus == entity.EmailStatusToBeVerified { + title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailUrl) + } else { + title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailUrl) + if err != nil { + return err + } } log.Infof("send email confirmation %s", verifyEmailUrl) From 220eefc2e70b1f0db8b2fc7cc29530cafeee6d3b Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 31 Oct 2022 16:53:59 +0800 Subject: [PATCH 0066/3337] update user change email --- internal/service/user_service.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 10befdab7..6d6838ec5 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -519,9 +519,9 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailUrl) } else { title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailUrl) - if err != nil { - return err - } + } + if err != nil { + return err } log.Infof("send email confirmation %s", verifyEmailUrl) From d72425b343043abc42291df22c99111afe36ea19 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Mon, 31 Oct 2022 18:40:02 +0800 Subject: [PATCH 0067/3337] fix(router-guard): fix some router guard logic --- ui/src/router/guarder.ts | 18 +++++++++++------- ui/src/utils/guards.ts | 34 ++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/ui/src/router/guarder.ts b/ui/src/router/guarder.ts index 8c5bf7c2b..e3b815982 100644 --- a/ui/src/router/guarder.ts +++ b/ui/src/router/guarder.ts @@ -7,17 +7,12 @@ import { isLoggedAndInactive, isLoggedAndSuspended, isNotLoggedOrInactive, + isNotLoggedOrNotSuspend, } from '@/utils/guards'; const RouteGuarder = { base: async () => { - return isNotLoggedOrNormal(); - }, - notLogged: async () => { - return isNotLogged(); - }, - notLoggedOrInactive: async () => { - return isNotLoggedOrInactive(); + return isNotLoggedOrNotSuspend(); }, loggedAndNormal: async () => { await pullLoggedUser(true); @@ -33,6 +28,15 @@ const RouteGuarder = { await pullLoggedUser(true); return isAdminLogged(); }, + notLogged: async () => { + return isNotLogged(); + }, + notLoggedOrNormal: async () => { + return isNotLoggedOrNormal(); + }, + notLoggedOrInactive: async () => { + return isNotLoggedOrInactive(); + }, }; export default RouteGuarder; diff --git a/ui/src/utils/guards.ts b/ui/src/utils/guards.ts index f0faf55b7..47f79605f 100644 --- a/ui/src/utils/guards.ts +++ b/ui/src/utils/guards.ts @@ -94,21 +94,21 @@ export const isNotLogged = () => { }; export const isLoggedAndInactive = () => { - const ret: GuardResult = { ok: false, redirect: undefined }; + const ret: GuardResult = { ok: true, redirect: undefined }; const userStat = deriveUserStat(); - if (!userStat.isActivated) { - ret.ok = true; - ret.redirect = RouteAlias.activation; + if (userStat.isActivated) { + ret.ok = false; + ret.redirect = RouteAlias.home; } return ret; }; export const isLoggedAndSuspended = () => { - const ret: GuardResult = { ok: false, redirect: undefined }; + const ret: GuardResult = { ok: true, redirect: undefined }; const userStat = deriveUserStat(); - if (userStat.isSuspended) { - ret.redirect = RouteAlias.suspended; - ret.ok = true; + if (!userStat.isSuspended) { + ret.ok = false; + ret.redirect = RouteAlias.home; } return ret; }; @@ -120,7 +120,7 @@ export const isLoggedAndNormal = () => { ret.ok = true; } else if (!userStat.isActivated) { ret.redirect = RouteAlias.activation; - } else if (!userStat.isSuspended) { + } else if (userStat.isSuspended) { ret.redirect = RouteAlias.suspended; } else if (!userStat.isLogged) { ret.redirect = RouteAlias.login; @@ -139,12 +139,26 @@ export const isNotLoggedOrNormal = () => { return ret; }; +export const isNotLoggedOrNotSuspend = () => { + const ret: GuardResult = { ok: true, redirect: undefined }; + const userStat = deriveUserStat(); + const gr = isLoggedAndNormal(); + if (!gr.ok && userStat.isSuspended) { + ret.ok = false; + ret.redirect = gr.redirect; + } + return ret; +}; + export const isNotLoggedOrInactive = () => { const ret: GuardResult = { ok: true, redirect: undefined }; const userStat = deriveUserStat(); - if (userStat.isLogged || userStat.isActivated) { + if (userStat.isActivated) { ret.ok = false; ret.redirect = RouteAlias.home; + } else if (userStat.isSuspended) { + ret.ok = false; + ret.redirect = RouteAlias.suspended; } return ret; }; From 2ad6a40d764a89c333381caa9309b56aec815859 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 31 Oct 2022 18:48:27 +0800 Subject: [PATCH 0068/3337] chore: add install pages --- ui/src/i18n/locales/en.json | 60 +++++++++++++++++++ .../Install/components/FifthStep/index.tsx | 33 ++++++++++ .../Install/components/FirstStep/index.tsx | 31 ++++++++++ .../Install/components/FourthStep/index.tsx | 53 ++++++++++++++++ .../Install/components/Progress/index.tsx | 22 +++++++ .../Install/components/SecondStep/index.tsx | 57 ++++++++++++++++++ .../Install/components/ThirdStep/index.tsx | 39 ++++++++++++ ui/src/pages/Install/components/index.ts | 7 +++ ui/src/pages/Install/index.tsx | 44 ++++++++++++++ ui/src/router/routes.ts | 4 ++ 10 files changed, 350 insertions(+) create mode 100644 ui/src/pages/Install/components/FifthStep/index.tsx create mode 100644 ui/src/pages/Install/components/FirstStep/index.tsx create mode 100644 ui/src/pages/Install/components/FourthStep/index.tsx create mode 100644 ui/src/pages/Install/components/Progress/index.tsx create mode 100644 ui/src/pages/Install/components/SecondStep/index.tsx create mode 100644 ui/src/pages/Install/components/ThirdStep/index.tsx create mode 100644 ui/src/pages/Install/components/index.ts create mode 100644 ui/src/pages/Install/index.tsx diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 94277ee9e..0bf94f198 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -732,6 +732,66 @@ "x_answers": "answers", "x_questions": "questions" }, + "install": { + "title": "Answer", + "next": "Next", + "done": "Done", + "choose_lang": { + "label": "Please choose a language" + }, + "database_engine": { + "label": "Database Engine" + }, + "username": { + "label": "Username", + "placeholder": "root" + }, + "password": { + "label": "Password", + "placeholder": "root" + }, + "database_host": { + "label": "Database Host", + "placeholder": "db:3306" + }, + "database_name": { + "label": "Database Name", + "placeholder": "answer" + }, + "table_prefix": { + "label": "Table Prefix (optional)", + "placeholder": "answer_" + }, + "config_yaml": { + "title": "Create config.yaml", + "label": "The config.yaml file created.", + "description": "You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it.", + "info": "After you’ve done that, click “Next” button." + }, + "site_information": "Site Information", + "admin_account": "Admin Account", + "site_name": { + "label": "Site Name" + }, + "contact_email": { + "label": "Contact Email", + "text": "Email address of key contact responsible for this site." + }, + "admin_name": { + "label": "Name" + }, + "admin_password": { + "label": "Password", + "text": "You will need this password to log in. Please store it in a secure location." + }, + "admin_email": { + "label": "Email", + "text": "You will need this email to log in." + }, + "ready_title": "Your Answer is Ready!", + "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", + "good_luck": "Have fun, and good luck!" + }, "page_404": { "description": "Unfortunately, this page doesn't exist.", "back_home": "Back to homepage" diff --git a/ui/src/pages/Install/components/FifthStep/index.tsx b/ui/src/pages/Install/components/FifthStep/index.tsx new file mode 100644 index 000000000..63f008e0b --- /dev/null +++ b/ui/src/pages/Install/components/FifthStep/index.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react'; +import { Button } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+
{t('ready_title')}
+

+ + If you ever feel like changing more settings, visit + admin section; find it in the site menu. + +

+

{t('good_luck')}

+ +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx new file mode 100644 index 000000000..156f568d2 --- /dev/null +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -0,0 +1,31 @@ +import { FC } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( + + + {t('choose_lang.label')} + + + + + +
+ + +
+ + ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx new file mode 100644 index 000000000..25fb47ef1 --- /dev/null +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -0,0 +1,53 @@ +import { FC } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+
{t('site_information')}
+ + {t('site_name.label')} + + + + {t('contact_email.label')} + + {t('contact_email.text')} + + +
{t('admin_account')}
+ + {t('admin_name.label')} + + + + + {t('admin_password.label')} + + {t('admin_password.text')} + + + + {t('admin_email.label')} + + {t('admin_email.text')} + + +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/Progress/index.tsx b/ui/src/pages/Install/components/Progress/index.tsx new file mode 100644 index 000000000..97f33ffec --- /dev/null +++ b/ui/src/pages/Install/components/Progress/index.tsx @@ -0,0 +1,22 @@ +import { FC, memo } from 'react'; +import { ProgressBar } from 'react-bootstrap'; + +interface IProps { + step: number; +} + +const Index: FC = ({ step }) => { + return ( +
+ + {step}/5 +
+ ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx new file mode 100644 index 000000000..7b21ab738 --- /dev/null +++ b/ui/src/pages/Install/components/SecondStep/index.tsx @@ -0,0 +1,57 @@ +import { FC } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} + +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+ + {t('database_engine.label')} + + + + + + + {t('username.label')} + + + + + {t('password.label')} + + + + + {t('database_host.label')} + + + + + {t('database_name.label')} + + + + + {t('table_prefix.label')} + + + +
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/ThirdStep/index.tsx b/ui/src/pages/Install/components/ThirdStep/index.tsx new file mode 100644 index 000000000..4d3f702f6 --- /dev/null +++ b/ui/src/pages/Install/components/ThirdStep/index.tsx @@ -0,0 +1,39 @@ +import { FC } from 'react'; +import { Form, Button, FormGroup } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import Progress from '../Progress'; + +interface Props { + visible: boolean; +} + +const Index: FC = ({ visible }) => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + + if (!visible) return null; + return ( +
+
{t('config_yaml.title')}
+
{t('config_yaml.label')}
+
+

+ }} + /> +

+
+ + + +
{t('config_yaml.info')}
+
+ + +
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Install/components/index.ts b/ui/src/pages/Install/components/index.ts new file mode 100644 index 000000000..ecc22539d --- /dev/null +++ b/ui/src/pages/Install/components/index.ts @@ -0,0 +1,7 @@ +import FirstStep from './FirstStep'; +import SecondStep from './SecondStep'; +import ThirdStep from './ThirdStep'; +import FourthStep from './FourthStep'; +import Fifth from './FifthStep'; + +export { FirstStep, SecondStep, ThirdStep, FourthStep, Fifth }; diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx new file mode 100644 index 000000000..e1425893a --- /dev/null +++ b/ui/src/pages/Install/index.tsx @@ -0,0 +1,44 @@ +import { FC, useState } from 'react'; +import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import { + FirstStep, + SecondStep, + ThirdStep, + FourthStep, + Fifth, +} from './components'; + +const Index: FC = () => { + const { t } = useTranslation('translation', { keyPrefix: 'install' }); + const [step] = useState(5); + + return ( +
+ + + +

{t('title')}

+ + + show error msg + + + + + + + + + + + + +
+
+
+ ); +}; + +export default Index; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 5b49fb1ae..69498f5d0 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -211,5 +211,9 @@ const routes: RouteNode[] = [ }, ], }, + { + path: 'install', + page: 'pages/Install', + }, ]; export default routes; From bd888c121a58d998ea10af17f9fa905220680364 Mon Sep 17 00:00:00 2001 From: kumfo Date: Tue, 1 Nov 2022 09:12:12 +0800 Subject: [PATCH 0069/3337] feat: admin's question, answer and user search --- docs/docs.go | 23 +++++--- docs/swagger.json | 23 +++++--- docs/swagger.yaml | 17 +++--- internal/controller/question_controller.go | 2 + .../user_backyard_controller.go | 5 +- internal/entity/answer_entity.go | 3 +- internal/repo/answer_repo.go | 57 ++++++++++++++++--- internal/repo/question_repo.go | 52 +++++++++++++++-- internal/repo/user/user_backyard_repo.go | 40 ++++++++++++- internal/schema/backyard_user_schema.go | 4 +- internal/schema/question_schema.go | 3 +- internal/service/answer_common/answer.go | 3 + .../service/user_backyard/user_backyard.go | 4 +- 13 files changed, 185 insertions(+), 51 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 81ccb1a7b..b41a96b36 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -56,6 +56,12 @@ const docTemplate = `{ "description": "user status", "name": "status", "in": "query" + }, + { + "type": "string", + "description": "answer id or question title", + "name": "query", + "in": "query" } ], "responses": { @@ -173,6 +179,12 @@ const docTemplate = `{ "description": "user status", "name": "status", "in": "query" + }, + { + "type": "string", + "description": "question id or title", + "name": "query", + "in": "query" } ], "responses": { @@ -709,19 +721,12 @@ const docTemplate = `{ }, { "type": "string", - "description": "username", - "name": "username", - "in": "query" - }, - { - "type": "string", - "description": "email", - "name": "e_mail", + "description": "search query: email, username or id:[id]", + "name": "query", "in": "query" }, { "enum": [ - "normal", "suspended", "deleted", "inactive" diff --git a/docs/swagger.json b/docs/swagger.json index c92a6b1e5..05097e5fb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -44,6 +44,12 @@ "description": "user status", "name": "status", "in": "query" + }, + { + "type": "string", + "description": "answer id or question title", + "name": "query", + "in": "query" } ], "responses": { @@ -161,6 +167,12 @@ "description": "user status", "name": "status", "in": "query" + }, + { + "type": "string", + "description": "question id or title", + "name": "query", + "in": "query" } ], "responses": { @@ -697,19 +709,12 @@ }, { "type": "string", - "description": "username", - "name": "username", - "in": "query" - }, - { - "type": "string", - "description": "email", - "name": "e_mail", + "description": "search query: email, username or id:[id]", + "name": "query", "in": "query" }, { "enum": [ - "normal", "suspended", "deleted", "inactive" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5a261821e..9e36de5b2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1390,6 +1390,10 @@ paths: in: query name: status type: string + - description: answer id or question title + in: query + name: query + type: string produces: - application/json responses: @@ -1463,6 +1467,10 @@ paths: in: query name: status type: string + - description: question id or title + in: query + name: query + type: string produces: - application/json responses: @@ -1788,17 +1796,12 @@ paths: in: query name: page_size type: integer - - description: username + - description: 'search query: email, username or id:[id]' in: query - name: username - type: string - - description: email - in: query - name: e_mail + name: query type: string - description: user status enum: - - normal - suspended - deleted - inactive diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 620f1e41f..dab6735f4 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -365,6 +365,7 @@ func (qc *QuestionController) UserCollectionList(ctx *gin.Context) { // @Param page query int false "page size" // @Param page_size query int false "page size" // @Param status query string false "user status" Enums(available, closed, deleted) +// @Param query query string false "question id or title" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/question/page [get] func (qc *QuestionController) CmsSearchList(ctx *gin.Context) { @@ -390,6 +391,7 @@ func (qc *QuestionController) CmsSearchList(ctx *gin.Context) { // @Param page query int false "page size" // @Param page_size query int false "page size" // @Param status query string false "user status" Enums(available,deleted) +// @Param query query string false "answer id or question title" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/answer/page [get] func (qc *QuestionController) CmsSearchAnswerList(ctx *gin.Context) { diff --git a/internal/controller_backyard/user_backyard_controller.go b/internal/controller_backyard/user_backyard_controller.go index 0c3177fbd..03d56d964 100644 --- a/internal/controller_backyard/user_backyard_controller.go +++ b/internal/controller_backyard/user_backyard_controller.go @@ -45,9 +45,8 @@ func (uc *UserBackyardController) UpdateUserStatus(ctx *gin.Context) { // @Produce json // @Param page query int false "page size" // @Param page_size query int false "page size" -// @Param username query string false "username" -// @Param e_mail query string false "email" -// @Param status query string false "user status" Enums(normal, suspended, deleted, inactive) +// @Param query query string false "search query: email, username or id:[id]" +// @Param status query string false "user status" Enums(suspended, deleted, inactive) // @Success 200 {object} handler.RespBody{data=pager.PageModel{records=[]schema.GetUserPageResp}} // @Router /answer/admin/api/users/page [get] func (uc *UserBackyardController) GetUserPage(ctx *gin.Context) { diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go index 3666f1065..8c33d08cd 100644 --- a/internal/entity/answer_entity.go +++ b/internal/entity/answer_entity.go @@ -43,7 +43,8 @@ type CmsAnswerSearch struct { Page int `json:"page" form:"page"` //Query number of pages PageSize int `json:"page_size" form:"page_size"` //Search page size Status int `json:"-" form:"-"` - StatusStr string `json:"status" form:"status"` //Status 1 Available 2 closed 10 Deleted + StatusStr string `json:"status" form:"status"` //Status 1 Available 2 closed 10 Deleted + Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string } type AdminSetAnswerStatusRequest struct { diff --git a/internal/repo/answer_repo.go b/internal/repo/answer_repo.go index dd4753e87..a4c427225 100644 --- a/internal/repo/answer_repo.go +++ b/internal/repo/answer_repo.go @@ -2,7 +2,10 @@ package repo import ( "context" + "strings" "time" + "unicode" + "xorm.io/builder" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/data" @@ -202,11 +205,16 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc } func (ar *answerRepo) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) { - var count int64 - var err error - if search.Status == 0 { - search.Status = 1 - } + var ( + count int64 + err error + session = ar.data.DB.Table([]string{entity.Answer{}.TableName(), "a"}).Select("a.*") + ) + + session.Where(builder.Eq{ + "a.status": search.Status, + }) + rows := make([]*entity.Answer, 0) if search.Page > 0 { search.Page = search.Page - 1 @@ -216,11 +224,42 @@ func (ar *answerRepo) CmsSearchList(ctx context.Context, search *entity.CmsAnswe if search.PageSize == 0 { search.PageSize = constant.Default_PageSize } + + // search by question title like or answer id + if len(search.Query) > 0 { + // check id search + var ( + idSearch = false + id = "" + ) + + if strings.Contains(search.Query, "id:") { + idSearch = true + id = strings.TrimSpace(strings.TrimPrefix(search.Query, "id:")) + for _, r := range id { + if !unicode.IsDigit(r) { + idSearch = false + break + } + } + } + + if idSearch { + session.And(builder.Eq{ + "id": id, + }) + } else { + session.Join("LEFT", []string{entity.Question{}.TableName(), "q"}, "q.id = a.question_id") + session.And(builder.Like{ + "q.title", search.Query, + }) + } + } + offset := search.Page * search.PageSize - session := ar.data.DB.Where("") - session = session.And("status =?", search.Status) - session = session.OrderBy("updated_at desc") - session = session.Limit(search.PageSize, offset) + session. + OrderBy("a.updated_at desc"). + Limit(search.PageSize, offset) count, err = session.FindAndCount(&rows) if err != nil { return rows, count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/repo/question_repo.go b/internal/repo/question_repo.go index f6f4e40b3..3142bcd26 100644 --- a/internal/repo/question_repo.go +++ b/internal/repo/question_repo.go @@ -2,7 +2,10 @@ package repo import ( "context" + "strings" "time" + "unicode" + "xorm.io/builder" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/data" @@ -225,8 +228,16 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS } func (qr *questionRepo) CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error) { - var count int64 - var err error + var ( + count int64 + err error + session = qr.data.DB.Table("question") + ) + + session.Where(builder.Eq{ + "status": search.Status, + }) + rows := make([]*entity.Question, 0) if search.Page > 0 { search.Page = search.Page - 1 @@ -236,11 +247,40 @@ func (qr *questionRepo) CmsSearchList(ctx context.Context, search *schema.CmsQue if search.PageSize == 0 { search.PageSize = constant.Default_PageSize } + + // search by question title like or question id + if len(search.Query) > 0 { + // check id search + var ( + idSearch = false + id = "" + ) + if strings.Contains(search.Query, "id:") { + idSearch = true + id = strings.TrimSpace(strings.TrimPrefix(search.Query, "id:")) + for _, r := range id { + if !unicode.IsDigit(r) { + idSearch = false + break + } + } + } + + if idSearch { + session.And(builder.Eq{ + "id": id, + }) + } else { + session.And(builder.Like{ + "title", search.Query, + }) + } + } + offset := search.Page * search.PageSize - session := qr.data.DB.Table("question") - session = session.And("status =?", search.Status) - session = session.OrderBy("updated_at desc") - session = session.Limit(search.PageSize, offset) + + session.OrderBy("updated_at desc"). + Limit(search.PageSize, offset) count, err = session.FindAndCount(&rows) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/repo/user/user_backyard_repo.go b/internal/repo/user/user_backyard_repo.go index 06ce7d2f0..790d7c411 100644 --- a/internal/repo/user/user_backyard_repo.go +++ b/internal/repo/user/user_backyard_repo.go @@ -3,7 +3,11 @@ package user import ( "context" "encoding/json" + "net/mail" + "strings" "time" + "unicode" + "xorm.io/builder" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/data" @@ -68,7 +72,7 @@ func (ur *userBackyardRepo) GetUserInfo(ctx context.Context, userID string) (use } // GetUserPage get user page -func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User) (users []*entity.User, total int64, err error) { +func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, query string) (users []*entity.User, total int64, err error) { users = make([]*entity.User, 0) session := ur.data.DB.NewSession() if user.Status == entity.UserStatusDeleted { @@ -78,6 +82,40 @@ func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, } else { session.Desc("created_at") } + + if len(query) > 0 { + if email, e := mail.ParseAddress(query); e == nil { + session.And(builder.Eq{"e_mail": email.Address}) + } else { + var ( + idSearch = false + id = "" + ) + + if strings.Contains(query, "id:") { + idSearch = true + id = strings.TrimSpace(strings.TrimPrefix(query, "id:")) + for _, r := range id { + if !unicode.IsDigit(r) { + idSearch = false + break + } + } + } + + if idSearch { + session.And(builder.Eq{ + "id": id, + }) + } else { + session.And(builder.Or( + builder.Like{"username", query}, + builder.Like{"display_name", query}, + )) + } + } + } + total, err = pager.Help(page, pageSize, &users, user, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/schema/backyard_user_schema.go b/internal/schema/backyard_user_schema.go index bccf1b63a..de158146f 100644 --- a/internal/schema/backyard_user_schema.go +++ b/internal/schema/backyard_user_schema.go @@ -26,10 +26,8 @@ type GetUserPageReq struct { Page int `validate:"omitempty,min=1" form:"page"` // page size PageSize int `validate:"omitempty,min=1" form:"page_size"` - // username - Username string `validate:"omitempty,gt=0,lte=50" form:"username"` // email - EMail string `validate:"omitempty,gt=0,lte=100" form:"e_mail"` + Query string `validate:"omitempty,gt=0,lte=100" form:"query"` // user status Status string `validate:"omitempty,oneof=suspended deleted inactive" form:"status"` } diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index 5bd3b4c18..7ca467b4d 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -170,7 +170,8 @@ 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 } type AdminSetQuestionStatusRequest struct { diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index 7fc65a6f4..a974d313a 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -42,6 +42,9 @@ func (as *AnswerCommon) SearchAnswered(ctx context.Context, userId, questionId s } func (as *AnswerCommon) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) { + if search.Status == 0 { + search.Status = 1 + } return as.answerRepo.CmsSearchList(ctx, search) } diff --git a/internal/service/user_backyard/user_backyard.go b/internal/service/user_backyard/user_backyard.go index c82ce1ae2..3523c137e 100644 --- a/internal/service/user_backyard/user_backyard.go +++ b/internal/service/user_backyard/user_backyard.go @@ -17,7 +17,7 @@ import ( type UserBackyardRepo interface { UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, email string) (err error) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) - GetUserPage(ctx context.Context, page, pageSize int, user *entity.User) (users []*entity.User, total int64, err error) + GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, query string) (users []*entity.User, total int64, err error) } // UserBackyardService user service @@ -91,7 +91,7 @@ func (us *UserBackyardService) GetUserPage(ctx context.Context, req *schema.GetU user.Status = entity.UserStatusDeleted } - users, total, err := us.userRepo.GetUserPage(ctx, req.Page, req.PageSize, user) + users, total, err := us.userRepo.GetUserPage(ctx, req.Page, req.PageSize, user, req.Query) if err != nil { return } From 792a9642ac116f408c61f812c4094f44b10a7a48 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 1 Nov 2022 09:48:44 +0800 Subject: [PATCH 0070/3337] fix: avatar size adjustment --- ui/src/components/Header/components/NavItems/index.tsx | 2 +- ui/src/components/UserCard/index.tsx | 4 ++-- ui/src/pages/Users/Personal/components/UserInfo/index.tsx | 4 ++-- ui/src/pages/Users/Settings/Profile/index.tsx | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/src/components/Header/components/NavItems/index.tsx b/ui/src/components/Header/components/NavItems/index.tsx index 67112758e..517119504 100644 --- a/ui/src/components/Header/components/NavItems/index.tsx +++ b/ui/src/components/Header/components/NavItems/index.tsx @@ -43,7 +43,7 @@ const Index: FC = ({ redDot, userInfo, logOut }) => { id="dropdown-basic" as="a" className="no-toggle pointer"> - + diff --git a/ui/src/components/UserCard/index.tsx b/ui/src/components/UserCard/index.tsx index 0110c82a0..9688f8182 100644 --- a/ui/src/components/UserCard/index.tsx +++ b/ui/src/components/UserCard/index.tsx @@ -23,7 +23,7 @@ const Index: FC = ({ data, time, preFix, className = '' }) => { avatar={data?.avatar} size="40px" className="me-2 d-none d-md-block" - searchStr="s=48" + searchStr="s=96" /> = ({ data, time, preFix, className = '' }) => { avatar={data?.avatar} size="40px" className="me-2" - searchStr="s=48" + searchStr="s=96" /> )}
diff --git a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx index 97caca3e1..9cc400045 100644 --- a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx +++ b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx @@ -19,10 +19,10 @@ const Index: FC = ({ data }) => {
{data?.status !== 'deleted' ? ( - + ) : ( - + )}
diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 92c1cbfc7..e5cd9295c 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -315,7 +315,7 @@ const Index: React.FC = () => { 0 ? `&t=${new Date().valueOf()}` : '' }`} className="me-3 rounded" @@ -348,7 +348,7 @@ const Index: React.FC = () => { <> From 05f48669092a8c7d8cc1580be1bf54010f8a0c7d Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 1 Nov 2022 10:04:08 +0800 Subject: [PATCH 0071/3337] fix(notifications): fix message disappear when reclick notification type --- ui/src/pages/Users/Notifications/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index 47e69a041..b24367b74 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -47,6 +47,9 @@ const Notifications = () => { const handleTypeChange = (evt, val) => { evt.preventDefault(); + if (type === val) { + return; + } setPage(1); setNotificationData([]); navigate(`/users/notifications/${val}`); From e4f08e138675beb97552a2c1e8f68ad2d94ddd06 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 1 Nov 2022 10:36:19 +0800 Subject: [PATCH 0072/3337] fix(router): add general route error element --- ui/src/router/index.tsx | 2 ++ ui/src/utils/request.ts | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index a7452ff85..84eb37a65 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -2,6 +2,7 @@ import React, { Suspense, lazy } from 'react'; import { RouteObject, createBrowserRouter, redirect } from 'react-router-dom'; import Layout from '@/pages/Layout'; +import ErrorBoundary from '@/pages/50X'; import baseRoutes, { RouteNode } from '@/router/routes'; import { floppyNavigation } from '@/utils'; @@ -11,6 +12,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => { routeNodes.forEach((rn) => { if (rn.path === '/') { rn.element = ; + rn.errorElement = ; } else { /** * cannot use a fully dynamic import statement diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index c9f9e67fa..d80cadf5d 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -132,11 +132,12 @@ class Request { return Promise.reject(false); } } - - toastStore.getState().show({ - msg: `statusCode: ${status}; ${respMsg || ''}`, - variant: 'danger', - }); + if (respMsg) { + toastStore.getState().show({ + msg: `statusCode: ${status}; ${respMsg || ''}`, + variant: 'danger', + }); + } return Promise.reject(false); }, ); From a741bd5cfc40df72ed6b7623016b4b425e086a72 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 1 Nov 2022 11:34:39 +0800 Subject: [PATCH 0073/3337] fix: add upgrad and maintenance page --- ui/src/i18n/locales/en.json | 23 +++++++++++-- ui/src/index.scss | 3 ++ ui/src/pages/Install/index.tsx | 27 ++++++++++++++-- ui/src/pages/Maintenance/index.tsx | 23 +++++++++++++ ui/src/pages/Upgrade/index.tsx | 52 ++++++++++++++++++++++++++++++ ui/src/router/routes.ts | 8 +++++ 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 ui/src/pages/Maintenance/index.tsx create mode 100644 ui/src/pages/Upgrade/index.tsx diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 0bf94f198..ecbb7b7c9 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -28,7 +28,10 @@ "confirm_email": "Confirm Email", "account_suspended": "Account Suspended", "admin": "Admin", - "change_email": "Modify Email" + "change_email": "Modify Email", + "install": "Answer Installation", + "upgrade": "Answer Upgrade", + "maintenance": "Webite Maintenance" }, "notifications": { "title": "Notifications", @@ -790,7 +793,20 @@ }, "ready_title": "Your Answer is Ready!", "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", - "good_luck": "Have fun, and good luck!" + "good_luck": "Have fun, and good luck!", + "warning": "Warning", + "warning_description": "The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now.", + "installed": "Already installed", + "installed_description": "You appear to have already installed. To reinstall please clear your old database tables first." + }, + "upgrade": { + "title": "Answer", + "update_btn": "Update data", + "update_title": "Data update required", + "update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.<1>The update process may take a little while, so please be patient.", + "done_title": "No update required", + "done_btn": "Done", + "done_desscription": "Your Answer data is already up-to-date." }, "page_404": { "description": "Unfortunately, this page doesn't exist.", @@ -800,6 +816,9 @@ "description": "The server encountered an error and could not complete your request.", "back_home": "Back to homepage" }, + "page_maintenance": { + "description": "We are under maintenance, we’ll be back soon." + }, "admin": { "admin_header": { "title": "Admin" diff --git a/ui/src/index.scss b/ui/src/index.scss index e81a900ca..dd25289fe 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -77,6 +77,9 @@ a { .page-wrap { min-height: calc(100vh - 148px); } +.page-wrap2 { + min-height: 100vh; +} .btn-no-border, .btn-no-border:hover, diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index e1425893a..8de7cb116 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -1,6 +1,6 @@ import { FC, useState } from 'react'; import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; -import { useTranslation } from 'react-i18next'; +import { useTranslation, Trans } from 'react-i18next'; import { FirstStep, @@ -10,12 +10,15 @@ import { Fifth, } from './components'; +import { PageTitle } from '@/components'; + const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step] = useState(5); + const [step] = useState(7); return (
+ @@ -32,6 +35,26 @@ const Index: FC = () => { + {step === 6 && ( +
+
{t('warning')}
+

+ + The file config.yaml already exists. If you + need to reset any of the configuration items in this + file, please delete it first. You may try{' '} + installing now. + +

+
+ )} + + {step === 7 && ( +
+
{t('installed')}
+

{t('installed_description')}

+
+ )} diff --git a/ui/src/pages/Maintenance/index.tsx b/ui/src/pages/Maintenance/index.tsx new file mode 100644 index 000000000..3a2c2d862 --- /dev/null +++ b/ui/src/pages/Maintenance/index.tsx @@ -0,0 +1,23 @@ +import { Container } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import { PageTitle } from '@/components'; + +const Index = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'page_maintenance', + }); + return ( + + +
+ (=‘_‘=) +
+
{t('description')}
+
+ ); +}; + +export default Index; diff --git a/ui/src/pages/Upgrade/index.tsx b/ui/src/pages/Upgrade/index.tsx new file mode 100644 index 000000000..aee2a37a9 --- /dev/null +++ b/ui/src/pages/Upgrade/index.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react'; +import { Container, Row, Col, Card, Button } from 'react-bootstrap'; +import { useTranslation, Trans } from 'react-i18next'; + +import { PageTitle } from '@/components'; + +const Index = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'upgrade', + }); + const [step, setStep] = useState(1); + + const handleUpdate = () => { + setStep(2); + }; + return ( + + + + +

{t('title')}

+ + + {step === 1 && ( + <> +
{t('update_title')}
+ }} + /> + + + )} + + {step === 2 && ( + <> +
{t('done_title')}
+

{t('done_desscription')}

+ + + )} +
+
+ +
+
+ ); +}; + +export default Index; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 69498f5d0..9436298a8 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -215,5 +215,13 @@ const routes: RouteNode[] = [ path: 'install', page: 'pages/Install', }, + { + path: '/maintenance', + page: 'pages/Maintenance', + }, + { + path: '/upgrade', + page: 'pages/Upgrade', + }, ]; export default routes; From c8175744aa0f465f9bce9dd6a7ad86ee899641f4 Mon Sep 17 00:00:00 2001 From: edocevol Date: Sun, 30 Oct 2022 21:34:18 +0800 Subject: [PATCH 0074/3337] refactor: repo code should procees error --- go.mod | 2 +- internal/repo/activity_common/follow.go | 5 ++++- internal/repo/activity_common/vote.go | 5 +++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 7fe0bfd9e..b6fa86d54 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/jinzhu/copier v0.3.5 github.com/jinzhu/now v1.1.5 github.com/lib/pq v1.10.2 - github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mojocn/base64Captcha v1.3.5 github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 @@ -60,6 +59,7 @@ require ( github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/internal/repo/activity_common/follow.go b/internal/repo/activity_common/follow.go index a793fdec5..a61ddcdb5 100644 --- a/internal/repo/activity_common/follow.go +++ b/internal/repo/activity_common/follow.go @@ -95,6 +95,9 @@ func (ar *FollowRepo) GetFollowUserIDs(ctx context.Context, objectID string) (us func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string) (followIDs []string, err error) { followIDs = make([]string, 0) activityType, err := ar.activityRepo.GetActivityTypeByObjKey(ctx, objectKey, "follow") + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } session := ar.data.DB.Select("object_id") session.Table(entity.Activity{}.TableName()) session.Where("user_id = ? AND activity_type = ?", userID, activityType) @@ -108,7 +111,7 @@ func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string // IsFollowed check user if follow object or not func (ar *FollowRepo) IsFollowed(userId, objectId string) (bool, error) { - activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(nil, objectId, "follow") + activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(context.TODO(), objectId, "follow") if err != nil { return false, err } diff --git a/internal/repo/activity_common/vote.go b/internal/repo/activity_common/vote.go index c2525145a..86db8f39c 100644 --- a/internal/repo/activity_common/vote.go +++ b/internal/repo/activity_common/vote.go @@ -6,13 +6,11 @@ import ( "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/service/activity_common" - "github.com/answerdev/answer/internal/service/unique" ) // VoteRepo activity repository type VoteRepo struct { data *data.Data - uniqueIDRepo unique.UniqueIDRepo activityRepo activity_common.ActivityRepo } @@ -28,6 +26,9 @@ func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectId, userId string) for _, action := range []string{"vote_up", "vote_down"} { at := &entity.Activity{} activityType, _, _, err := vr.activityRepo.GetActivityTypeByObjID(ctx, objectId, action) + if err != nil { + return "" + } has, err := vr.data.DB.Where("object_id =? AND cancelled=0 AND activity_type=? AND user_id=?", objectId, activityType, userId).Get(at) if err != nil { return "" From e1d320a04785c44b1fe15980eb9806cf30fdc200 Mon Sep 17 00:00:00 2001 From: Brandon Hansen Date: Thu, 27 Oct 2022 14:31:02 -0700 Subject: [PATCH 0075/3337] Update chinese.go There is no reason to keep a count of Chinese characters only to do a count check and return a bool. Simply return true the first char we find. Otherwise return false. --- pkg/checker/chinese.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/checker/chinese.go b/pkg/checker/chinese.go index 3a6ebd228..01b863129 100644 --- a/pkg/checker/chinese.go +++ b/pkg/checker/chinese.go @@ -3,12 +3,10 @@ package checker import "unicode" func IsChinese(str string) bool { - var count int for _, v := range str { if unicode.Is(unicode.Han, v) { - count++ - break + return true } } - return count > 0 + return false } From 4f38e387144ddb39a0f0e3ef6e69f5a5c63b4c0c Mon Sep 17 00:00:00 2001 From: ppchart Date: Tue, 25 Oct 2022 11:34:28 +0800 Subject: [PATCH 0076/3337] Fix windows endOfLine error --- ui/.prettierrc.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/.prettierrc.json b/ui/.prettierrc.json index 83fdbcaad..bf0ed214b 100644 --- a/ui/.prettierrc.json +++ b/ui/.prettierrc.json @@ -3,5 +3,6 @@ "tabWidth": 2, "singleQuote": true, "jsxBracketSameLine": true, - "printWidth": 80 -} + "printWidth": 80, + "endOfLine": "auto" +} \ No newline at end of file From 59e2e1f304f93b850a0d63752c38c875dbff2749 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Tue, 1 Nov 2022 00:25:44 -0700 Subject: [PATCH 0077/3337] Running gofumpt against repo (#49) * starting on gofumpt * continuing on gofumpt * continuing on gofumpt * finish gofumpt review * added changes requested by LinkinStars' review Co-authored-by: LinkinStars --- cmd/answer/command.go | 10 +- cmd/answer/wire_gen.go | 2 +- internal/base/constant/constant.go | 2 +- internal/base/constant/report.go | 10 +- internal/base/handler/handler.go | 1 - internal/base/middleware/auth.go | 14 +- internal/base/reason/reason.go | 2 +- internal/base/validator/validator.go | 8 +- internal/cli/install.go | 3 +- internal/controller/answer_controller.go | 13 +- internal/controller/question_controller.go | 3 +- internal/controller/user_controller.go | 43 ++-- internal/entity/answer_entity.go | 16 +- internal/entity/user_entity.go | 6 +- internal/migrations/init.go | 210 +++++++++--------- internal/repo/activity/answer_repo.go | 9 +- internal/repo/activity/follow_repo.go | 34 +-- internal/repo/activity/vote_repo.go | 81 ++++--- internal/repo/activity_common/follow.go | 16 +- internal/repo/activity_common/vote.go | 2 +- internal/repo/activity_repo.go | 9 +- internal/repo/answer_repo.go | 29 +-- .../repo/collection/collection_group_repo.go | 9 +- internal/repo/collection/collection_repo.go | 19 +- internal/repo/comment/comment_repo.go | 6 +- internal/repo/common/common.go | 10 +- .../repo/notification/notification_repo.go | 2 +- internal/repo/question_repo.go | 25 ++- internal/repo/rank/user_rank_repo.go | 30 ++- internal/repo/report/report_repo.go | 3 +- internal/repo/revision/revision_repo.go | 6 +- internal/repo/search_repo.go | 68 +++--- internal/repo/siteinfo_repo.go | 2 +- internal/repo/tag/tag_rel_repo.go | 15 +- internal/repo/tag/tag_repo.go | 12 +- internal/repo/user/user_backyard_repo.go | 10 +- internal/schema/answer_schema.go | 30 +-- internal/schema/collection_group_schema.go | 4 +- internal/schema/forbidden_schema.go | 2 +- internal/schema/question_schema.go | 43 ++-- internal/schema/tag_schema.go | 2 +- internal/schema/user_schema.go | 34 +-- internal/service/action/captcha_service.go | 5 +- internal/service/answer_common/answer.go | 18 +- internal/service/answer_service.go | 79 ++++--- internal/service/collection_group_service.go | 2 +- internal/service/collection_service.go | 26 ++- internal/service/question_common/question.go | 88 ++++---- internal/service/question_service.go | 55 ++--- internal/service/report/report_service.go | 12 +- internal/service/siteinfo_service.go | 7 +- internal/service/tag_common/tag_common.go | 15 +- internal/service/user_common/user.go | 2 +- internal/service/user_service.go | 62 +++--- internal/service/vote_service.go | 12 +- pkg/checker/password.go | 4 +- pkg/uid/id.go | 2 +- 57 files changed, 632 insertions(+), 612 deletions(-) diff --git a/cmd/answer/command.go b/cmd/answer/command.go index fd9d8f3bd..d0efb0aba 100644 --- a/cmd/answer/command.go +++ b/cmd/answer/command.go @@ -48,7 +48,7 @@ To run answer, use: Use: "run", Short: "Run the application", Long: `Run the application`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { runApp() }, } @@ -58,7 +58,7 @@ To run answer, use: Use: "init", Short: "init answer application", Long: `init answer application`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { cli.InstallAllInitialEnvironment(dataDirPath) c, err := readConfig() if err != nil { @@ -79,7 +79,7 @@ To run answer, use: Use: "upgrade", Short: "upgrade Answer version", Long: `upgrade Answer version`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { c, err := readConfig() if err != nil { fmt.Println("read config failed: ", err.Error()) @@ -98,7 +98,7 @@ To run answer, use: Use: "dump", Short: "back up data", Long: `back up data`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { fmt.Println("Answer is backing up data") c, err := readConfig() if err != nil { @@ -119,7 +119,7 @@ To run answer, use: Use: "check", Short: "checking the required environment", Long: `Check if the current environment meets the startup requirements`, - Run: func(cmd *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { fmt.Println("Start checking the required environment...") if cli.CheckConfigFile(configFilePath) { fmt.Println("config file exists [✔]") diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index cc83240d0..0d87b7f0b 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -67,7 +67,7 @@ import ( // Injectors from wire.go: // initApplication init application. -func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, logConf log.Logger) (*pacman.Application, func(), error) { +func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, _ log.Logger) (*pacman.Application, func(), error) { staticRouter := router.NewStaticRouter(serviceConf) i18nTranslator, err := translator.NewTranslator(i18nConf) if err != nil { diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 9ccd519a7..7722adf87 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -3,7 +3,7 @@ package constant import "time" const ( - Default_PageSize = 20 //Default number of pages + DefaultPageSize = 20 // Default number of pages UserStatusChangedCacheKey = "answer:user:status:" UserStatusChangedCacheTime = 7 * 24 * time.Hour UserTokenCacheKey = "answer:user:token:" diff --git a/internal/base/constant/report.go b/internal/base/constant/report.go index 4bcca92f5..28a8548e1 100644 --- a/internal/base/constant/report.go +++ b/internal/base/constant/report.go @@ -13,7 +13,7 @@ const ( ReportNotAnswerDescription = "report.not_answer.description" ReportNotNeedName = "report.not_need.name" ReportNotNeedDescription = "report.not_need.description" - //question close + // question close QuestionCloseDuplicateName = "question.close.duplicate.name" QuestionCloseDuplicateDescription = "question.close.duplicate.description" QuestionCloseGuidelineName = "question.close.guideline.name" @@ -27,8 +27,8 @@ const ( const ( // TODO put this in database // TODO need reason controller to resolve - QuestionCloseJson = `[{"name":"question.close.duplicate.name","description":"question.close.duplicate.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"question.close.guideline.name","description":"question.close.guideline.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"question.close.multiple.name","description":"question.close.multiple.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"question.close.other.name","description":"question.close.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]` - QuestionReportJson = `[{"name":"report.spam.name","description":"report.spam.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"report.duplicate.name","description":"report.duplicate.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]` - AnswerReportJson = `[{"name":"report.spam.name","description":"report.spam.description","source":"answer","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"answer","type":2,"have_content":false,"content_type":""},{"name":"report.not_answer.name","description":"report.not_answer.description","source":"answer","type":3,"have_content":false,"content_type":""},{"name":"report.other.name","description":"report.other.description","source":"answer","type":4,"have_content":true,"content_type":"textarea"}]` - CommentReportJson = `[{"name":"report.spam.name","description":"report.spam.description","source":"comment","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"comment","type":2,"have_content":false,"content_type":""},{"name":"report.not_need.name","description":"report.not_need.description","source":"comment","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"comment","type":4,"have_content":true,"content_type":"textarea"}]` + QuestionCloseJSON = `[{"name":"question.close.duplicate.name","description":"question.close.duplicate.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"question.close.guideline.name","description":"question.close.guideline.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"question.close.multiple.name","description":"question.close.multiple.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"question.close.other.name","description":"question.close.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]` + QuestionReportJSON = `[{"name":"report.spam.name","description":"report.spam.description","source":"question","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"question","type":2,"have_content":false,"content_type":""},{"name":"report.duplicate.name","description":"report.duplicate.description","source":"question","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"question","type":4,"have_content":true,"content_type":"textarea"}]` + AnswerReportJSON = `[{"name":"report.spam.name","description":"report.spam.description","source":"answer","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"answer","type":2,"have_content":false,"content_type":""},{"name":"report.not_answer.name","description":"report.not_answer.description","source":"answer","type":3,"have_content":false,"content_type":""},{"name":"report.other.name","description":"report.other.description","source":"answer","type":4,"have_content":true,"content_type":"textarea"}]` + CommentReportJSON = `[{"name":"report.spam.name","description":"report.spam.description","source":"comment","type":1,"have_content":false,"content_type":""},{"name":"report.rude.name","description":"report.rude.description","source":"comment","type":2,"have_content":false,"content_type":""},{"name":"report.not_need.name","description":"report.not_need.description","source":"comment","type":3,"have_content":true,"content_type":"text"},{"name":"report.other.name","description":"report.other.description","source":"comment","type":4,"have_content":true,"content_type":"textarea"}]` ) diff --git a/internal/base/handler/handler.go b/internal/base/handler/handler.go index 2fde62de7..3b4a5b658 100644 --- a/internal/base/handler/handler.go +++ b/internal/base/handler/handler.go @@ -40,7 +40,6 @@ func HandleResponse(ctx *gin.Context, err error, data interface{}) { respBody.Data = data } ctx.JSON(myErr.Code, respBody) - return } // BindAndCheck bind request and check diff --git a/internal/base/middleware/auth.go b/internal/base/middleware/auth.go index cf9c87301..813effe35 100644 --- a/internal/base/middleware/auth.go +++ b/internal/base/middleware/auth.go @@ -14,9 +14,7 @@ import ( "github.com/segmentfault/pacman/errors" ) -var ( - ctxUuidKey = "ctxUuidKey" -) +var ctxUUIDKey = "ctxUuidKey" // AuthUserMiddleware auth user middleware type AuthUserMiddleware struct { @@ -44,7 +42,7 @@ func (am *AuthUserMiddleware) Auth() gin.HandlerFunc { return } if userInfo != nil { - ctx.Set(ctxUuidKey, userInfo) + ctx.Set(ctxUUIDKey, userInfo) } ctx.Next() } @@ -82,7 +80,7 @@ func (am *AuthUserMiddleware) MustAuth() gin.HandlerFunc { ctx.Abort() return } - ctx.Set(ctxUuidKey, userInfo) + ctx.Set(ctxUUIDKey, userInfo) ctx.Next() } } @@ -107,7 +105,7 @@ func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc { ctx.Abort() return } - ctx.Set(ctxUuidKey, userInfo) + ctx.Set(ctxUUIDKey, userInfo) } ctx.Next() } @@ -115,7 +113,7 @@ func (am *AuthUserMiddleware) CmsAuth() gin.HandlerFunc { // GetLoginUserIDFromContext get user id from context func GetLoginUserIDFromContext(ctx *gin.Context) (userID string) { - userInfo, exist := ctx.Get(ctxUuidKey) + userInfo, exist := ctx.Get(ctxUUIDKey) if !exist { return "" } @@ -128,7 +126,7 @@ func GetLoginUserIDFromContext(ctx *gin.Context) (userID string) { // GetUserInfoFromContext get user info from context func GetUserInfoFromContext(ctx *gin.Context) (u *entity.UserCacheInfo) { - userInfo, exist := ctx.Get(ctxUuidKey) + userInfo, exist := ctx.Get(ctxUUIDKey) if !exist { return nil } diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index fb64cd781..284bb0b32 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -27,7 +27,7 @@ const ( UsernameInvalid = "error.user.username_invalid" UsernameDuplicate = "error.user.username_duplicate" EmailDuplicate = "error.email.duplicate" - EmailVerifyUrlExpired = "error.email.verify_url_expired" + EmailVerifyURLExpired = "error.email.verify_url_expired" EmailNeedToBeVerified = "error.email.need_to_be_verified" UserSuspended = "error.user.suspended" ObjectNotFound = "error.object.not_found" diff --git a/internal/base/validator/validator.go b/internal/base/validator/validator.go index 9695af171..4a0f22b15 100644 --- a/internal/base/validator/validator.go +++ b/internal/base/validator/validator.go @@ -9,7 +9,7 @@ import ( "github.com/go-playground/locales" english "github.com/go-playground/locales/en" zhongwen "github.com/go-playground/locales/zh" - "github.com/go-playground/universal-translator" + ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10/translations/en" "github.com/go-playground/validator/v10/translations/zh" @@ -31,10 +31,8 @@ type ErrorField struct { Value string `json:"value"` } -var ( - // GlobalValidatorMapping is a mapping from validator to translator used - GlobalValidatorMapping = make(map[string]*MyValidator, 0) -) +// GlobalValidatorMapping is a mapping from validator to translator used +var GlobalValidatorMapping = make(map[string]*MyValidator, 0) func init() { zhTran, zhVal := getTran(zhongwen.New(), i18n.LanguageChinese.Abbr()), createDefaultValidator(i18n.LanguageChinese) diff --git a/internal/cli/install.go b/internal/cli/install.go index 34da2e5a1..8c8ee64f0 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -31,7 +31,6 @@ func InstallAllInitialEnvironment(dataDirPath string) { installUploadDir() installI18nBundle() fmt.Println("install all initial environment done") - return } func installConfigFile() { @@ -96,7 +95,7 @@ func installI18nBundle() { } func writerFile(filePath, content string) error { - file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666) + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666) if err != nil { return err } diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go index 1223b0377..43ea51a59 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -62,9 +62,9 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) { // @Success 200 {string} string "" func (ac *AnswerController) Get(ctx *gin.Context) { id := ctx.Query("id") - userId := middleware.GetLoginUserIDFromContext(ctx) + userID := middleware.GetLoginUserIDFromContext(ctx) - info, questionInfo, has, err := ac.answerService.Get(ctx, id, userId) + info, questionInfo, has, err := ac.answerService.Get(ctx, id, userID) if err != nil { handler.HandleResponse(ctx, err, gin.H{}) return @@ -101,18 +101,18 @@ func (ac *AnswerController) Add(ctx *gin.Context) { return } - answerId, err := ac.answerService.Insert(ctx, req) + answerID, err := ac.answerService.Insert(ctx, req) if err != nil { handler.HandleResponse(ctx, err, nil) return } - info, questionInfo, has, err := ac.answerService.Get(ctx, answerId, req.UserID) + info, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID) if err != nil { handler.HandleResponse(ctx, err, nil) return } if !has { - //todo !has + // todo !has handler.HandleResponse(ctx, nil, nil) return } @@ -120,7 +120,6 @@ func (ac *AnswerController) Add(ctx *gin.Context) { "info": info, "question": questionInfo, }) - } // Update godoc @@ -156,7 +155,7 @@ func (ac *AnswerController) Update(ctx *gin.Context) { return } if !has { - //todo !has + // todo !has handler.HandleResponse(ctx, nil, nil) return } diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index 620f1e41f..399f323d0 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -66,7 +66,7 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - req.UserId = middleware.GetLoginUserIDFromContext(ctx) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := qc.questionService.CloseQuestion(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -114,7 +114,6 @@ func (qc *QuestionController) SimilarQuestion(ctx *gin.Context) { "list": list, "count": count, }) - } // Index godoc diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 390b72ced..086dccdc8 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -35,7 +35,8 @@ func NewUserController( userService *service.UserService, actionService *action.CaptchaService, emailService *export.EmailService, - uploaderService *uploader.UploaderService) *UserController { + uploaderService *uploader.UploaderService, +) *UserController { return &UserController{ authService: authService, userService: userService, @@ -112,7 +113,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) { return } - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { resp := schema.UserVerifyEmailErrorResponse{ Key: "captcha_code", @@ -125,7 +126,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) { resp, err := uc.userService.EmailLogin(ctx, req) if err != nil { - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP()) + _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) resp := schema.UserVerifyEmailErrorResponse{ Key: "e_mail", Value: "error.object.email_or_password_incorrect", @@ -134,7 +135,7 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) return } - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Login, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) handler.HandleResponse(ctx, nil, resp) } @@ -152,7 +153,7 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { resp := schema.UserVerifyEmailErrorResponse{ Key: "captcha_code", @@ -162,7 +163,7 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) return } - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP()) + _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP()) code, err := uc.userService.RetrievePassWord(ctx, req) handler.HandleResponse(ctx, err, code) } @@ -184,13 +185,13 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) { req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { - handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyUrlExpired), - &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUrlExpired}) + handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } resp, err := uc.userService.UseRePassWord(ctx, req) - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Find_Pass, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP()) handler.HandleResponse(ctx, err, resp) } @@ -245,8 +246,8 @@ func (uc *UserController) UserVerifyEmail(ctx *gin.Context) { req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { - handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyUrlExpired), - &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUrlExpired}) + handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } @@ -256,7 +257,7 @@ func (uc *UserController) UserVerifyEmail(ctx *gin.Context) { return } - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) handler.HandleResponse(ctx, err, resp) } @@ -282,7 +283,7 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) { return } - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP(), + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { resp := schema.UserVerifyEmailErrorResponse{ @@ -294,7 +295,7 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) { return } - uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) + uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) err := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID) handler.HandleResponse(ctx, err, nil) } @@ -314,7 +315,7 @@ func (uc *UserController) UserModifyPassWord(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - req.UserId = middleware.GetLoginUserIDFromContext(ctx) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req) if err != nil { @@ -360,7 +361,7 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - req.UserId = middleware.GetLoginUserIDFromContext(ctx) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) err := uc.userService.UpdateInfo(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -436,7 +437,7 @@ func (uc *UserController) ActionRecord(ctx *gin.Context) { if handler.BindAndCheck(ctx, req) { return } - req.Ip = ctx.ClientIP() + req.IP = ctx.ClientIP() resp, err := uc.actionService.ActionRecord(ctx, req) handler.HandleResponse(ctx, err, resp) @@ -458,8 +459,8 @@ func (uc *UserController) UserNoticeSet(ctx *gin.Context) { return } - req.UserId = middleware.GetLoginUserIDFromContext(ctx) - resp, err := uc.userService.UserNoticeSet(ctx, req.UserId, req.NoticeSwitch) + req.UserID = middleware.GetLoginUserIDFromContext(ctx) + resp, err := uc.userService.UserNoticeSet(ctx, req.UserID, req.NoticeSwitch) handler.HandleResponse(ctx, err, resp) } @@ -500,8 +501,8 @@ func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) { } req.Content = uc.emailService.VerifyUrlExpired(ctx, req.Code) if len(req.Content) == 0 { - handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyUrlExpired), - &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeUrlExpired}) + handler.HandleResponse(ctx, errors.Forbidden(reason.EmailVerifyURLExpired), + &schema.ForbiddenResp{Type: schema.ForbiddenReasonTypeURLExpired}) return } diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go index 3666f1065..86172ff7f 100644 --- a/internal/entity/answer_entity.go +++ b/internal/entity/answer_entity.go @@ -3,9 +3,9 @@ package entity import "time" const ( - Answer_Search_OrderBy_Default = "default" - Answer_Search_OrderBy_Time = "updated" - Answer_Search_OrderBy_Vote = "vote" + AnswerSearchOrderByDefault = "default" + AnswerSearchOrderByTime = "updated" + AnswerSearchOrderByVote = "vote" AnswerStatusAvailable = 1 AnswerStatusDeleted = 10 @@ -35,15 +35,15 @@ type Answer struct { type AnswerSearch struct { Answer Order string `json:"order_by" ` // default or updated - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size } type CmsAnswerSearch struct { - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size Status int `json:"-" form:"-"` - StatusStr string `json:"status" form:"status"` //Status 1 Available 2 closed 10 Deleted + StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 Deleted } type AdminSetAnswerStatusRequest struct { diff --git a/internal/entity/user_entity.go b/internal/entity/user_entity.go index f87e651ed..7ab239a7f 100644 --- a/internal/entity/user_entity.go +++ b/internal/entity/user_entity.go @@ -40,7 +40,7 @@ type User struct { Avatar string `xorm:"not null default '' VARCHAR(255) avatar"` Mobile string `xorm:"not null VARCHAR(20) mobile"` Bio string `xorm:"not null TEXT bio"` - BioHtml string `xorm:"not null TEXT bio_html"` + BioHTML string `xorm:"not null TEXT bio_html"` Website string `xorm:"not null default '' VARCHAR(255) website"` Location string `xorm:"not null default '' VARCHAR(100) location"` IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"` @@ -54,6 +54,6 @@ func (User) TableName() string { type UserSearch struct { User - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size } diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 0601ffdd7..b44a225fd 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -8,27 +8,25 @@ import ( "xorm.io/xorm" ) -var ( - tables = []interface{}{ - &entity.Activity{}, - &entity.Answer{}, - &entity.Collection{}, - &entity.CollectionGroup{}, - &entity.Comment{}, - &entity.Config{}, - &entity.Meta{}, - &entity.Notification{}, - &entity.Question{}, - &entity.Report{}, - &entity.Revision{}, - &entity.SiteInfo{}, - &entity.Tag{}, - &entity.TagRel{}, - &entity.Uniqid{}, - &entity.User{}, - &entity.Version{}, - } -) +var tables = []interface{}{ + &entity.Activity{}, + &entity.Answer{}, + &entity.Collection{}, + &entity.CollectionGroup{}, + &entity.Comment{}, + &entity.Config{}, + &entity.Meta{}, + &entity.Notification{}, + &entity.Question{}, + &entity.Report{}, + &entity.Revision{}, + &entity.SiteInfo{}, + &entity.Tag{}, + &entity.TagRel{}, + &entity.Uniqid{}, + &entity.User{}, + &entity.Version{}, +} // InitDB init db func InitDB(dataConf *data.Database) (err error) { @@ -94,91 +92,91 @@ func initSiteInfo(engine *xorm.Engine) error { func initConfigTable(engine *xorm.Engine) error { defaultConfigTable := []*entity.Config{ - {1, "answer.accepted", `15`}, - {2, "answer.voted_up", `10`}, - {3, "question.voted_up", `10`}, - {4, "tag.edit_accepted", `2`}, - {5, "answer.accept", `2`}, - {6, "answer.voted_down_cancel", `2`}, - {7, "question.voted_down_cancel", `2`}, - {8, "answer.vote_down_cancel", `1`}, - {9, "question.vote_down_cancel", `1`}, - {10, "user.activated", `1`}, - {11, "edit.accepted", `2`}, - {12, "answer.vote_down", `-1`}, - {13, "question.voted_down", `-2`}, - {14, "answer.voted_down", `-2`}, - {15, "answer.accept_cancel", `-2`}, - {16, "answer.deleted", `-5`}, - {17, "question.voted_up_cancel", `-10`}, - {18, "answer.voted_up_cancel", `-10`}, - {19, "answer.accepted_cancel", `-15`}, - {20, "object.reported", `-100`}, - {21, "edit.rejected", `-2`}, - {22, "daily_rank_limit", `200`}, - {23, "daily_rank_limit.exclude", `["answer.accepted"]`}, - {24, "user.follow", `0`}, - {25, "comment.vote_up", `0`}, - {26, "comment.vote_up_cancel", `0`}, - {27, "question.vote_down", `0`}, - {28, "question.vote_up", `0`}, - {29, "question.vote_up_cancel", `0`}, - {30, "answer.vote_up", `0`}, - {31, "answer.vote_up_cancel", `0`}, - {32, "question.follow", `0`}, - {33, "email.config", `{"from_name":"answer","from_email":"answer@answer.com","smtp_host":"smtp.answer.org","smtp_port":465,"smtp_password":"answer","smtp_username":"answer@answer.com","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`}, - {35, "tag.follow", `0`}, - {36, "rank.question.add", `0`}, - {37, "rank.question.edit", `0`}, - {38, "rank.question.delete", `0`}, - {39, "rank.question.vote_up", `0`}, - {40, "rank.question.vote_down", `0`}, - {41, "rank.answer.add", `0`}, - {42, "rank.answer.edit", `0`}, - {43, "rank.answer.delete", `0`}, - {44, "rank.answer.accept", `0`}, - {45, "rank.answer.vote_up", `0`}, - {46, "rank.answer.vote_down", `0`}, - {47, "rank.comment.add", `0`}, - {48, "rank.comment.edit", `0`}, - {49, "rank.comment.delete", `0`}, - {50, "rank.report.add", `0`}, - {51, "rank.tag.add", `0`}, - {52, "rank.tag.edit", `0`}, - {53, "rank.tag.delete", `0`}, - {54, "rank.tag.synonym", `0`}, - {55, "rank.link.url_limit", `0`}, - {56, "rank.vote.detail", `0`}, - {57, "reason.spam", `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`}, - {58, "reason.rude_or_abusive", `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`}, - {59, "reason.something", `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`}, - {60, "reason.a_duplicate", `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`}, - {61, "reason.not_a_answer", `{"name":"not a answer","description":"This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.","content_type":""}`}, - {62, "reason.no_longer_needed", `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`}, - {63, "reason.community_specific", `{"name":"a community-specific reason","description":"This question doesn’t meet a community guideline."}`}, - {64, "reason.not_clarity", `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only.","content_type":"text"}`}, - {65, "reason.normal", `{"name":"normal","description":"A normal post available to everyone."}`}, - {66, "reason.normal.user", `{"name":"normal","description":"A normal user can ask and answer questions."}`}, - {67, "reason.closed", `{"name":"closed","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, - {68, "reason.deleted", `{"name":"deleted","description":"All reputation gained and lost will be restored."}`}, - {69, "reason.deleted.user", `{"name":"deleted","description":"Delete profile, authentication associations."}`}, - {70, "reason.suspended", `{"name":"suspended","description":"A suspended user can’t log in."}`}, - {71, "reason.inactive", `{"name":"inactive","description":"An inactive user must re-validate their email."}`}, - {72, "reason.looks_ok", `{"name":"looks ok","description":"This post is good as-is and not low quality."}`}, - {73, "reason.needs_edit", `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`}, - {74, "reason.needs_close", `{"name":"needs close","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, - {75, "reason.needs_delete", `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`}, - {76, "question.flag.reasons", `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`}, - {77, "answer.flag.reasons", `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`}, - {78, "comment.flag.reasons", `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`}, - {79, "question.close.reasons", `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`}, - {80, "question.status.reasons", `["reason.normal","reason.closed","reason.deleted"]`}, - {81, "answer.status.reasons", `["reason.normal","reason.deleted"]`}, - {82, "comment.status.reasons", `["reason.normal","reason.deleted"]`}, - {83, "user.status.reasons", `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`}, - {84, "question.review.reasons", `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`}, - {85, "answer.review.reasons", `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, - {86, "comment.review.reasons", `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, + {ID: 1, Key: "answer.accepted", Value: `15`}, + {ID: 2, Key: "answer.voted_up", Value: `10`}, + {ID: 3, Key: "question.voted_up", Value: `10`}, + {ID: 4, Key: "tag.edit_accepted", Value: `2`}, + {ID: 5, Key: "answer.accept", Value: `2`}, + {ID: 6, Key: "answer.voted_down_cancel", Value: `2`}, + {ID: 7, Key: "question.voted_down_cancel", Value: `2`}, + {ID: 8, Key: "answer.vote_down_cancel", Value: `1`}, + {ID: 9, Key: "question.vote_down_cancel", Value: `1`}, + {ID: 10, Key: "user.activated", Value: `1`}, + {ID: 11, Key: "edit.accepted", Value: `2`}, + {ID: 12, Key: "answer.vote_down", Value: `-1`}, + {ID: 13, Key: "question.voted_down", Value: `-2`}, + {ID: 14, Key: "answer.voted_down", Value: `-2`}, + {ID: 15, Key: "answer.accept_cancel", Value: `-2`}, + {ID: 16, Key: "answer.deleted", Value: `-5`}, + {ID: 17, Key: "question.voted_up_cancel", Value: `-10`}, + {ID: 18, Key: "answer.voted_up_cancel", Value: `-10`}, + {ID: 19, Key: "answer.accepted_cancel", Value: `-15`}, + {ID: 20, Key: "object.reported", Value: `-100`}, + {ID: 21, Key: "edit.rejected", Value: `-2`}, + {ID: 22, Key: "daily_rank_limit", Value: `200`}, + {ID: 23, Key: "daily_rank_limit.exclude", Value: `["answer.accepted"]`}, + {ID: 24, Key: "user.follow", Value: `0`}, + {ID: 25, Key: "comment.vote_up", Value: `0`}, + {ID: 26, Key: "comment.vote_up_cancel", Value: `0`}, + {ID: 27, Key: "question.vote_down", Value: `0`}, + {ID: 28, Key: "question.vote_up", Value: `0`}, + {ID: 29, Key: "question.vote_up_cancel", Value: `0`}, + {ID: 30, Key: "answer.vote_up", Value: `0`}, + {ID: 31, Key: "answer.vote_up_cancel", Value: `0`}, + {ID: 32, Key: "question.follow", Value: `0`}, + {ID: 33, Key: "email.config", Value: `{"from_name":"answer","from_email":"answer@answer.com","smtp_host":"smtp.answer.org","smtp_port":465,"smtp_password":"answer","smtp_username":"answer@answer.com","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`}, + {ID: 35, Key: "tag.follow", Value: `0`}, + {ID: 36, Key: "rank.question.add", Value: `0`}, + {ID: 37, Key: "rank.question.edit", Value: `0`}, + {ID: 38, Key: "rank.question.delete", Value: `0`}, + {ID: 39, Key: "rank.question.vote_up", Value: `0`}, + {ID: 40, Key: "rank.question.vote_down", Value: `0`}, + {ID: 41, Key: "rank.answer.add", Value: `0`}, + {ID: 42, Key: "rank.answer.edit", Value: `0`}, + {ID: 43, Key: "rank.answer.delete", Value: `0`}, + {ID: 44, Key: "rank.answer.accept", Value: `0`}, + {ID: 45, Key: "rank.answer.vote_up", Value: `0`}, + {ID: 46, Key: "rank.answer.vote_down", Value: `0`}, + {ID: 47, Key: "rank.comment.add", Value: `0`}, + {ID: 48, Key: "rank.comment.edit", Value: `0`}, + {ID: 49, Key: "rank.comment.delete", Value: `0`}, + {ID: 50, Key: "rank.report.add", Value: `0`}, + {ID: 51, Key: "rank.tag.add", Value: `0`}, + {ID: 52, Key: "rank.tag.edit", Value: `0`}, + {ID: 53, Key: "rank.tag.delete", Value: `0`}, + {ID: 54, Key: "rank.tag.synonym", Value: `0`}, + {ID: 55, Key: "rank.link.url_limit", Value: `0`}, + {ID: 56, Key: "rank.vote.detail", Value: `0`}, + {ID: 57, Key: "reason.spam", Value: `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`}, + {ID: 58, Key: "reason.rude_or_abusive", Value: `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`}, + {ID: 59, Key: "reason.something", Value: `{"name":"something else","description":"This post requires staff attention for another reason not listed above.","content_type":"textarea"}`}, + {ID: 60, Key: "reason.a_duplicate", Value: `{"name":"a duplicate","description":"This question has been asked before and already has an answer.","content_type":"text"}`}, + {ID: 61, Key: "reason.not_a_answer", Value: `{"name":"not a answer","description":"This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.","content_type":""}`}, + {ID: 62, Key: "reason.no_longer_needed", Value: `{"name":"no longer needed","description":"This comment is outdated, conversational or not relevant to this post."}`}, + {ID: 63, Key: "reason.community_specific", Value: `{"name":"a community-specific reason","description":"This question doesn’t meet a community guideline."}`}, + {ID: 64, Key: "reason.not_clarity", Value: `{"name":"needs details or clarity","description":"This question currently includes multiple questions in one. It should focus on one problem only.","content_type":"text"}`}, + {ID: 65, Key: "reason.normal", Value: `{"name":"normal","description":"A normal post available to everyone."}`}, + {ID: 66, Key: "reason.normal.user", Value: `{"name":"normal","description":"A normal user can ask and answer questions."}`}, + {ID: 67, Key: "reason.closed", Value: `{"name":"closed","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, + {ID: 68, Key: "reason.deleted", Value: `{"name":"deleted","description":"All reputation gained and lost will be restored."}`}, + {ID: 69, Key: "reason.deleted.user", Value: `{"name":"deleted","description":"Delete profile, authentication associations."}`}, + {ID: 70, Key: "reason.suspended", Value: `{"name":"suspended","description":"A suspended user can’t log in."}`}, + {ID: 71, Key: "reason.inactive", Value: `{"name":"inactive","description":"An inactive user must re-validate their email."}`}, + {ID: 72, Key: "reason.looks_ok", Value: `{"name":"looks ok","description":"This post is good as-is and not low quality."}`}, + {ID: 73, Key: "reason.needs_edit", Value: `{"name":"needs edit, and I did it","description":"Improve and correct problems with this post yourself."}`}, + {ID: 74, Key: "reason.needs_close", Value: `{"name":"needs close","description":"A closed question can’t answer, but still can edit, vote and comment."}`}, + {ID: 75, Key: "reason.needs_delete", Value: `{"name":"needs delete","description":"All reputation gained and lost will be restored."}`}, + {ID: 76, Key: "question.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.a_duplicate"]`}, + {ID: 77, Key: "answer.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.not_a_answer"]`}, + {ID: 78, Key: "comment.flag.reasons", Value: `["reason.spam","reason.rude_or_abusive","reason.something","reason.no_longer_needed"]`}, + {ID: 79, Key: "question.close.reasons", Value: `["reason.a_duplicate","reason.community_specific","reason.not_clarity","reason.something"]`}, + {ID: 80, Key: "question.status.reasons", Value: `["reason.normal","reason.closed","reason.deleted"]`}, + {ID: 81, Key: "answer.status.reasons", Value: `["reason.normal","reason.deleted"]`}, + {ID: 82, Key: "comment.status.reasons", Value: `["reason.normal","reason.deleted"]`}, + {ID: 83, Key: "user.status.reasons", Value: `["reason.normal.user","reason.suspended","reason.deleted.user","reason.inactive"]`}, + {ID: 84, Key: "question.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_close","reason.needs_delete"]`}, + {ID: 85, Key: "answer.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, + {ID: 86, Key: "comment.review.reasons", Value: `["reason.looks_ok","reason.needs_edit","reason.needs_delete"]`}, } _, err := engine.Insert(defaultConfigTable) return err diff --git a/internal/repo/activity/answer_repo.go b/internal/repo/activity/answer_repo.go index 74ab0db60..01602b1e6 100644 --- a/internal/repo/activity/answer_repo.go +++ b/internal/repo/activity/answer_repo.go @@ -44,7 +44,6 @@ func NewAnswerActivityRepo( userRankRepo rank.UserRankRepo, ) activity.AnswerActivityRepo { return &AnswerActivityRepo{ - data: data, activityRepo: activityRepo, userRankRepo: userRankRepo, @@ -125,7 +124,8 @@ func (ar *AnswerActivityRepo) DeleteQuestion(ctx context.Context, questionID str // AcceptAnswer accept other answer func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string, isSelf bool) (err error) { + answerObjID, questionUserID, answerUserID string, isSelf bool, +) (err error) { addActivityList := make([]*entity.Activity, 0) for _, action := range acceptActionList { // get accept answer need add rank amount @@ -173,7 +173,7 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, } if exists { - if _, e := session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). + if _, e = session.Where("id = ?", existsActivity.ID).Cols("`cancelled`"). Update(&entity.Activity{Cancelled: entity.ActivityAvailable}); e != nil { return nil, errors.InternalServer(reason.DatabaseError).WithError(e).WithStack() } @@ -222,7 +222,8 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context, // CancelAcceptAnswer accept other answer func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context, - answerObjID, questionUserID, answerUserID string) (err error) { + answerObjID, questionUserID, answerUserID string, +) (err error) { addActivityList := make([]*entity.Activity, 0) for _, action := range acceptActionList { // get accept answer need add rank amount diff --git a/internal/repo/activity/follow_repo.go b/internal/repo/activity/follow_repo.go index 916459a2c..14238718c 100644 --- a/internal/repo/activity/follow_repo.go +++ b/internal/repo/activity/follow_repo.go @@ -37,8 +37,8 @@ func NewFollowRepo( } } -func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error { - activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(ctx, objectId, "follow") +func (ar *FollowRepo) Follow(ctx context.Context, objectID, userID string) error { + activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(ctx, objectID, "follow") if err != nil { return err } @@ -51,8 +51,8 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error result = nil has, err = session.Where(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"user_id": userId}). - And(builder.Eq{"object_id": objectId}). + And(builder.Eq{"user_id": userID}). + And(builder.Eq{"object_id": objectID}). Get(&existsActivity) if err != nil { @@ -72,8 +72,8 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error } else { // update existing activity with new user id and u object id _, err = session.Insert(&entity.Activity{ - UserID: userId, - ObjectID: objectId, + UserID: userID, + ObjectID: objectID, ActivityType: activityType, Cancelled: 0, Rank: 0, @@ -87,7 +87,7 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error } // start update followers when everything is fine - err = ar.updateFollows(ctx, session, objectId, 1) + err = ar.updateFollows(ctx, session, objectID, 1) if err != nil { log.Error(err) } @@ -98,8 +98,8 @@ func (ar *FollowRepo) Follow(ctx context.Context, objectId, userId string) error return err } -func (ar *FollowRepo) FollowCancel(ctx context.Context, objectId, userId string) error { - activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(ctx, objectId, "follow") +func (ar *FollowRepo) FollowCancel(ctx context.Context, objectID, userID string) error { + activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(ctx, objectID, "follow") if err != nil { return err } @@ -112,8 +112,8 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectId, userId string) result = nil has, err = session.Where(builder.Eq{"activity_type": activityType}). - And(builder.Eq{"user_id": userId}). - And(builder.Eq{"object_id": objectId}). + And(builder.Eq{"user_id": userID}). + And(builder.Eq{"object_id": objectID}). Get(&existsActivity) if err != nil || !has { @@ -130,24 +130,24 @@ func (ar *FollowRepo) FollowCancel(ctx context.Context, objectId, userId string) }); err != nil { return } - err = ar.updateFollows(ctx, session, objectId, -1) + err = ar.updateFollows(ctx, session, objectID, -1) return }) return err } -func (ar *FollowRepo) updateFollows(ctx context.Context, session *xorm.Session, objectId string, follows int) error { - objectType, err := obj.GetObjectTypeStrByObjectID(objectId) +func (ar *FollowRepo) updateFollows(ctx context.Context, session *xorm.Session, objectID string, follows int) error { + objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return err } switch objectType { case "question": - _, err = session.Where("id = ?", objectId).Incr("follow_count", follows).Update(&entity.Question{}) + _, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.Question{}) case "user": - _, err = session.Where("id = ?", objectId).Incr("follow_count", follows).Update(&entity.User{}) + _, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.User{}) case "tag": - _, err = session.Where("id = ?", objectId).Incr("follow_count", follows).Update(&entity.Tag{}) + _, err = session.Where("id = ?", objectID).Incr("follow_count", follows).Update(&entity.Tag{}) default: err = errors.InternalServer(reason.DisallowFollow).WithMsg("this object can't be followed") } diff --git a/internal/repo/activity/vote_repo.go b/internal/repo/activity/vote_repo.go index bcc95f934..00ccd04b1 100644 --- a/internal/repo/activity/vote_repo.go +++ b/internal/repo/activity/vote_repo.go @@ -2,9 +2,10 @@ package activity import ( "context" - "github.com/answerdev/answer/pkg/converter" "strings" + "github.com/answerdev/answer/pkg/converter" + "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/service/config" "github.com/answerdev/answer/internal/service/notice_queue" @@ -42,7 +43,8 @@ func NewVoteRepo( configRepo config.ConfigRepo, activityRepo activity_common.ActivityRepo, userRankRepo rank.UserRankRepo, - voteCommon activity_common.VoteRepo) service.VoteRepo { + voteCommon activity_common.VoteRepo, +) service.VoteRepo { return &VoteRepo{ data: data, uniqueIDRepo: uniqueIDRepo, @@ -65,7 +67,7 @@ var LimitDownActions = map[string][]string{ "comment": {"vote_down"}, } -func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserId string, actions []string) (resp *schema.VoteResp, err error) { +func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) { resp = &schema.VoteResp{} _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { result = nil @@ -75,24 +77,24 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse insertActivity entity.Activity has bool triggerUserID, - activityUserId string + activityUserID string activityType, deltaRank, hasRank int ) - activityUserId, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserId, userID, action) + activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action) if err != nil { return } triggerUserID = userID - if userID == activityUserId { + if userID == activityUserID { triggerUserID = "0" } // check is voted up has, _ = session. Where(builder.Eq{"object_id": objectID}). - And(builder.Eq{"user_id": activityUserId}). + And(builder.Eq{"user_id": activityUserID}). And(builder.Eq{"trigger_user_id": triggerUserID}). And(builder.Eq{"activity_type": activityType}). Get(&existsActivity) @@ -104,7 +106,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse insertActivity = entity.Activity{ ObjectID: objectID, - UserID: activityUserId, + UserID: activityUserID, TriggerUserID: converter.StringToInt64(triggerUserID), ActivityType: activityType, Rank: deltaRank, @@ -114,7 +116,8 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse // trigger user rank and send notification if hasRank != 0 { - isReachStandard, err := vr.userRankRepo.TriggerUserRank(ctx, session, activityUserId, deltaRank, activityType) + var isReachStandard bool + isReachStandard, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, deltaRank, activityType) if err != nil { return nil, err } @@ -122,7 +125,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse insertActivity.Rank = 0 } - vr.sendNotification(ctx, activityUserId, objectUserId, objectID) + vr.sendNotification(ctx, activityUserID, objectUserID, objectID) } if has { @@ -163,7 +166,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse return } -func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserId string, actions []string) (resp *schema.VoteResp, err error) { +func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) { resp = &schema.VoteResp{} _, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) { for _, action := range actions { @@ -171,24 +174,24 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj existsActivity entity.Activity has bool triggerUserID, - activityUserId string + activityUserID string activityType, deltaRank, hasRank int ) result = nil - activityUserId, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserId, userID, action) + activityUserID, activityType, deltaRank, hasRank, err = vr.CheckRank(ctx, objectID, objectUserID, userID, action) if err != nil { return } triggerUserID = userID - if userID == activityUserId { + if userID == activityUserID { triggerUserID = "0" } has, err = session. - Where(builder.Eq{"user_id": activityUserId}). + Where(builder.Eq{"user_id": activityUserID}). And(builder.Eq{"trigger_user_id": triggerUserID}). And(builder.Eq{"activity_type": activityType}). And(builder.Eq{"object_id": objectID}). @@ -211,12 +214,12 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj // trigger user rank and send notification if hasRank != 0 { - _, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserId, -deltaRank, activityType) + _, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType) if err != nil { return } - vr.sendNotification(ctx, activityUserId, objectUserId, objectID) + vr.sendNotification(ctx, activityUserID, objectUserID, objectID) } // update votes @@ -242,7 +245,7 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj return } -func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserId string) (resp *schema.VoteResp, err error) { +func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { resp = &schema.VoteResp{} objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { @@ -256,11 +259,11 @@ func (vr *VoteRepo) VoteUp(ctx context.Context, objectID string, userID, objectU return } - _, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserId) - return vr.vote(ctx, objectID, userID, objectUserId, actions) + _, _ = vr.VoteDownCancel(ctx, objectID, userID, objectUserID) + return vr.vote(ctx, objectID, userID, objectUserID, actions) } -func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserId string) (resp *schema.VoteResp, err error) { +func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { resp = &schema.VoteResp{} objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { @@ -273,14 +276,12 @@ func (vr *VoteRepo) VoteDown(ctx context.Context, objectID string, userID, objec return } - _, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserId) - return vr.vote(ctx, objectID, userID, objectUserId, actions) + _, _ = vr.VoteUpCancel(ctx, objectID, userID, objectUserID) + return vr.vote(ctx, objectID, userID, objectUserID, actions) } -func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserId string) (resp *schema.VoteResp, err error) { - var ( - objectType string - ) +func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { + var objectType string resp = &schema.VoteResp{} objectType, err = obj.GetObjectTypeStrByObjectID(objectID) @@ -294,13 +295,11 @@ func (vr *VoteRepo) VoteUpCancel(ctx context.Context, objectID string, userID, o return } - return vr.voteCancel(ctx, objectID, userID, objectUserId, actions) + return vr.voteCancel(ctx, objectID, userID, objectUserID, actions) } -func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserId string) (resp *schema.VoteResp, err error) { - var ( - objectType string - ) +func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, objectUserID string) (resp *schema.VoteResp, err error) { + var objectType string resp = &schema.VoteResp{} objectType, err = obj.GetObjectTypeStrByObjectID(objectID) @@ -314,22 +313,22 @@ func (vr *VoteRepo) VoteDownCancel(ctx context.Context, objectID string, userID, return } - return vr.voteCancel(ctx, objectID, userID, objectUserId, actions) + return vr.voteCancel(ctx, objectID, userID, objectUserID, actions) } -func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserId, userID string, action string) (activityUserId string, activityType, rank, hasRank int, err error) { +func (vr *VoteRepo) CheckRank(ctx context.Context, objectID, objectUserID, userID string, action string) (activityUserID string, activityType, rank, hasRank int, err error) { activityType, rank, hasRank, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) if err != nil { return } - activityUserId = userID + activityUserID = userID if strings.Contains(action, "voted") { - activityUserId = objectUserId + activityUserID = objectUserID } - return activityUserId, activityType, rank, hasRank, nil + return activityUserID, activityType, rank, hasRank, nil } func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string) (resp *schema.VoteResp, err error) { @@ -341,7 +340,7 @@ func (vr *VoteRepo) GetVoteResultByObjectId(ctx context.Context, objectID string activityType int ) - activityType, _, _, err = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) + activityType, _, _, _ = vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) votes, err = vr.data.DB.Where(builder.Eq{"object_id": objectID}). And(builder.Eq{"activity_type": activityType}). @@ -417,15 +416,15 @@ func (vr *VoteRepo) updateVotes(ctx context.Context, session *xorm.Session, obje } // sendNotification send rank triggered notification -func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserId, objectUserId, objectID string) { +func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, objectUserID, objectID string) { objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return } msg := &schema.NotificationMsg{ - ReceiverUserID: activityUserId, - TriggerUserID: objectUserId, + ReceiverUserID: activityUserID, + TriggerUserID: objectUserID, Type: schema.NotificationTypeAchievement, ObjectID: objectID, ObjectType: objectType, diff --git a/internal/repo/activity_common/follow.go b/internal/repo/activity_common/follow.go index a61ddcdb5..aee42454a 100644 --- a/internal/repo/activity_common/follow.go +++ b/internal/repo/activity_common/follow.go @@ -33,27 +33,27 @@ func NewFollowRepo( } // GetFollowAmount get object id's follows -func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectId string) (follows int, err error) { - objectType, err := obj.GetObjectTypeStrByObjectID(objectId) +func (ar *FollowRepo) GetFollowAmount(ctx context.Context, objectID string) (follows int, err error) { + objectType, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return 0, err } switch objectType { case "question": model := &entity.Question{} - _, err = ar.data.DB.Where("id = ?", objectId).Cols("`follow_count`").Get(model) + _, err = ar.data.DB.Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = int(model.FollowCount) } case "user": model := &entity.User{} - _, err = ar.data.DB.Where("id = ?", objectId).Cols("`follow_count`").Get(model) + _, err = ar.data.DB.Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = int(model.FollowCount) } case "tag": model := &entity.Tag{} - _, err = ar.data.DB.Where("id = ?", objectId).Cols("`follow_count`").Get(model) + _, err = ar.data.DB.Where("id = ?", objectID).Cols("`follow_count`").Get(model) if err == nil { follows = int(model.FollowCount) } @@ -110,14 +110,14 @@ func (ar *FollowRepo) GetFollowIDs(ctx context.Context, userID, objectKey string } // IsFollowed check user if follow object or not -func (ar *FollowRepo) IsFollowed(userId, objectId string) (bool, error) { - activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(context.TODO(), objectId, "follow") +func (ar *FollowRepo) IsFollowed(userID, objectID string) (bool, error) { + activityType, _, _, err := ar.activityRepo.GetActivityTypeByObjID(context.TODO(), objectID, "follow") if err != nil { return false, err } at := &entity.Activity{} - has, err := ar.data.DB.Where("user_id = ? AND object_id = ? AND activity_type = ?", userId, objectId, activityType).Get(at) + has, err := ar.data.DB.Where("user_id = ? AND object_id = ? AND activity_type = ?", userID, objectID, activityType).Get(at) if err != nil { return false, err } diff --git a/internal/repo/activity_common/vote.go b/internal/repo/activity_common/vote.go index 86db8f39c..2c40305a8 100644 --- a/internal/repo/activity_common/vote.go +++ b/internal/repo/activity_common/vote.go @@ -22,7 +22,7 @@ func NewVoteRepo(data *data.Data, activityRepo activity_common.ActivityRepo) act } } -func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectId, userId string) (status string) { +func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string) (status string) { for _, action := range []string{"vote_up", "vote_down"} { at := &entity.Activity{} activityType, _, _, err := vr.activityRepo.GetActivityTypeByObjID(ctx, objectId, action) diff --git a/internal/repo/activity_repo.go b/internal/repo/activity_repo.go index df14ae019..ed10c5378 100644 --- a/internal/repo/activity_repo.go +++ b/internal/repo/activity_repo.go @@ -37,14 +37,14 @@ func NewActivityRepo( } } -func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectId string, action string) (activityType, rank, hasRank int, err error) { - objectKey, err := obj.GetObjectTypeStrByObjectID(objectId) +func (ar *ActivityRepo) GetActivityTypeByObjID(ctx context.Context, objectID string, action string) (activityType, rank, hasRank int, err error) { + objectKey, err := obj.GetObjectTypeStrByObjectID(objectID) if err != nil { return } confKey := fmt.Sprintf("%s.%s", objectKey, action) - activityType, err = ar.configRepo.GetConfigType(confKey) + activityType, _ = ar.configRepo.GetConfigType(confKey) rank, err = ar.configRepo.GetInt(confKey) hasRank = 0 @@ -64,7 +64,8 @@ func (ar *ActivityRepo) GetActivityTypeByObjKey(ctx context.Context, objectKey, } func (ar *ActivityRepo) GetActivity(ctx context.Context, session *xorm.Session, - objectID, userID string, activityType int) (existsActivity *entity.Activity, exist bool, err error) { + objectID, userID string, activityType int, +) (existsActivity *entity.Activity, exist bool, err error) { existsActivity = &entity.Activity{} exist, err = session. Where(builder.Eq{"object_id": objectID}). diff --git a/internal/repo/answer_repo.go b/internal/repo/answer_repo.go index dd4753e87..72b68d67e 100644 --- a/internal/repo/answer_repo.go +++ b/internal/repo/answer_repo.go @@ -89,7 +89,8 @@ func (ar *answerRepo) UpdateAnswerStatus(ctx context.Context, answer *entity.Ans // GetAnswer get answer one func (ar *answerRepo) GetAnswer(ctx context.Context, id string) ( - answer *entity.Answer, exist bool, err error) { + answer *entity.Answer, exist bool, err error, +) { answer = &entity.Answer{} exist, err = ar.data.DB.ID(id).Get(answer) if err != nil { @@ -120,20 +121,20 @@ func (ar *answerRepo) GetAnswerPage(ctx context.Context, page, pageSize int, ans // UpdateAdopted // If no answer is selected, the answer id can be 0 -func (ar *answerRepo) UpdateAdopted(ctx context.Context, id string, questionId string) error { - if questionId == "" { +func (ar *answerRepo) UpdateAdopted(ctx context.Context, id string, questionID string) error { + if questionID == "" { return nil } var data entity.Answer data.ID = id - data.Adopted = schema.Answer_Adopted_Failed - _, err := ar.data.DB.Where("question_id =?", questionId).Cols("adopted").Update(&data) + data.Adopted = schema.AnswerAdoptedFailed + _, err := ar.data.DB.Where("question_id =?", questionID).Cols("adopted").Update(&data) if err != nil { return err } if id != "0" { - data.Adopted = schema.Answer_Adopted_Enable + data.Adopted = schema.AnswerAdoptedEnable _, err = ar.data.DB.Where("id = ?", id).Cols("adopted").Update(&data) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -152,9 +153,9 @@ func (ar *answerRepo) GetByID(ctx context.Context, id string) (*entity.Answer, b return &resp, has, nil } -func (ar *answerRepo) GetByUserIdQuestionId(ctx context.Context, userId string, questionId string) (*entity.Answer, bool, error) { +func (ar *answerRepo) GetByUserIDQuestionID(ctx context.Context, userID string, questionID string) (*entity.Answer, bool, error) { var resp entity.Answer - has, err := ar.data.DB.Where("question_id =? and user_id = ?", questionId, userId).Get(&resp) + has, err := ar.data.DB.Where("question_id =? and user_id = ?", questionID, userID).Get(&resp) if err != nil { return &resp, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -172,7 +173,7 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc search.Page = 0 } if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize + search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize session := ar.data.DB.Where("") @@ -183,14 +184,14 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc if len(search.UserID) > 0 { session = session.And("user_id = ?", search.UserID) } - if search.Order == entity.Answer_Search_OrderBy_Time { + switch search.Order { + case entity.AnswerSearchOrderByTime: session = session.OrderBy("created_at desc") - } else if search.Order == entity.Answer_Search_OrderBy_Vote { + case entity.AnswerSearchOrderByVote: session = session.OrderBy("vote_count desc") - } else { + default: session = session.OrderBy("adopted desc,vote_count desc") } - session = session.And("status = ?", entity.AnswerStatusAvailable) session = session.Limit(search.PageSize, offset) @@ -214,7 +215,7 @@ func (ar *answerRepo) CmsSearchList(ctx context.Context, search *entity.CmsAnswe search.Page = 0 } if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize + search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize session := ar.data.DB.Where("") diff --git a/internal/repo/collection/collection_group_repo.go b/internal/repo/collection/collection_group_repo.go index cac96cfcf..8f915bb67 100644 --- a/internal/repo/collection/collection_group_repo.go +++ b/internal/repo/collection/collection_group_repo.go @@ -37,7 +37,7 @@ func (cr *collectionGroupRepo) AddCollectionGroup(ctx context.Context, collectio func (cr *collectionGroupRepo) AddCollectionDefaultGroup(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, err error) { defaultGroup := &entity.CollectionGroup{ Name: "default", - DefaultGroup: schema.CG_DEFAULT, + DefaultGroup: schema.CGDefault, UserID: userID, } _, err = cr.data.DB.Insert(defaultGroup) @@ -60,7 +60,8 @@ func (cr *collectionGroupRepo) UpdateCollectionGroup(ctx context.Context, collec // GetCollectionGroup get collection group one func (cr *collectionGroupRepo) GetCollectionGroup(ctx context.Context, id string) ( - collectionGroup *entity.CollectionGroup, exist bool, err error) { + collectionGroup *entity.CollectionGroup, exist bool, err error, +) { collectionGroup = &entity.CollectionGroup{} exist, err = cr.data.DB.ID(id).Get(collectionGroup) if err != nil { @@ -84,9 +85,9 @@ func (cr *collectionGroupRepo) GetCollectionGroupPage(ctx context.Context, page, return } -func (cr *collectionGroupRepo) GetDefaultID(ctx context.Context, userId string) (collectionGroup *entity.CollectionGroup, has bool, err error) { +func (cr *collectionGroupRepo) GetDefaultID(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, has bool, err error) { collectionGroup = &entity.CollectionGroup{} - has, err = cr.data.DB.Where("user_id =? and default_group = ?", userId, schema.CG_DEFAULT).Get(collectionGroup) + has, err = cr.data.DB.Where("user_id =? and default_group = ?", userID, schema.CGDefault).Get(collectionGroup) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return diff --git a/internal/repo/collection/collection_repo.go b/internal/repo/collection/collection_repo.go index 2fb4d92cf..f73c74b76 100644 --- a/internal/repo/collection/collection_repo.go +++ b/internal/repo/collection/collection_repo.go @@ -74,9 +74,9 @@ func (cr *collectionRepo) GetCollectionList(ctx context.Context, collection *ent } // GetOneByObjectIDAndUser get one by object TagID and user -func (cr *collectionRepo) GetOneByObjectIDAndUser(ctx context.Context, userId string, objectId string) (collection *entity.Collection, exist bool, err error) { +func (cr *collectionRepo) GetOneByObjectIDAndUser(ctx context.Context, userID string, objectID string) (collection *entity.Collection, exist bool, err error) { collection = &entity.Collection{} - exist, err = cr.data.DB.Where("user_id = ? and object_id = ?", userId, objectId).Get(collection) + exist, err = cr.data.DB.Where("user_id = ? and object_id = ?", userID, objectID).Get(collection) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -84,9 +84,9 @@ func (cr *collectionRepo) GetOneByObjectIDAndUser(ctx context.Context, userId st } // SearchByObjectIDsAndUser search by object IDs and user -func (cr *collectionRepo) SearchByObjectIDsAndUser(ctx context.Context, userId string, objectIds []string) ([]*entity.Collection, error) { +func (cr *collectionRepo) SearchByObjectIDsAndUser(ctx context.Context, userID string, objectIDs []string) ([]*entity.Collection, error) { collectionList := make([]*entity.Collection, 0) - err := cr.data.DB.Where("user_id = ?", userId).In("object_id", objectIds).Find(&collectionList) + err := cr.data.DB.Where("user_id = ?", userID).In("object_id", objectIDs).Find(&collectionList) if err != nil { return collectionList, err } @@ -94,9 +94,9 @@ func (cr *collectionRepo) SearchByObjectIDsAndUser(ctx context.Context, userId s } // CountByObjectID count by object TagID -func (cr *collectionRepo) CountByObjectID(ctx context.Context, objectId string) (total int64, err error) { +func (cr *collectionRepo) CountByObjectID(ctx context.Context, objectID string) (total int64, err error) { collection := &entity.Collection{} - total, err = cr.data.DB.Where("object_id = ?", objectId).Count(collection) + total, err = cr.data.DB.Where("object_id = ?", objectID).Count(collection) if err != nil { return 0, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -105,7 +105,6 @@ func (cr *collectionRepo) CountByObjectID(ctx context.Context, objectId string) // GetCollectionPage get collection page func (cr *collectionRepo) GetCollectionPage(ctx context.Context, page, pageSize int, collection *entity.Collection) (collectionList []*entity.Collection, total int64, err error) { - collectionList = make([]*entity.Collection, 0) session := cr.data.DB.NewSession() @@ -124,9 +123,9 @@ func (cr *collectionRepo) GetCollectionPage(ctx context.Context, page, pageSize } // SearchObjectCollected check object is collected or not -func (cr *collectionRepo) SearchObjectCollected(ctx context.Context, userId string, objectIds []string) (map[string]bool, error) { +func (cr *collectionRepo) SearchObjectCollected(ctx context.Context, userID string, objectIds []string) (map[string]bool, error) { collectedMap := make(map[string]bool) - list, err := cr.SearchByObjectIDsAndUser(ctx, userId, objectIds) + list, err := cr.SearchByObjectIDsAndUser(ctx, userID, objectIds) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() return collectedMap, err @@ -148,7 +147,7 @@ func (cr *collectionRepo) SearchList(ctx context.Context, search *entity.Collect search.Page = 0 } if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize + search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize session := cr.data.DB.Where("") diff --git a/internal/repo/comment/comment_repo.go b/internal/repo/comment/comment_repo.go index f2da8db59..a1a7ab901 100644 --- a/internal/repo/comment/comment_repo.go +++ b/internal/repo/comment/comment_repo.go @@ -69,7 +69,8 @@ func (cr *commentRepo) UpdateComment(ctx context.Context, comment *entity.Commen // GetComment get comment one func (cr *commentRepo) GetComment(ctx context.Context, commentID string) ( - comment *entity.Comment, exist bool, err error) { + comment *entity.Comment, exist bool, err error, +) { comment = &entity.Comment{} exist, err = cr.data.DB.ID(commentID).Get(comment) if err != nil { @@ -80,7 +81,8 @@ func (cr *commentRepo) GetComment(ctx context.Context, commentID string) ( // GetCommentPage get comment page func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) ( - commentList []*entity.Comment, total int64, err error) { + commentList []*entity.Comment, total int64, err error, +) { commentList = make([]*entity.Comment, 0) session := cr.data.DB.NewSession() diff --git a/internal/repo/common/common.go b/internal/repo/common/common.go index 89b154538..59d1f304d 100644 --- a/internal/repo/common/common.go +++ b/internal/repo/common/common.go @@ -37,15 +37,13 @@ func (cr *CommonRepo) GetRootObjectID(objectID string) (rootObjectID string, err exist, err = cr.data.DB.ID(objectID).Get(&answer) if !exist { err = errors.BadRequest(reason.ObjectNotFound) - } else { - objectID = answer.QuestionID } case "comment": - exist, err = cr.data.DB.ID(objectID).Get(&comment) + exist, _ = cr.data.DB.ID(objectID).Get(&comment) if !exist { err = errors.BadRequest(reason.ObjectNotFound) } else { - objectID, err = cr.GetRootObjectID(comment.ObjectID) + _, err = cr.GetRootObjectID(comment.ObjectID) } default: rootObjectID = objectID @@ -72,7 +70,7 @@ func (cr *CommonRepo) GetObjectIDMap(objectID string) (objectIDMap map[string]st } switch objectType { case "answer": - exist, err = cr.data.DB.ID(objectID).Get(&answer) + exist, _ = cr.data.DB.ID(objectID).Get(&answer) if !exist { err = errors.BadRequest(reason.ObjectNotFound) } else { @@ -80,7 +78,7 @@ func (cr *CommonRepo) GetObjectIDMap(objectID string) (objectIDMap map[string]st ID = answer.ID } case "comment": - exist, err = cr.data.DB.ID(objectID).Get(&comment) + exist, _ = cr.data.DB.ID(objectID).Get(&comment) if !exist { err = errors.BadRequest(reason.ObjectNotFound) } else { diff --git a/internal/repo/notification/notification_repo.go b/internal/repo/notification/notification_repo.go index deaf431aa..9df464220 100644 --- a/internal/repo/notification/notification_repo.go +++ b/internal/repo/notification/notification_repo.go @@ -102,7 +102,7 @@ func (nr *notificationRepo) SearchList(ctx context.Context, search *schema.Notif search.Page = 0 } if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize + search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize session := nr.data.DB.Where("") diff --git a/internal/repo/question_repo.go b/internal/repo/question_repo.go index f6f4e40b3..d7b9ff8b1 100644 --- a/internal/repo/question_repo.go +++ b/internal/repo/question_repo.go @@ -64,27 +64,27 @@ func (qr *questionRepo) UpdateQuestion(ctx context.Context, question *entity.Que return } -func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionId string) (err error) { +func (qr *questionRepo) UpdatePvCount(ctx context.Context, questionID string) (err error) { question := &entity.Question{} - _, err = qr.data.DB.Where("id =?", questionId).Incr("view_count", 1).Update(question) + _, err = qr.data.DB.Where("id =?", questionID).Incr("view_count", 1).Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionId string, num int) (err error) { +func (qr *questionRepo) UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error) { question := &entity.Question{} - _, err = qr.data.DB.Where("id =?", questionId).Incr("answer_count", num).Update(question) + _, err = qr.data.DB.Where("id =?", questionID).Incr("answer_count", num).Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return nil } -func (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionId string, num int) (err error) { +func (qr *questionRepo) UpdateCollectionCount(ctx context.Context, questionID string, num int) (err error) { question := &entity.Question{} - _, err = qr.data.DB.Where("id =?", questionId).Incr("collection_count", num).Update(question) + _, err = qr.data.DB.Where("id =?", questionID).Incr("collection_count", num).Update(question) if err != nil { return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -119,7 +119,8 @@ func (qr *questionRepo) UpdateLastAnswer(ctx context.Context, question *entity.Q // GetQuestion get question one func (qr *questionRepo) GetQuestion(ctx context.Context, id string) ( - question *entity.Question, exist bool, err error) { + question *entity.Question, exist bool, err error, +) { question = &entity.Question{} question.ID = id exist, err = qr.data.DB.Where("id = ?", id).Get(question) @@ -179,7 +180,7 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS search.Page = 0 } if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize + search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize session := qr.data.DB.Table("question") @@ -187,7 +188,7 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS if len(search.TagIDs) > 0 { session = session.Join("LEFT", "tag_rel", "question.id = tag_rel.object_id") session = session.And("tag_rel.tag_id =?", search.TagIDs[0]) - //session = session.In("tag_rel.tag_id ", search.TagIDs) + // session = session.In("tag_rel.tag_id ", search.TagIDs) session = session.And("tag_rel.status =?", entity.TagRelStatusAvailable) } @@ -199,8 +200,8 @@ func (qr *questionRepo) SearchList(ctx context.Context, search *schema.QuestionS // if search.Status > 0 { // session = session.And("question.status = ?", search.Status) // } - //switch - //newest, active,frequent,score,unanswered + // switch + // newest, active,frequent,score,unanswered switch search.Order { case "newest": session = session.OrderBy("question.created_at desc") @@ -234,7 +235,7 @@ func (qr *questionRepo) CmsSearchList(ctx context.Context, search *schema.CmsQue search.Page = 0 } if search.PageSize == 0 { - search.PageSize = constant.Default_PageSize + search.PageSize = constant.DefaultPageSize } offset := search.Page * search.PageSize session := qr.data.DB.Table("question") diff --git a/internal/repo/rank/user_rank_repo.go b/internal/repo/rank/user_rank_repo.go index 8f473e536..13996817f 100644 --- a/internal/repo/rank/user_rank_repo.go +++ b/internal/repo/rank/user_rank_repo.go @@ -34,26 +34,28 @@ func NewUserRankRepo(data *data.Data, configRepo config.ConfigRepo) rank.UserRan // session is need provider, it means this action must be success or failure // if outer action is failed then this action is need rollback func (ur *UserRankRepo) TriggerUserRank(ctx context.Context, - session *xorm.Session, userId string, deltaRank int, activityType int) (isReachStandard bool, err error) { + session *xorm.Session, userID string, deltaRank int, activityType int, +) (isReachStandard bool, err error) { if deltaRank == 0 { return false, nil } if deltaRank < 0 { // if user rank is lower than 1 after this action, then user rank will be set to 1 only. - isReachMin, err := ur.checkUserMinRank(ctx, session, userId, activityType) + var isReachMin bool + isReachMin, err = ur.checkUserMinRank(ctx, session, userID, activityType) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } if isReachMin { - _, err = session.Where(builder.Eq{"id": userId}).Update(&entity.User{Rank: 1}) + _, err = session.Where(builder.Eq{"id": userID}).Update(&entity.User{Rank: 1}) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } return false, nil } } else { - isReachStandard, err = ur.checkUserTodayRank(ctx, session, userId, activityType) + isReachStandard, err = ur.checkUserTodayRank(ctx, session, userID, activityType) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -61,7 +63,7 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context, return isReachStandard, nil } } - _, err = session.Where(builder.Eq{"id": userId}).Incr("`rank`", deltaRank).Update(&entity.User{}) + _, err = session.Where(builder.Eq{"id": userID}).Incr("`rank`", deltaRank).Update(&entity.User{}) if err != nil { return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -69,7 +71,8 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context, } func (ur *UserRankRepo) checkUserMinRank(ctx context.Context, session *xorm.Session, userID string, deltaRank int) ( - isReachStandard bool, err error) { + isReachStandard bool, err error, +) { bean := &entity.User{ID: userID} _, err = session.Select("rank").Get(bean) if err != nil { @@ -83,11 +86,13 @@ func (ur *UserRankRepo) checkUserMinRank(ctx context.Context, session *xorm.Sess } func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context, - session *xorm.Session, userID string, activityType int) (isReachStandard bool, err error) { + session *xorm.Session, userID string, activityType int, +) (isReachStandard bool, err error) { // exclude daily rank - exclude, err := ur.configRepo.GetArrayString("daily_rank_limit.exclude") + exclude, _ := ur.configRepo.GetArrayString("daily_rank_limit.exclude") for _, item := range exclude { - excludeActivityType, err := ur.configRepo.GetInt(item) + var excludeActivityType int + excludeActivityType, err = ur.configRepo.GetInt(item) if err != nil { return false, err } @@ -123,14 +128,15 @@ func (ur *UserRankRepo) checkUserTodayRank(ctx context.Context, return true, nil } -func (ur *UserRankRepo) UserRankPage(ctx context.Context, userId string, page, pageSize int) ( - rankPage []*entity.Activity, total int64, err error) { +func (ur *UserRankRepo) UserRankPage(ctx context.Context, userID string, page, pageSize int) ( + rankPage []*entity.Activity, total int64, err error, +) { rankPage = make([]*entity.Activity, 0) session := ur.data.DB.Where(builder.Eq{"has_rank": 1}.And(builder.Eq{"cancelled": 0})) session.Desc("created_at") - cond := &entity.Activity{UserID: userId} + cond := &entity.Activity{UserID: userID} total, err = pager.Help(page, pageSize, &rankPage, cond, session) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/repo/report/report_repo.go b/internal/repo/report/report_repo.go index bb8246a9f..1ebf820ee 100644 --- a/internal/repo/report/report_repo.go +++ b/internal/repo/report/report_repo.go @@ -86,7 +86,8 @@ func (ar *reportRepo) GetByID(ctx context.Context, id string) (report entity.Rep func (ar *reportRepo) UpdateByID( ctx context.Context, id string, - handleData entity.Report) (err error) { + handleData entity.Report, +) (err error) { _, err = ar.data.DB.ID(id).Update(&handleData) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/repo/revision/revision_repo.go b/internal/repo/revision/revision_repo.go index 4529caedc..9cb547fb6 100644 --- a/internal/repo/revision/revision_repo.go +++ b/internal/repo/revision/revision_repo.go @@ -81,7 +81,8 @@ func (rr *revisionRepo) UpdateObjectRevisionId(ctx context.Context, revision *en // GetRevision get revision one func (rr *revisionRepo) GetRevision(ctx context.Context, id string) ( - revision *entity.Revision, exist bool, err error) { + revision *entity.Revision, exist bool, err error, +) { revision = &entity.Revision{} exist, err = rr.data.DB.ID(id).Get(revision) if err != nil { @@ -92,7 +93,8 @@ func (rr *revisionRepo) GetRevision(ctx context.Context, id string) ( // GetLastRevisionByObjectID get object's last revision by object TagID func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID string) ( - revision *entity.Revision, exist bool, err error) { + revision *entity.Revision, exist bool, err error, +) { revision = &entity.Revision{} exist, err = rr.data.DB.Where("object_id = ?", objectID).OrderBy("create_time DESC").Get(revision) if err != nil { diff --git a/internal/repo/search_repo.go b/internal/repo/search_repo.go index abf533606..6ced77e64 100644 --- a/internal/repo/search_repo.go +++ b/internal/repo/search_repo.go @@ -21,7 +21,7 @@ import ( ) var ( - q_fields = []string{ + qFields = []string{ "`question`.`id`", "`question`.`id` as `question_id`", "`title`", @@ -34,7 +34,7 @@ var ( "`question`.`status` as `status`", "`post_update_time`", } - a_fields = []string{ + aFields = []string{ "`answer`.`id` as `id`", "`question_id`", "`question`.`title` as `title`", @@ -70,8 +70,8 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, var ( b *builder.Builder ub *builder.Builder - qfs = q_fields - afs = a_fields + qfs = qFields + afs = aFields argsQ = []interface{}{} argsA = []interface{}{} ) @@ -141,11 +141,11 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, b = b.Union("all", ub) - querySql, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() + querySQL, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() if err != nil { return } - countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() + countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() if err != nil { return } @@ -153,11 +153,11 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, queryArgs := []interface{}{} countArgs := []interface{}{} - queryArgs = append(queryArgs, querySql) + queryArgs = append(queryArgs, querySQL) queryArgs = append(queryArgs, argsQ...) queryArgs = append(queryArgs, argsA...) - countArgs = append(countArgs, countSql) + countArgs = append(countArgs, countSQL) countArgs = append(countArgs, argsQ...) countArgs = append(countArgs, argsA...) @@ -182,7 +182,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) { var ( - qfs = q_fields + qfs = qFields args = []interface{}{} ) if order == "relevance" { @@ -223,18 +223,18 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit queryArgs := []interface{}{} countArgs := []interface{}{} - querySql, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() + querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() if err != nil { return } - countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() + countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() if err != nil { return } - queryArgs = append(queryArgs, querySql) + queryArgs = append(queryArgs, querySQL) queryArgs = append(queryArgs, args...) - countArgs = append(countArgs, countSql) + countArgs = append(countArgs, countSQL) countArgs = append(countArgs, args...) res, err := sr.data.DB.Query(queryArgs...) @@ -250,10 +250,6 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, limit if len(tr) != 0 { total = converter.StringToInt64(string(tr[0]["total"])) } - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return - } resp, err = sr.parseResult(ctx, res) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -264,7 +260,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) { var ( - afs = a_fields + afs = aFields args = []interface{}{} ) if order == "relevance" { @@ -289,8 +285,8 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAc } if limitAccepted { - b.Where(builder.Eq{"adopted": schema.Answer_Adopted_Enable}) - args = append(args, schema.Answer_Adopted_Enable) + b.Where(builder.Eq{"adopted": schema.AnswerAdoptedEnable}) + args = append(args, schema.AnswerAdoptedEnable) } if questionID != "" { @@ -301,18 +297,18 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAc queryArgs := []interface{}{} countArgs := []interface{}{} - querySql, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() + querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() if err != nil { return } - countSql, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() + countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() if err != nil { return } - queryArgs = append(queryArgs, querySql) + queryArgs = append(queryArgs, querySQL) queryArgs = append(queryArgs, args...) - countArgs = append(countArgs, countSql) + countArgs = append(countArgs, countSQL) countArgs = append(countArgs, args...) res, err := sr.data.DB.Query(queryArgs...) @@ -326,10 +322,6 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, limitAc } total = converter.StringToInt64(string(tr[0]["total"])) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - return - } resp, err = sr.parseResult(ctx, res) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -438,7 +430,7 @@ func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.Us Avatar: dbinfo.Avatar, Website: dbinfo.Website, Location: dbinfo.Location, - IpInfo: dbinfo.IPInfo, + IPInfo: dbinfo.IPInfo, } } @@ -451,24 +443,24 @@ func cutOutParsedText(parsedText string) string { return parsedText } -func addRelevanceField(search_fields, words, fields []string) (res []string, args []interface{}) { - var relevanceRes = []string{} +func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) { + relevanceRes := []string{} args = []interface{}{} - for _, search_field := range search_fields { + for _, searchField := range searchFields { var ( - relevance = "(LENGTH(" + search_field + ") - LENGTH(%s))" - replacement = "REPLACE(%s, ?, '')" - replace_field = search_field - replaced string - argsField = []interface{}{} + relevance = "(LENGTH(" + searchField + ") - LENGTH(%s))" + replacement = "REPLACE(%s, ?, '')" + replaceField = searchField + replaced string + argsField = []interface{}{} ) res = fields for i, word := range words { if i == 0 { argsField = append(argsField, word) - replaced = fmt.Sprintf(replacement, replace_field) + replaced = fmt.Sprintf(replacement, replaceField) } else { argsField = append(argsField, word) replaced = fmt.Sprintf(replacement, replaced) diff --git a/internal/repo/siteinfo_repo.go b/internal/repo/siteinfo_repo.go index 579d9bb15..4e6ad8a68 100644 --- a/internal/repo/siteinfo_repo.go +++ b/internal/repo/siteinfo_repo.go @@ -27,7 +27,7 @@ func (sr *siteInfoRepo) SaveByType(ctx context.Context, siteType string, data *e old = &entity.SiteInfo{} exist bool ) - exist, err = sr.data.DB.Where(builder.Eq{"type": siteType}).Get(old) + exist, _ = sr.data.DB.Where(builder.Eq{"type": siteType}).Get(old) if exist { _, err = sr.data.DB.ID(old.ID).Update(data) if err != nil { diff --git a/internal/repo/tag/tag_rel_repo.go b/internal/repo/tag/tag_rel_repo.go index c54bf8c0d..c3926ba22 100644 --- a/internal/repo/tag/tag_rel_repo.go +++ b/internal/repo/tag/tag_rel_repo.go @@ -32,8 +32,8 @@ func (tr *tagListRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagR } // RemoveTagRelListByObjectID delete tag list -func (tr *tagListRepo) RemoveTagRelListByObjectID(ctx context.Context, objectId string) (err error) { - _, err = tr.data.DB.Where("object_id = ?", objectId).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) +func (tr *tagListRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) { + _, err = tr.data.DB.Where("object_id = ?", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } @@ -50,10 +50,11 @@ func (tr *tagListRepo) RemoveTagRelListByIDs(ctx context.Context, ids []int64) ( } // GetObjectTagRelWithoutStatus get object tag relation no matter status -func (tr *tagListRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) ( - tagRel *entity.TagRel, exist bool, err error) { +func (tr *tagListRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectID, tagID string) ( + tagRel *entity.TagRel, exist bool, err error, +) { tagRel = &entity.TagRel{} - session := tr.data.DB.Where("object_id = ?", objectId).And("tag_id = ?", tagID) + session := tr.data.DB.Where("object_id = ?", objectID).And("tag_id = ?", tagID) exist, err = session.Get(tagRel) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() @@ -71,9 +72,9 @@ func (tr *tagListRepo) EnableTagRelByIDs(ctx context.Context, ids []int64) (err } // GetObjectTagRelList get object tag relation list all -func (tr *tagListRepo) GetObjectTagRelList(ctx context.Context, objectId string) (tagListList []*entity.TagRel, err error) { +func (tr *tagListRepo) GetObjectTagRelList(ctx context.Context, objectID string) (tagListList []*entity.TagRel, err error) { tagListList = make([]*entity.TagRel, 0) - session := tr.data.DB.Where("object_id = ?", objectId) + session := tr.data.DB.Where("object_id = ?", objectID) session.Where("status = ?", entity.TagRelStatusAvailable) err = session.Find(&tagListList) if err != nil { diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 5243402e5..e5a5b835a 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -34,7 +34,8 @@ func NewTagRepo( // AddTagList add tag func (tr *tagRepo) AddTagList(ctx context.Context, tagList []*entity.Tag) (err error) { for _, item := range tagList { - ID, err := tr.uniqueIDRepo.GenUniqueID(ctx, item.TableName()) + var ID int64 + ID, err = tr.uniqueIDRepo.GenUniqueID(ctx, item.TableName()) if err != nil { return err } @@ -128,7 +129,8 @@ func (tr *tagRepo) UpdateTagQuestionCount(ctx context.Context, tagID string, que // UpdateTagSynonym update synonym tag func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []string, mainTagID int64, - mainTagSlugName string) (err error) { + mainTagSlugName string, +) (err error) { bean := &entity.Tag{MainTagID: mainTagID, MainTagSlugName: mainTagSlugName} session := tr.data.DB.In("slug_name", tagSlugNameList).MustCols("main_tag_id", "main_tag_slug_name") _, err = session.Update(bean) @@ -140,7 +142,8 @@ func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []strin // GetTagByID get tag one func (tr *tagRepo) GetTagByID(ctx context.Context, tagID string) ( - tag *entity.Tag, exist bool, err error) { + 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}) @@ -164,7 +167,8 @@ func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []* // 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 []*entity.Tag, total int64, err error, +) { tagList = make([]*entity.Tag, 0) session := tr.data.DB.NewSession() diff --git a/internal/repo/user/user_backyard_repo.go b/internal/repo/user/user_backyard_repo.go index 06ce7d2f0..367a646cf 100644 --- a/internal/repo/user/user_backyard_repo.go +++ b/internal/repo/user/user_backyard_repo.go @@ -29,7 +29,8 @@ func NewUserBackyardRepo(data *data.Data) user_backyard.UserBackyardRepo { // UpdateUserStatus update user status func (ur *userBackyardRepo) UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, - email string) (err error) { + email string, +) (err error) { cond := &entity.User{Status: userStatus, MailStatus: mailStatus, EMail: email} switch userStatus { case entity.UserStatusSuspended: @@ -71,11 +72,12 @@ func (ur *userBackyardRepo) GetUserInfo(ctx context.Context, userID string) (use func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User) (users []*entity.User, total int64, err error) { users = make([]*entity.User, 0) session := ur.data.DB.NewSession() - if user.Status == entity.UserStatusDeleted { + switch user.Status { + case entity.UserStatusDeleted: session.Desc("deleted_at") - } else if user.Status == entity.UserStatusSuspended { + case entity.UserStatusSuspended: session.Desc("suspended_at") - } else { + default: session.Desc("created_at") } total, err = pager.Help(page, pageSize, &users, user, session) diff --git a/internal/schema/answer_schema.go b/internal/schema/answer_schema.go index fde11165a..0c396865e 100644 --- a/internal/schema/answer_schema.go +++ b/internal/schema/answer_schema.go @@ -9,44 +9,44 @@ type RemoveAnswerReq struct { } const ( - Answer_Adopted_Failed = 1 - Answer_Adopted_Enable = 2 + AnswerAdoptedFailed = 1 + AnswerAdoptedEnable = 2 ) type AnswerAddReq struct { - QuestionId string `json:"question_id" ` // question_id + QuestionID string `json:"question_id" ` // question_id Content string `json:"content" ` // content - Html string `json:"html" ` // html + HTML string `json:"html" ` // html UserID string `json:"-" ` // user_id } type AnswerUpdateReq struct { ID string `json:"id"` // id - QuestionId string `json:"question_id" ` // question_id + QuestionID string `json:"question_id" ` // question_id UserID string `json:"-" ` // user_id Title string `json:"title" ` // title Content string `json:"content"` // content - Html string `json:"html" ` // html - EditSummary string `validate:"omitempty" json:"edit_summary"` //edit_summary + HTML string `json:"html" ` // html + EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary } type AnswerList struct { - QuestionId string `json:"question_id" form:"question_id"` // question_id + QuestionID string `json:"question_id" form:"question_id"` // question_id Order string `json:"order" form:"order"` // 1 Default 2 time - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size LoginUserID string `json:"-" ` } type AnswerInfo struct { ID string `json:"id" xorm:"id"` // id - QuestionId string `json:"question_id" xorm:"question_id"` // question_id + QuestionID string `json:"question_id" xorm:"question_id"` // question_id Content string `json:"content" xorm:"content"` // content - Html string `json:"html" xorm:"html"` // html + HTML string `json:"html" xorm:"html"` // html CreateTime int64 `json:"create_time" xorm:"created"` // create_time UpdateTime int64 `json:"update_time" xorm:"updated"` // update_time Adopted int `json:"adopted"` // 1 Failed 2 Adopted - UserId string `json:"-" ` + UserID string `json:"-" ` UserInfo *UserBasicInfo `json:"user_info,omitempty"` UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` Collected bool `json:"collected"` @@ -60,12 +60,12 @@ type AnswerInfo struct { type AdminAnswerInfo struct { ID string `json:"id"` - QuestionId string `json:"question_id"` + QuestionID string `json:"question_id"` Description string `json:"description"` CreateTime int64 `json:"create_time"` UpdateTime int64 `json:"update_time"` Adopted int `json:"adopted"` - UserId string `json:"-" ` + UserID string `json:"-" ` UserInfo *UserBasicInfo `json:"user_info"` VoteCount int `json:"vote_count"` QuestionInfo struct { diff --git a/internal/schema/collection_group_schema.go b/internal/schema/collection_group_schema.go index 19d6f4d5c..966582132 100644 --- a/internal/schema/collection_group_schema.go +++ b/internal/schema/collection_group_schema.go @@ -3,8 +3,8 @@ package schema import "time" const ( - CG_DEFAULT = 1 - CG_DIY = 2 + CGDefault = 1 + CGDIY = 2 ) // CollectionSwitchReq switch collection request diff --git a/internal/schema/forbidden_schema.go b/internal/schema/forbidden_schema.go index 7ac7c5652..e8ae7a06b 100644 --- a/internal/schema/forbidden_schema.go +++ b/internal/schema/forbidden_schema.go @@ -2,7 +2,7 @@ package schema const ( ForbiddenReasonTypeInactive = "inactive" - ForbiddenReasonTypeUrlExpired = "url_expired" + ForbiddenReasonTypeURLExpired = "url_expired" ForbiddenReasonTypeUserSuspended = "suspended" ) diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index a619c4544..24508fb2a 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -5,12 +5,11 @@ type RemoveQuestionReq struct { // question id ID string `validate:"required" comment:"question id" json:"id"` UserID string `json:"-" ` // user_id - } type CloseQuestionReq struct { ID string `validate:"required" comment:"question id" json:"id"` - UserId string `json:"-" ` // user_id + UserID string `json:"-" ` // user_id CloseType int `json:"close_type" ` // close_type CloseMsg string `json:"close_msg" ` // close_type } @@ -26,7 +25,7 @@ type QuestionAdd struct { // content Content string `validate:"required,gte=6,lte=65535" json:"content"` // html - Html string `validate:"required,gte=6,lte=65535" json:"html"` + HTML string `validate:"required,gte=6,lte=65535" json:"html"` // tags Tags []*TagItem `validate:"required,dive" json:"tags"` // user id @@ -41,7 +40,7 @@ type QuestionUpdate struct { // content Content string `validate:"required,gte=6,lte=65535" json:"content"` // html - Html string `validate:"required,gte=6,lte=65535" json:"html"` + HTML string `validate:"required,gte=6,lte=65535" json:"html"` // tags Tags []*TagItem `validate:"required,dive" json:"tags"` // edit summary @@ -65,7 +64,7 @@ type QuestionInfo struct { ID string `json:"id" ` Title string `json:"title" xorm:"title"` // title Content string `json:"content" xorm:"content"` // content - Html string `json:"html" xorm:"html"` // html + HTML string `json:"html" xorm:"html"` // html Tags []*TagResp `json:"tags" ` // tags ViewCount int `json:"view_count" xorm:"view_count"` // view_count UniqueViewCount int `json:"unique_view_count" xorm:"unique_view_count"` // unique_view_count @@ -73,15 +72,15 @@ type QuestionInfo struct { AnswerCount int `json:"answer_count" xorm:"answer_count"` // answer count CollectionCount int `json:"collection_count" xorm:"collection_count"` // collection count FollowCount int `json:"follow_count" xorm:"follow_count"` // follow count - AcceptedAnswerId string `json:"accepted_answer_id" ` // accepted_answer_id - LastAnswerId string `json:"last_answer_id" ` // last_answer_id + AcceptedAnswerID string `json:"accepted_answer_id" ` // accepted_answer_id + LastAnswerID string `json:"last_answer_id" ` // last_answer_id CreateTime int64 `json:"create_time" ` // create_time UpdateTime int64 `json:"-"` // update_time PostUpdateTime int64 `json:"update_time"` QuestionUpdateTime int64 `json:"edit_time"` Status int `json:"status"` Operation *Operation `json:"operation,omitempty"` - UserId string `json:"-" ` + UserID string `json:"-" ` UserInfo *UserBasicInfo `json:"user_info"` UpdateUserInfo *UserBasicInfo `json:"update_user_info,omitempty"` LastAnsweredUserInfo *UserBasicInfo `json:"last_answered_user_info,omitempty"` @@ -108,10 +107,10 @@ type AdminQuestionInfo struct { } type Operation struct { - Operation_Type string `json:"operation_type"` - Operation_Description string `json:"operation_description"` - Operation_Msg string `json:"operation_msg"` - Operation_Time int64 `json:"operation_time"` + OperationType string `json:"operation_type"` + OperationDescription string `json:"operation_description"` + OperationMsg string `json:"operation_msg"` + OperationTime int64 `json:"operation_time"` } type GetCloseTypeResp struct { @@ -151,25 +150,25 @@ type UserQuestionInfo struct { AnswerCount int `json:"answer_count"` CollectionCount int `json:"collection_count"` CreateTime int `json:"create_time"` - AcceptedAnswerId string `json:"accepted_answer_id"` + AcceptedAnswerID string `json:"accepted_answer_id"` Status string `json:"status"` } type QuestionSearch struct { - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size - Order string `json:"order" form:"order"` //Search order by - Tags []string `json:"tags" form:"tags"` //Search tag - TagIDs []string `json:"-" form:"-"` //Search tag - UserName string `json:"username" form:"username"` //Search username + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size + Order string `json:"order" form:"order"` // Search order by + Tags []string `json:"tags" form:"tags"` // Search tag + TagIDs []string `json:"-" form:"-"` // Search tag + UserName string `json:"username" form:"username"` // Search username UserID string `json:"-" form:"-"` } type CmsQuestionSearch struct { - Page int `json:"page" form:"page"` //Query number of pages - PageSize int `json:"page_size" form:"page_size"` //Search page size + 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 } type AdminSetQuestionStatusRequest struct { diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go index 7680c5db1..d6b70c397 100644 --- a/internal/schema/tag_schema.go +++ b/internal/schema/tag_schema.go @@ -107,7 +107,7 @@ func (tr *GetTagPageResp) GetExcerpt() { } type TagChange struct { - ObjectId string `json:"object_id"` // object_id + ObjectID string `json:"object_id"` // object_id Tags []*TagItem `json:"tags"` // tags name // user id UserID string `json:"-"` diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 757926c79..2910a065d 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -55,7 +55,7 @@ type GetUserResp struct { // bio markdown Bio string `json:"bio"` // bio html - BioHtml string `json:"bio_html"` + BioHTML string `json:"bio_html"` // website Website string `json:"website"` // location @@ -114,7 +114,7 @@ type GetOtherUserInfoByUsernameResp struct { // bio markdown Bio string `json:"bio"` // bio html - BioHtml string `json:"bio_html"` + BioHTML string `json:"bio_html"` // website Website string `json:"website"` // location @@ -146,20 +146,19 @@ func (r *GetOtherUserInfoByUsernameResp) GetFromUserEntity(userInfo *entity.User r.StatusMsg = statusMsgShow } } - } const ( - Mail_State_Pass = 1 - Mail_State_Verifi = 2 + MailStatePass = 1 + MailStateVerifi = 2 - Notice_Status_On = 1 - Notice_Status_Off = 2 + NoticeStatusOn = 1 + NoticeStatusOff = 2 - //ActionRecord ReportType - ActionRecord_Type_Login = "login" - ActionRecord_Type_Email = "e_mail" - ActionRecord_Type_Find_Pass = "find_pass" + // ActionRecord ReportType + ActionRecordTypeLogin = "login" + ActionRecordTypeEmail = "e_mail" + ActionRecordTypeFindPass = "find_pass" ) var UserStatusShow = map[int]string{ @@ -167,6 +166,7 @@ var UserStatusShow = map[int]string{ 9: "forbidden", 10: "deleted", } + var UserStatusShowMsg = map[int]string{ 1: "", 9: "This user was suspended forever. This user doesn’t meet a community guideline.", @@ -207,7 +207,7 @@ func (u *UserRegisterReq) Check() (errField *validator.ErrorField, err error) { // UserModifyPassWordRequest type UserModifyPassWordRequest struct { - UserId string `json:"-" ` // user_id + UserID string `json:"-" ` // user_id OldPass string `json:"old_pass" ` // old password Pass string `json:"pass" ` // password } @@ -234,13 +234,13 @@ type UpdateInfoRequest struct { // bio Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"` // bio - BioHtml string `validate:"omitempty,gt=0,lte=4096" json:"bio_html"` + BioHTML string `validate:"omitempty,gt=0,lte=4096" json:"bio_html"` // website Website string `validate:"omitempty,gt=0,lte=500" json:"website"` // location Location string `validate:"omitempty,gt=0,lte=100" json:"location"` // user id - UserId string `json:"-" ` + UserID string `json:"-" ` } func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error) { @@ -283,7 +283,7 @@ func (u *UserRePassWordRequest) Check() (errField *validator.ErrorField, err err } type UserNoticeSetRequest struct { - UserId string `json:"-" ` // user_id + UserID string `json:"-" ` // user_id NoticeSwitch bool `json:"notice_switch" ` } @@ -294,7 +294,7 @@ type UserNoticeSetResp struct { type ActionRecordReq struct { // action Action string `validate:"required,oneof=login e_mail find_pass" form:"action"` - Ip string `json:"-"` + IP string `json:"-"` } type ActionRecordResp struct { @@ -311,7 +311,7 @@ type UserBasicInfo struct { Avatar string `json:"avatar" ` // avatar Website string `json:"website" ` // website Location string `json:"location" ` // location - IpInfo string `json:"ip_info"` // ip info + IPInfo string `json:"ip_info"` // ip info Status string `json:"status"` // status } diff --git a/internal/service/action/captcha_service.go b/internal/service/action/captcha_service.go index 380e2a8c5..9d79c0fe6 100644 --- a/internal/service/action/captcha_service.go +++ b/internal/service/action/captcha_service.go @@ -36,7 +36,7 @@ func NewCaptchaService(captchaRepo CaptchaRepo) *CaptchaService { // ActionRecord action record func (cs *CaptchaService) ActionRecord(ctx context.Context, req *schema.ActionRecordReq) (resp *schema.ActionRecordResp, err error) { resp = &schema.ActionRecordResp{} - num, err := cs.captchaRepo.GetActionType(ctx, req.Ip, req.Action) + num, err := cs.captchaRepo.GetActionType(ctx, req.IP, req.Action) if err != nil { num = 0 } @@ -51,7 +51,8 @@ func (cs *CaptchaService) ActionRecord(ctx context.Context, req *schema.ActionRe // ActionRecordVerifyCaptcha // Verify that you need to enter a CAPTCHA, and that the CAPTCHA is correct func (cs *CaptchaService) ActionRecordVerifyCaptcha( - ctx context.Context, actionType string, ip string, id string, VerifyValue string) bool { + ctx context.Context, actionType string, ip string, id string, VerifyValue string, +) bool { num, cahceErr := cs.captchaRepo.GetActionType(ctx, ip, actionType) if cahceErr != nil { return true diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index 7fc65a6f4..2e262b8e7 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -14,9 +14,9 @@ type AnswerRepo interface { GetAnswer(ctx context.Context, id string) (answer *entity.Answer, exist bool, err error) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) GetAnswerPage(ctx context.Context, page, pageSize int, answer *entity.Answer) (answerList []*entity.Answer, total int64, err error) - UpdateAdopted(ctx context.Context, id string, questionId string) error + UpdateAdopted(ctx context.Context, id string, questionID string) error GetByID(ctx context.Context, id string) (*entity.Answer, bool, error) - GetByUserIdQuestionId(ctx context.Context, userId string, questionId string) (*entity.Answer, bool, error) + GetByUserIDQuestionID(ctx context.Context, userID string, questionID string) (*entity.Answer, bool, error) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error) @@ -33,8 +33,8 @@ func NewAnswerCommon(answerRepo AnswerRepo) *AnswerCommon { } } -func (as *AnswerCommon) SearchAnswered(ctx context.Context, userId, questionId string) (bool, error) { - _, has, err := as.answerRepo.GetByUserIdQuestionId(ctx, userId, questionId) +func (as *AnswerCommon) SearchAnswered(ctx context.Context, userID, questionID string) (bool, error) { + _, has, err := as.answerRepo.GetByUserIDQuestionID(ctx, userID, questionID) if err != nil { return has, err } @@ -56,26 +56,26 @@ func (as *AnswerCommon) Search(ctx context.Context, search *entity.AnswerSearch) func (as *AnswerCommon) ShowFormat(ctx context.Context, data *entity.Answer) *schema.AnswerInfo { info := schema.AnswerInfo{} info.ID = data.ID - info.QuestionId = data.QuestionID + info.QuestionID = data.QuestionID info.Content = data.OriginalText - info.Html = data.ParsedText + info.HTML = data.ParsedText info.Adopted = data.Adopted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() - info.UserId = data.UserID + info.UserID = data.UserID return &info } func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer) *schema.AdminAnswerInfo { info := schema.AdminAnswerInfo{} info.ID = data.ID - info.QuestionId = data.QuestionID + info.QuestionID = data.QuestionID info.Description = data.ParsedText info.Adopted = data.Adopted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() - info.UserId = data.UserID + info.UserID = data.UserID return &info } diff --git a/internal/service/answer_service.go b/internal/service/answer_service.go index 11e729644..5d0ba33d2 100644 --- a/internal/service/answer_service.go +++ b/internal/service/answer_service.go @@ -74,7 +74,7 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, id string) (err error return nil } - //user add question count + // user add question count err = as.questionCommon.UpdateAnswerCount(ctx, answerInfo.QuestionID, -1) if err != nil { log.Error("IncreaseAnswerCount error", err.Error()) @@ -97,7 +97,7 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, id string) (err error } func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) (string, error) { - questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionId) + questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID) if err != nil { return "", err } @@ -108,24 +108,24 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( insertData := new(entity.Answer) insertData.UserID = req.UserID insertData.OriginalText = req.Content - insertData.ParsedText = req.Html - insertData.Adopted = schema.Answer_Adopted_Failed - insertData.QuestionID = req.QuestionId + insertData.ParsedText = req.HTML + insertData.Adopted = schema.AnswerAdoptedFailed + insertData.QuestionID = req.QuestionID insertData.RevisionID = "0" insertData.Status = entity.AnswerStatusAvailable insertData.UpdatedAt = now - if err := as.answerRepo.AddAnswer(ctx, insertData); err != nil { + if err = as.answerRepo.AddAnswer(ctx, insertData); err != nil { return "", err } - err = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionId, 1) + err = as.questionCommon.UpdateAnswerCount(ctx, req.QuestionID, 1) if err != nil { log.Error("IncreaseAnswerCount error", err.Error()) } - err = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionId, insertData.ID) + err = as.questionCommon.UpdateLastAnswer(ctx, req.QuestionID, insertData.ID) if err != nil { log.Error("UpdateLastAnswer error", err.Error()) } - err = as.questionCommon.UpdataPostTime(ctx, req.QuestionId) + err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID) if err != nil { return insertData.ID, err } @@ -140,8 +140,8 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( ObjectID: insertData.ID, Title: "", } - InfoJson, _ := json.Marshal(insertData) - revisionDTO.Content = string(InfoJson) + infoJSON, _ := json.Marshal(insertData) + revisionDTO.Content = string(infoJSON) err = as.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return insertData.ID, err @@ -151,7 +151,7 @@ func (as *AnswerService) Insert(ctx context.Context, req *schema.AnswerAddReq) ( } func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq) (string, error) { - questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionId) + questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID) if err != nil { return "", err } @@ -161,15 +161,15 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq now := time.Now() insertData := new(entity.Answer) insertData.ID = req.ID - insertData.QuestionID = req.QuestionId + insertData.QuestionID = req.QuestionID insertData.UserID = req.UserID insertData.OriginalText = req.Content - insertData.ParsedText = req.Html + insertData.ParsedText = req.HTML insertData.UpdatedAt = now - if err := as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "update_time"}); err != nil { + if err = as.answerRepo.UpdateAnswer(ctx, insertData, []string{"original_text", "parsed_text", "update_time"}); err != nil { return "", err } - err = as.questionCommon.UpdataPostTime(ctx, req.QuestionId) + err = as.questionCommon.UpdataPostTime(ctx, req.QuestionID) if err != nil { return insertData.ID, err } @@ -179,8 +179,8 @@ func (as *AnswerService) Update(ctx context.Context, req *schema.AnswerUpdateReq Title: "", Log: req.EditSummary, } - InfoJson, _ := json.Marshal(insertData) - revisionDTO.Content = string(InfoJson) + infoJSON, _ := json.Marshal(insertData) + revisionDTO.Content = string(infoJSON) err = as.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return insertData.ID, err @@ -228,7 +228,7 @@ func (as *AnswerService) UpdateAdopted(ctx context.Context, req *schema.AnswerAd var oldAnswerInfo *entity.Answer if len(questionInfo.AcceptedAnswerID) > 0 && questionInfo.AcceptedAnswerID != "0" { - oldAnswerInfo, exist, err = as.answerRepo.GetByID(ctx, questionInfo.AcceptedAnswerID) + oldAnswerInfo, _, err = as.answerRepo.GetByID(ctx, questionInfo.AcceptedAnswerID) if err != nil { return err } @@ -249,8 +249,8 @@ func (as *AnswerService) UpdateAdopted(ctx context.Context, req *schema.AnswerAd } func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string, - questionInfo *entity.Question, newAnswerInfo *entity.Answer, oldAnswerInfo *entity.Answer) { - + questionInfo *entity.Question, newAnswerInfo *entity.Answer, oldAnswerInfo *entity.Answer, +) { // if this question is already been answered, should cancel old answer rank if oldAnswerInfo != nil { err := as.answerActivityService.CancelAcceptAnswer( @@ -266,21 +266,20 @@ func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string, log.Error(err) } } - } -func (as *AnswerService) Get(ctx context.Context, answerID, loginUserId string) (*schema.AnswerInfo, *schema.QuestionInfo, bool, error) { +func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string) (*schema.AnswerInfo, *schema.QuestionInfo, bool, error) { answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID) if err != nil { return nil, nil, has, err } info := as.ShowFormat(ctx, answerInfo) - //todo questionFunc - questionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserId) + // todo questionFunc + questionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserID) if err != nil { return nil, nil, has, err } - //todo UserFunc + // todo UserFunc userinfo, has, err := as.userCommon.GetUserBasicInfoByID(ctx, answerInfo.UserID) if err != nil { return nil, nil, has, err @@ -290,13 +289,13 @@ func (as *AnswerService) Get(ctx context.Context, answerID, loginUserId string) info.UpdateUserInfo = userinfo } - if loginUserId == "" { + if loginUserID == "" { return info, questionInfo, has, nil } - info.VoteStatus = as.voteRepo.GetVoteStatus(ctx, answerID, loginUserId) + info.VoteStatus = as.voteRepo.GetVoteStatus(ctx, answerID, loginUserID) - CollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserId, []string{answerInfo.ID}) + CollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{answerInfo.ID}) if err != nil { log.Error("CollectionFunc.SearchObjectCollected error", err) } @@ -348,7 +347,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID stri func (as *AnswerService) SearchList(ctx context.Context, search *schema.AnswerList) ([]*schema.AnswerInfo, int64, error) { list := make([]*schema.AnswerInfo, 0) dbSearch := entity.AnswerSearch{} - dbSearch.QuestionID = search.QuestionId + dbSearch.QuestionID = search.QuestionID dbSearch.Page = search.Page dbSearch.PageSize = search.PageSize dbSearch.Order = search.Order @@ -363,7 +362,7 @@ func (as *AnswerService) SearchList(ctx context.Context, search *schema.AnswerLi 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) ([]*schema.AnswerInfo, error) { list := make([]*schema.AnswerInfo, 0) objectIds := make([]string, 0) userIds := make([]string, 0) @@ -372,9 +371,9 @@ func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity. list = append(list, item) objectIds = append(objectIds, dbitem.ID) userIds = append(userIds, dbitem.UserID) - if loginUserId != "" { - //item.VoteStatus = as.activityFunc.GetVoteStatus(ctx, item.TagID, loginUserId) - item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, loginUserId) + if loginUserID != "" { + // item.VoteStatus = as.activityFunc.GetVoteStatus(ctx, item.TagID, loginUserId) + item.VoteStatus = as.voteRepo.GetVoteStatus(ctx, item.ID, loginUserID) } } userInfoMap, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIds) @@ -382,18 +381,18 @@ func (as *AnswerService) SearchFormatInfo(ctx context.Context, dblist []*entity. return list, err } for _, item := range list { - _, ok := userInfoMap[item.UserId] + _, ok := userInfoMap[item.UserID] if ok { - item.UserInfo = userInfoMap[item.UserId] - item.UpdateUserInfo = userInfoMap[item.UserId] + item.UserInfo = userInfoMap[item.UserID] + item.UpdateUserInfo = userInfoMap[item.UserID] } } - if loginUserId == "" { + if loginUserID == "" { return list, nil } - CollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserId, objectIds) + CollectedMap, err := as.collectionCommon.SearchObjectCollected(ctx, loginUserID, objectIds) if err != nil { log.Error("CollectionFunc.SearchObjectCollected error", err) } @@ -406,7 +405,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(loginUserID, item.UserID) } return list, nil diff --git a/internal/service/collection_group_service.go b/internal/service/collection_group_service.go index 3c6a3a2be..82a59bd44 100644 --- a/internal/service/collection_group_service.go +++ b/internal/service/collection_group_service.go @@ -17,7 +17,7 @@ type CollectionGroupRepo interface { UpdateCollectionGroup(ctx context.Context, collectionGroup *entity.CollectionGroup, cols []string) (err error) GetCollectionGroup(ctx context.Context, id string) (collectionGroup *entity.CollectionGroup, exist bool, err error) GetCollectionGroupPage(ctx context.Context, page, pageSize int, collectionGroup *entity.CollectionGroup) (collectionGroupList []*entity.CollectionGroup, total int64, err error) - GetDefaultID(ctx context.Context, userId string) (collectionGroup *entity.CollectionGroup, has bool, err error) + GetDefaultID(ctx context.Context, userID string) (collectionGroup *entity.CollectionGroup, has bool, err error) } // CollectionGroupService user service diff --git a/internal/service/collection_service.go b/internal/service/collection_service.go index d22342894..7ea280922 100644 --- a/internal/service/collection_service.go +++ b/internal/service/collection_service.go @@ -23,7 +23,6 @@ func NewCollectionService( collectionRepo collectioncommon.CollectionRepo, collectionGroupRepo CollectionGroupRepo, questionCommon *questioncommon.QuestionCommon, - ) *CollectionService { return &CollectionService{ collectionRepo: collectionRepo, @@ -31,6 +30,7 @@ func NewCollectionService( questionCommon: questionCommon, } } + func (cs *CollectionService) CollectionSwitch(ctx context.Context, dto *schema.CollectionSwitchDTO) (resp *schema.CollectionSwitchResp, err error) { resp = &schema.CollectionSwitchResp{} dbData, has, err := cs.collectionRepo.GetOneByObjectIDAndUser(ctx, dto.UserID, dto.ObjectID) @@ -46,7 +46,8 @@ func (cs *CollectionService) CollectionSwitch(ctx context.Context, dto *schema.C if err != nil { log.Error("UpdateCollectionCount", err.Error()) } - count, err := cs.objectCollectionCount(ctx, dto.ObjectID) + var count int64 + count, err = cs.objectCollectionCount(ctx, dto.ObjectID) if err != nil { return resp, err } @@ -56,12 +57,17 @@ func (cs *CollectionService) CollectionSwitch(ctx context.Context, dto *schema.C } if dto.GroupID == "" || dto.GroupID == "0" { - defaultGroup, has, err := cs.collectionGroupRepo.GetDefaultID(ctx, dto.UserID) + var ( + defaultGroup *entity.CollectionGroup + has bool + ) + defaultGroup, has, err = cs.collectionGroupRepo.GetDefaultID(ctx, dto.UserID) if err != nil { return nil, err } if !has { - dbdefaultGroup, err := cs.collectionGroupRepo.AddCollectionDefaultGroup(ctx, dto.UserID) + var dbdefaultGroup *entity.CollectionGroup + dbdefaultGroup, err = cs.collectionGroupRepo.AddCollectionDefaultGroup(ctx, dto.UserID) if err != nil { return nil, err } @@ -93,8 +99,8 @@ func (cs *CollectionService) CollectionSwitch(ctx context.Context, dto *schema.C return } -func (cs *CollectionService) objectCollectionCount(ctx context.Context, objectId string) (int64, error) { - count, err := cs.collectionRepo.CountByObjectID(ctx, objectId) +func (cs *CollectionService) objectCollectionCount(ctx context.Context, objectID string) (int64, error) { + count, err := cs.collectionRepo.CountByObjectID(ctx, objectID) return count, err } @@ -108,12 +114,16 @@ func (cs *CollectionService) add(ctx context.Context, collection *entity.Collect } if collection.UserCollectionGroupID == "" || collection.UserCollectionGroupID == "0" { - defaultGroup, has, err := cs.collectionGroupRepo.GetDefaultID(ctx, collection.UserID) + var ( + defaultGroup *entity.CollectionGroup + has bool + ) + defaultGroup, has, err = cs.collectionGroupRepo.GetDefaultID(ctx, collection.UserID) if err != nil { return err } if !has { - defaultGroup, err := cs.collectionGroupRepo.AddCollectionDefaultGroup(ctx, collection.UserID) + defaultGroup, err = cs.collectionGroupRepo.AddCollectionDefaultGroup(ctx, collection.UserID) if err != nil { return err } diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index c9c105c8a..0e550d435 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -31,9 +31,9 @@ type QuestionRepo interface { SearchList(ctx context.Context, search *schema.QuestionSearch) ([]*entity.QuestionTag, int64, error) UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error) SearchByTitleLike(ctx context.Context, title string) (questionList []*entity.Question, err error) - UpdatePvCount(ctx context.Context, questionId string) (err error) - UpdateAnswerCount(ctx context.Context, questionId string, num int) (err error) - UpdateCollectionCount(ctx context.Context, questionId string, num int) (err error) + UpdatePvCount(ctx context.Context, questionID string) (err error) + UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error) + UpdateCollectionCount(ctx context.Context, questionID string, num int) (err error) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) @@ -64,7 +64,6 @@ func NewQuestionCommon(questionRepo QuestionRepo, answerCommon *answercommon.AnswerCommon, metaService *meta.MetaService, configRepo config.ConfigRepo, - ) *QuestionCommon { return &QuestionCommon{ questionRepo: questionRepo, @@ -80,42 +79,44 @@ func NewQuestionCommon(questionRepo QuestionRepo, } } -func (qs *QuestionCommon) UpdataPv(ctx context.Context, questionId string) error { - return qs.questionRepo.UpdatePvCount(ctx, questionId) +func (qs *QuestionCommon) UpdataPv(ctx context.Context, questionID string) error { + return qs.questionRepo.UpdatePvCount(ctx, questionID) } -func (qs *QuestionCommon) UpdateAnswerCount(ctx context.Context, questionId string, num int) error { - return qs.questionRepo.UpdateAnswerCount(ctx, questionId, num) + +func (qs *QuestionCommon) UpdateAnswerCount(ctx context.Context, questionID string, num int) error { + return qs.questionRepo.UpdateAnswerCount(ctx, questionID, num) } -func (qs *QuestionCommon) UpdateCollectionCount(ctx context.Context, questionId string, num int) error { - return qs.questionRepo.UpdateCollectionCount(ctx, questionId, num) + +func (qs *QuestionCommon) UpdateCollectionCount(ctx context.Context, questionID string, num int) error { + return qs.questionRepo.UpdateCollectionCount(ctx, questionID, num) } -func (qs *QuestionCommon) UpdateAccepted(ctx context.Context, questionId, AnswerId string) error { +func (qs *QuestionCommon) UpdateAccepted(ctx context.Context, questionID, AnswerID string) error { question := &entity.Question{} - question.ID = questionId - question.AcceptedAnswerID = AnswerId + question.ID = questionID + question.AcceptedAnswerID = AnswerID return qs.questionRepo.UpdateAccepted(ctx, question) } -func (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionId, AnswerId string) error { +func (qs *QuestionCommon) UpdateLastAnswer(ctx context.Context, questionID, AnswerID string) error { question := &entity.Question{} - question.ID = questionId - question.LastAnswerID = AnswerId + question.ID = questionID + question.LastAnswerID = AnswerID return qs.questionRepo.UpdateLastAnswer(ctx, question) } -func (qs *QuestionCommon) UpdataPostTime(ctx context.Context, questionId string) error { +func (qs *QuestionCommon) UpdataPostTime(ctx context.Context, questionID string) error { questioninfo := &entity.Question{} now := time.Now() - questioninfo.ID = questionId + questioninfo.ID = questionID questioninfo.PostUpdateTime = now return qs.questionRepo.UpdateQuestion(ctx, questioninfo, []string{"post_update_time"}) } -func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIds []string, loginUserID string) (map[string]*schema.QuestionInfo, error) { +func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIDs []string, loginUserID string) (map[string]*schema.QuestionInfo, error) { list := make(map[string]*schema.QuestionInfo) listAddTag := make([]*entity.QuestionTag, 0) - questionList, err := qs.questionRepo.FindByID(ctx, questionIds) + questionList, err := qs.questionRepo.FindByID(ctx, questionIDs) if err != nil { return list, err } @@ -134,8 +135,8 @@ func (qs *QuestionCommon) FindInfoByID(ctx context.Context, questionIds []string return list, nil } -func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUserID string) (showinfo *schema.QuestionInfo, err error) { - dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionId) +func (qs *QuestionCommon) Info(ctx context.Context, questionID string, loginUserID string) (showinfo *schema.QuestionInfo, err error) { + dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, questionID) if err != nil { return showinfo, err } @@ -145,13 +146,14 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUser showinfo = qs.ShowFormat(ctx, dbinfo) if showinfo.Status == 2 { - metainfo, err := qs.metaService.GetMetaByObjectIdAndKey(ctx, dbinfo.ID, entity.QuestionCloseReasonKey) + var metainfo *entity.Meta + metainfo, err = qs.metaService.GetMetaByObjectIdAndKey(ctx, dbinfo.ID, entity.QuestionCloseReasonKey) if err != nil { log.Error(err) } else { - //metainfo.Value + // metainfo.Value closemsg := &schema.CloseQuestionMeta{} - err := json.Unmarshal([]byte(metainfo.Value), closemsg) + err = json.Unmarshal([]byte(metainfo.Value), closemsg) if err != nil { log.Error("json.Unmarshal CloseQuestionMeta error", err.Error()) } else { @@ -161,10 +163,10 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUser log.Error("json.Unmarshal QuestionCloseJson error", err.Error()) } else { operation := &schema.Operation{} - operation.Operation_Type = closeinfo.Name - operation.Operation_Description = closeinfo.Description - operation.Operation_Msg = closemsg.CloseMsg - operation.Operation_Time = metainfo.CreatedAt.Unix() + operation.OperationType = closeinfo.Name + operation.OperationDescription = closeinfo.Description + operation.OperationMsg = closemsg.CloseMsg + operation.OperationTime = metainfo.CreatedAt.Unix() showinfo.Operation = operation } @@ -173,7 +175,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUser } } - tagmap, err := qs.tagCommon.GetObjectTag(ctx, questionId) + tagmap, err := qs.tagCommon.GetObjectTag(ctx, questionID) if err != nil { return showinfo, err } @@ -193,10 +195,10 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUser return showinfo, nil } - showinfo.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionId, loginUserID) + showinfo.VoteStatus = qs.voteRepo.GetVoteStatus(ctx, questionID, loginUserID) // // check is followed - isFollowed, _ := qs.followCommon.IsFollowed(loginUserID, questionId) + isFollowed, _ := qs.followCommon.IsFollowed(loginUserID, questionID) showinfo.IsFollowed = isFollowed has, err = qs.AnswerCommon.SearchAnswered(ctx, loginUserID, dbinfo.ID) @@ -205,7 +207,7 @@ func (qs *QuestionCommon) Info(ctx context.Context, questionId string, loginUser } showinfo.Answered = has - //login user Collected information + // login user Collected information CollectedMap, err := qs.collectionCommon.SearchObjectCollected(ctx, loginUserID, []string{dbinfo.ID}) if err != nil { @@ -245,11 +247,11 @@ func (qs *QuestionCommon) ListFormat(ctx context.Context, questionList []*entity if ok { item.Tags = tagsMap[item.ID] } - _, ok = userInfoMap[item.UserId] + _, ok = userInfoMap[item.UserID] if ok { - item.UserInfo = userInfoMap[item.UserId] - item.UpdateUserInfo = userInfoMap[item.UserId] - item.LastAnsweredUserInfo = userInfoMap[item.UserId] + item.UserInfo = userInfoMap[item.UserID] + item.UpdateUserInfo = userInfoMap[item.UserID] + item.LastAnsweredUserInfo = userInfoMap[item.UserID] } } @@ -286,7 +288,7 @@ func (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.Remove return err } - //user add question count + // user add question count err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, -1) if err != nil { log.Error("user UpdateQuestionCount error", err.Error()) @@ -332,7 +334,7 @@ func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err erro return nil } - //user add question count + // user add question count err = as.UpdateAnswerCount(ctx, answerinfo.QuestionID, -1) if err != nil { @@ -356,21 +358,21 @@ func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question) info.ID = data.ID info.Title = data.Title info.Content = data.OriginalText - info.Html = data.ParsedText + info.HTML = data.ParsedText info.ViewCount = data.ViewCount info.UniqueViewCount = data.UniqueViewCount info.VoteCount = data.VoteCount info.AnswerCount = data.AnswerCount info.CollectionCount = data.CollectionCount info.FollowCount = data.FollowCount - info.AcceptedAnswerId = data.AcceptedAnswerID - info.LastAnswerId = data.LastAnswerID + info.AcceptedAnswerID = data.AcceptedAnswerID + info.LastAnswerID = data.LastAnswerID info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() info.PostUpdateTime = data.PostUpdateTime.Unix() info.QuestionUpdateTime = data.UpdatedAt.Unix() info.Status = data.Status - info.UserId = data.UserID + info.UserID = data.UserID info.Tags = make([]*schema.TagResp, 0) return &info } diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 881786543..69aa16692 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -89,9 +89,10 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ // CloseMsgList list close question condition func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language) ( - resp []*schema.GetCloseTypeResp, err error) { + resp []*schema.GetCloseTypeResp, err error, +) { resp = make([]*schema.GetCloseTypeResp, 0) - err = json.Unmarshal([]byte(constant.QuestionCloseJson), &resp) + err = json.Unmarshal([]byte(constant.QuestionCloseJSON), &resp) if err != nil { return nil, errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } @@ -110,7 +111,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question question.UserID = req.UserID question.Title = req.Title question.OriginalText = req.Content - question.ParsedText = req.Html + question.ParsedText = req.HTML question.AcceptedAnswerID = "0" question.LastAnswerID = "0" question.PostUpdateTime = now @@ -123,7 +124,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question return } objectTagData := schema.TagChange{} - objectTagData.ObjectId = question.ID + objectTagData.ObjectID = question.ID objectTagData.Tags = req.Tags objectTagData.UserID = req.UserID err = qs.ChangeTag(ctx, &objectTagData) @@ -136,14 +137,14 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question ObjectID: question.ID, Title: "", } - InfoJson, _ := json.Marshal(question) - revisionDTO.Content = string(InfoJson) + infoJSON, _ := json.Marshal(question) + revisionDTO.Content = string(infoJSON) err = qs.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return } - //user add question count + // user add question count err = qs.userCommon.UpdateQuestionCount(ctx, question.UserID, 1) if err != nil { log.Error("user IncreaseQuestionCount error", err.Error()) @@ -168,7 +169,7 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov return err } - //user add question count + // user add question count err = qs.userCommon.UpdateQuestionCount(ctx, questionInfo.UserID, -1) if err != nil { log.Error("user IncreaseQuestionCount error", err.Error()) @@ -190,7 +191,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest question.UserID = req.UserID question.Title = req.Title question.OriginalText = req.Content - question.ParsedText = req.Html + question.ParsedText = req.HTML question.ID = req.ID question.UpdatedAt = now dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, question.ID) @@ -208,7 +209,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest return } objectTagData := schema.TagChange{} - objectTagData.ObjectId = question.ID + objectTagData.ObjectID = question.ID objectTagData.Tags = req.Tags objectTagData.UserID = req.UserID err = qs.ChangeTag(ctx, &objectTagData) @@ -222,8 +223,8 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest Title: "", Log: req.EditSummary, } - InfoJson, _ := json.Marshal(question) - revisionDTO.Content = string(InfoJson) + infoJSON, _ := json.Marshal(question) + revisionDTO.Content = string(infoJSON) err = qs.revisionService.AddRevision(ctx, revisionDTO, true) if err != nil { return @@ -246,7 +247,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, id, loginUserID stri } } - question.MemberActions = permission.GetQuestionPermission(loginUserID, question.UserId) + question.MemberActions = permission.GetQuestionPermission(loginUserID, question.UserID) return question, nil } @@ -300,9 +301,9 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o answersearch.PageSize = pageSize answersearch.Page = page if order == "newest" { - answersearch.Order = entity.Answer_Search_OrderBy_Time + answersearch.Order = entity.AnswerSearchOrderByTime } else { - answersearch.Order = entity.Answer_Search_OrderBy_Default + answersearch.Order = entity.AnswerSearchOrderByDefault } questionIDs := make([]string, 0) answerList, count, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) @@ -319,16 +320,16 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o return userAnswerlist, count, err } for _, item := range answerlist { - _, ok := questionMaps[item.QuestionId] + _, ok := questionMaps[item.QuestionID] if ok { - item.QuestionInfo = questionMaps[item.QuestionId] + item.QuestionInfo = questionMaps[item.QuestionID] } } for _, item := range answerlist { info := &schema.UserAnswerInfo{} _ = copier.Copy(info, item) info.AnswerID = item.ID - info.QuestionID = item.QuestionId + info.QuestionID = item.QuestionID userAnswerlist = append(userAnswerlist, info) } return userAnswerlist, count, nil @@ -366,7 +367,7 @@ func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, p questionMaps[id].LastAnsweredUserInfo = nil questionMaps[id].UpdateUserInfo = nil questionMaps[id].Content = "" - questionMaps[id].Html = "" + questionMaps[id].HTML = "" list = append(list, questionMaps[id]) } } @@ -399,7 +400,7 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin answersearch := &entity.AnswerSearch{} answersearch.UserID = userinfo.ID answersearch.PageSize = 5 - answersearch.Order = entity.Answer_Search_OrderBy_Vote + answersearch.Order = entity.AnswerSearchOrderByVote questionIDs := make([]string, 0) answerList, _, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch) if err != nil { @@ -415,9 +416,9 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin return userQuestionlist, userAnswerlist, err } for _, item := range answerlist { - _, ok := questionMaps[item.QuestionId] + _, ok := questionMaps[item.QuestionID] if ok { - item.QuestionInfo = questionMaps[item.QuestionId] + item.QuestionInfo = questionMaps[item.QuestionID] } } @@ -431,7 +432,7 @@ func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName strin info := &schema.UserAnswerInfo{} _ = copier.Copy(info, item) info.AnswerID = item.ID - info.QuestionID = item.QuestionId + info.QuestionID = item.QuestionID userAnswerlist = append(userAnswerlist, info) } @@ -626,13 +627,13 @@ func (qs *QuestionService) CmsSearchAnswerList(ctx context.Context, search *enti return answerlist, count, err } for _, item := range answerlist { - _, ok := questionMaps[item.QuestionId] + _, ok := questionMaps[item.QuestionID] if ok { - item.QuestionInfo.Title = questionMaps[item.QuestionId].Title + item.QuestionInfo.Title = questionMaps[item.QuestionID].Title } - _, ok = userInfoMap[item.UserId] + _, ok = userInfoMap[item.UserID] if ok { - item.UserInfo = userInfoMap[item.UserId] + item.UserInfo = userInfoMap[item.UserID] } } return answerlist, count, nil diff --git a/internal/service/report/report_service.go b/internal/service/report/report_service.go index ab57aeafa..251c3239c 100644 --- a/internal/service/report/report_service.go +++ b/internal/service/report/report_service.go @@ -24,7 +24,8 @@ type ReportService struct { // NewReportService new report service func NewReportService(reportRepo report_common.ReportRepo, - objectInfoService *object_info.ObjService) *ReportService { + objectInfoService *object_info.ObjService, +) *ReportService { return &ReportService{ reportRepo: reportRepo, objectInfoService: objectInfoService, @@ -58,15 +59,16 @@ func (rs *ReportService) AddReport(ctx context.Context, req *schema.AddReportReq // GetReportTypeList get report list all func (rs *ReportService) GetReportTypeList(ctx context.Context, lang i18n.Language, req *schema.GetReportListReq) ( - resp []*schema.GetReportTypeResp, err error) { + resp []*schema.GetReportTypeResp, err error, +) { resp = make([]*schema.GetReportTypeResp, 0) switch req.Source { case constant.QuestionObjectType: - err = json.Unmarshal([]byte(constant.QuestionReportJson), &resp) + err = json.Unmarshal([]byte(constant.QuestionReportJSON), &resp) case constant.AnswerObjectType: - err = json.Unmarshal([]byte(constant.AnswerReportJson), &resp) + err = json.Unmarshal([]byte(constant.AnswerReportJSON), &resp) case constant.CommentObjectType: - err = json.Unmarshal([]byte(constant.CommentReportJson), &resp) + err = json.Unmarshal([]byte(constant.CommentReportJSON), &resp) } if err != nil { err = errors.BadRequest(reason.UnknownError) diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go index f1ae6e8da..a4f23fd3d 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo_service.go @@ -64,7 +64,7 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe siteType = "general" content []byte ) - content, err = json.Marshal(req) + content, _ = json.Marshal(req) data := entity.SiteInfo{ Type: siteType, @@ -107,7 +107,7 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site return } - content, err = json.Marshal(req) + content, _ = json.Marshal(req) data := entity.SiteInfo{ Type: siteType, @@ -120,7 +120,8 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site // GetSMTPConfig get smtp config func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) ( - resp *schema.GetSMTPConfigResp, err error) { + resp *schema.GetSMTPConfigResp, err error, +) { emailConfig, err := s.emailService.GetEmailConfig() if err != nil { return nil, err diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index dd8353eba..7892b9a9e 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -47,7 +47,8 @@ type TagCommonService struct { // NewTagCommonService new tag service func NewTagCommonService(tagRepo TagRepo, tagRelRepo TagRelRepo, - revisionService *revision_common.RevisionService) *TagCommonService { + revisionService *revision_common.RevisionService, +) *TagCommonService { return &TagCommonService{ tagRepo: tagRepo, tagRelRepo: tagRelRepo, @@ -97,20 +98,20 @@ func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) ( // 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) + objectIDTagMap := make(map[string][]*schema.TagResp) tagIDList := make([]string, 0) tagsInfoMap := make(map[string]*entity.Tag) tagList, err := ts.tagRelRepo.BatchGetObjectTagRelList(ctx, objectIds) if err != nil { - return objectIdTagMap, err + return objectIDTagMap, err } for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } tagsInfoList, err := ts.tagRepo.GetTagListByIDs(ctx, tagIDList) if err != nil { - return objectIdTagMap, err + return objectIDTagMap, err } for _, item := range tagsInfoList { tagsInfoMap[item.ID] = item @@ -124,10 +125,10 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s DisplayName: tagInfo.DisplayName, MainTagSlugName: tagInfo.MainTagSlugName, } - objectIdTagMap[item.ObjectID] = append(objectIdTagMap[item.ObjectID], t) + objectIDTagMap[item.ObjectID] = append(objectIDTagMap[item.ObjectID], t) } } - return objectIdTagMap, nil + return objectIDTagMap, nil } // ObjectChangeTag change object tag list @@ -191,7 +192,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData * } } - err = ts.CreateOrUpdateTagRelList(ctx, objectTagData.ObjectId, thisObjTagIDList) + err = ts.CreateOrUpdateTagRelList(ctx, objectTagData.ObjectID, thisObjTagIDList) if err != nil { return err } diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 0297ae477..4c7f49db7 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -83,7 +83,7 @@ func (us *UserCommon) UserBasicInfoFormat(ctx context.Context, userInfo *entity. userBasicInfo.Avatar = userInfo.Avatar userBasicInfo.Website = userInfo.Website userBasicInfo.Location = userInfo.Location - userBasicInfo.IpInfo = userInfo.IPInfo + userBasicInfo.IPInfo = userInfo.IPInfo userBasicInfo.Status = schema.UserStatusShow[userInfo.Status] if userBasicInfo.Status == schema.UserDeleted { userBasicInfo.Avatar = "" diff --git a/internal/service/user_service.go b/internal/service/user_service.go index af2d4d6fc..370dbd8f5 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -39,7 +39,8 @@ func NewUserService(userRepo usercommon.UserRepo, userActivity activity.UserActiveActivityRepo, emailService *export.EmailService, authService *auth.AuthService, - serviceConfig *service_config.ServiceConfig) *UserService { + serviceConfig *service_config.ServiceConfig, +) *UserService { return &UserService{ userRepo: userRepo, userActivity: userActivity, @@ -94,7 +95,8 @@ func (us *UserService) GetUserStatus(ctx context.Context, userID, token string) } func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) ( - resp *schema.GetOtherUserInfoResp, err error) { + resp *schema.GetOtherUserInfoResp, err error, +) { userInfo, exist, err := us.userRepo.GetByUsername(ctx, username) if err != nil { return nil, err @@ -165,8 +167,8 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet UserID: userInfo.ID, } code := uuid.NewString() - verifyEmailUrl := fmt.Sprintf("%s/users/password-reset?code=%s", us.serviceConfig.WebHost, code) - title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailUrl) + verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.serviceConfig.WebHost, code) + title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL) if err != nil { return "", err } @@ -179,7 +181,7 @@ func (us *UserService) UseRePassWord(ctx context.Context, req *schema.UserRePass data := &schema.EmailCodeContent{} err = data.FromJSONString(req.Content) if err != nil { - return nil, errors.BadRequest(reason.EmailVerifyUrlExpired) + return nil, errors.BadRequest(reason.EmailVerifyURLExpired) } userInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email) @@ -203,8 +205,7 @@ func (us *UserService) UseRePassWord(ctx context.Context, req *schema.UserRePass } func (us *UserService) UserModifyPassWordVerification(ctx context.Context, request *schema.UserModifyPassWordRequest) (bool, error) { - - userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserId) + userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID) if err != nil { return false, err } @@ -225,7 +226,7 @@ func (us *UserService) UserModifyPassWord(ctx context.Context, request *schema.U if err != nil { return err } - userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserId) + userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID) if err != nil { return err } @@ -251,17 +252,17 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq if err != nil { return err } - if exist && userInfo.ID != req.UserId { + if exist && userInfo.ID != req.UserID { return errors.BadRequest(reason.UsernameDuplicate) } } userInfo := entity.User{} - userInfo.ID = req.UserId + userInfo.ID = req.UserID userInfo.Avatar = req.Avatar userInfo.DisplayName = req.DisplayName userInfo.Bio = req.Bio - userInfo.BioHtml = req.BioHtml + userInfo.BioHTML = req.BioHTML userInfo.Location = req.Location userInfo.Website = req.Website userInfo.Username = req.Username @@ -281,7 +282,8 @@ func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, er // UserRegisterByEmail user register func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) ( - resp *schema.GetUserResp, err error) { + resp *schema.GetUserResp, err error, +) { _, has, err := us.userRepo.GetByEmail(ctx, registerUserInfo.Email) if err != nil { return nil, err @@ -315,8 +317,8 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo UserID: userInfo.ID, } code := uuid.NewString() - verifyEmailUrl := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) - title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailUrl) + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) + title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return nil, err } @@ -358,8 +360,8 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e UserID: userInfo.ID, } code := uuid.NewString() - verifyEmailUrl := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) - title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailUrl) + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) + title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return err } @@ -367,9 +369,10 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e return nil } -func (us *UserService) UserNoticeSet(ctx context.Context, userId string, noticeSwitch bool) ( - resp *schema.UserNoticeSetResp, err error) { - userInfo, has, err := us.userRepo.GetByUserID(ctx, userId) +func (us *UserService) UserNoticeSet(ctx context.Context, userID string, noticeSwitch bool) ( + resp *schema.UserNoticeSetResp, err error, +) { + userInfo, has, err := us.userRepo.GetByUserID(ctx, userID) if err != nil { return nil, err } @@ -377,9 +380,9 @@ func (us *UserService) UserNoticeSet(ctx context.Context, userId string, noticeS return nil, errors.BadRequest(reason.UserNotFound) } if noticeSwitch { - userInfo.NoticeStatus = schema.Notice_Status_On + userInfo.NoticeStatus = schema.NoticeStatusOn } else { - userInfo.NoticeStatus = schema.Notice_Status_Off + userInfo.NoticeStatus = schema.NoticeStatusOff } err = us.userRepo.UpdateNoticeStatus(ctx, userInfo.ID, userInfo.NoticeStatus) return &schema.UserNoticeSetResp{NoticeSwitch: noticeSwitch}, err @@ -389,7 +392,7 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri data := &schema.EmailCodeContent{} err = data.FromJSONString(req.Content) if err != nil { - return nil, errors.BadRequest(reason.EmailVerifyUrlExpired) + return nil, errors.BadRequest(reason.EmailVerifyURLExpired) } userInfo, has, err := us.userRepo.GetByEmail(ctx, data.Email) @@ -471,17 +474,14 @@ func (us *UserService) makeUsername(ctx context.Context, displayName string) (us // Compare whether the password is correct func (us *UserService) verifyPassword(ctx context.Context, LoginPass, UserPass string) bool { err := bcrypt.CompareHashAndPassword([]byte(UserPass), []byte(LoginPass)) - if err != nil { - return false - } - return true + return err == nil } // encryptPassword // The password does irreversible encryption. func (us *UserService) encryptPassword(ctx context.Context, Pass string) (string, error) { hashPwd, err := bcrypt.GenerateFromPassword([]byte(Pass), bcrypt.DefaultCost) - //This encrypted string can be saved to the database and can be used as password matching verification + // This encrypted string can be saved to the database and can be used as password matching verification return string(hashPwd), err } @@ -508,12 +508,12 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. UserID: req.UserID, } code := uuid.NewString() - verifyEmailUrl := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.serviceConfig.WebHost, code) - title, body, err := us.emailService.ChangeEmailTemplate(ctx, verifyEmailUrl) + verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.serviceConfig.WebHost, code) + title, body, err := us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL) if err != nil { return err } - log.Infof("send email confirmation %s", verifyEmailUrl) + log.Infof("send email confirmation %s", verifyEmailURL) go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString()) return nil @@ -524,7 +524,7 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string data := &schema.EmailCodeContent{} err = data.FromJSONString(content) if err != nil { - return errors.BadRequest(reason.EmailVerifyUrlExpired) + return errors.BadRequest(reason.EmailVerifyURLExpired) } _, exist, err := us.userRepo.GetByEmail(ctx, data.Email) diff --git a/internal/service/vote_service.go b/internal/service/vote_service.go index 74789529c..b77fe4020 100644 --- a/internal/service/vote_service.go +++ b/internal/service/vote_service.go @@ -67,7 +67,7 @@ func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteRes var objectUserID string - objectUserID, err = as.GetObjectUserId(ctx, dto.ObjectID) + objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID) if err != nil { return } @@ -91,7 +91,7 @@ func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteR var objectUserID string - objectUserID, err = as.GetObjectUserId(ctx, dto.ObjectID) + objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID) if err != nil { return } @@ -109,7 +109,7 @@ func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteR } } -func (vs *VoteService) GetObjectUserId(ctx context.Context, objectID string) (userID string, err error) { +func (vs *VoteService) GetObjectUserID(ctx context.Context, objectID string) (userID string, err error) { var objectKey string objectKey, err = obj.GetObjectTypeStrByObjectID(objectID) @@ -162,7 +162,8 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith ) for _, typeKey := range typeKeys { - t, err := vs.configRepo.GetConfigType(typeKey) + var t int + t, err = vs.configRepo.GetConfigType(typeKey) if err != nil { continue } @@ -175,7 +176,8 @@ func (vs *VoteService) ListUserVotes(ctx context.Context, req schema.GetVoteWith } for _, voteInfo := range voteList { - objInfo, err := vs.objectService.GetInfo(ctx, voteInfo.ObjectID) + var objInfo *schema.SimpleObjectInfo + objInfo, err = vs.objectService.GetInfo(ctx, voteInfo.ObjectID) if err != nil { log.Error(err) } diff --git a/pkg/checker/password.go b/pkg/checker/password.go index 6c1bd6e65..966b851c4 100644 --- a/pkg/checker/password.go +++ b/pkg/checker/password.go @@ -30,7 +30,7 @@ func CheckPassword(minLength, maxLength, minLevel int, pwd string) error { // The password strength level is initialized to D. // The regular is used to verify the password strength. // If the matching is successful, the password strength increases by 1 - var level int = levelD + level := levelD patternList := []string{`[0-9]+`, `[a-z]+`, `[A-Z]+`, `[~!@#$%^&*?_-]+`} for _, pattern := range patternList { match, _ := regexp.MatchString(pattern, pwd) @@ -41,7 +41,7 @@ func CheckPassword(minLength, maxLength, minLevel int, pwd string) error { // If the final password strength falls below the required minimum strength, return with an error if level < minLevel { - return fmt.Errorf("The password does not satisfy the current policy requirements. ") + return fmt.Errorf("the password does not satisfy the current policy requirements") } return nil } diff --git a/pkg/uid/id.go b/pkg/uid/id.go index bd29f0833..a89daa6bf 100644 --- a/pkg/uid/id.go +++ b/pkg/uid/id.go @@ -15,7 +15,7 @@ type SnowFlakeID struct { var snowFlakeIDGenerator *SnowFlakeID func init() { - //todo + // todo rand.Seed(time.Now().UnixNano()) node, err := snowflake.NewNode(int64(rand.Intn(1000)) + 1) if err != nil { From 014472a37bc47e6b49d1bcce08ba9b95d7e43f4a Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Tue, 1 Nov 2022 15:27:37 +0800 Subject: [PATCH 0078/3337] style: formatting naming --- internal/repo/activity_common/vote.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/repo/activity_common/vote.go b/internal/repo/activity_common/vote.go index 2c40305a8..abbea28fd 100644 --- a/internal/repo/activity_common/vote.go +++ b/internal/repo/activity_common/vote.go @@ -25,11 +25,11 @@ func NewVoteRepo(data *data.Data, activityRepo activity_common.ActivityRepo) act func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string) (status string) { for _, action := range []string{"vote_up", "vote_down"} { at := &entity.Activity{} - activityType, _, _, err := vr.activityRepo.GetActivityTypeByObjID(ctx, objectId, action) + activityType, _, _, err := vr.activityRepo.GetActivityTypeByObjID(ctx, objectID, action) if err != nil { return "" } - has, err := vr.data.DB.Where("object_id =? AND cancelled=0 AND activity_type=? AND user_id=?", objectId, activityType, userId).Get(at) + has, err := vr.data.DB.Where("object_id =? AND cancelled=0 AND activity_type=? AND user_id=?", objectID, activityType, userID).Get(at) if err != nil { return "" } From 8232e1ec2d552af1c1fd2fef8bb4c5bddfa8d8f8 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Tue, 1 Nov 2022 15:38:06 +0800 Subject: [PATCH 0079/3337] style: formatting naming --- internal/controller/user_controller.go | 6 +++--- internal/service/user_service.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index b4976cc37..4f2a9b636 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -491,7 +491,7 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { // If the user is not logged in, the api cannot be used. // If the user email is not verified, that also can use this api to modify the email. - captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) + captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { resp := schema.UserVerifyEmailErrorResponse{ Key: "captcha_code", @@ -506,7 +506,7 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) return } - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) + _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) err := uc.userService.UserChangeEmailSendCode(ctx, req) handler.HandleResponse(ctx, err, nil) } @@ -534,6 +534,6 @@ func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) { } err := uc.userService.UserChangeEmailVerify(ctx, req.Content) - uc.actionService.ActionRecordDel(ctx, schema.ActionRecord_Type_Email, ctx.ClientIP()) + uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) handler.HandleResponse(ctx, err, nil) } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index ff1ebea94..472ff061c 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -516,9 +516,9 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. var title, body string verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.serviceConfig.WebHost, code) if userInfo.MailStatus == entity.EmailStatusToBeVerified { - title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailUrl) + title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL) } else { - title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailUrl) + title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL) } if err != nil { return err From e91ef5c4447c2da0e650db54b9dfdc1f074a732f Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Tue, 1 Nov 2022 15:41:21 +0800 Subject: [PATCH 0080/3337] feat: add sqlite3 and update go mod --- go.mod | 38 +++++++++++++++--------------- go.sum | 48 ++++++++++++++++++++++++++++++++++++++ internal/base/data/data.go | 1 + 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index e489a8b25..85f05a6fe 100644 --- a/go.mod +++ b/go.mod @@ -17,22 +17,22 @@ require ( github.com/google/wire v0.5.0 github.com/jinzhu/copier v0.3.5 github.com/jinzhu/now v1.1.5 - github.com/lib/pq v1.10.2 - github.com/mattn/go-sqlite3 v1.14.15 + github.com/lib/pq v1.10.7 + github.com/mattn/go-sqlite3 v1.14.16 github.com/mojocn/base64Captcha v1.3.5 github.com/segmentfault/pacman v1.0.1 - github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 - github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347 - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347 - github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347 - github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347 - github.com/spf13/cobra v1.5.0 - github.com/stretchr/testify v1.8.0 + github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 + github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 + github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 + github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 + github.com/spf13/cobra v1.6.1 + github.com/stretchr/testify v1.8.1 github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a github.com/swaggo/gin-swagger v1.5.3 - github.com/swaggo/swag v1.8.6 - golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be - golang.org/x/net v0.0.0-20220927171203-f486391704dc + github.com/swaggo/swag v1.8.7 + golang.org/x/crypto v0.1.0 + golang.org/x/net v0.1.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df xorm.io/builder v0.3.12 xorm.io/core v0.7.3 @@ -41,9 +41,9 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect - github.com/andybalholm/brotli v1.0.1 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect @@ -52,7 +52,7 @@ require ( github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect @@ -81,10 +81,10 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect - golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect - golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.12 // indirect + golang.org/x/image v0.1.0 // indirect + golang.org/x/sys v0.1.0 // indirect + golang.org/x/text v0.4.0 // indirect + golang.org/x/tools v0.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index db884bf40..eb1358262 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267 h1:vDHsaEcs/Q0dw github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267/go.mod h1:Yj3yPP/vi87JjwylUTCMyd6FrOfGqP1AHk0305hDm2o= github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -131,6 +133,8 @@ github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= @@ -298,6 +302,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -397,6 +403,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= @@ -426,6 +434,8 @@ github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -539,14 +549,24 @@ github.com/segmentfault/pacman v1.0.1 h1:GFdvPtNxvVVjnDM4ty02D/+4unHwG9PmjcOZSc2 github.com/segmentfault/pacman v1.0.1/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 h1:0xWBBXHHuemzMY61KYJXh7F5FW/4K8g98RYKNXodTCc= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= +github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 h1:rXsXgC/HR7m4V425l9pDBW/qxJv6zCh6pEvvO1ZCNsI= +github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347 h1:WpnEbmZFE8FYIgvseX+NJtDgGJlM1KSaKJhoxJywUgo= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= +github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk= +github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347 h1:Q29Ky9ZUGhdLIygfX6jwPYeEa7Wqn8o3f1NJWb8LvvE= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347 h1:7Adjc296AKv32dg88S0T8t9K3+N+PFYLSCctpPnCUr0= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= +github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= +github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347 h1:CfuRhTPK2CBQIZruq5ceuTVthspe8U1FDjWXXI2RWdo= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40= +github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A= +github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -565,6 +585,8 @@ github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -579,6 +601,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -588,6 +611,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= @@ -597,10 +622,13 @@ github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89 github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg= github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= +github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU= +github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= @@ -614,6 +642,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -663,10 +692,13 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -683,6 +715,8 @@ golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= +golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk= +golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -706,6 +740,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -749,8 +784,11 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ= golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -771,6 +809,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -830,9 +869,14 @@ golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -845,6 +889,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -909,6 +955,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/base/data/data.go b/internal/base/data/data.go index 98c97d782..356c71570 100644 --- a/internal/base/data/data.go +++ b/internal/base/data/data.go @@ -7,6 +7,7 @@ import ( "github.com/answerdev/answer/pkg/dir" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" "github.com/segmentfault/pacman/cache" "github.com/segmentfault/pacman/contrib/cache/memory" "github.com/segmentfault/pacman/log" From 0cf53506c42b59c9fa040a1d5db7d1d67a00b1ea Mon Sep 17 00:00:00 2001 From: mingcheng Date: Tue, 1 Nov 2022 15:50:46 +0800 Subject: [PATCH 0081/3337] optimize: add compile universal binary --- .gitignore | 3 ++- Makefile | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 239e76e67..5f3135288 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ Thumbs*.db tmp vendor/ .husky -answer-data/ +/answer-data/ +/answer diff --git a/Makefile b/Makefile index 9feb089d0..ff29cadc0 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,13 @@ GO=$(GO_ENV) $(shell which go) build: @$(GO_ENV) $(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC) +# https://dev.to/thewraven/universal-macos-binaries-with-go-1-16-3mm3 +universal: + @GOOS=darwin GOARCH=amd64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_amd64 $(DIR_SRC) + @GOOS=darwin GOARCH=arm64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_arm64 $(DIR_SRC) + @lipo -create -output ${BIN} ${BIN}_amd64 ${BIN}_arm64 + @rm -f ${BIN}_amd64 ${BIN}_arm64 + generate: go get github.com/google/wire/cmd/wire@latest go generate ./... @@ -23,6 +30,7 @@ test: # clean all build result clean: + @$(GO) clean ./... @rm -f $(BIN) From 53084a727707264f4fc5cc71264b861228468851 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Tue, 1 Nov 2022 15:52:38 +0800 Subject: [PATCH 0082/3337] doc: update swagger --- cmd/answer/wire_gen.go | 2 +- docs/docs.go | 2 +- docs/swagger.json | 2 +- docs/swagger.yaml | 2 +- go.sum | 36 +----------------------------------- 5 files changed, 5 insertions(+), 39 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index f7d2cbe36..051564d4e 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -67,7 +67,7 @@ import ( // Injectors from wire.go: // initApplication init application. -func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, _ log.Logger) (*pacman.Application, func(), error) { +func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cacheConf *data.CacheConf, i18nConf *translator.I18n, swaggerConf *router.SwaggerConfig, serviceConf *service_config.ServiceConfig, logConf log.Logger) (*pacman.Application, func(), error) { staticRouter := router.NewStaticRouter(serviceConf) i18nTranslator, err := translator.NewTranslator(i18nConf) if err != nil { diff --git a/docs/docs.go b/docs/docs.go index b41a96b36..4162d8a03 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5080,7 +5080,7 @@ const docTemplate = `{ "type": "integer" }, "tag": { - "description": "Tags []string ` + "`" + `json:\"tags\" form:\"tags\"` + "`" + ` //Search tag", + "description": "Tags []string ` + "`" + `json:\"tags\" form:\"tags\"` + "`" + ` // Search tag", "type": "string" }, "username": { diff --git a/docs/swagger.json b/docs/swagger.json index 05097e5fb..e60de534a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5068,7 +5068,7 @@ "type": "integer" }, "tag": { - "description": "Tags []string `json:\"tags\" form:\"tags\"` //Search tag", + "description": "Tags []string `json:\"tags\" form:\"tags\"` // Search tag", "type": "string" }, "username": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 9e36de5b2..810cfef8c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -846,7 +846,7 @@ definitions: description: Search page size type: integer tag: - description: Tags []string `json:"tags" form:"tags"` //Search + description: Tags []string `json:"tags" form:"tags"` // Search tag type: string username: diff --git a/go.sum b/go.sum index eb1358262..507f92e5e 100644 --- a/go.sum +++ b/go.sum @@ -62,7 +62,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267 h1:vDHsaEcs/Q0dwetADENtwus6W1ccaZ9h3KBTm0d2X0g= github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267/go.mod h1:Yj3yPP/vi87JjwylUTCMyd6FrOfGqP1AHk0305hDm2o= -github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= @@ -131,8 +130,6 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -300,7 +297,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -401,7 +397,6 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -432,8 +427,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= -github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -547,24 +540,14 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentfault/pacman v1.0.1 h1:GFdvPtNxvVVjnDM4ty02D/+4unHwG9PmjcOZSc2wRXE= github.com/segmentfault/pacman v1.0.1/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= -github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347 h1:0xWBBXHHuemzMY61KYJXh7F5FW/4K8g98RYKNXodTCc= -github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20220929065758-260b3093a347/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 h1:rXsXgC/HR7m4V425l9pDBW/qxJv6zCh6pEvvO1ZCNsI= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= -github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347 h1:WpnEbmZFE8FYIgvseX+NJtDgGJlM1KSaKJhoxJywUgo= -github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20220929065758-260b3093a347/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347 h1:Q29Ky9ZUGhdLIygfX6jwPYeEa7Wqn8o3f1NJWb8LvvE= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20220929065758-260b3093a347/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= -github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347 h1:7Adjc296AKv32dg88S0T8t9K3+N+PFYLSCctpPnCUr0= -github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20220929065758-260b3093a347/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= -github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347 h1:CfuRhTPK2CBQIZruq5ceuTVthspe8U1FDjWXXI2RWdo= -github.com/segmentfault/pacman/contrib/server/http v0.0.0-20220929065758-260b3093a347/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05/go.mod h1:UjNiOFYv1uGCq1ZCcONaKq4eE7MW3nbgpLqgl8f9N40= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= @@ -583,8 +566,6 @@ github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcD github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= @@ -609,7 +590,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -620,15 +600,12 @@ github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9J github.com/swaggo/gin-swagger v1.5.3 h1:8mWmHLolIbrhJJTflsaFoZzRBYVmEE7JZGIq08EiC0Q= github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89Yp4uA50HpI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= -github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg= -github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU= github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= @@ -695,8 +672,6 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= -golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -713,8 +688,6 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= -golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk= golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -739,8 +712,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -785,8 +758,6 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ= -golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -868,13 +839,10 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -887,7 +855,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -953,7 +920,6 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= From 875a32aedad2149b55be864fae124dc53f5b5911 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 1 Nov 2022 16:15:30 +0800 Subject: [PATCH 0083/3337] fix: navbar style adjustment --- ui/src/components/Header/index.scss | 10 ++++++++++ ui/src/components/Header/index.tsx | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ui/src/components/Header/index.scss b/ui/src/components/Header/index.scss index 348632a37..1d664db04 100644 --- a/ui/src/components/Header/index.scss +++ b/ui/src/components/Header/index.scss @@ -65,3 +65,13 @@ } +@media (max-width: 576px) { + #header { + .logo { + max-width: 93px; + max-height: auto; + } + } +} + + diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 54d6cee74..abbd0c3c4 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -72,8 +72,8 @@ const Header: FC = () => { id="navBarToggle" /> -
- +
+ {interfaceInfo.logo ? ( Date: Tue, 1 Nov 2022 16:22:51 +0800 Subject: [PATCH 0084/3337] merge: repo unit test --- cmd/answer/wire_gen.go | 19 ++++---- go.mod | 21 +++++++++ go.sum | 76 +++++++++++++++++++++++++++++++ internal/repo/tag/tag_rel_repo.go | 2 +- internal/schema/user_schema.go | 3 ++ 5 files changed, 112 insertions(+), 9 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 051564d4e..a965ed8b7 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -14,9 +14,9 @@ import ( "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/controller" "github.com/answerdev/answer/internal/controller_backyard" - "github.com/answerdev/answer/internal/repo" "github.com/answerdev/answer/internal/repo/activity" "github.com/answerdev/answer/internal/repo/activity_common" + "github.com/answerdev/answer/internal/repo/answer" "github.com/answerdev/answer/internal/repo/auth" "github.com/answerdev/answer/internal/repo/captcha" "github.com/answerdev/answer/internal/repo/collection" @@ -26,10 +26,13 @@ import ( "github.com/answerdev/answer/internal/repo/export" "github.com/answerdev/answer/internal/repo/meta" "github.com/answerdev/answer/internal/repo/notification" + "github.com/answerdev/answer/internal/repo/question" "github.com/answerdev/answer/internal/repo/rank" "github.com/answerdev/answer/internal/repo/reason" "github.com/answerdev/answer/internal/repo/report" "github.com/answerdev/answer/internal/repo/revision" + "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/unique" "github.com/answerdev/answer/internal/repo/user" @@ -92,11 +95,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, configRepo := config.NewConfigRepo(dataData) userRepo := user.NewUserRepo(dataData, configRepo) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) - activityRepo := repo.NewActivityRepo(dataData, uniqueIDRepo, configRepo) + activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo) userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) emailRepo := export.NewEmailRepo(dataData) - siteInfoRepo := repo.NewSiteInfo(dataData) + siteInfoRepo := site_info.NewSiteInfo(dataData) emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) captchaRepo := captcha.NewCaptchaRepo(dataData) @@ -106,8 +109,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) userCommon := usercommon.NewUserCommon(userRepo) - answerRepo := repo.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo) - questionRepo := repo.NewQuestionRepo(dataData, uniqueIDRepo) + 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) voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) @@ -130,7 +133,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, followController := controller.NewFollowController(followService) collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo) collectionGroupRepo := collection.NewCollectionGroupRepo(dataData) - tagRelRepo := tag.NewTagListRepo(dataData) + tagRelRepo := tag.NewTagRelRepo(dataData) tagCommonService := tagcommon.NewTagCommonService(tagRepo, tagRelRepo, revisionService) collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) answerCommon := answercommon.NewAnswerCommon(answerRepo) @@ -146,7 +149,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) answerController := controller.NewAnswerController(answerService, rankService) - searchRepo := repo.NewSearchRepo(dataData, uniqueIDRepo, userCommon) + searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) searchController := controller.NewSearchController(searchService) serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService) @@ -156,7 +159,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reportHandle := report_handle_backyard.NewReportHandle(questionCommon, commentRepo, configRepo) reportBackyardService := report_backyard.NewReportBackyardService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo) controller_backyardReportController := controller_backyard.NewReportController(reportBackyardService) - userBackyardRepo := user.NewUserBackyardRepo(dataData) + userBackyardRepo := user.NewUserBackyardRepo(dataData, authRepo) userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo) userBackyardController := controller_backyard.NewUserBackyardController(userBackyardService) reasonRepo := reason.NewReasonRepo(configRepo) diff --git a/go.mod b/go.mod index 85f05a6fe..d6d85117f 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/lib/pq v1.10.7 github.com/mattn/go-sqlite3 v1.14.16 github.com/mojocn/base64Captcha v1.3.5 + github.com/ory/dockertest/v3 v3.9.1 github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 @@ -40,18 +41,30 @@ require ( ) require ( + github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/andybalholm/brotli v1.0.4 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/containerd/continuity v0.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v20.10.14+incompatible // indirect + github.com/docker/docker v20.10.7+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/spec v0.20.7 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -62,14 +75,19 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nicksnyder/go-i18n/v2 v2.2.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.2 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -78,6 +96,9 @@ require ( github.com/subosito/gotenv v1.4.1 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/ugorji/go/codec v1.2.7 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect diff --git a/go.sum b/go.sum index 507f92e5e..be85073b8 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -49,6 +51,10 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -83,11 +89,15 @@ github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgIS github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -96,15 +106,22 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -112,6 +129,14 @@ github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/docker/cli v20.10.14+incompatible h1:dSBKJOVesDgHo7rbxlYjYsXe7gPzrTT+/cKQgpDAazg= +github.com/docker/cli v20.10.14+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= +github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -128,6 +153,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -183,12 +209,16 @@ github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -252,6 +282,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -297,6 +329,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -370,6 +404,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -440,6 +475,9 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -449,6 +487,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0= github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= @@ -469,6 +508,14 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.2 h1:2VSZwLx5k/BfsBxMMipG/LYUnmqOD/BPkIVgQUcTlLw= +github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -477,6 +524,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= +github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= @@ -538,6 +587,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/segmentfault/pacman v1.0.1 h1:GFdvPtNxvVVjnDM4ty02D/+4unHwG9PmjcOZSc2wRXE= github.com/segmentfault/pacman v1.0.1/go.mod h1:5lNp5REd8QMThmBUvR3Fi9Y3AsOB4GRq7soCB4QLqOs= github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 h1:rXsXgC/HR7m4V425l9pDBW/qxJv6zCh6pEvvO1ZCNsI= @@ -557,6 +607,9 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -571,6 +624,7 @@ github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUq github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= @@ -602,6 +656,7 @@ github.com/swaggo/gin-swagger v1.5.3/go.mod h1:3XJKSfHjDMB5dBo/0rrTXidPmgLeqsX89 github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU= github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -613,6 +668,14 @@ github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -798,12 +861,14 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -821,6 +886,7 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -828,6 +894,7 @@ golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -837,7 +904,10 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -878,6 +948,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -908,6 +979,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -917,6 +989,7 @@ golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= @@ -1027,6 +1100,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= @@ -1065,6 +1139,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/repo/tag/tag_rel_repo.go b/internal/repo/tag/tag_rel_repo.go index 6bc211bbd..edb25d470 100644 --- a/internal/repo/tag/tag_rel_repo.go +++ b/internal/repo/tag/tag_rel_repo.go @@ -32,7 +32,7 @@ func (tr *tagRelRepo) AddTagRelList(ctx context.Context, tagList []*entity.TagRe } // RemoveTagRelListByObjectID delete tag list -func (tr *tagListRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) { +func (tr *tagRelRepo) RemoveTagRelListByObjectID(ctx context.Context, objectID string) (err error) { _, err = tr.data.DB.Where("object_id = ?", objectID).Update(&entity.TagRel{Status: entity.TagRelStatusDeleted}) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 556f45ea8..3a160cfc1 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -198,6 +198,9 @@ const ( MailStatePass = 1 MailStateVerifi = 2 + NoticeStatusOn = 1 + NoticeStatusOff = 2 + ActionRecordTypeLogin = "login" ActionRecordTypeEmail = "e_mail" ActionRecordTypeFindPass = "find_pass" From 2ff08c8b43abc66a163449abc258212214510dfd Mon Sep 17 00:00:00 2001 From: robin Date: Tue, 1 Nov 2022 16:26:42 +0800 Subject: [PATCH 0085/3337] feat(admin): question,answer,users adds filter --- ui/src/common/interface.ts | 2 ++ ui/src/pages/Admin/Answers/index.tsx | 14 +++++++++++--- ui/src/pages/Admin/Questions/index.tsx | 14 +++++++++++--- ui/src/pages/Admin/Users/index.tsx | 20 ++++++++++++-------- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 8dbc8b0db..7516a2d7e 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -228,6 +228,7 @@ export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted'; export interface AdminContentsReq extends Paging { status: AdminContentsFilterBy; + query?: string; } /** @@ -263,6 +264,7 @@ export interface AdminSettingsInterface { logo: string; language: string; theme: string; + time_zone: string; } export interface AdminSettingsSmtp { diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 2a25f53cb..89a0ae26d 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -23,10 +23,11 @@ import '../index.scss'; const answerFilterItems: Type.AdminContentsFilterBy[] = ['normal', 'deleted']; const Answers: FC = () => { - const [urlSearchParams] = useSearchParams(); + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('status') || answerFilterItems[0]; const PAGE_SIZE = 20; const curPage = Number(urlSearchParams.get('page')) || 1; + const curQuery = urlSearchParams.get('query') || ''; const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' }); const { @@ -37,6 +38,7 @@ const Answers: FC = () => { page_size: PAGE_SIZE, page: curPage, status: curFilter as Type.AdminContentsFilterBy, + query: curQuery, }); const count = listData?.count || 0; @@ -78,6 +80,11 @@ const Answers: FC = () => { }); }; + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; return ( <>

{t('page_title')}

@@ -90,10 +97,11 @@ const Answers: FC = () => { />
diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 1a1885129..c939bc060 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -32,9 +32,10 @@ const questionFilterItems: Type.AdminContentsFilterBy[] = [ const PAGE_SIZE = 20; const Questions: FC = () => { - const [urlSearchParams] = useSearchParams(); + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('status') || questionFilterItems[0]; const curPage = Number(urlSearchParams.get('page')) || 1; + const curQuery = urlSearchParams.get('query') || ''; const { t } = useTranslation('translation', { keyPrefix: 'admin.questions' }); const { @@ -45,6 +46,7 @@ const Questions: FC = () => { page_size: PAGE_SIZE, page: curPage, status: curFilter as Type.AdminContentsFilterBy, + query: curQuery, }); const count = listData?.count || 0; @@ -97,6 +99,11 @@ const Questions: FC = () => { }); }; + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; return ( <>

{t('page_title')}

@@ -109,10 +116,11 @@ const Questions: FC = () => { />
diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index de434084e..4c1299308 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react'; +import { FC } from 'react'; import { Button, Form, Table, Badge } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -34,11 +34,11 @@ const bgMap = { const PAGE_SIZE = 10; const Users: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.users' }); - const [userName, setUserName] = useState(''); - const [urlSearchParams] = useSearchParams(); + const [urlSearchParams, setUrlSearchParams] = useSearchParams(); const curFilter = urlSearchParams.get('filter') || UserFilterKeys[0]; const curPage = Number(urlSearchParams.get('page') || '1'); + const curQuery = urlSearchParams.get('query') || ''; const { data, isLoading, @@ -46,7 +46,7 @@ const Users: FC = () => { } = useQueryUsers({ page: curPage, page_size: PAGE_SIZE, - ...(userName ? { username: userName } : {}), + query: curQuery, ...(curFilter === 'all' ? {} : { status: curFilter }), }); const changeModal = useChangeModal({ @@ -60,6 +60,11 @@ const Users: FC = () => { }); }; + const handleFilter = (e) => { + urlSearchParams.set('query', e.target.value); + urlSearchParams.delete('page'); + setUrlSearchParams(urlSearchParams); + }; return ( <>

{t('title')}

@@ -72,11 +77,10 @@ const Users: FC = () => { /> setUserName(e.target.value)} - placeholder="Filter by name" + value={curQuery} + onChange={handleFilter} + placeholder={t('filter.placeholder')} style={{ width: '12.25rem' }} />
From c10930d16065a9325a40f436dff315dfffc0d3b6 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Tue, 1 Nov 2022 16:27:33 +0800 Subject: [PATCH 0086/3337] fix: revision use created_at as the condition --- internal/repo/repo_test/user_backyard_repo_test.go | 2 +- internal/repo/revision/revision_repo.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/repo/repo_test/user_backyard_repo_test.go b/internal/repo/repo_test/user_backyard_repo_test.go index 2d67cd79d..7fe3be051 100644 --- a/internal/repo/repo_test/user_backyard_repo_test.go +++ b/internal/repo/repo_test/user_backyard_repo_test.go @@ -20,7 +20,7 @@ func Test_userBackyardRepo_GetUserInfo(t *testing.T) { func Test_userBackyardRepo_GetUserPage(t *testing.T) { userBackyardRepo := user.NewUserBackyardRepo(testDataSource, auth.NewAuthRepo(testDataSource)) - got, total, err := userBackyardRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}) + got, total, err := userBackyardRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "") assert.NoError(t, err) assert.Equal(t, int64(1), total) assert.Equal(t, "1", got[0].ID) diff --git a/internal/repo/revision/revision_repo.go b/internal/repo/revision/revision_repo.go index 9cb547fb6..0d1c474a8 100644 --- a/internal/repo/revision/revision_repo.go +++ b/internal/repo/revision/revision_repo.go @@ -96,7 +96,7 @@ func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID revision *entity.Revision, exist bool, err error, ) { revision = &entity.Revision{} - exist, err = rr.data.DB.Where("object_id = ?", objectID).OrderBy("create_time DESC").Get(revision) + exist, err = rr.data.DB.Where("object_id = ?", objectID).OrderBy("created_at DESC").Get(revision) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } From d9bddb8210f3c1a04a07eee14f72e4e23c1080a4 Mon Sep 17 00:00:00 2001 From: kumfo Date: Tue, 1 Nov 2022 16:51:01 +0800 Subject: [PATCH 0087/3337] feat: admin's id search condition --- internal/repo/answer/answer_repo.go | 4 ++-- internal/repo/question/question_repo.go | 4 ++-- internal/repo/user/user_backyard_repo.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index a16c695b2..2020fc2ff 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -234,9 +234,9 @@ func (ar *answerRepo) CmsSearchList(ctx context.Context, search *entity.CmsAnswe id = "" ) - if strings.Contains(search.Query, "id:") { + if strings.Contains(search.Query, "answer:") { idSearch = true - id = strings.TrimSpace(strings.TrimPrefix(search.Query, "id:")) + id = strings.TrimSpace(strings.TrimPrefix(search.Query, "answer:")) for _, r := range id { if !unicode.IsDigit(r) { idSearch = false diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index a89c04e42..2e26006f3 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -256,9 +256,9 @@ func (qr *questionRepo) CmsSearchList(ctx context.Context, search *schema.CmsQue idSearch = false id = "" ) - if strings.Contains(search.Query, "id:") { + if strings.Contains(search.Query, "question:") { idSearch = true - id = strings.TrimSpace(strings.TrimPrefix(search.Query, "id:")) + id = strings.TrimSpace(strings.TrimPrefix(search.Query, "question:")) for _, r := range id { if !unicode.IsDigit(r) { idSearch = false diff --git a/internal/repo/user/user_backyard_repo.go b/internal/repo/user/user_backyard_repo.go index fefe4faa3..ad20794db 100644 --- a/internal/repo/user/user_backyard_repo.go +++ b/internal/repo/user/user_backyard_repo.go @@ -95,9 +95,9 @@ func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, id = "" ) - if strings.Contains(query, "id:") { + if strings.Contains(query, "user:") { idSearch = true - id = strings.TrimSpace(strings.TrimPrefix(query, "id:")) + id = strings.TrimSpace(strings.TrimPrefix(query, "user:")) for _, r := range id { if !unicode.IsDigit(r) { idSearch = false From 05268e820d22b93dde9e1ea904c5c384edbf6278 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 1 Nov 2022 17:03:54 +0800 Subject: [PATCH 0088/3337] fix: gravatar add default params --- ui/src/components/Avatar/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/components/Avatar/index.tsx b/ui/src/components/Avatar/index.tsx index 0436f12ac..9a8c3d6d0 100644 --- a/ui/src/components/Avatar/index.tsx +++ b/ui/src/components/Avatar/index.tsx @@ -17,7 +17,9 @@ const Index: FC = ({ avatar, size, className, searchStr = '' }) => { let url = ''; if (typeof avatar === 'string') { if (avatar.length > 1) { - url = `${avatar}?${searchStr}`; + url = `${avatar}?${searchStr}${ + avatar?.includes('gravatar') ? '&d=identicon' : '' + }`; } } else if (avatar?.type === 'gravatar') { url = `${avatar.gravatar}?${searchStr}&d=identicon`; From 92fbdbbc81f773f36692009af8bd565af15f145f Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Tue, 1 Nov 2022 17:05:12 +0800 Subject: [PATCH 0089/3337] fix user avatar Unmarshal --- internal/schema/user_schema.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 8a0c60a74..079b11c31 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -96,10 +96,8 @@ func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) { r.Status = statusShow } avatarInfo := &AvatarInfo{} - err := json.Unmarshal([]byte(userInfo.Avatar), avatarInfo) - if err != nil { - log.Error("AvatarInfo json.Unmarshal Error", err) - } + _ = json.Unmarshal([]byte(userInfo.Avatar), avatarInfo) + // if json.Unmarshal Error avatarInfo.Type is Empty r.Avatar = avatarInfo } From c3f42f1d9e8debadc857e447dcbd9f37146d79c0 Mon Sep 17 00:00:00 2001 From: kumfo Date: Tue, 1 Nov 2022 17:20:34 +0800 Subject: [PATCH 0090/3337] fix: search sql parse error when query words is empty --- internal/repo/search_common/search_repo.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index dfa79f3c9..b2db83c31 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -67,6 +67,9 @@ 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) { + if words = filterWords(words); len(words) == 0 { + return + } var ( b *builder.Builder ub *builder.Builder @@ -181,6 +184,9 @@ 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) { + if words = filterWords(words); len(words) == 0 { + return + } var ( qfs = qFields args = []interface{}{} @@ -259,6 +265,9 @@ 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) { + if words = filterWords(words); len(words) == 0 { + return + } var ( afs = aFields args = []interface{}{} @@ -475,3 +484,12 @@ func addRelevanceField(searchFields, words, fields []string) (res []string, args res = append(res, "("+strings.Join(relevanceRes, " + ")+") as relevance") return } + +func filterWords(words []string) (res []string) { + for _, word := range words { + if strings.TrimSpace(word) != "" { + res = append(res, word) + } + } + return +} From 984e05edac641e89468115705ec5120a5795ba61 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Tue, 1 Nov 2022 17:35:46 +0800 Subject: [PATCH 0091/3337] doc: update README docker volume path --- README.md | 2 +- README_CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e84fa377c..fcba31c8b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ To learn more about the project, visit [answer.dev](https://answer.dev). ### Running with docker ```bash -docker run -d -p 9080:80 -v $PWD/answer-data/data:/data --name answer answerdev/answer:latest +docker run -d -p 9080:80 -v $PWD/answer-data:/data --name answer answerdev/answer:latest ``` For more information, see [INSTALL.md](./INSTALL.md) diff --git a/README_CN.md b/README_CN.md index 24455ff03..43973008d 100644 --- a/README_CN.md +++ b/README_CN.md @@ -22,7 +22,7 @@ ### 使用 docker 快速搭建 ```bash -docker run -d -p 9080:80 -v $PWD/answer-data/data:/data --name answer answerdev/answer:latest +docker run -d -p 9080:80 -v $PWD/answer-data:/data --name answer answerdev/answer:latest ``` 其他安装配置细节请参考 [INSTALL.md](./INSTALL.md) From c6d703321fc32ca8218759b533e39890e31e9860 Mon Sep 17 00:00:00 2001 From: kumfo Date: Tue, 1 Nov 2022 18:15:18 +0800 Subject: [PATCH 0092/3337] feat: admin's answer list filter by question id --- internal/controller/question_controller.go | 1 + internal/entity/answer_entity.go | 11 ++++++----- internal/repo/answer/answer_repo.go | 7 +++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/controller/question_controller.go b/internal/controller/question_controller.go index ae40d01e6..281a7e1b2 100644 --- a/internal/controller/question_controller.go +++ b/internal/controller/question_controller.go @@ -391,6 +391,7 @@ func (qc *QuestionController) CmsSearchList(ctx *gin.Context) { // @Param page_size query int false "page size" // @Param status query string false "user status" Enums(available,deleted) // @Param query query string false "answer id or question title" +// @Param question_id query string false "question id" // @Success 200 {object} handler.RespBody // @Router /answer/admin/api/answer/page [get] func (qc *QuestionController) CmsSearchAnswerList(ctx *gin.Context) { diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go index 0cc9b31e2..c51ae1577 100644 --- a/internal/entity/answer_entity.go +++ b/internal/entity/answer_entity.go @@ -40,11 +40,12 @@ type AnswerSearch struct { } type CmsAnswerSearch struct { - Page int `json:"page" form:"page"` // Query number of pages - PageSize int `json:"page_size" form:"page_size"` // Search page size - Status int `json:"-" form:"-"` - StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 Deleted - Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string + Page int `json:"page" form:"page"` // Query number of pages + PageSize int `json:"page_size" form:"page_size"` // Search page size + Status int `json:"-" form:"-"` + StatusStr string `json:"status" form:"status"` // Status 1 Available 2 closed 10 Deleted + Query string `validate:"omitempty,gt=0,lte=100" json:"query" form:"query" ` //Query string + QuestionID string `validate:"omitempty,gt=0,lte=24" json:"question_id" form:"question_id" ` //Query string } type AdminSetAnswerStatusRequest struct { diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index a16c695b2..4897f05f0 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -257,6 +257,13 @@ func (ar *answerRepo) CmsSearchList(ctx context.Context, search *entity.CmsAnswe } } + // check search by question id + if len(search.QuestionID) > 0 { + session.And(builder.Eq{ + "question_id": search.QuestionID, + }) + } + offset := search.Page * search.PageSize session. OrderBy("a.updated_at desc"). From 1711ab5753396ef4ac49ff9b8cffc2393c9ef1b7 Mon Sep 17 00:00:00 2001 From: kumfo Date: Tue, 1 Nov 2022 18:18:29 +0800 Subject: [PATCH 0093/3337] feat: admin's answer list filter by question id --- docs/docs.go | 6 ++++++ docs/swagger.json | 6 ++++++ docs/swagger.yaml | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/docs/docs.go b/docs/docs.go index 4162d8a03..f21f508cf 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -62,6 +62,12 @@ const docTemplate = `{ "description": "answer id or question title", "name": "query", "in": "query" + }, + { + "type": "string", + "description": "question id", + "name": "question_id", + "in": "query" } ], "responses": { diff --git a/docs/swagger.json b/docs/swagger.json index e60de534a..039f67f5e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -50,6 +50,12 @@ "description": "answer id or question title", "name": "query", "in": "query" + }, + { + "type": "string", + "description": "question id", + "name": "question_id", + "in": "query" } ], "responses": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 810cfef8c..c419b60ee 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1394,6 +1394,10 @@ paths: in: query name: query type: string + - description: question id + in: query + name: question_id + type: string produces: - application/json responses: From a4d55e629cb2dbd44ab43f8e72ae3a210d4d7f55 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 2 Nov 2022 10:15:08 +0800 Subject: [PATCH 0094/3337] fix: custom avatar need upload --- ui/src/components/Avatar/index.tsx | 4 ++-- ui/src/i18n/locales/en.json | 3 ++- ui/src/pages/Users/Settings/Profile/index.tsx | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ui/src/components/Avatar/index.tsx b/ui/src/components/Avatar/index.tsx index 9a8c3d6d0..f9d5bd6f2 100644 --- a/ui/src/components/Avatar/index.tsx +++ b/ui/src/components/Avatar/index.tsx @@ -21,9 +21,9 @@ const Index: FC = ({ avatar, size, className, searchStr = '' }) => { avatar?.includes('gravatar') ? '&d=identicon' : '' }`; } - } else if (avatar?.type === 'gravatar') { + } else if (avatar?.type === 'gravatar' && avatar.gravatar) { url = `${avatar.gravatar}?${searchStr}&d=identicon`; - } else if (avatar?.type === 'custom') { + } else if (avatar?.type === 'custom' && avatar.custom) { url = `${avatar.custom}?${searchStr}`; } diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 94277ee9e..127e24f24 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -523,7 +523,8 @@ "custom": "Custom", "btn_refresh": "Refresh", "custom_text": "You can upload your image.", - "default": "Default" + "default": "Default", + "msg": "Please upload an avatar" }, "bio": { "label": "About Me (optional)" diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index e5cd9295c..84478fd18 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -120,6 +120,17 @@ const Index: React.FC = () => { }; } + if (formData.avatar.type === 'custom' && !formData.avatar.custom) { + bol = false; + formData.avatar = { + ...formData.avatar, + custom: '', + value: '', + isInvalid: true, + errorMsg: t('avatar.msg'), + }; + } + const reg = /^(http|https):\/\//g; if (website.value && !website.value.match(reg)) { bol = false; @@ -368,6 +379,13 @@ const Index: React.FC = () => { )}
+ + + {formData.avatar.errorMsg} + From e45ecdc0ef9904eabb256f86ed6e95b3d9867527 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 11:12:05 +0800 Subject: [PATCH 0095/3337] refactor: Answer links for improved question management --- ui/src/pages/Admin/Answers/index.tsx | 9 ++++++--- ui/src/services/admin/answer.ts | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 89a0ae26d..aa2c47d49 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -28,6 +28,7 @@ const Answers: FC = () => { const PAGE_SIZE = 20; const curPage = Number(urlSearchParams.get('page')) || 1; const curQuery = urlSearchParams.get('query') || ''; + const questionId = urlSearchParams.get('questionId') || ''; const { t } = useTranslation('translation', { keyPrefix: 'admin.answers' }); const { @@ -39,6 +40,7 @@ const Answers: FC = () => { page: curPage, status: curFilter as Type.AdminContentsFilterBy, query: curQuery, + question_id: questionId, }); const count = listData?.count || 0; @@ -105,12 +107,12 @@ const Answers: FC = () => { style={{ width: '12.25rem' }} />
- +
- + - + {curFilter !== 'deleted' && } @@ -141,6 +143,7 @@ const Answers: FC = () => { __html: li.description, }} className="last-p text-truncate-2 fs-14" + style={{ maxWidth: '30rem' }} /> diff --git a/ui/src/services/admin/answer.ts b/ui/src/services/admin/answer.ts index 6fd0fbb66..cd6bc8249 100644 --- a/ui/src/services/admin/answer.ts +++ b/ui/src/services/admin/answer.ts @@ -4,7 +4,9 @@ import qs from 'qs'; import request from '@answer/utils/request'; import type * as Type from '@answer/common/interface'; -export const useAnswerSearch = (params: Type.AdminContentsReq) => { +export const useAnswerSearch = ( + params: Type.AdminContentsReq & { question_id?: string }, +) => { const apiUrl = `/answer/admin/api/answer/page?${qs.stringify(params)}`; const { data, error, mutate } = useSWR( [apiUrl], From f6545f2de3732cc583af0726e9dbb6ad0ded8871 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 11:12:57 +0800 Subject: [PATCH 0096/3337] refactor: update en.json --- ui/src/i18n/locales/en.json | 45 +++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 94277ee9e..a7d94f6d3 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -759,7 +759,30 @@ "dashboard": { "title": "Dashboard", "welcome": "Welcome to Answer Admin !", - "version": "Version" + "site_statistics": "Site Statistics", + "questions": "Questions:", + "answers": "Answers:", + "comments": "Comments:", + "votes": "Votes:", + "active_users": "Active users:", + "flags": "Flags:", + "site_health_status": "Site Health Status", + "version": "Version:", + "https": "HTTPS:", + "uploading_files": "Uploading files:", + "smtp": "SMTP:", + "timezone": "Timezone:", + "system_info": "System Info", + "storage_used": "Storage used:", + "uptime": "Uptime:", + "answer_links": "Answer Links", + "documents": "Documents", + "feedback": "Feedback", + "review": "Review", + "config": "Config", + "update_to": "Update to", + "latest": "Latest", + "check_failed": "Check failed" }, "flags": { "title": "Flags", @@ -816,7 +839,10 @@ "inactive": "Inactive", "suspended": "Suspended", "deleted": "Deleted", - "normal": "Normal" + "normal": "Normal", + "filter": { + "placeholder": "Filter by name, user:id" + } }, "questions": { "page_title": "Questions", @@ -829,7 +855,10 @@ "created": "Created", "status": "Status", "action": "Action", - "change": "Change" + "change": "Change", + "filter": { + "placeholder": "Filter by title, question:id" + } }, "answers": { "page_title": "Answers", @@ -840,7 +869,10 @@ "created": "Created", "status": "Status", "action": "Action", - "change": "Change" + "change": "Change", + "filter": { + "placeholder": "Filter by title, answer:id" + } }, "general": { "page_title": "General", @@ -876,6 +908,11 @@ "label": "Interface Language", "msg": "Interface language cannot be empty.", "text": "User interface language. It will change when you refresh the page." + }, + "timezone": { + "label": "Timezone", + "msg": "Timezone cannot be empty.", + "text": "Choose a UTC (Coordinated Universal Time) time offset." } }, "smtp": { From 5a3bd799f772ed5a4bcd60c448925482dc282d33 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 11:24:12 +0800 Subject: [PATCH 0097/3337] refactor: refactor: Answer links for improved question management --- ui/src/pages/Admin/Questions/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index c939bc060..c31110012 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; import { Button, Form, Table, Stack, Badge } from 'react-bootstrap'; -import { useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { @@ -156,12 +156,11 @@ const Questions: FC = () => { -

{t('title')}

- - - {step === 1 && ( - <> -
{t('update_title')}
- }} - /> - - - )} +
+ + + +
+

{t('title')}

+ + + {step === 1 && ( + <> +
{t('update_title')}
+ }} + /> + + + )} - {step === 2 && ( - <> -
{t('done_title')}
-

{t('done_desscription')}

- - - )} -
-
- - - + {step === 2 && ( + <> +
{t('done_title')}
+

{t('done_desscription')}

+ + + )} + + + + + + ); }; diff --git a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx index 980a2df30..e249067f6 100644 --- a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx +++ b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx @@ -34,7 +34,7 @@ const Index: FC = ({ visible, tabName, data }) => { : null} -
+
{tabName === 'bookmarks' && ( <> diff --git a/ui/src/pages/Users/Personal/components/NavBar/index.tsx b/ui/src/pages/Users/Personal/components/NavBar/index.tsx index 75fe68eea..4c98ce50b 100644 --- a/ui/src/pages/Users/Personal/components/NavBar/index.tsx +++ b/ui/src/pages/Users/Personal/components/NavBar/index.tsx @@ -44,7 +44,10 @@ const list = [ const Index: FC = ({ slug, tabName = 'overview', isSelf }) => { const { t } = useTranslation('translation', { keyPrefix: 'personal' }); return ( -
+ From 784f9631f7df5a6b8bcfa3ee13ae0a20944f12e2 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:30:02 +0800 Subject: [PATCH 0115/3337] update install api --- internal/base/server/install.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/base/server/install.go b/internal/base/server/install.go index d4b44d903..f2d28bbe1 100644 --- a/internal/base/server/install.go +++ b/internal/base/server/install.go @@ -2,16 +2,28 @@ package server import ( "embed" + "fmt" + "io/fs" "net/http" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/log" ) +const UIStaticPath = "build/static" + type _resource struct { fs embed.FS } +// Open to implement the interface by http.FS required +func (r *_resource) Open(name string) (fs.File, error) { + name = fmt.Sprintf(UIStaticPath+"/%s", name) + log.Debugf("open static path %s", name) + return r.fs.Open(name) +} + // NewHTTPServer new http server. func NewInstallHTTPServer() *gin.Engine { r := gin.New() @@ -20,7 +32,10 @@ func NewInstallHTTPServer() *gin.Engine { r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK??") }) // gin.SetMode(gin.ReleaseMode) - r.StaticFS("/static", http.FS(ui.Build)) + + r.StaticFS("/static", http.FS(&_resource{ + fs: ui.Build, + })) return r } From 847c9a343e139bdb3e4504b14e8193fd789c43ad Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 2 Nov 2022 17:33:04 +0800 Subject: [PATCH 0116/3337] fix: path alias --- ui/src/pages/Install/components/FirstStep/index.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx index 2a2226d6d..5690ca8e1 100644 --- a/ui/src/pages/Install/components/FirstStep/index.tsx +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -2,13 +2,8 @@ import { FC, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type { - LangsType, - FormValue, - FormDataType, -} from '@answer/common/interface'; +import type { LangsType, FormValue, FormDataType } from '@/common/interface'; import Progress from '../Progress'; - import { languages } from '@/services'; interface Props { From 6ece061e375743d64da3d10462b9acf671c85326 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:40:11 +0800 Subject: [PATCH 0117/3337] update install api --- internal/base/server/install.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/base/server/install.go b/internal/base/server/install.go index f2d28bbe1..db1bcc629 100644 --- a/internal/base/server/install.go +++ b/internal/base/server/install.go @@ -37,5 +37,23 @@ func NewInstallHTTPServer() *gin.Engine { fs: ui.Build, })) + installApi := r.Group("") + installApi.GET("/install", Install) + return r } + +func Install(c *gin.Context) { + filePath := "" + var file []byte + var err error + filePath = "build/index.html" + c.Header("content-type", "text/html;charset=utf-8") + file, err = ui.Build.ReadFile(filePath) + if err != nil { + log.Error(err) + c.Status(http.StatusNotFound) + return + } + c.String(http.StatusOK, string(file)) +} From cd7f80a189441c87791dce16819a9c6ae359d9d7 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:55:58 +0800 Subject: [PATCH 0118/3337] add demo install api --- internal/router/ui.go | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/internal/router/ui.go b/internal/router/ui.go index 3498265c6..3350c6be2 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -7,6 +7,7 @@ import ( "net/http" "os" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -66,6 +67,55 @@ func (a *UIRouter) Register(r *gin.Engine) { fs: ui.Build, })) + // Install godoc + // @Summary Install + // @Description Install + // @Tags Install + // @Accept json + // @Produce json + // @Success 200 {object} handler.RespBody{} + // @Router /install [get] + r.GET("/install", func(c *gin.Context) { + filePath := "" + var file []byte + var err error + filePath = "build/index.html" + c.Header("content-type", "text/html;charset=utf-8") + file, err = ui.Build.ReadFile(filePath) + if err != nil { + log.Error(err) + c.Status(http.StatusNotFound) + return + } + c.String(http.StatusOK, string(file)) + }) + + // Install godoc + // @Summary Install + // @Description Install + // @Tags Install + // @Accept json + // @Produce json + // @Param data body schema.FollowReq true "follow" + // @Success 200 {object} handler.RespBody{} + // @Router /install/db/check [put] + r.PUT("/install/db/check", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + // Install godoc + // @Summary Install + // @Description Install + // @Tags Install + // @Accept json + // @Produce json + // @Param data body schema.FollowReq true "follow" + // @Success 200 {object} handler.RespBody{} + // @Router /install [put] + r.PUT("/install", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + // specify the not router for default routes and redirect r.NoRoute(func(c *gin.Context) { name := c.Request.URL.Path From 1556d815cfbb4fadf393568f603926bdead8b514 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:02:23 +0800 Subject: [PATCH 0119/3337] add install api --- internal/router/ui.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/router/ui.go b/internal/router/ui.go index 655d03a09..d74fbe6cb 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -7,9 +7,8 @@ import ( "net/http" "os" - - "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/i18n" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -119,6 +118,10 @@ func (a *UIRouter) Register(r *gin.Engine) { handler.HandleResponse(c, nil, gin.H{}) }) + r.PUT("/install/siteconfig", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + // specify the not router for default routes and redirect r.NoRoute(func(c *gin.Context) { name := c.Request.URL.Path From cfcc4c69e3077adbfbbde9189715e1779526969d Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:07:59 +0800 Subject: [PATCH 0120/3337] update dashboardInfo --- internal/schema/dashboard_schema.go | 5 +++-- internal/service/dashboard/dashboard_service.go | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index f14c9e9ba..74eb8ca95 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -7,8 +7,9 @@ type DashboardInfo struct { VoteCount int64 `json:"vote_count"` UserCount int64 `json:"user_count"` ReportCount int64 `json:"report_count"` - UploadingFiles string `json:"uploading_files"` //Allowed or Not allowed - SMTP string `json:"smtp"` //Enabled or Disabled + UploadingFiles bool `json:"uploading_files"` + SMTP bool `json:"smtp"` + HTTPS bool `json:"https"` TimeZone string `json:"time_zone"` OccupyingStorageSpace string `json:"occupying_storage_space"` AppStartTime string `json:"app_start_time"` diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index aaeabcfce..2abfa4159 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -97,8 +97,9 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.UserCount = userCount dashboardInfo.ReportCount = reportCount - dashboardInfo.UploadingFiles = "Allowed" - dashboardInfo.SMTP = "Enabled" + dashboardInfo.UploadingFiles = true + dashboardInfo.SMTP = true + dashboardInfo.HTTPS = true dashboardInfo.OccupyingStorageSpace = "1MB" dashboardInfo.AppStartTime = "102" return dashboardInfo, nil From 44da3c24fd806c1536efe39448f7ccffce807cad Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 18:16:27 +0800 Subject: [PATCH 0121/3337] feat(admin): dashboard module --- ui/src/common/interface.ts | 18 +++++++ .../components/AnswerLinks/index.tsx | 31 +++++++++++ .../components/HealthStatus/index.tsx | 54 +++++++++++++++++++ .../Dashboard/components/Statistics/index.tsx | 51 ++++++++++++++++++ .../Dashboard/components/SystemInfo/index.tsx | 34 ++++++++++++ .../pages/Admin/Dashboard/components/index.ts | 6 +++ ui/src/pages/Admin/Dashboard/index.tsx | 29 ++++++++++ ui/src/services/admin/settings.ts | 13 +++++ ui/src/utils/common.ts | 16 ++++++ 9 files changed, 252 insertions(+) create mode 100644 ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx create mode 100644 ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx create mode 100644 ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx create mode 100644 ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx create mode 100644 ui/src/pages/Admin/Dashboard/components/index.ts diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 732964d1e..98f7c0466 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -323,3 +323,21 @@ export interface SearchResItem { export interface SearchRes extends ListResult { extra: any; } + +export interface AdminDashboard { + info: { + question_count: number; + answer_count: number; + comment_count: number; + vote_count: number; + user_count: number; + report_count: number; + uploading_files: boolean; + smtp: boolean; + time_zone: string; + occupying_storage_space: string; + app_start_time: number; + app_version: string; + https: boolean; + }; +} diff --git a/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx new file mode 100644 index 000000000..8f5048f81 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/AnswerLinks/index.tsx @@ -0,0 +1,31 @@ +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +const AnswerLinks = () => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('answer_links')}
+ +
+ + {t('documents')} + + + + + {t('feedback')} + + + + + + ); +}; + +export default AnswerLinks; diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx new file mode 100644 index 000000000..72c3b9a60 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -0,0 +1,54 @@ +import { FC } from 'react'; +import { Card, Row, Col, Badge } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; + +import type * as Type from '@answer/common/interface'; + +interface IProps { + data: Type.AdminDashboard['info']; +} + +const HealthStatus: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('site_health_status')}
+ +
+ {t('version')} + 90 + + {t('update_to')} {data.app_version} + + + + {t('https')} + {data.https ? t('yes') : t('yes')} + + + {t('uploading_files')} + + {data.uploading_files ? t('allowed') : t('not_allowed')} + + + + {t('smtp')} + {data.smtp ? t('enabled') : t('disabled')} + + {t('config')} + + + + {t('timezone')} + {data.time_zone} + + + + + ); +}; + +export default HealthStatus; diff --git a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx new file mode 100644 index 000000000..43e1fa2dc --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx @@ -0,0 +1,51 @@ +import { FC } from 'react'; +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@answer/common/interface'; + +interface IProps { + data: Type.AdminDashboard['info']; +} +const Statistics: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('site_statistics')}
+ +
+ {t('questions')} + {data.question_count} + + + {t('answers')} + {data.answer_count} + + + {t('comments')} + {data.comment_count} + + + {t('votes')} + {data.vote_count} + + + {t('active_users')} + {data.user_count} + + + {t('flags')} + {data.report_count} + + {t('review')} + + + + + + ); +}; + +export default Statistics; diff --git a/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx new file mode 100644 index 000000000..2bf222f54 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react'; +import { Card, Row, Col } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@answer/common/interface'; + +import { formatUptime } from '@/utils'; + +interface IProps { + data: Type.AdminDashboard['info']; +} +const SystemInfo: FC = ({ data }) => { + const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + + return ( + + +
{t('system_info')}
+ +
+ {t('storage_used')} + {data.occupying_storage_space} + + + {t('uptime')} + {formatUptime(data.app_start_time)} + + + + + ); +}; + +export default SystemInfo; diff --git a/ui/src/pages/Admin/Dashboard/components/index.ts b/ui/src/pages/Admin/Dashboard/components/index.ts new file mode 100644 index 000000000..877f643f2 --- /dev/null +++ b/ui/src/pages/Admin/Dashboard/components/index.ts @@ -0,0 +1,6 @@ +import SystemInfo from './SystemInfo'; +import Statistics from './Statistics'; +import AnswerLinks from './AnswerLinks'; +import HealthStatus from './HealthStatus'; + +export { SystemInfo, Statistics, AnswerLinks, HealthStatus }; diff --git a/ui/src/pages/Admin/Dashboard/index.tsx b/ui/src/pages/Admin/Dashboard/index.tsx index 45c19721d..473938311 100644 --- a/ui/src/pages/Admin/Dashboard/index.tsx +++ b/ui/src/pages/Admin/Dashboard/index.tsx @@ -1,12 +1,41 @@ import { FC } from 'react'; +import { Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import { + AnswerLinks, + HealthStatus, + Statistics, + SystemInfo, +} from './components'; + +import { useDashBoard } from '@/services'; + const Dashboard: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); + const { data } = useDashBoard(); + + if (!data) { + return null; + } return ( <>

{t('title')}

{t('welcome')}

+ + + + + + + + + + + + + + {process.env.REACT_APP_VERSION && (

{`${t('version')} `} diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index 274e24eb0..5f3702608 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -70,3 +70,16 @@ export const updateSmtpSetting = (params: Type.AdminSettingsSmtp) => { const apiUrl = `/answer/admin/api/setting/smtp`; return request.put(apiUrl, params); }; + +export const useDashBoard = () => { + const apiUrl = `/answer/admin/api/dashboard`; + const { data, error } = useSWR( + [apiUrl], + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + }; +}; diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index 169dc1ad0..dcc2d0840 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -1,3 +1,5 @@ +import i18next from 'i18next'; + function getQueryString(name: string): string { const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`); const r = window.location.search.substr(1).match(reg); @@ -70,6 +72,19 @@ function parseUserInfo(markdown) { return markdown.replace(globalReg, '[@$1](/u/$1)'); } +function formatUptime(value) { + const t = i18next.t.bind(i18next); + const second = parseInt(value, 10); + + if (second > 60 * 60 && second < 60 * 60 * 24) { + return `${Math.floor(second / 3600)} ${t('dates.hour')}`; + } + if (second > 60 * 60 * 24) { + return `${Math.floor(second / 3600 / 24)} ${t('dates.day')}`; + } + + return `< 1 ${t('dates.hour')}`; +} export { getQueryString, thousandthDivision, @@ -77,4 +92,5 @@ export { scrollTop, matchedUsers, parseUserInfo, + formatUptime, }; From 364a5a0e189b9defd0981fd7eba2a889cc5b565a Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 18:16:54 +0800 Subject: [PATCH 0122/3337] refactor(i18n): update en.json --- ui/src/i18n/locales/en.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index ec9012b2c..8bc92db1c 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -293,7 +293,9 @@ "now": "now", "x_seconds_ago": "{{count}}s ago", "x_minutes_ago": "{{count}}m ago", - "x_hours_ago": "{{count}}h ago" + "x_hours_ago": "{{count}}h ago", + "hour": "hour", + "day": "day" }, "comment": { "btn_add_comment": "Add comment", @@ -866,7 +868,13 @@ "config": "Config", "update_to": "Update to", "latest": "Latest", - "check_failed": "Check failed" + "check_failed": "Check failed", + "yes": "Yes", + "no": "No", + "not_allowed": "Not allowed", + "allowed": "Allowed", + "enabled": "Enabled", + "disabled": "Disabled" }, "flags": { "title": "Flags", From 41ff3dbbc798a0e7ca5717022204811ab0ba1111 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 18:19:05 +0800 Subject: [PATCH 0123/3337] refactor: Modify the import package path --- ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx | 2 +- ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx | 2 +- ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx index 72c3b9a60..1b1c7ced3 100644 --- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -3,7 +3,7 @@ import { Card, Row, Col, Badge } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import type * as Type from '@answer/common/interface'; +import type * as Type from '@/common/interface'; interface IProps { data: Type.AdminDashboard['info']; diff --git a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx index 43e1fa2dc..9e6c979ea 100644 --- a/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/Statistics/index.tsx @@ -2,7 +2,7 @@ import { FC } from 'react'; import { Card, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; +import type * as Type from '@/common/interface'; interface IProps { data: Type.AdminDashboard['info']; diff --git a/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx index 2bf222f54..cbc065c7b 100644 --- a/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/SystemInfo/index.tsx @@ -2,8 +2,7 @@ import { FC } from 'react'; import { Card, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; - +import type * as Type from '@/common/interface'; import { formatUptime } from '@/utils'; interface IProps { From 549f816d3e6a62749353fbdab5ceabec06759547 Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 18:24:26 +0800 Subject: [PATCH 0124/3337] style: format --- ui/src/pages/Admin/Dashboard/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/Admin/Dashboard/index.tsx b/ui/src/pages/Admin/Dashboard/index.tsx index 473938311..2037016e9 100644 --- a/ui/src/pages/Admin/Dashboard/index.tsx +++ b/ui/src/pages/Admin/Dashboard/index.tsx @@ -2,6 +2,8 @@ import { FC } from 'react'; import { Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import { useDashBoard } from '@/services'; + import { AnswerLinks, HealthStatus, @@ -9,8 +11,6 @@ import { SystemInfo, } from './components'; -import { useDashBoard } from '@/services'; - const Dashboard: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); const { data } = useDashBoard(); From 6f999a5a289aeb82005db95edf9894c9a7518196 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:33:23 +0800 Subject: [PATCH 0125/3337] update install api --- cmd/answer/command.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/answer/command.go b/cmd/answer/command.go index 276554434..072131347 100644 --- a/cmd/answer/command.go +++ b/cmd/answer/command.go @@ -4,7 +4,6 @@ import ( "fmt" "os" - "github.com/answerdev/answer/internal/base/server" "github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/migrations" "github.com/spf13/cobra" @@ -60,8 +59,8 @@ To run answer, use: Short: "init answer application", Long: `init answer application`, Run: func(_ *cobra.Command, _ []string) { - installwebapi := server.NewInstallHTTPServer() - installwebapi.Run(":8088") + // installwebapi := server.NewInstallHTTPServer() + // installwebapi.Run(":8088") cli.InstallAllInitialEnvironment(dataDirPath) c, err := readConfig() if err != nil { From 2ad8b8f4a992198ff4ec639a2b7321d06918b271 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 10:33:48 +0800 Subject: [PATCH 0126/3337] style: reformat get site info code --- internal/base/constant/constant.go | 5 +++ internal/base/reason/reason.go | 1 + internal/controller/siteinfo_controller.go | 25 ++++-------- internal/router/answer_api_router.go | 2 +- internal/service/siteinfo_service.go | 46 ++++++++++------------ 5 files changed, 36 insertions(+), 43 deletions(-) diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 7722adf87..5a1827161 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -47,3 +47,8 @@ var ( 8: ReportObjectType, } ) + +const ( + SiteTypeGeneral = "general" + SiteTypeInterface = "interface" +) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index abe1bd23a..30e9f532f 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -38,4 +38,5 @@ const ( LangNotFound = "error.lang.not_found" ReportHandleFailed = "error.report.handle_failed" ReportNotFound = "error.report.not_found" + SiteInfoNotFound = "error.site_info.not_found" ) diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index abcbc1782..7148b7ad2 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -18,30 +18,21 @@ func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoCo } } -// GetInfo godoc -// @Summary Get siteinfo -// @Description Get siteinfo +// GetSiteInfo get site info +// @Summary get site info +// @Description get site info // @Tags site // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} // @Router /answer/api/v1/siteinfo [get] -func (sc *SiteinfoController) GetInfo(ctx *gin.Context) { - var ( - resp = &schema.SiteInfoResp{} - general schema.SiteGeneralResp - face schema.SiteInterfaceResp - err error - ) - - general, err = sc.siteInfoService.GetSiteGeneral(ctx) - resp.General = &general +func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) { + var err error + resp := &schema.SiteInfoResp{} + resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) if err != nil { handler.HandleResponse(ctx, err, resp) return } - - face, err = sc.siteInfoService.GetSiteInterface(ctx) - resp.Face = &face - + resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 56a972a8e..dac54f339 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -131,7 +131,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) //siteinfo - r.GET("/siteinfo", a.siteinfoController.GetInfo) + r.GET("/siteinfo", a.siteinfoController.GetSiteInfo) } func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go index a4f23fd3d..096314939 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo_service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" @@ -25,38 +26,33 @@ func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService } } -func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) { - var ( - siteType = "general" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteGeneralResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) +// GetSiteGeneral get site info general +func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } if !exist { - return + return nil, errors.BadRequest(reason.SiteInfoNotFound) } - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + resp = &schema.SiteGeneralResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } -func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) { - var ( - siteType = "interface" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteInterfaceResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) +// GetSiteInterface get site info interface +func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err + } if !exist { - return + return nil, errors.BadRequest(reason.SiteInfoNotFound) } - - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { From d091d8d5667770bd1c61fb6c0c29660f08f49ccd Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 15:02:27 +0800 Subject: [PATCH 0127/3337] feat: add time zone config --- docs/docs.go | 30 ++++++++++++------- docs/swagger.json | 30 ++++++++++++------- docs/swagger.yaml | 24 ++++++++++----- .../siteinfo_controller.go | 24 +++++++-------- internal/schema/siteinfo_schema.go | 13 ++++---- 5 files changed, 75 insertions(+), 46 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 4f0bfb5ac..16ac77ca9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -509,14 +509,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -544,14 +544,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -580,14 +580,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "get site interface", "parameters": [ { "description": "general", @@ -626,14 +626,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -5348,7 +5348,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5362,6 +5363,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5369,7 +5374,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5383,6 +5389,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/swagger.json b/docs/swagger.json index e856d606d..8476352cb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -497,14 +497,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -532,14 +532,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -568,14 +568,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "get site interface", "parameters": [ { "description": "general", @@ -614,14 +614,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -5336,7 +5336,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5350,6 +5351,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5357,7 +5362,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5371,6 +5377,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index adc19f984..81c96d96c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1024,9 +1024,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.SiteInterfaceResp: properties: @@ -1039,9 +1043,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.TagItem: properties: @@ -1675,7 +1683,7 @@ paths: - admin /answer/admin/api/siteinfo/general: get: - description: Get siteinfo general + description: get site general information produces: - application/json responses: @@ -1690,11 +1698,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo general + summary: get site general information tags: - admin put: - description: Get siteinfo interface + description: update site general information parameters: - description: general in: body @@ -1711,12 +1719,12 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site general information tags: - admin /answer/admin/api/siteinfo/interface: get: - description: Get siteinfo interface + description: get site interface parameters: - description: general in: body @@ -1738,11 +1746,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: get site interface tags: - admin put: - description: Get siteinfo interface + description: update site info interface parameters: - description: general in: body @@ -1759,7 +1767,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site info interface tags: - admin /answer/admin/api/theme/options: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 30bfd1bfd..821b517e1 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -18,9 +18,9 @@ func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoCo } } -// GetGeneral godoc -// @Summary Get siteinfo general -// @Description Get siteinfo general +// GetGeneral get site general information +// @Summary get site general information +// @Description get site general information // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -31,9 +31,9 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// GetInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// GetInterface get site interface +// @Summary get site interface +// @Description get site interface // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -45,9 +45,9 @@ func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// UpdateGeneral godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// UpdateGeneral update site general information +// @Summary update site general information +// @Description update site general information // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -63,9 +63,9 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) } -// UpdateInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// UpdateInterface update site interface +// @Summary update site info interface +// @Description update site info interface // @Security ApiKeyAuth // @Tags admin // @Produce json diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index ff93e72d7..446b986db 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -2,16 +2,17 @@ package schema // SiteGeneralReq site general request type SiteGeneralReq struct { - Name string `validate:"required,gt=1,lte=128" comment:"site name" form:"name" json:"name"` - ShortDescription string `validate:"required,gt=3,lte=255" comment:"short site description" form:"short_description" json:"short_description"` - Description string `validate:"required,gt=3,lte=2000" comment:"site description" form:"description" json:"description"` + Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` + ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` + Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` } // SiteInterfaceReq site interface request type SiteInterfaceReq struct { - Logo string `validate:"omitempty,gt=0,lte=256" comment:"logo" form:"logo" json:"logo"` - Theme string `validate:"required,gt=1,lte=128" comment:"theme" form:"theme" json:"theme"` - Language string `validate:"required,gt=1,lte=128" comment:"interface language" form:"language" json:"language"` + 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"` } // SiteGeneralResp site general response From b2256a5d1b1df5e6fcfbc797f1ab53c9a39cdbc4 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 16:01:17 +0800 Subject: [PATCH 0128/3337] fix: swagger wrong request parameter --- docs/docs.go | 11 ----------- docs/swagger.json | 11 ----------- docs/swagger.yaml | 7 ------- internal/controller_backyard/siteinfo_controller.go | 1 - 4 files changed, 30 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 16ac77ca9..1b81becfa 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -588,17 +588,6 @@ const docTemplate = `{ "admin" ], "summary": "get site interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], "responses": { "200": { "description": "OK", diff --git a/docs/swagger.json b/docs/swagger.json index 8476352cb..425eb9485 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -576,17 +576,6 @@ "admin" ], "summary": "get site interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], "responses": { "200": { "description": "OK", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 81c96d96c..c8db90c22 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1725,13 +1725,6 @@ paths: /answer/admin/api/siteinfo/interface: get: description: get site interface - parameters: - - description: general - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.AddCommentReq' produces: - application/json responses: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 821b517e1..339765aae 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -39,7 +39,6 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp} // @Router /answer/admin/api/siteinfo/interface [get] -// @Param data body schema.AddCommentReq true "general" func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) From b7f7fb7201258a7e11fe7779d50706dd1d65e683 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 10:33:48 +0800 Subject: [PATCH 0129/3337] style: reformat get site info code --- internal/base/constant/constant.go | 5 +++ internal/base/reason/reason.go | 1 + internal/controller/siteinfo_controller.go | 25 ++++-------- internal/router/answer_api_router.go | 2 +- internal/service/siteinfo_service.go | 46 ++++++++++------------ 5 files changed, 36 insertions(+), 43 deletions(-) diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 7722adf87..5a1827161 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -47,3 +47,8 @@ var ( 8: ReportObjectType, } ) + +const ( + SiteTypeGeneral = "general" + SiteTypeInterface = "interface" +) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index abe1bd23a..30e9f532f 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -38,4 +38,5 @@ const ( LangNotFound = "error.lang.not_found" ReportHandleFailed = "error.report.handle_failed" ReportNotFound = "error.report.not_found" + SiteInfoNotFound = "error.site_info.not_found" ) diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index abcbc1782..7148b7ad2 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -18,30 +18,21 @@ func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoCo } } -// GetInfo godoc -// @Summary Get siteinfo -// @Description Get siteinfo +// GetSiteInfo get site info +// @Summary get site info +// @Description get site info // @Tags site // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} // @Router /answer/api/v1/siteinfo [get] -func (sc *SiteinfoController) GetInfo(ctx *gin.Context) { - var ( - resp = &schema.SiteInfoResp{} - general schema.SiteGeneralResp - face schema.SiteInterfaceResp - err error - ) - - general, err = sc.siteInfoService.GetSiteGeneral(ctx) - resp.General = &general +func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) { + var err error + resp := &schema.SiteInfoResp{} + resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) if err != nil { handler.HandleResponse(ctx, err, resp) return } - - face, err = sc.siteInfoService.GetSiteInterface(ctx) - resp.Face = &face - + resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index dbe34405b..0415222b9 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -134,7 +134,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) //siteinfo - r.GET("/siteinfo", a.siteinfoController.GetInfo) + r.GET("/siteinfo", a.siteinfoController.GetSiteInfo) } func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go index a4f23fd3d..096314939 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo_service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" @@ -25,38 +26,33 @@ func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService } } -func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) { - var ( - siteType = "general" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteGeneralResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) +// GetSiteGeneral get site info general +func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } if !exist { - return + return nil, errors.BadRequest(reason.SiteInfoNotFound) } - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + resp = &schema.SiteGeneralResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } -func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) { - var ( - siteType = "interface" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteInterfaceResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) +// GetSiteInterface get site info interface +func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err + } if !exist { - return + return nil, errors.BadRequest(reason.SiteInfoNotFound) } - - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { From b1d2eb0c1d9f23c21859c1fa39b09ebd9bafeab5 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 11:12:20 +0800 Subject: [PATCH 0130/3337] feat: dashboard add time zone --- cmd/answer/wire_gen.go | 9 ++-- internal/controller/siteinfo_controller.go | 6 +-- .../siteinfo_controller.go | 6 +-- .../service/dashboard/dashboard_service.go | 40 +++++++++------ internal/service/provider.go | 5 +- .../{ => siteinfo}/siteinfo_service.go | 2 +- internal/service/siteinfo_common/siteinfo.go | 1 + .../siteinfo_common/siteinfo_service.go | 50 +++++++++++++++++++ 8 files changed, 93 insertions(+), 26 deletions(-) rename internal/service/{ => siteinfo}/siteinfo_service.go (99%) create mode 100644 internal/service/siteinfo_common/siteinfo_service.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 2d9025046..67a921fa1 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -59,6 +59,8 @@ import ( "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" "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" "github.com/answerdev/answer/internal/service/uploader" @@ -149,7 +151,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo) + siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) @@ -168,9 +171,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_backyard.NewThemeController() - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) + siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) - siteinfoController := controller.NewSiteinfoController(siteInfoService) + siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index 7148b7ad2..5f547eb79 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -3,16 +3,16 @@ package controller import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" ) type SiteinfoController struct { - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewSiteinfoController new siteinfo controller. -func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController { +func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController { return &SiteinfoController{ siteInfoService: siteInfoService, } diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 339765aae..3a554c226 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -3,16 +3,16 @@ package controller_backyard import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo" "github.com/gin-gonic/gin" ) type SiteInfoController struct { - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo.SiteInfoService } // NewSiteInfoController new siteinfo controller. -func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController { +func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController { return &SiteInfoController{ siteInfoService: siteInfoService, } diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index 2abfa4159..fcea03dd1 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -10,17 +10,19 @@ import ( "github.com/answerdev/answer/internal/service/config" questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/report_common" + "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" ) type DashboardService struct { - questionRepo questioncommon.QuestionRepo - answerRepo answercommon.AnswerRepo - commentRepo comment_common.CommentCommonRepo - voteRepo activity_common.VoteRepo - userRepo usercommon.UserRepo - reportRepo report_common.ReportRepo - configRepo config.ConfigRepo + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + commentRepo comment_common.CommentCommonRepo + voteRepo activity_common.VoteRepo + userRepo usercommon.UserRepo + reportRepo report_common.ReportRepo + configRepo config.ConfigRepo + siteInfoService *siteinfo_common.SiteInfoCommonService } func NewDashboardService( @@ -31,16 +33,17 @@ func NewDashboardService( userRepo usercommon.UserRepo, reportRepo report_common.ReportRepo, configRepo config.ConfigRepo, - + siteInfoService *siteinfo_common.SiteInfoCommonService, ) *DashboardService { return &DashboardService{ - questionRepo: questionRepo, - answerRepo: answerRepo, - commentRepo: commentRepo, - voteRepo: voteRepo, - userRepo: userRepo, - reportRepo: reportRepo, - configRepo: configRepo, + questionRepo: questionRepo, + answerRepo: answerRepo, + commentRepo: commentRepo, + voteRepo: voteRepo, + userRepo: userRepo, + reportRepo: reportRepo, + configRepo: configRepo, + siteInfoService: siteInfoService, } } @@ -90,6 +93,12 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI if err != nil { return dashboardInfo, err } + + siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx) + if err != nil { + return dashboardInfo, err + } + dashboardInfo.QuestionCount = questionCount dashboardInfo.AnswerCount = answerCount dashboardInfo.CommentCount = commentCount @@ -102,5 +111,6 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.HTTPS = true dashboardInfo.OccupyingStorageSpace = "1MB" dashboardInfo.AppStartTime = "102" + dashboardInfo.TimeZone = siteInfoInterface.TimeZone return dashboardInfo, nil } diff --git a/internal/service/provider.go b/internal/service/provider.go index 4562572aa..5c0e4ab9f 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -22,6 +22,8 @@ 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/siteinfo" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/internal/service/tag" tagcommon "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/uploader" @@ -62,7 +64,8 @@ var ProviderSetService = wire.NewSet( report_backyard.NewReportBackyardService, user_backyard.NewUserBackyardService, reason.NewReasonService, - NewSiteInfoService, + siteinfo_common.NewSiteInfoCommonService, + siteinfo.NewSiteInfoService, notficationcommon.NewNotificationCommon, notification.NewNotificationService, activity.NewAnswerActivityService, diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go similarity index 99% rename from internal/service/siteinfo_service.go rename to internal/service/siteinfo/siteinfo_service.go index 096314939..cc84ba32d 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -1,4 +1,4 @@ -package service +package siteinfo import ( "context" diff --git a/internal/service/siteinfo_common/siteinfo.go b/internal/service/siteinfo_common/siteinfo.go index ff9066b09..1c501d2dd 100644 --- a/internal/service/siteinfo_common/siteinfo.go +++ b/internal/service/siteinfo_common/siteinfo.go @@ -2,6 +2,7 @@ package siteinfo_common import ( "context" + "github.com/answerdev/answer/internal/entity" ) diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go new file mode 100644 index 000000000..5dd565d0f --- /dev/null +++ b/internal/service/siteinfo_common/siteinfo_service.go @@ -0,0 +1,50 @@ +package siteinfo_common + +import ( + "context" + "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 { + siteInfoRepo SiteInfoRepo +} + +func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService { + return &SiteInfoCommonService{ + siteInfoRepo: siteInfoRepo, + } +} + +// GetSiteGeneral get site info general +func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + + 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) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil +} From 0306ee01edd197508ee03864f23fa32daf711080 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 3 Nov 2022 11:46:54 +0800 Subject: [PATCH 0131/3337] update install api --- internal/base/server/install.go | 17 +++++++++++++++++ internal/router/ui.go | 28 +++++++--------------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/internal/base/server/install.go b/internal/base/server/install.go index db1bcc629..da5652ccf 100644 --- a/internal/base/server/install.go +++ b/internal/base/server/install.go @@ -6,6 +6,7 @@ import ( "io/fs" "net/http" + "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -40,6 +41,22 @@ func NewInstallHTTPServer() *gin.Engine { installApi := r.Group("") installApi.GET("/install", Install) + installApi.POST("/installation/db/check", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + installApi.POST("/installation/config-file/check", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + installApi.POST("/installation/init", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + installApi.POST("/installation/base-info", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + return r } diff --git a/internal/router/ui.go b/internal/router/ui.go index d74fbe6cb..99bcaf07b 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -92,33 +92,19 @@ func (a *UIRouter) Register(r *gin.Engine) { c.String(http.StatusOK, string(file)) }) - // Install godoc - // @Summary Install - // @Description Install - // @Tags Install - // @Accept json - // @Produce json - // @Param data body schema.FollowReq true "follow" - // @Success 200 {object} handler.RespBody{} - // @Router /install/db/check [put] - r.PUT("/install/db/check", func(c *gin.Context) { + r.POST("/installation/db/check", func(c *gin.Context) { handler.HandleResponse(c, nil, gin.H{}) }) - // Install godoc - // @Summary Install - // @Description Install - // @Tags Install - // @Accept json - // @Produce json - // @Param data body schema.FollowReq true "follow" - // @Success 200 {object} handler.RespBody{} - // @Router /install [put] - r.PUT("/install", func(c *gin.Context) { + r.POST("/installation/config-file/check", func(c *gin.Context) { + handler.HandleResponse(c, nil, gin.H{}) + }) + + r.POST("/installation/init", func(c *gin.Context) { handler.HandleResponse(c, nil, gin.H{}) }) - r.PUT("/install/siteconfig", func(c *gin.Context) { + r.POST("/installation/base-info", func(c *gin.Context) { handler.HandleResponse(c, nil, gin.H{}) }) From 08d1facf2264b74ef6bf23d970ae7b178d85aabe Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 14:26:11 +0800 Subject: [PATCH 0132/3337] fix: To regenerate the wire --- cmd/answer/wire_gen.go | 3 +-- internal/controller/lang_controller.go | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 57f5a5f61..0fc320991 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -96,7 +96,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, configRepo := config.NewConfigRepo(dataData) emailRepo := export.NewEmailRepo(dataData) emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) + siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService) langController := controller.NewLangController(i18nTranslator, siteInfoService) authRepo := auth.NewAuthRepo(dataData) authService := auth2.NewAuthService(authRepo) @@ -172,7 +172,6 @@ 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) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 384d711c0..ed17d5413 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -5,18 +5,18 @@ import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { translator i18n.Translator - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo.SiteInfoService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController { +func NewLangController(tr i18n.Translator, siteInfoService *siteinfo.SiteInfoService) *LangController { return &LangController{translator: tr, siteInfoService: siteInfoService} } From 4d8e4b5ca4c5b0dbc9d3d58e6f884cc5539266ae Mon Sep 17 00:00:00 2001 From: fen Date: Thu, 3 Nov 2022 14:36:38 +0800 Subject: [PATCH 0133/3337] fix typo --- ui/src/components/Footer/index.tsx | 2 +- ui/src/i18n/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/components/Footer/index.tsx b/ui/src/components/Footer/index.tsx index 920da0d40..916fe094b 100644 --- a/ui/src/components/Footer/index.tsx +++ b/ui/src/components/Footer/index.tsx @@ -10,7 +10,7 @@ const Index = () => { Built on Answer - - the open-source software that power Q&A communities. + - the open-source software that powers Q&A communities.
Made with love. © 2022 Answer .
diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 81e349264..bbb012674 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -403,7 +403,7 @@ } }, "footer": { - "build_on": "Build on <1> Answer - the open-source software that power Q&A communities.
Made with love. © 2022 Answer." + "build_on": "Built on <1> Answer - the open-source software that powers Q&A communities.
Made with love. © 2022 Answer." }, "upload_img": { "name": "Change", From 1bcd3abbcbce947a0e6c3bd9d53b28735ea9ce7b Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 14:40:43 +0800 Subject: [PATCH 0134/3337] feat: add default language option --- internal/base/translator/provider.go | 12 ++++++++++-- internal/controller/lang_controller.go | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index a81576bd5..e047ceb28 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -21,8 +21,13 @@ type LangOption struct { Value string `json:"value"` } -// LanguageOptions language -var LanguageOptions []*LangOption +// DefaultLangOption default language option. If user config the language is default, the language option is admin choose. +const DefaultLangOption = "Default" + +var ( + // LanguageOptions language + LanguageOptions []*LangOption +) // NewTranslator new a translator func NewTranslator(c *I18n) (tr i18n.Translator, err error) { @@ -49,6 +54,9 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) { // CheckLanguageIsValid check user input language is valid func CheckLanguageIsValid(lang string) bool { + if lang == DefaultLangOption { + return true + } for _, option := range LanguageOptions { if option.Value == lang { return true diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 384d711c0..8d4242db2 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -64,7 +64,7 @@ func (u *LangController) GetUserLangOptions(ctx *gin.Context) { options := translator.LanguageOptions if len(siteInterfaceResp.Language) > 0 { defaultOption := []*translator.LangOption{ - {Label: "Default", Value: siteInterfaceResp.Language}, + {Label: translator.DefaultLangOption, Value: siteInterfaceResp.Language}, } options = append(defaultOption, options...) } From 0ce3b14bd25266c8c6c463c57dec4af29d961901 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 16:01:17 +0800 Subject: [PATCH 0135/3337] fix: swagger wrong request parameter --- docs/docs.go | 152 +++++++----------- docs/swagger.json | 152 +++++++----------- docs/swagger.yaml | 100 +++++------- .../siteinfo_controller.go | 1 - 4 files changed, 153 insertions(+), 252 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 7a37d7e30..1b81becfa 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -62,12 +62,6 @@ const docTemplate = `{ "description": "answer id or question title", "name": "query", "in": "query" - }, - { - "type": "string", - "description": "question id", - "name": "question_id", - "in": "query" } ], "responses": { @@ -119,8 +113,41 @@ const docTemplate = `{ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Get language options", "produces": [ "application/json" @@ -482,14 +509,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -517,14 +544,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -553,25 +580,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], + "summary": "get site interface", "responses": { "200": { "description": "OK", @@ -599,14 +615,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -1427,6 +1443,11 @@ const docTemplate = `{ }, "/answer/api/v1/language/options": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Get language options", "produces": [ "application/json" @@ -3381,52 +3402,6 @@ const docTemplate = `{ } } }, - "/answer/api/v1/user/interface": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "UserUpdateInterface update user interface config", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "UserUpdateInterface update user interface config", - "parameters": [ - { - "type": "string", - "description": "access-token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "UpdateInfoRequest", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", @@ -4858,10 +4833,6 @@ const docTemplate = `{ "description": "is admin", "type": "boolean" }, - "language": { - "description": "language", - "type": "string" - }, "last_login_date": { "description": "last login date", "type": "integer" @@ -4958,10 +4929,6 @@ const docTemplate = `{ "description": "is admin", "type": "boolean" }, - "language": { - "description": "language", - "type": "string" - }, "last_login_date": { "description": "last login date", "type": "integer" @@ -5370,7 +5337,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5384,6 +5352,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5391,7 +5363,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5405,6 +5378,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5617,19 +5594,6 @@ const docTemplate = `{ } } }, - "schema.UpdateUserInterfaceRequest": { - "type": "object", - "required": [ - "language" - ], - "properties": { - "language": { - "description": "language", - "type": "string", - "maxLength": 100 - } - } - }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 405c5d2ac..425eb9485 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -50,12 +50,6 @@ "description": "answer id or question title", "name": "query", "in": "query" - }, - { - "type": "string", - "description": "question id", - "name": "question_id", - "in": "query" } ], "responses": { @@ -107,8 +101,41 @@ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Get language options", "produces": [ "application/json" @@ -470,14 +497,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -505,14 +532,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -541,25 +568,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], + "summary": "get site interface", "responses": { "200": { "description": "OK", @@ -587,14 +603,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -1415,6 +1431,11 @@ }, "/answer/api/v1/language/options": { "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], "description": "Get language options", "produces": [ "application/json" @@ -3369,52 +3390,6 @@ } } }, - "/answer/api/v1/user/interface": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "UserUpdateInterface update user interface config", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" - ], - "summary": "UserUpdateInterface update user interface config", - "parameters": [ - { - "type": "string", - "description": "access-token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "UpdateInfoRequest", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.RespBody" - } - } - } - } - }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", @@ -4846,10 +4821,6 @@ "description": "is admin", "type": "boolean" }, - "language": { - "description": "language", - "type": "string" - }, "last_login_date": { "description": "last login date", "type": "integer" @@ -4946,10 +4917,6 @@ "description": "is admin", "type": "boolean" }, - "language": { - "description": "language", - "type": "string" - }, "last_login_date": { "description": "last login date", "type": "integer" @@ -5358,7 +5325,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5372,6 +5340,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5379,7 +5351,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5393,6 +5366,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5605,19 +5582,6 @@ } } }, - "schema.UpdateUserInterfaceRequest": { - "type": "object", - "required": [ - "language" - ], - "properties": { - "language": { - "description": "language", - "type": "string", - "maxLength": 100 - } - } - }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d2a9dab8b..c8db90c22 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -652,9 +652,6 @@ definitions: is_admin: description: is admin type: boolean - language: - description: language - type: string last_login_date: description: last login date type: integer @@ -726,9 +723,6 @@ definitions: is_admin: description: is admin type: boolean - language: - description: language - type: string last_login_date: description: last login date type: integer @@ -1030,9 +1024,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.SiteInterfaceResp: properties: @@ -1045,9 +1043,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.TagItem: properties: @@ -1201,15 +1203,6 @@ definitions: - synonym_tag_list - tag_id type: object - schema.UpdateUserInterfaceRequest: - properties: - language: - description: language - maxLength: 100 - type: string - required: - - language - type: object schema.UpdateUserStatusReq: properties: status: @@ -1409,10 +1402,6 @@ paths: in: query name: query type: string - - description: question id - in: query - name: question_id - type: string produces: - application/json responses: @@ -1449,6 +1438,23 @@ paths: summary: AdminSetAnswerStatus tags: - admin + /answer/admin/api/dashboard: + get: + consumes: + - application/json + description: DashboardInfo + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: DashboardInfo + tags: + - admin /answer/admin/api/language/options: get: description: Get language options @@ -1459,6 +1465,8 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] summary: Get language options tags: - Lang @@ -1675,7 +1683,7 @@ paths: - admin /answer/admin/api/siteinfo/general: get: - description: Get siteinfo general + description: get site general information produces: - application/json responses: @@ -1690,11 +1698,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo general + summary: get site general information tags: - admin put: - description: Get siteinfo interface + description: update site general information parameters: - description: general in: body @@ -1711,19 +1719,12 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site general information tags: - admin /answer/admin/api/siteinfo/interface: get: - description: Get siteinfo interface - parameters: - - description: general - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.AddCommentReq' + description: get site interface produces: - application/json responses: @@ -1738,11 +1739,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: get site interface tags: - admin put: - description: Get siteinfo interface + description: update site info interface parameters: - description: general in: body @@ -1759,7 +1760,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site info interface tags: - admin /answer/admin/api/theme/options: @@ -2250,6 +2251,8 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] summary: Get language options tags: - Lang @@ -3436,35 +3439,6 @@ paths: summary: UserUpdateInfo update user info tags: - User - /answer/api/v1/user/interface: - put: - consumes: - - application/json - description: UserUpdateInterface update user interface config - parameters: - - description: access-token - in: header - name: Authorization - required: true - type: string - - description: UpdateInfoRequest - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.UpdateUserInterfaceRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] - summary: UserUpdateInterface update user interface config - tags: - - User /answer/api/v1/user/login/email: post: consumes: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 30bfd1bfd..f193bd2c4 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -39,7 +39,6 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp} // @Router /answer/admin/api/siteinfo/interface [get] -// @Param data body schema.AddCommentReq true "general" func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) From eb1cfd9aa8d4959f14f32fd9a5a153a6fd674266 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 10:33:48 +0800 Subject: [PATCH 0136/3337] style: reformat get site info code --- internal/base/constant/constant.go | 5 +++ internal/base/reason/reason.go | 1 + internal/controller/siteinfo_controller.go | 25 ++++-------- internal/router/answer_api_router.go | 2 +- internal/service/siteinfo_service.go | 46 ++++++++++------------ 5 files changed, 36 insertions(+), 43 deletions(-) diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 7722adf87..5a1827161 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -47,3 +47,8 @@ var ( 8: ReportObjectType, } ) + +const ( + SiteTypeGeneral = "general" + SiteTypeInterface = "interface" +) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index abe1bd23a..30e9f532f 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -38,4 +38,5 @@ const ( LangNotFound = "error.lang.not_found" ReportHandleFailed = "error.report.handle_failed" ReportNotFound = "error.report.not_found" + SiteInfoNotFound = "error.site_info.not_found" ) diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index abcbc1782..7148b7ad2 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -18,30 +18,21 @@ func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoCo } } -// GetInfo godoc -// @Summary Get siteinfo -// @Description Get siteinfo +// GetSiteInfo get site info +// @Summary get site info +// @Description get site info // @Tags site // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} // @Router /answer/api/v1/siteinfo [get] -func (sc *SiteinfoController) GetInfo(ctx *gin.Context) { - var ( - resp = &schema.SiteInfoResp{} - general schema.SiteGeneralResp - face schema.SiteInterfaceResp - err error - ) - - general, err = sc.siteInfoService.GetSiteGeneral(ctx) - resp.General = &general +func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) { + var err error + resp := &schema.SiteInfoResp{} + resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx) if err != nil { handler.HandleResponse(ctx, err, resp) return } - - face, err = sc.siteInfoService.GetSiteInterface(ctx) - resp.Face = &face - + resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 068486877..d7d0c9dce 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -131,7 +131,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) //siteinfo - r.GET("/siteinfo", a.siteinfoController.GetInfo) + r.GET("/siteinfo", a.siteinfoController.GetSiteInfo) } func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go index 03e598e4c..08a88b9f2 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo_service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/entity" @@ -26,38 +27,33 @@ func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService } } -func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) { - var ( - siteType = "general" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteGeneralResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) +// GetSiteGeneral get site info general +func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } if !exist { - return + return nil, errors.BadRequest(reason.SiteInfoNotFound) } - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + resp = &schema.SiteGeneralResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } -func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) { - var ( - siteType = "interface" - siteInfo *entity.SiteInfo - exist bool - ) - resp = schema.SiteInterfaceResp{} - - siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType) +// GetSiteInterface get site info interface +func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err + } if !exist { - return + return nil, errors.BadRequest(reason.SiteInfoNotFound) } - - _ = json.Unmarshal([]byte(siteInfo.Content), &resp) - return + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil } func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { From 6b13eca4ef4d34dce6db232d62654c6c393a67da Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 3 Nov 2022 14:55:08 +0800 Subject: [PATCH 0137/3337] add mock api --- internal/router/ui.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/internal/router/ui.go b/internal/router/ui.go index 99bcaf07b..1b5b24296 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -4,6 +4,7 @@ import ( "embed" "fmt" "io/fs" + "math/rand" "net/http" "os" @@ -93,11 +94,29 @@ func (a *UIRouter) Register(r *gin.Engine) { }) r.POST("/installation/db/check", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) + num := rand.Intn(10) + if num > 5 { + err := fmt.Errorf("connection error") + handler.HandleResponse(c, err, gin.H{}) + } else { + handler.HandleResponse(c, nil, gin.H{ + "connection_success": true, + }) + } }) r.POST("/installation/config-file/check", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) + num := rand.Intn(10) + if num > 5 { + handler.HandleResponse(c, nil, gin.H{ + "exist": true, + }) + } else { + handler.HandleResponse(c, nil, gin.H{ + "exist": false, + }) + } + }) r.POST("/installation/init", func(c *gin.Context) { From 961fd21b9c557c11f2d4b578085b594441dea990 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 3 Nov 2022 14:56:47 +0800 Subject: [PATCH 0138/3337] add mock install api --- internal/router/ui.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/router/ui.go b/internal/router/ui.go index 1b5b24296..852f6b06c 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -12,6 +12,7 @@ import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -96,7 +97,7 @@ func (a *UIRouter) Register(r *gin.Engine) { r.POST("/installation/db/check", func(c *gin.Context) { num := rand.Intn(10) if num > 5 { - err := fmt.Errorf("connection error") + err := errors.BadRequest("connection error") handler.HandleResponse(c, err, gin.H{}) } else { handler.HandleResponse(c, nil, gin.H{ From 97fa333ea053ec954440f5f08d596e3b58d6e759 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 11:12:20 +0800 Subject: [PATCH 0139/3337] feat: user info response handling the default language --- cmd/answer/wire_gen.go | 17 ++++--- internal/controller/lang_controller.go | 6 +-- internal/controller/siteinfo_controller.go | 6 +-- .../siteinfo_controller.go | 6 +-- internal/service/provider.go | 5 +- .../{ => siteinfo}/siteinfo_service.go | 2 +- internal/service/siteinfo_common/siteinfo.go | 1 + .../siteinfo_common/siteinfo_service.go | 50 +++++++++++++++++++ internal/service/user_service.go | 33 ++++++++---- 9 files changed, 98 insertions(+), 28 deletions(-) rename internal/service/{ => siteinfo}/siteinfo_service.go (99%) create mode 100644 internal/service/siteinfo_common/siteinfo_service.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 746321b70..0ae8cd10a 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -58,6 +58,8 @@ import ( "github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/revision_common" "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" "github.com/answerdev/answer/internal/service/uploader" @@ -90,19 +92,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, return nil, nil, err } siteInfoRepo := site_info.NewSiteInfo(dataData) - configRepo := config.NewConfigRepo(dataData) - emailRepo := export.NewEmailRepo(dataData) - emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) - langController := controller.NewLangController(i18nTranslator, siteInfoService) + siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) + langController := controller.NewLangController(i18nTranslator, siteInfoCommonService) authRepo := auth.NewAuthRepo(dataData) authService := auth2.NewAuthService(authRepo) + configRepo := config.NewConfigRepo(dataData) userRepo := user.NewUserRepo(dataData, configRepo) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo) userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) - userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) + emailRepo := export.NewEmailRepo(dataData) + emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) + userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) uploaderService := uploader.NewUploaderService(serviceConf) @@ -167,8 +169,9 @@ 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) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) - siteinfoController := controller.NewSiteinfoController(siteInfoService) + siteinfoController := controller.NewSiteinfoController(siteInfoCommonService) notificationRepo := notification.NewNotificationRepo(dataData) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 8d4242db2..1e947adfc 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -5,18 +5,18 @@ import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { translator i18n.Translator - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController { +func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController { return &LangController{translator: tr, siteInfoService: siteInfoService} } diff --git a/internal/controller/siteinfo_controller.go b/internal/controller/siteinfo_controller.go index 7148b7ad2..5f547eb79 100644 --- a/internal/controller/siteinfo_controller.go +++ b/internal/controller/siteinfo_controller.go @@ -3,16 +3,16 @@ package controller import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" ) type SiteinfoController struct { - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewSiteinfoController new siteinfo controller. -func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController { +func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController { return &SiteinfoController{ siteInfoService: siteInfoService, } diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index f193bd2c4..7ecce395b 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -3,16 +3,16 @@ package controller_backyard import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/schema" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo" "github.com/gin-gonic/gin" ) type SiteInfoController struct { - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo.SiteInfoService } // NewSiteInfoController new siteinfo controller. -func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController { +func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController { return &SiteInfoController{ siteInfoService: siteInfoService, } diff --git a/internal/service/provider.go b/internal/service/provider.go index 3901766ef..93d091fb3 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -21,6 +21,8 @@ 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/siteinfo" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/internal/service/tag" tagcommon "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/uploader" @@ -61,7 +63,8 @@ var ProviderSetService = wire.NewSet( report_backyard.NewReportBackyardService, user_backyard.NewUserBackyardService, reason.NewReasonService, - NewSiteInfoService, + siteinfo_common.NewSiteInfoCommonService, + siteinfo.NewSiteInfoService, notficationcommon.NewNotificationCommon, notification.NewNotificationService, activity.NewAnswerActivityService, diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go similarity index 99% rename from internal/service/siteinfo_service.go rename to internal/service/siteinfo/siteinfo_service.go index 08a88b9f2..822c7bde8 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -1,4 +1,4 @@ -package service +package siteinfo import ( "context" diff --git a/internal/service/siteinfo_common/siteinfo.go b/internal/service/siteinfo_common/siteinfo.go index ff9066b09..1c501d2dd 100644 --- a/internal/service/siteinfo_common/siteinfo.go +++ b/internal/service/siteinfo_common/siteinfo.go @@ -2,6 +2,7 @@ package siteinfo_common import ( "context" + "github.com/answerdev/answer/internal/entity" ) diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go new file mode 100644 index 000000000..5dd565d0f --- /dev/null +++ b/internal/service/siteinfo_common/siteinfo_service.go @@ -0,0 +1,50 @@ +package siteinfo_common + +import ( + "context" + "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 { + siteInfoRepo SiteInfoRepo +} + +func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService { + return &SiteInfoCommonService{ + siteInfoRepo: siteInfoRepo, + } +} + +// GetSiteGeneral get site info general +func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + + 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) { + siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface) + if err != nil { + return nil, err + } + if !exist { + return nil, errors.BadRequest(reason.SiteInfoNotFound) + } + resp = &schema.SiteInterfaceResp{} + _ = json.Unmarshal([]byte(siteInfo.Content), resp) + return resp, nil +} diff --git a/internal/service/user_service.go b/internal/service/user_service.go index fbc227419..6447184ec 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -18,6 +18,7 @@ import ( "github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" "github.com/answerdev/answer/pkg/checker" "github.com/google/uuid" @@ -30,11 +31,12 @@ import ( // UserService user service type UserService struct { - userRepo usercommon.UserRepo - userActivity activity.UserActiveActivityRepo - serviceConfig *service_config.ServiceConfig - emailService *export.EmailService - authService *auth.AuthService + userRepo usercommon.UserRepo + userActivity activity.UserActiveActivityRepo + serviceConfig *service_config.ServiceConfig + emailService *export.EmailService + authService *auth.AuthService + siteInfoService *siteinfo_common.SiteInfoCommonService } func NewUserService(userRepo usercommon.UserRepo, @@ -42,13 +44,15 @@ func NewUserService(userRepo usercommon.UserRepo, emailService *export.EmailService, authService *auth.AuthService, serviceConfig *service_config.ServiceConfig, + siteInfoService *siteinfo_common.SiteInfoCommonService, ) *UserService { return &UserService{ - userRepo: userRepo, - userActivity: userActivity, - emailService: emailService, - serviceConfig: serviceConfig, - authService: authService, + userRepo: userRepo, + userActivity: userActivity, + emailService: emailService, + serviceConfig: serviceConfig, + authService: authService, + siteInfoService: siteInfoService, } } @@ -64,6 +68,15 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st resp = &schema.GetUserToSetShowResp{} resp.GetFromUserEntity(userInfo) resp.AccessToken = token + + // if user choose the default language, Use the language configured by the administrator. + if resp.Language == translator.DefaultLangOption { + siteInterface, err := us.siteInfoService.GetSiteInterface(ctx) + if err != nil { + return nil, err + } + resp.Language = siteInterface.Language + } return resp, nil } From d16ecc99628ee18f196a00f1d25bf8bd9a83cd8d Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 16:12:23 +0800 Subject: [PATCH 0140/3337] update: use default language option in value --- cmd/answer/wire_gen.go | 1 - internal/controller/lang_controller.go | 2 +- internal/service/user_service.go | 9 --------- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 475a61afd..302a67def 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -152,7 +152,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 1e947adfc..b48080ee3 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -64,7 +64,7 @@ func (u *LangController) GetUserLangOptions(ctx *gin.Context) { options := translator.LanguageOptions if len(siteInterfaceResp.Language) > 0 { defaultOption := []*translator.LangOption{ - {Label: translator.DefaultLangOption, Value: siteInterfaceResp.Language}, + {Label: translator.DefaultLangOption, Value: translator.DefaultLangOption}, } options = append(defaultOption, options...) } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 6447184ec..76ce0d8a0 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -68,15 +68,6 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st resp = &schema.GetUserToSetShowResp{} resp.GetFromUserEntity(userInfo) resp.AccessToken = token - - // if user choose the default language, Use the language configured by the administrator. - if resp.Language == translator.DefaultLangOption { - siteInterface, err := us.siteInfoService.GetSiteInterface(ctx) - if err != nil { - return nil, err - } - resp.Language = siteInterface.Language - } return resp, nil } From 2c60e16141917ad838e81ea3bfece040dbe1802c Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Nov 2022 16:19:57 +0800 Subject: [PATCH 0141/3337] refactor(i18n): update en.json --- ui/src/i18n/locales/en.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 19422b243..055d6b246 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -976,6 +976,11 @@ "msg": "Site name cannot be empty.", "text": "The name of this site, as used in the title tag." }, + "url": { + "label": "Site URL", + "msg": "Site url cannot be empty.", + "text": "The address of your site." + }, "short_description": { "label": "Short Site Description (optional)", "msg": "Short site description cannot be empty.", @@ -985,6 +990,11 @@ "label": "Site Description (optional)", "msg": "Site description cannot be empty.", "text": "Describe this site in one sentence, as used in the meta description tag." + }, + "email": { + "label": "Contact Email", + "msg": "Contact email cannot be empty.", + "text": "Email address of key contact responsible for this site." } }, "interface": { @@ -1004,7 +1014,7 @@ "msg": "Interface language cannot be empty.", "text": "User interface language. It will change when you refresh the page." }, - "timezone": { + "time_zone": { "label": "Timezone", "msg": "Timezone cannot be empty.", "text": "Choose a UTC (Coordinated Universal Time) time offset." From 2a849395f0869ed6b5ee5137bea68b0f825e1700 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 3 Nov 2022 16:40:40 +0800 Subject: [PATCH 0142/3337] add siteinfo --- internal/schema/siteinfo_schema.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 446b986db..da78327ef 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -5,6 +5,8 @@ type SiteGeneralReq struct { Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` + SiteUrl string `validate:"required,gt=1,lte=128" form:"site_url" json:"site_url"` + ContactEmail string `validate:"required,gt=1,lte=128" form:"contact_email" json:"contact_email"` } // SiteInterfaceReq site interface request From aa9687c1bdb82dfdd2158b869f0cd82dac80b51a Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Nov 2022 17:19:53 +0800 Subject: [PATCH 0143/3337] refactor(admin): Add form fields --- ui/src/pages/Admin/General/index.tsx | 60 +++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 14b959788..cdce67bbe 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -23,6 +23,11 @@ const General: FC = () => { isInvalid: false, errorMsg: '', }, + site_url: { + value: '', + isInvalid: false, + errorMsg: '', + }, short_description: { value: '', isInvalid: false, @@ -33,10 +38,15 @@ const General: FC = () => { isInvalid: false, errorMsg: '', }, + contact_email: { + value: '', + isInvalid: false, + errorMsg: '', + }, }); const checkValidated = (): boolean => { let ret = true; - const { name } = formData; + const { name, site_url, contact_email } = formData; if (!name.value) { ret = false; formData.name = { @@ -45,6 +55,22 @@ const General: FC = () => { errorMsg: t('name.msg'), }; } + if (!site_url.value) { + ret = false; + formData.site_url = { + value: '', + isInvalid: true, + errorMsg: t('site_url.msg'), + }; + } + if (!contact_email.value) { + ret = false; + formData.contact_email = { + value: '', + isInvalid: true, + errorMsg: t('contact_email.msg'), + }; + } setFormData({ ...formData, }); @@ -61,6 +87,8 @@ const General: FC = () => { name: formData.name.value, description: formData.description.value, short_description: formData.short_description.value, + site_url: formData.site_url.value, + contact_email: formData.contact_email.value, }; updateGeneralSetting(reqParams) @@ -100,7 +128,7 @@ const General: FC = () => { Object.keys(setting).forEach((k) => { formMeta[k] = { ...formData[k], value: setting[k] }; }); - setFormData(formMeta); + setFormData({ ...formData, ...formMeta }); }, [setting]); return ( <> @@ -120,6 +148,20 @@ const General: FC = () => { {formData.name.errorMsg} + + {t('site_url.label')} + onFieldChange('site_url', evt.target.value)} + /> + {t('site_url.text')} + + {formData.site_url.errorMsg} + + {t('short_description.label')} { {formData.description.errorMsg} + + {t('contact_email.label')} + onFieldChange('contact_email', evt.target.value)} + /> + {t('contact_email.text')} + + {formData.contact_email.errorMsg} + +

{t('flags')}{data.report_count} - + {t('review')} - + From db6736ff4d709a74970d65703ac08b2205b8ab87 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Nov 2022 17:20:27 +0800 Subject: [PATCH 0145/3337] refactor: update interface.ts --- ui/src/common/interface.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 98f7c0466..a4c7dcd26 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -258,6 +258,8 @@ export interface AdminSettingsGeneral { name: string; short_description: string; description: string; + site_url: string; + contact_email: string; } export interface AdminSettingsInterface { From 0cada831eed1829ef4cb88e240234e68f23e80d8 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Nov 2022 17:20:48 +0800 Subject: [PATCH 0146/3337] refactor(i18n): update en.json --- ui/src/i18n/locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 055d6b246..98779100d 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -976,7 +976,7 @@ "msg": "Site name cannot be empty.", "text": "The name of this site, as used in the title tag." }, - "url": { + "site_url": { "label": "Site URL", "msg": "Site url cannot be empty.", "text": "The address of your site." @@ -991,7 +991,7 @@ "msg": "Site description cannot be empty.", "text": "Describe this site in one sentence, as used in the meta description tag." }, - "email": { + "contact_email": { "label": "Contact Email", "msg": "Contact email cannot be empty.", "text": "Email address of key contact responsible for this site." From 48654aa995430901b8d8b55723681aad1a4f5bf9 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 3 Nov 2022 17:44:59 +0800 Subject: [PATCH 0147/3337] fix: add installation process --- ui/config-overrides.js | 7 +- ui/src/i18n/locales/en.json | 31 +++- .../Install/components/FifthStep/index.tsx | 8 +- .../Install/components/FirstStep/index.tsx | 11 +- .../Install/components/FourthStep/index.tsx | 63 ++++++- .../Install/components/ThirdStep/index.tsx | 42 +++-- ui/src/pages/Install/index.tsx | 154 ++++++++++++++---- ui/src/services/index.ts | 1 + ui/src/services/install/index.ts | 21 +++ ui/src/utils/request.ts | 9 +- 10 files changed, 290 insertions(+), 57 deletions(-) create mode 100644 ui/src/services/install/index.ts diff --git a/ui/config-overrides.js b/ui/config-overrides.js index ac52fa636..c8812ce69 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -18,7 +18,12 @@ module.exports = { const config = configFunction(proxy, allowedHost); config.proxy = { '/answer': { - target: 'http://10.0.10.98:2060', + target: 'http://10.0.20.88:8080', + changeOrigin: true, + secure: false, + }, + '/installation': { + target: 'http://10.0.20.88:8080', changeOrigin: true, secure: false, }, diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index ec9012b2c..27a50f4de 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -739,6 +739,7 @@ "title": "Answer", "next": "Next", "done": "Done", + "config_yaml_error": "Can’t create the config.yaml file.", "lang": { "label": "Please choose a language" }, @@ -779,22 +780,42 @@ "site_information": "Site Information", "admin_account": "Admin Account", "site_name": { - "label": "Site Name" + "label": "Site Name", + "msg": "Site Name cannot be empty." + }, + "site_url": { + "label": "Site URL", + "text": "The address of your site.", + "msg": { + "empty": "Site URL cannot be empty.", + "incorrect": "Site URL incorrect format." + } }, "contact_email": { "label": "Contact Email", - "text": "Email address of key contact responsible for this site." + "text": "Email address of key contact responsible for this site.", + "msg": { + "empty": "Contact Email cannot be empty.", + "incorrect": "Contact Email incorrect format." + } + }, "admin_name": { - "label": "Name" + "label": "Name", + "msg": "Name cannot be empty." }, "admin_password": { "label": "Password", - "text": "You will need this password to log in. Please store it in a secure location." + "text": "You will need this password to log in. Please store it in a secure location.", + "msg": "Password cannot be empty." }, "admin_email": { "label": "Email", - "text": "You will need this email to log in." + "text": "You will need this email to log in.", + "msg": { + "empty": "Email cannot be empty.", + "incorrect": "Email incorrect format." + } }, "ready_title": "Your Answer is Ready!", "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", diff --git a/ui/src/pages/Install/components/FifthStep/index.tsx b/ui/src/pages/Install/components/FifthStep/index.tsx index 63f008e0b..ab94f30f6 100644 --- a/ui/src/pages/Install/components/FifthStep/index.tsx +++ b/ui/src/pages/Install/components/FifthStep/index.tsx @@ -6,8 +6,9 @@ import Progress from '../Progress'; interface Props { visible: boolean; + siteUrl: string; } -const Index: FC = ({ visible }) => { +const Index: FC = ({ visible, siteUrl = '' }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); if (!visible) return null; @@ -17,14 +18,15 @@ const Index: FC = ({ visible }) => {

If you ever feel like changing more settings, visit - admin section; find it in the site menu. + admin section; find it in the + site menu.

{t('good_luck')}

- +
); diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx index 5690ca8e1..78b4aac8b 100644 --- a/ui/src/pages/Install/components/FirstStep/index.tsx +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormValue, FormDataType } from '@/common/interface'; import Progress from '../Progress'; -import { languages } from '@/services'; +import { getInstallLangOptions } from '@/services'; interface Props { data: FormValue; @@ -18,8 +18,15 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const [langs, setLangs] = useState(); const getLangs = async () => { - const res: LangsType[] = await languages(); + const res: LangsType[] = await getInstallLangOptions(); setLangs(res); + changeCallback({ + lang: { + value: res[0].value, + isInvalid: false, + errorMsg: '', + }, + }); }; const handleSubmit = () => { diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index ccd44be2d..fae0c53ce 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -18,6 +18,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { let bol = true; const { site_name, + site_url, contact_email, admin_name, admin_password, @@ -33,12 +34,40 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { }; } + if (!site_url.value) { + bol = false; + data.site_url = { + value: '', + isInvalid: true, + errorMsg: t('site_name.msg.empty'), + }; + } + const reg = /^(http|https):\/\//g; + if (site_url.value && !site_url.value.match(reg)) { + bol = false; + data.site_url = { + value: site_url.value, + isInvalid: true, + errorMsg: t('site_url.msg.incorrect'), + }; + } + if (!contact_email.value) { bol = false; data.contact_email = { value: '', isInvalid: true, - errorMsg: t('contact_email.msg'), + errorMsg: t('contact_email.msg.empty'), + }; + } + + const mailReg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/; + if (contact_email.value && !contact_email.value.match(mailReg)) { + bol = false; + data.contact_email = { + value: contact_email.value, + isInvalid: true, + errorMsg: t('contact_email.msg.incorrect'), }; } @@ -65,7 +94,16 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { data.admin_email = { value: '', isInvalid: true, - errorMsg: t('admin_email.msg'), + errorMsg: t('admin_email.msg.empty'), + }; + } + + if (admin_email.value && !admin_email.value.match(mailReg)) { + bol = false; + data.admin_email = { + value: '', + isInvalid: true, + errorMsg: t('admin_email.msg.incorrect'), }; } @@ -108,6 +146,27 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {data.site_name.errorMsg} + + {t('site_url.label')} + { + changeCallback({ + site_url: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + {t('site_url.text')} + + {data.site_url.errorMsg} + + {t('contact_email.label')} void; } -const Index: FC = ({ visible, nextCallback }) => { +const Index: FC = ({ visible, errorMsg, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); if (!visible) return null; return (
{t('config_yaml.title')}
-
{t('config_yaml.label')}
-
-

- }} - /> -

-
- - - -
{t('config_yaml.info')}
+ + {errorMsg?.msg?.length > 0 ? ( + <> +
+

+ }} + /> +

+
+ + + +
{t('config_yaml.info')}
+ + ) : ( +
{t('config_yaml.label')}
+ )} +
diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index e8c40d5a5..f407e156c 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -3,8 +3,13 @@ import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; import { useTranslation, Trans } from 'react-i18next'; import type { FormDataType } from '@/common/interface'; -import { Storage } from '@/utils'; import { PageTitle } from '@/components'; +import { + dbCheck, + installInit, + installBaseInfo, + checkConfigFileExists, +} from '@/services'; import { FirstStep, @@ -16,8 +21,12 @@ import { const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step, setStep] = useState(1); - const [showError] = useState(false); + const [step, setStep] = useState(3); + const [loading, setLoading] = useState(true); + const [errorData, setErrorData] = useState<{ [propName: string]: any }>({ + msg: '', + }); + const [tableExist, setTableExist] = useState(false); const [formData, setFormData] = useState({ lang: { @@ -26,7 +35,7 @@ const Index: FC = () => { errorMsg: '', }, db_type: { - value: '', + value: 'mysql', isInvalid: false, errorMsg: '', }, @@ -55,12 +64,16 @@ const Index: FC = () => { isInvalid: false, errorMsg: '', }, - site_name: { value: '', isInvalid: false, errorMsg: '', }, + site_url: { + value: '', + isInvalid: false, + errorMsg: '', + }, contact_email: { value: '', isInvalid: false, @@ -88,33 +101,107 @@ const Index: FC = () => { setFormData({ ...formData, ...params }); }; - const handleStep = () => { + const handleErr = (data) => { + window.scrollTo(0, 0); + setErrorData(data); + }; + + const handleNext = async () => { + setErrorData({ + msg: '', + }); setStep((pre) => pre + 1); }; - // const handleSubmit = () => { - // const params = { - // lang: formData.lang.value, - // db_type: formData.db_type.value, - // db_username: formData.db_username.value, - // db_password: formData.db_password.value, - // db_host: formData.db_host.value, - // db_name: formData.db_name.value, - // db_file: formData.db_file.value, - // site_name: formData.site_name.value, - // contact_email: formData.contact_email.value, - // admin_name: formData.admin_name.value, - // admin_password: formData.admin_password.value, - // admin_email: formData.admin_email.value, - // }; - - // console.log(params); - // }; + const submitDatabaseForm = () => { + const params = { + lang: formData.lang.value, + db_type: formData.db_type.value, + db_username: formData.db_username.value, + db_password: formData.db_password.value, + db_host: formData.db_host.value, + db_name: formData.db_name.value, + db_file: formData.db_file.value, + }; + dbCheck(params) + .then(() => { + handleNext(); + }) + .catch((err) => { + console.log(err); + handleErr(err); + }); + }; + + const checkInstall = () => { + installInit() + .then(() => { + handleNext(); + }) + .catch((err) => { + handleErr(err); + }); + }; + + const submitSiteConfig = () => { + const params = { + site_name: formData.site_name.value, + contact_email: formData.contact_email.value, + admin_name: formData.admin_name.value, + admin_password: formData.admin_password.value, + admin_email: formData.admin_email.value, + }; + installBaseInfo(params) + .then(() => { + handleNext(); + }) + .catch((err) => { + handleErr(err); + }); + }; + + const handleStep = () => { + if (step === 2) { + submitDatabaseForm(); + } else if (step === 3) { + checkInstall(); + } else if (step === 4) { + submitSiteConfig(); + } else { + handleNext(); + } + }; + + const handleInstallNow = (e) => { + e.preventDefault(); + if (tableExist) { + setStep(7); + } else { + setStep(4); + } + }; + + const configYmlCheck = () => { + checkConfigFileExists() + .then((res) => { + setTableExist(res?.db_table_exist); + if (res && res.config_file_exist) { + setStep(5); + } + }) + .finally(() => { + setLoading(false); + }); + }; useEffect(() => { - console.log('step===', Storage.get('INSTALL_STEP')); + configYmlCheck(); }, []); + if (loading) { + return
; + } + return (
@@ -124,7 +211,9 @@ const Index: FC = () => {

{t('title')}

- {showError && show error msg } + {errorData?.msg && ( + {errorData?.msg} + )} { nextCallback={handleStep} /> - + { nextCallback={handleStep} /> - + {step === 6 && (
{t('warning')}
@@ -158,7 +251,10 @@ const Index: FC = () => { The file config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try{' '} - installing now. + handleInstallNow(e)}> + installing now + + .

diff --git a/ui/src/services/index.ts b/ui/src/services/index.ts index a6923bcf9..a3e936ec0 100644 --- a/ui/src/services/index.ts +++ b/ui/src/services/index.ts @@ -1,3 +1,4 @@ export * from './admin'; export * from './common'; export * from './client'; +export * from './install'; diff --git a/ui/src/services/install/index.ts b/ui/src/services/install/index.ts new file mode 100644 index 000000000..31d50690e --- /dev/null +++ b/ui/src/services/install/index.ts @@ -0,0 +1,21 @@ +import request from '@/utils/request'; + +export const checkConfigFileExists = () => { + return request.post('/installation/config-file/check'); +}; + +export const dbCheck = (params) => { + return request.post('/installation/db/check', params); +}; + +export const installInit = () => { + return request.post('/installation/init'); +}; + +export const installBaseInfo = (params) => { + return request.post('/installation/base-info', params); +}; + +export const getInstallLangOptions = () => { + return request.get('/installation/language/options'); +}; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index 4e878cb34..8167448dc 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -73,7 +73,14 @@ class Request { }); } - if (data.type === 'modal') { + if (data.err_type === 'alert') { + return Promise.reject({ + msg, + ...data, + }); + } + + if (data.err_type === 'modal') { // modal error message Modal.confirm({ content: msg, From a85a45425a4618a44b8e7870b0d6a471c53e57b7 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 3 Nov 2022 18:44:28 +0800 Subject: [PATCH 0148/3337] chore: update process --- ui/src/pages/Upgrade/index.tsx | 34 ++++++++++++++++++++++++++-------- ui/src/services/common.ts | 4 ++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/ui/src/pages/Upgrade/index.tsx b/ui/src/pages/Upgrade/index.tsx index c65c90981..c264e46c5 100644 --- a/ui/src/pages/Upgrade/index.tsx +++ b/ui/src/pages/Upgrade/index.tsx @@ -1,17 +1,20 @@ import { useState } from 'react'; -import { Container, Row, Col, Card, Button } from 'react-bootstrap'; +import { Container, Row, Col, Card, Button, Spinner } from 'react-bootstrap'; import { useTranslation, Trans } from 'react-i18next'; import { PageTitle } from '@/components'; +import { upgradSystem } from '@/services'; const Index = () => { const { t } = useTranslation('translation', { keyPrefix: 'upgrade', }); - const [step, setStep] = useState(1); + const [step] = useState(1); + const [loading, setLoading] = useState(false); - const handleUpdate = () => { - setStep(2); + const handleUpdate = async () => { + await upgradSystem(); + setLoading(true); }; return (
@@ -29,9 +32,22 @@ const Index = () => { i18nKey="upgrade.update_description" components={{ 1:

}} /> - + {loading ? ( + + ) : ( + + )} )} @@ -39,7 +55,9 @@ const Index = () => { <>

{t('done_title')}

{t('done_desscription')}

- + )} diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index f27eb0a91..614628862 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -266,3 +266,7 @@ export const useSiteSettings = () => { error, }; }; + +export const upgradSystem = () => { + return request.post('/answer/api/v1/upgradation'); +}; From 914defc446b93293a494765188c0f6a47e24e8a7 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 3 Nov 2022 20:09:04 +0800 Subject: [PATCH 0149/3337] feat: add install api and functions --- cmd/answer/command.go | 44 ++++-- cmd/answer/main.go | 20 +-- i18n/en_US.yaml | 10 ++ internal/base/conf/conf.go | 20 +++ internal/base/reason/reason.go | 3 + internal/cli/install.go | 24 +-- internal/cli/install_check.go | 16 +- internal/install/install_controller.go | 146 ++++++++++++++++++ internal/install/install_main.go | 30 ++++ internal/install/install_req.go | 81 ++++++++++ .../install.go => install/install_server.go} | 32 ++-- internal/router/ui.go | 5 + internal/schema/err_schema.go | 2 + 13 files changed, 365 insertions(+), 68 deletions(-) create mode 100644 internal/install/install_controller.go create mode 100644 internal/install/install_main.go create mode 100644 internal/install/install_req.go rename internal/{base/server/install.go => install/install_server.go} (56%) diff --git a/cmd/answer/command.go b/cmd/answer/command.go index 072131347..1ddd844be 100644 --- a/cmd/answer/command.go +++ b/cmd/answer/command.go @@ -3,8 +3,11 @@ package main import ( "fmt" "os" + "path/filepath" + "github.com/answerdev/answer/internal/base/conf" "github.com/answerdev/answer/internal/cli" + "github.com/answerdev/answer/internal/install" "github.com/answerdev/answer/internal/migrations" "github.com/spf13/cobra" ) @@ -59,20 +62,31 @@ To run answer, use: Short: "init answer application", Long: `init answer application`, Run: func(_ *cobra.Command, _ []string) { - // installwebapi := server.NewInstallHTTPServer() - // installwebapi.Run(":8088") + // check config file and database. if config file exists and database is already created, init done cli.InstallAllInitialEnvironment(dataDirPath) - c, err := readConfig() - if err != nil { - fmt.Println("read config failed: ", err.Error()) - return + // set default config file path + if len(configFilePath) == 0 { + configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName) } - fmt.Println("read config successfully") - if err := migrations.InitDB(c.Data.Database); err != nil { - fmt.Println("init database error: ", err.Error()) - return + + configFileExist := cli.CheckConfigFile(configFilePath) + if configFileExist { + fmt.Println("config file exists, try to read the config...") + c, err := conf.ReadConfig(configFilePath) + if err != nil { + fmt.Println("read config failed: ", err.Error()) + return + } + + fmt.Println("config file read successfully, try to connect database...") + if cli.CheckDB(c.Data.Database, true) { + fmt.Println("connect to database successfully and table already exists, do nothing.") + return + } } - fmt.Println("init database successfully") + + // start installation server to install + install.Run(configFilePath) }, } @@ -82,7 +96,7 @@ To run answer, use: Short: "upgrade Answer version", Long: `upgrade Answer version`, Run: func(_ *cobra.Command, _ []string) { - c, err := readConfig() + c, err := conf.ReadConfig(configFilePath) if err != nil { fmt.Println("read config failed: ", err.Error()) return @@ -102,7 +116,7 @@ To run answer, use: Long: `back up data`, Run: func(_ *cobra.Command, _ []string) { fmt.Println("Answer is backing up data") - c, err := readConfig() + c, err := conf.ReadConfig(configFilePath) if err != nil { fmt.Println("read config failed: ", err.Error()) return @@ -135,13 +149,13 @@ To run answer, use: fmt.Println("upload directory not exists [x]") } - c, err := readConfig() + c, err := conf.ReadConfig(configFilePath) if err != nil { fmt.Println("read config failed: ", err.Error()) return } - if cli.CheckDB(c.Data.Database) { + if cli.CheckDB(c.Data.Database, false) { fmt.Println("db connection successfully [✔]") } else { fmt.Println("db connection failed [x]") diff --git a/cmd/answer/main.go b/cmd/answer/main.go index 082142440..0f1c7d940 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -2,13 +2,10 @@ package main import ( "os" - "path/filepath" "github.com/answerdev/answer/internal/base/conf" - "github.com/answerdev/answer/internal/cli" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman" - "github.com/segmentfault/pacman/contrib/conf/viper" "github.com/segmentfault/pacman/contrib/log/zap" "github.com/segmentfault/pacman/contrib/server/http" "github.com/segmentfault/pacman/log" @@ -41,7 +38,7 @@ func runApp() { log.SetLogger(zap.NewLogger( log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) - c, err := readConfig() + c, err := conf.ReadConfig(configFilePath) if err != nil { panic(err) } @@ -56,21 +53,6 @@ func runApp() { } } -func readConfig() (c *conf.AllConfig, err error) { - if len(configFilePath) == 0 { - configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName) - } - c = &conf.AllConfig{} - config, err := viper.NewWithPath(configFilePath) - if err != nil { - return nil, err - } - if err = config.Parse(&c); err != nil { - return nil, err - } - return c, nil -} - func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application { return pacman.NewApp( pacman.WithName(Name), diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 824c92e66..84afa373d 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -89,6 +89,15 @@ error: set_avatar: other: "Avatar set failed." + config: + read_config_failed: + other: "Read config failed" + database: + connection_failed: + other: "Database connection failed" + install: + create_config_failed: + other: "Can’t create the config.yaml file." report: spam: name: @@ -170,3 +179,4 @@ notification: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" + diff --git a/internal/base/conf/conf.go b/internal/base/conf/conf.go index 97859ac03..558468259 100644 --- a/internal/base/conf/conf.go +++ b/internal/base/conf/conf.go @@ -1,11 +1,15 @@ package conf import ( + "path/filepath" + "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/server" "github.com/answerdev/answer/internal/base/translator" + "github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/router" "github.com/answerdev/answer/internal/service/service_config" + "github.com/segmentfault/pacman/contrib/conf/viper" ) // AllConfig all config @@ -28,3 +32,19 @@ type Data struct { Database *data.Database `json:"database" mapstructure:"database"` Cache *data.CacheConf `json:"cache" mapstructure:"cache"` } + +// ReadConfig read config +func ReadConfig(configFilePath string) (c *AllConfig, err error) { + if len(configFilePath) == 0 { + configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName) + } + c = &AllConfig{} + config, err := viper.NewWithPath(configFilePath) + if err != nil { + return nil, err + } + if err = config.Parse(&c); err != nil { + return nil, err + } + return c, nil +} diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index abe1bd23a..ddd4cfc1e 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -38,4 +38,7 @@ const ( 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" + InstallConfigFailed = "error.install.create_config_failed" ) diff --git a/internal/cli/install.go b/internal/cli/install.go index 8c8ee64f0..eee7b89a6 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -27,33 +27,35 @@ func InstallAllInitialEnvironment(dataDirPath string) { UploadFilePath = filepath.Join(dataDirPath, UploadFilePath) I18nPath = filepath.Join(dataDirPath, I18nPath) - installConfigFile() installUploadDir() installI18nBundle() fmt.Println("install all initial environment done") } -func installConfigFile() { - fmt.Println("[config-file] try to install...") - defaultConfigFile := filepath.Join(ConfigFilePath, DefaultConfigFileName) +func InstallConfigFile(configFilePath string) error { + if len(configFilePath) == 0 { + configFilePath = filepath.Join(ConfigFilePath, DefaultConfigFileName) + } + fmt.Println("[config-file] try to create at ", configFilePath) // if config file already exists do nothing. - if CheckConfigFile(defaultConfigFile) { - fmt.Printf("[config-file] %s already exists\n", defaultConfigFile) - return + if CheckConfigFile(configFilePath) { + fmt.Printf("[config-file] %s already exists\n", configFilePath) + return nil } if err := dir.CreateDirIfNotExist(ConfigFilePath); err != nil { fmt.Printf("[config-file] create directory fail %s\n", err.Error()) - return + return fmt.Errorf("create directory fail %s", err.Error()) } - fmt.Printf("[config-file] create directory success, config file is %s\n", defaultConfigFile) + fmt.Printf("[config-file] create directory success, config file is %s\n", configFilePath) - if err := writerFile(defaultConfigFile, string(configs.Config)); err != nil { + if err := writerFile(configFilePath, string(configs.Config)); err != nil { fmt.Printf("[config-file] install fail %s\n", err.Error()) - return + return fmt.Errorf("write file failed %s", err) } fmt.Printf("[config-file] install success\n") + return nil } func installUploadDir() { diff --git a/internal/cli/install_check.go b/internal/cli/install_check.go index fb05e33f4..947d71e07 100644 --- a/internal/cli/install_check.go +++ b/internal/cli/install_check.go @@ -2,6 +2,7 @@ package cli import ( "github.com/answerdev/answer/internal/base/data" + "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/pkg/dir" ) @@ -13,7 +14,9 @@ func CheckUploadDir() bool { return dir.CheckDirExist(UploadFilePath) } -func CheckDB(dataConf *data.Database) bool { +// CheckDB check database whether the connection is normal +// if mustInstalled is true, will check table if already exists +func CheckDB(dataConf *data.Database, mustInstalled bool) bool { db, err := data.NewDB(false, dataConf) if err != nil { return false @@ -21,5 +24,16 @@ func CheckDB(dataConf *data.Database) bool { if err = db.Ping(); err != nil { return false } + if !mustInstalled { + return true + } + + exist, err := db.IsTableExist(&entity.Version{}) + if err != nil { + return false + } + if !exist { + return false + } return true } diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go new file mode 100644 index 000000000..42a676710 --- /dev/null +++ b/internal/install/install_controller.go @@ -0,0 +1,146 @@ +package install + +import ( + "github.com/answerdev/answer/configs" + "github.com/answerdev/answer/internal/base/conf" + "github.com/answerdev/answer/internal/base/data" + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/base/translator" + "github.com/answerdev/answer/internal/cli" + "github.com/answerdev/answer/internal/migrations" + "github.com/answerdev/answer/internal/schema" + "github.com/gin-gonic/gin" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" +) + +// 1、校验配置文件 post installation/config-file/check +//2、校验数据库 post installation/db/check +//3、创建配置文件和数据库 post installation/init +//4、配置网站基本信息和超级管理员信息 post installation/base-info + +// LangOptions get installation language options +// @Summary get installation language options +// @Description get installation language options +// @Tags Lang +// @Produce json +// @Success 200 {object} handler.RespBody{data=[]*translator.LangOption} +// @Router /installation/language/options [get] +func LangOptions(ctx *gin.Context) { + handler.HandleResponse(ctx, nil, translator.LanguageOptions) +} + +// CheckConfigFile check config file if exist when installation +// @Summary check config file if exist when installation +// @Description check config file if exist when installation +// @Tags installation +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/config-file/check [post] +func CheckConfigFile(ctx *gin.Context) { + resp := &CheckConfigFileResp{} + resp.ConfigFileExist = cli.CheckConfigFile(confPath) + if !resp.ConfigFileExist { + handler.HandleResponse(ctx, nil, resp) + return + } + allConfig, err := conf.ReadConfig(confPath) + if err != nil { + log.Error(err) + err = errors.BadRequest(reason.ReadConfigFailed) + handler.HandleResponse(ctx, err, nil) + return + } + resp.DbTableExist = cli.CheckDB(allConfig.Data.Database, true) + handler.HandleResponse(ctx, nil, resp) +} + +// CheckDatabase check database if exist when installation +// @Summary check database if exist when installation +// @Description check database if exist when installation +// @Tags installation +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/db/check [post] +func CheckDatabase(ctx *gin.Context) { + req := &CheckDatabaseReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp := &CheckDatabaseResp{} + dataConf := &data.Database{ + Driver: req.DbType, + Connection: req.GetConnection(), + } + resp.ConnectionSuccess = cli.CheckDB(dataConf, true) + if !resp.ConnectionSuccess { + handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) + return + } + handler.HandleResponse(ctx, nil, resp) +} + +// InitEnvironment init environment +// @Summary init environment +// @Description init environment +// @Tags installation +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/init [post] +func InitEnvironment(ctx *gin.Context) { + req := &CheckDatabaseReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + err := cli.InstallConfigFile(confPath) + if err != nil { + handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{ + Success: false, + CreateConfigFailed: true, + DefaultConfig: string(configs.Config), + ErrType: schema.ErrTypeAlert.ErrType, + }) + return + } + + c, err := conf.ReadConfig(confPath) + if err != nil { + log.Errorf("read config failed %s", err) + err = errors.BadRequest(reason.ReadConfigFailed) + handler.HandleResponse(ctx, err, nil) + return + } + + if err := migrations.InitDB(c.Data.Database); err != nil { + log.Error("init database error: ", err.Error()) + handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) + return + } + handler.HandleResponse(ctx, nil, nil) +} + +// InitBaseInfo init base info +// @Summary init base info +// @Description init base info +// @Tags installation +// @Accept json +// @Produce json +// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}} +// @Router /installation/base-info [post] +func InitBaseInfo(ctx *gin.Context) { + req := &InitBaseInfoReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + // 修改配置文件 + // 修改管理员和对应信息 + handler.HandleResponse(ctx, nil, nil) + return +} diff --git a/internal/install/install_main.go b/internal/install/install_main.go new file mode 100644 index 000000000..a961c2806 --- /dev/null +++ b/internal/install/install_main.go @@ -0,0 +1,30 @@ +package install + +import ( + "os" + + "github.com/answerdev/answer/internal/base/translator" + "github.com/answerdev/answer/internal/cli" +) + +var ( + port = os.Getenv("INSTALL_PORT") + confPath = "" +) + +func Run(configPath string) { + confPath = configPath + // initialize translator for return internationalization error when installing. + _, err := translator.NewTranslator(&translator.I18n{BundleDir: cli.I18nPath}) + if err != nil { + panic(err) + } + + installServer := NewInstallHTTPServer() + if len(port) == 0 { + port = "80" + } + if err = installServer.Run(":" + port); err != nil { + panic(err) + } +} diff --git a/internal/install/install_req.go b/internal/install/install_req.go new file mode 100644 index 000000000..38070c605 --- /dev/null +++ b/internal/install/install_req.go @@ -0,0 +1,81 @@ +package install + +import ( + "fmt" + "strings" + + "xorm.io/xorm/schemas" +) + +// CheckConfigFileResp check config file if exist or not response +type CheckConfigFileResp struct { + ConfigFileExist bool `json:"config_file_exist"` + DbTableExist bool `json:"db_table_exist"` +} + +// CheckDatabaseReq check database +type CheckDatabaseReq struct { + DbType string `json:"db_type"` + DbUsername string `json:"db_username"` + DbPassword string `json:"db_password"` + DbHost string `json:"db_host"` + DbName string `json:"db_name"` + DbFile string `json:"db_file"` +} + +// GetConnection get connection string +func (r *CheckDatabaseReq) GetConnection() string { + if r.DbType == string(schemas.SQLITE) { + return r.DbFile + } + if r.DbType == string(schemas.MYSQL) { + return fmt.Sprintf("%s:%s@tcp(%s)/%s", + r.DbUsername, r.DbPassword, r.DbHost, r.DbName) + } + if r.DbType == string(schemas.POSTGRES) { + host, port := parsePgSQLHostPort(r.DbHost) + return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s", + host, port, r.DbUsername, r.DbPassword, r.DbName) + } + return "" +} + +func parsePgSQLHostPort(dbHost string) (host string, port string) { + if strings.Contains(dbHost, ":") { + idx := strings.LastIndex(dbHost, ":") + host, port = dbHost[:idx], dbHost[idx+1:] + } else if len(dbHost) > 0 { + host = dbHost + } + if host == "" { + host = "127.0.0.1" + } + if port == "" { + port = "5432" + } + return host, port +} + +// CheckDatabaseResp check database response +type CheckDatabaseResp struct { + ConnectionSuccess bool `json:"connection_success"` +} + +// InitEnvironmentResp init environment response +type InitEnvironmentResp struct { + Success bool `json:"success"` + CreateConfigFailed bool `json:"create_config_failed"` + DefaultConfig string `json:"default_config"` + ErrType string `json:"err_type"` +} + +// InitBaseInfoReq init base info request +type InitBaseInfoReq struct { + Language string `json:"language"` + SiteName string `json:"site_name"` + SiteURL string `json:"site_url"` + ContactEmail string `json:"contact_email"` + AdminName string `json:"admin_name"` + AdminPassword string `json:"admin_password"` + AdminEmail string `json:"admin_email"` +} diff --git a/internal/base/server/install.go b/internal/install/install_server.go similarity index 56% rename from internal/base/server/install.go rename to internal/install/install_server.go index da5652ccf..a301c1978 100644 --- a/internal/base/server/install.go +++ b/internal/install/install_server.go @@ -1,4 +1,4 @@ -package server +package install import ( "embed" @@ -6,7 +6,6 @@ import ( "io/fs" "net/http" - "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -25,42 +24,31 @@ func (r *_resource) Open(name string) (fs.File, error) { return r.fs.Open(name) } -// NewHTTPServer new http server. +// NewInstallHTTPServer new install http server. func NewInstallHTTPServer() *gin.Engine { r := gin.New() gin.SetMode(gin.DebugMode) - - r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK??") }) - - // gin.SetMode(gin.ReleaseMode) - + r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") }) r.StaticFS("/static", http.FS(&_resource{ fs: ui.Build, })) installApi := r.Group("") - installApi.GET("/install", Install) + installApi.GET("/install", WebPage) - installApi.POST("/installation/db/check", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) + installApi.GET("/installation/language/options", LangOptions) - installApi.POST("/installation/config-file/check", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) + installApi.POST("/installation/db/check", CheckDatabase) - installApi.POST("/installation/init", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) + installApi.POST("/installation/config-file/check", CheckConfigFile) - installApi.POST("/installation/base-info", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) + installApi.POST("/installation/init", InitEnvironment) + installApi.POST("/installation/base-info", InitBaseInfo) return r } -func Install(c *gin.Context) { +func WebPage(c *gin.Context) { filePath := "" var file []byte var err error diff --git a/internal/router/ui.go b/internal/router/ui.go index 852f6b06c..84231b0c0 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -10,6 +10,7 @@ import ( "github.com/answerdev/answer/i18n" "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" @@ -94,6 +95,10 @@ func (a *UIRouter) Register(r *gin.Engine) { c.String(http.StatusOK, string(file)) }) + r.GET("/installation/language/options", func(c *gin.Context) { + handler.HandleResponse(c, nil, translator.LanguageOptions) + }) + r.POST("/installation/db/check", func(c *gin.Context) { num := rand.Intn(10) if num > 5 { diff --git a/internal/schema/err_schema.go b/internal/schema/err_schema.go index 3270b365d..8acb48ca8 100644 --- a/internal/schema/err_schema.go +++ b/internal/schema/err_schema.go @@ -7,3 +7,5 @@ type ErrTypeData struct { var ErrTypeModal = ErrTypeData{ErrType: "modal"} var ErrTypeToast = ErrTypeData{ErrType: "toast"} + +var ErrTypeAlert = ErrTypeData{ErrType: "alert"} From 260ff708d2ed88dec3439f819a731eab4ba73a81 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 10:23:53 +0800 Subject: [PATCH 0150/3337] fix: queryGroup component add pathname param --- ui/config-overrides.js | 4 ++-- ui/src/components/QueryGroup/index.tsx | 21 +++++++++++++++++---- ui/src/components/QuestionList/index.tsx | 1 + ui/src/pages/Install/index.tsx | 2 +- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ui/config-overrides.js b/ui/config-overrides.js index c8812ce69..35510f123 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -18,12 +18,12 @@ module.exports = { const config = configFunction(proxy, allowedHost); config.proxy = { '/answer': { - target: 'http://10.0.20.88:8080', + target: 'http://10.0.10.98:2060', changeOrigin: true, secure: false, }, '/installation': { - target: 'http://10.0.20.88:8080', + target: 'http://10.0.10.98:2060', changeOrigin: true, secure: false, }, diff --git a/ui/src/components/QueryGroup/index.tsx b/ui/src/components/QueryGroup/index.tsx index fae016210..15c3ad493 100644 --- a/ui/src/components/QueryGroup/index.tsx +++ b/ui/src/components/QueryGroup/index.tsx @@ -1,6 +1,6 @@ import { FC, memo } from 'react'; import { ButtonGroup, Button, DropdownButton, Dropdown } from 'react-bootstrap'; -import { useSearchParams } from 'react-router-dom'; +import { useSearchParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; @@ -11,6 +11,7 @@ interface Props { currentSort: string; sortKey?: string; className?: string; + pathname?: string; } const MAX_BUTTON_COUNT = 3; const Index: FC = ({ @@ -19,8 +20,10 @@ const Index: FC = ({ sortKey = 'order', i18nKeyPrefix = '', className = '', + pathname = '', }) => { const [searchParams, setUrlSearchParams] = useSearchParams(); + const navigate = useNavigate(); const { t } = useTranslation('translation', { keyPrefix: i18nKeyPrefix, @@ -36,7 +39,11 @@ const Index: FC = ({ const handleClick = (e, type) => { e.preventDefault(); const str = handleParams(type); - setUrlSearchParams(str); + if (pathname) { + navigate(`${pathname}${str}`); + } else { + setUrlSearchParams(str); + } }; const filteredData = data.filter((_, index) => index > MAX_BUTTON_COUNT - 2); @@ -69,7 +76,9 @@ const Index: FC = ({ } : {} } - href={handleParams(key)} + href={ + pathname ? `${pathname}${handleParams(key)}` : handleParams(key) + } onClick={(evt) => handleClick(evt, key)}> {t(name)} @@ -95,7 +104,11 @@ const Index: FC = ({ 'd-block d-md-none', className, )} - href={handleParams(key)} + href={ + pathname + ? `${pathname}${handleParams(key)}` + : handleParams(key) + } onClick={(evt) => handleClick(evt, key)}> {t(name)} diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index a353d9bd0..469a6e584 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -115,6 +115,7 @@ const QuestionList: FC = ({ source }) => { diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index f407e156c..287f4f11b 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -21,7 +21,7 @@ import { const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step, setStep] = useState(3); + const [step, setStep] = useState(1); const [loading, setLoading] = useState(true); const [errorData, setErrorData] = useState<{ [propName: string]: any }>({ msg: '', From 7a8f5b67bc9597717909b5bc192f55f9f1a81120 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 10:35:01 +0800 Subject: [PATCH 0151/3337] feat(local): set up app with language and time zone --- ui/src/App.tsx | 1 + ui/src/i18n/init.ts | 3 +- ui/src/index.tsx | 8 +--- ui/src/pages/Admin/Interface/index.tsx | 4 +- .../Install/components/FirstStep/index.tsx | 4 +- ui/src/pages/Layout/index.tsx | 30 ++------------ ui/src/pages/Users/Login/index.tsx | 6 +-- .../pages/Users/Settings/Interface/index.tsx | 39 +++++++++++-------- ui/src/router/routes.ts | 36 ++++++++--------- ui/src/services/client/index.ts | 1 + ui/src/services/client/settings.ts | 18 +++++++++ ui/src/services/common.ts | 22 +---------- ui/src/stores/interface.ts | 11 ++---- ui/src/stores/siteInfo.ts | 10 ++--- ui/src/stores/userInfo.ts | 1 + ui/src/utils/guard.ts | 25 +++++++++++- ui/src/utils/index.ts | 3 +- ui/src/utils/localize.ts | 32 +++++++++++++++ 18 files changed, 142 insertions(+), 112 deletions(-) create mode 100644 ui/src/services/client/settings.ts create mode 100644 ui/src/utils/localize.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 878ca1ab1..92e9da21e 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,5 +1,6 @@ import { RouterProvider } from 'react-router-dom'; +import './i18n/init'; import { routes, createBrowserRouter } from '@/router'; function App() { diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index e8b41e314..d60413bb4 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -28,7 +28,8 @@ i18next escapeValue: false, }, react: { - transSupportBasicHtmlNodes: true, // allow
and simple html elements in translations + transSupportBasicHtmlNodes: true, + // allow
and simple html elements in translations transKeepBasicHtmlNodesFor: ['br', 'strong', 'i'], }, // backend: { diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 67e5ee50a..93bc4ed8c 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -2,11 +2,10 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { Guard } from '@/utils'; +import { guard } from '@/utils'; import App from './App'; -import './i18n/init'; import './index.scss'; const root = ReactDOM.createRoot( @@ -14,10 +13,7 @@ const root = ReactDOM.createRoot( ); async function bootstrapApp() { - /** - * NOTICE: must pre init logged user info for router - */ - await Guard.pullLoggedUser(); + await guard.setupApp(); root.render( diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 395b9616b..8a477bdba 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -12,7 +12,7 @@ import { interfaceStore } from '@/stores'; import { UploadImg } from '@/components'; import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; import { - languages, + getLanguageOptions, uploadAvatar, updateInterfaceSetting, useInterfaceSetting, @@ -52,7 +52,7 @@ const Interface: FC = () => { }, }); const getLangs = async () => { - const res: LangsType[] = await languages(); + const res: LangsType[] = await getLanguageOptions(); setLangs(res); if (!formData.language.value) { // set default theme value diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx index 5690ca8e1..4f84e3b5e 100644 --- a/ui/src/pages/Install/components/FirstStep/index.tsx +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormValue, FormDataType } from '@/common/interface'; import Progress from '../Progress'; -import { languages } from '@/services'; +import { getLanguageOptions } from '@/services'; interface Props { data: FormValue; @@ -18,7 +18,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const [langs, setLangs] = useState(); const getLangs = async () => { - const res: LangsType[] = await languages(); + const res: LangsType[] = await getLanguageOptions(); setLangs(res); }; diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 363c9b011..b335a3ee2 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -1,42 +1,19 @@ -import { FC, useEffect, memo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { FC, memo } from 'react'; import { Outlet } from 'react-router-dom'; import { Helmet, HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { siteInfoStore, interfaceStore, toastStore } from '@/stores'; +import { siteInfoStore, toastStore } from '@/stores'; import { Header, AdminHeader, Footer, Toast } from '@/components'; -import { useSiteSettings } from '@/services'; -import Storage from '@/utils/storage'; -import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; -let isMounted = false; const Layout: FC = () => { - const { siteInfo, update: siteStoreUpdate } = siteInfoStore(); - const { update: interfaceStoreUpdate } = interfaceStore(); - const { data: siteSettings } = useSiteSettings(); const { msg: toastMsg, variant, clear: toastClear } = toastStore(); - const { i18n } = useTranslation(); - + const { siteInfo } = siteInfoStore.getState(); const closeToast = () => { toastClear(); }; - useEffect(() => { - if (siteSettings) { - siteStoreUpdate(siteSettings.general); - interfaceStoreUpdate(siteSettings.interface); - } - }, [siteSettings]); - if (!isMounted) { - isMounted = true; - const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); - if (lang) { - i18n.changeLanguage(lang); - } - } - return ( @@ -47,6 +24,7 @@ const Layout: FC = () => { revalidateOnFocus: false, }}>
+ {/* TODO: move admin header to Admin/Index */}
diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index 423a66543..b8794ff4e 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -10,7 +10,7 @@ import type { } from '@/common/interface'; import { PageTitle, Unactivate } from '@/components'; import { loggedUserInfoStore } from '@/stores'; -import { getQueryString, Guard, floppyNavigation } from '@/utils'; +import { getQueryString, guard, floppyNavigation } from '@/utils'; import { login, checkImgCode } from '@/services'; import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; @@ -104,7 +104,7 @@ const Index: React.FC = () => { login(params) .then((res) => { updateUser(res); - const userStat = Guard.deriveLoginState(); + const userStat = guard.deriveLoginState(); if (userStat.isNotActivated) { // inactive setStep(2); @@ -159,7 +159,7 @@ const Index: React.FC = () => { if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { setStep(2); } else { - Guard.tryNormalLogged(); + guard.tryNormalLogged(); } }, []); diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index 3a3941b9c..c39bf0b97 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -2,49 +2,54 @@ import React, { useEffect, useState, FormEvent } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import dayjs from 'dayjs'; -import en from 'dayjs/locale/en'; -import zh from 'dayjs/locale/zh-cn'; - import type { LangsType, FormDataType } from '@/common/interface'; import { useToast } from '@/hooks'; -import { languages } from '@/services'; -import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; +import { getLanguageOptions, updateUserInterface } from '@/services'; +import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; +import { localize } from '@/utils'; +import { loggedUserInfoStore } from '@/stores'; const Index = () => { - const { t, i18n } = useTranslation('translation', { + const { t } = useTranslation('translation', { keyPrefix: 'settings.interface', }); + const loggedUserInfo = loggedUserInfoStore.getState().user; const toast = useToast(); const [langs, setLangs] = useState(); const [formData, setFormData] = useState({ lang: { - value: true, + // FIXME: userinfo? or userInfo.language + value: loggedUserInfo, isInvalid: false, errorMsg: '', }, }); const getLangs = async () => { - const res: LangsType[] = await languages(); + const res: LangsType[] = await getLanguageOptions(); setLangs(res); }; const handleSubmit = (event: FormEvent) => { event.preventDefault(); - - Storage.set(CURRENT_LANG_STORAGE_KEY, formData.lang.value); - dayjs.locale(formData.lang.value === DEFAULT_LANG ? en : zh); - i18n.changeLanguage(formData.lang.value); - toast.onShow({ - msg: t('update', { keyPrefix: 'toast' }), - variant: 'success', + const lang = formData.lang.value; + updateUserInterface(lang).then(() => { + loggedUserInfoStore.getState().update({ + ...loggedUserInfo, + language: lang, + }); + localize.setupAppLanguage(); + toast.onShow({ + msg: t('update', { keyPrefix: 'toast' }), + variant: 'success', + }); }); }; useEffect(() => { getLangs(); + // TODO: get default lang by interface api const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); if (lang) { setFormData({ @@ -74,7 +79,7 @@ const Index = () => { }}> {langs?.map((item) => { return ( - ); diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index b5adce7c0..5723a4e20 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -1,6 +1,6 @@ import { RouteObject } from 'react-router-dom'; -import { Guard } from '@/utils'; +import { guard } from '@/utils'; import type { TGuardResult } from '@/utils/guard'; export interface RouteNode extends RouteObject { @@ -21,7 +21,7 @@ const routes: RouteNode[] = [ path: '/', page: 'pages/Layout', guard: async () => { - return Guard.notForbidden(); + return guard.notForbidden(); }, children: [ // question and answer @@ -46,14 +46,14 @@ const routes: RouteNode[] = [ path: 'questions/ask', page: 'pages/Questions/Ask', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { path: 'posts/:qid/edit', page: 'pages/Questions/Ask', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { @@ -81,7 +81,7 @@ const routes: RouteNode[] = [ path: 'tags/:tagId/edit', page: 'pages/Tags/Edit', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, // users @@ -97,7 +97,7 @@ const routes: RouteNode[] = [ path: 'users/settings', page: 'pages/Users/Settings', guard: async () => { - return Guard.logged(); + return guard.logged(); }, children: [ { @@ -130,25 +130,25 @@ const routes: RouteNode[] = [ path: 'users/login', page: 'pages/Users/Login', guard: async () => { - const notLogged = Guard.notLogged(); + const notLogged = guard.notLogged(); if (notLogged.ok) { return notLogged; } - return Guard.notActivated(); + return guard.notActivated(); }, }, { path: 'users/register', page: 'pages/Users/Register', guard: async () => { - return Guard.notLogged(); + return guard.notLogged(); }, }, { path: 'users/account-recovery', page: 'pages/Users/AccountForgot', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { @@ -160,32 +160,32 @@ const routes: RouteNode[] = [ path: 'users/password-reset', page: 'pages/Users/PasswordReset', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { path: 'users/account-activation', page: 'pages/Users/ActiveEmail', guard: async () => { - const notActivated = Guard.notActivated(); + const notActivated = guard.notActivated(); if (notActivated.ok) { return notActivated; } - return Guard.notLogged(); + return guard.notLogged(); }, }, { path: 'users/account-activation/success', page: 'pages/Users/ActivationResult', guard: async () => { - return Guard.activated(); + return guard.activated(); }, }, { path: '/users/account-activation/failed', page: 'pages/Users/ActivationResult', guard: async () => { - return Guard.notActivated(); + return guard.notActivated(); }, }, { @@ -197,7 +197,7 @@ const routes: RouteNode[] = [ path: '/users/account-suspended', page: 'pages/Users/Suspended', guard: async () => { - return Guard.forbidden(); + return guard.forbidden(); }, }, // for admin @@ -205,8 +205,8 @@ const routes: RouteNode[] = [ path: 'admin', page: 'pages/Admin', guard: async () => { - await Guard.pullLoggedUser(true); - return Guard.admin(); + await guard.pullLoggedUser(true); + return guard.admin(); }, children: [ { diff --git a/ui/src/services/client/index.ts b/ui/src/services/client/index.ts index 1f1af66aa..17cfb7e0b 100644 --- a/ui/src/services/client/index.ts +++ b/ui/src/services/client/index.ts @@ -4,3 +4,4 @@ export * from './notification'; export * from './question'; export * from './search'; export * from './tag'; +export * from './settings'; diff --git a/ui/src/services/client/settings.ts b/ui/src/services/client/settings.ts new file mode 100644 index 000000000..a47262eb0 --- /dev/null +++ b/ui/src/services/client/settings.ts @@ -0,0 +1,18 @@ +// import useSWR from 'swr'; + +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; + +export const loadLang = () => { + return request.get('/answer/api/v1/language/config'); +}; + +export const getLanguageOptions = () => { + return request.get('/answer/api/v1/language/options'); +}; + +export const updateUserInterface = (lang: string) => { + return request.put('/answer/api/v1/user/interface', { + language: lang, + }); +}; diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index f27eb0a91..1ddc7f189 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -163,14 +163,6 @@ export const questionDetail = (id: string) => { ); }; -export const langConfig = () => { - return request.get('/answer/api/v1/language/config'); -}; - -export const languages = () => { - return request.get('/answer/api/v1/language/options'); -}; - export const getAnswers = (params: Type.AnswersReq) => { const apiUrl = `/answer/api/v1/answer/page?${qs.stringify(params)}`; return request.get>(apiUrl); @@ -253,16 +245,6 @@ export const changeEmailVerify = (params: { code: string }) => { return request.put('/answer/api/v1/user/email', params); }; -export const useSiteSettings = () => { - const apiUrl = `/answer/api/v1/siteinfo`; - const { data, error } = useSWR( - [apiUrl], - request.instance.get, - ); - - return { - data, - isLoading: !data && !error, - error, - }; +export const getAppSettings = () => { + return request.get('/answer/api/v1/siteinfo'); }; diff --git a/ui/src/stores/interface.ts b/ui/src/stores/interface.ts index ea964efbb..eddd1db8b 100644 --- a/ui/src/stores/interface.ts +++ b/ui/src/stores/interface.ts @@ -1,14 +1,10 @@ import create from 'zustand'; -interface updateParams { - logo: string; - theme: string; - language: string; -} +import { AdminSettingsInterface } from '@/common/interface'; interface InterfaceType { - interface: updateParams; - update: (params: updateParams) => void; + interface: AdminSettingsInterface; + update: (params: AdminSettingsInterface) => void; } const interfaceSetting = create((set) => ({ @@ -16,6 +12,7 @@ const interfaceSetting = create((set) => ({ logo: '', theme: '', language: '', + time_zone: '', }, update: (params) => set(() => { diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts index f5a058dcf..626fbf046 100644 --- a/ui/src/stores/siteInfo.ts +++ b/ui/src/stores/siteInfo.ts @@ -1,14 +1,10 @@ import create from 'zustand'; -interface updateParams { - name: string; - description: string; - short_description: string; -} +import { AdminSettingsGeneral } from '@/common/interface'; interface SiteInfoType { - siteInfo: updateParams; - update: (params: updateParams) => void; + siteInfo: AdminSettingsGeneral; + update: (params: AdminSettingsGeneral) => void; } const siteInfo = create((set) => ({ diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index 017c3149c..cf1aa8604 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -25,6 +25,7 @@ const initUser: UserInfoRes = { website: '', status: '', mail_status: 1, + language: '', }; const loggedUserInfoStore = create((set) => ({ diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index bc73ab9a3..c9517d7a8 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -1,8 +1,9 @@ -import { getLoggedUserInfo } from '@/services'; -import { loggedUserInfoStore } from '@/stores'; +import { getLoggedUserInfo, getAppSettings } from '@/services'; +import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores'; import { RouteAlias } from '@/router/alias'; import Storage from '@/utils/storage'; import { LOGGED_USER_STORAGE_KEY } from '@/common/constants'; +import { setupAppLanguage, setupAppTimeZone } from '@/utils/localize'; import { floppyNavigation } from './floppyNavigation'; @@ -180,3 +181,23 @@ export const tryNormalLogged = (autoLogin: boolean = false) => { return false; }; + +export const initAppSettingsStore = async () => { + const appSettings = await getAppSettings(); + if (appSettings) { + siteInfoStore.getState().update(appSettings.general); + interfaceStore.getState().update(appSettings.interface); + } +}; + +export const setupApp = async () => { + /** + * WARN: + * 1. must pre init logged user info for router guard + * 2. must pre init app settings for app render + */ + // TODO: optimize `initAppSettingsStore` by server render + await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]); + setupAppLanguage(); + setupAppTimeZone(); +}; diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts index 69f70696c..de9e8fefd 100644 --- a/ui/src/utils/index.ts +++ b/ui/src/utils/index.ts @@ -2,5 +2,6 @@ export { default as request } from './request'; export { default as Storage } from './storage'; export { floppyNavigation } from './floppyNavigation'; -export * as Guard from './guard'; +export * as guard from './guard'; +export * as localize from './localize'; export * from './common'; diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts new file mode 100644 index 000000000..4a2b5c053 --- /dev/null +++ b/ui/src/utils/localize.ts @@ -0,0 +1,32 @@ +import dayjs from 'dayjs'; +import i18next from 'i18next'; + +import { interfaceStore, loggedUserInfoStore } from '@/stores'; +import { DEFAULT_LANG } from '@/common/constants'; + +const localDayjs = (langName) => { + langName = langName.replace('_', '-').toLowerCase(); + dayjs.locale(langName); +}; + +export const getCurrentLang = () => { + const loggedUser = loggedUserInfoStore.getState().user; + const adminInterface = interfaceStore.getState().interface; + let currentLang = loggedUser.language; + // `default` mean use language value from admin interface + if (/default/i.test(currentLang) && adminInterface.language) { + currentLang = adminInterface.language; + } + currentLang ||= DEFAULT_LANG; + return currentLang; +}; + +export const setupAppLanguage = () => { + const lang = getCurrentLang(); + localDayjs(lang); + i18next.changeLanguage(lang); +}; + +export const setupAppTimeZone = () => { + // FIXME +}; From 78914647a88129b15efa4afc273f90a8461f837c Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 10:50:13 +0800 Subject: [PATCH 0152/3337] fix(siteinfostore): fix default site info value in siteInfo store --- ui/src/stores/siteInfo.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts index 626fbf046..14f3a2732 100644 --- a/ui/src/stores/siteInfo.ts +++ b/ui/src/stores/siteInfo.ts @@ -12,6 +12,8 @@ const siteInfo = create((set) => ({ name: '', description: '', short_description: '', + site_url: '', + contact_email: '', }, update: (params) => set(() => { From d88aa11edf26e89fc1e56d006696e2568124c2ef Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 10:54:27 +0800 Subject: [PATCH 0153/3337] fix: installation init add params --- ui/src/pages/Install/index.tsx | 11 ++++++++++- ui/src/services/install/index.ts | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 287f4f11b..6201f2cd2 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -134,7 +134,16 @@ const Index: FC = () => { }; const checkInstall = () => { - installInit() + const params = { + lang: formData.lang.value, + db_type: formData.db_type.value, + db_username: formData.db_username.value, + db_password: formData.db_password.value, + db_host: formData.db_host.value, + db_name: formData.db_name.value, + db_file: formData.db_file.value, + }; + installInit(params) .then(() => { handleNext(); }) diff --git a/ui/src/services/install/index.ts b/ui/src/services/install/index.ts index 31d50690e..a2026a7e7 100644 --- a/ui/src/services/install/index.ts +++ b/ui/src/services/install/index.ts @@ -8,8 +8,8 @@ export const dbCheck = (params) => { return request.post('/installation/db/check', params); }; -export const installInit = () => { - return request.post('/installation/init'); +export const installInit = (params) => { + return request.post('/installation/init', params); }; export const installBaseInfo = (params) => { From bd53500116075a57cfe2e87a13e19f4a0b956d6d Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 11:03:27 +0800 Subject: [PATCH 0154/3337] refactor(admin): move admin header to admin Index --- ui/src/pages/Admin/index.tsx | 3 ++- ui/src/pages/Layout/index.tsx | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx index 27ef85650..f8b389d63 100644 --- a/ui/src/pages/Admin/index.tsx +++ b/ui/src/pages/Admin/index.tsx @@ -3,7 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { AccordionNav, PageTitle } from '@/components'; +import { AccordionNav, AdminHeader, PageTitle } from '@/components'; import { ADMIN_NAV_MENUS } from '@/common/constants'; import './index.scss'; @@ -13,6 +13,7 @@ const Dashboard: FC = () => { return ( <> +
diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index b335a3ee2..7539c2fa2 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -5,7 +5,7 @@ import { Helmet, HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; import { siteInfoStore, toastStore } from '@/stores'; -import { Header, AdminHeader, Footer, Toast } from '@/components'; +import { Header, Footer, Toast } from '@/components'; const Layout: FC = () => { const { msg: toastMsg, variant, clear: toastClear } = toastStore(); @@ -24,8 +24,6 @@ const Layout: FC = () => { revalidateOnFocus: false, }}>
- {/* TODO: move admin header to Admin/Index */} -
From 3f04532003f1b72ed6b70907aeab195c447a81c2 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 11:16:22 +0800 Subject: [PATCH 0155/3337] fix(user-interface): fix how to get current lang --- ui/src/common/interface.ts | 5 ++++- ui/src/pages/Users/Settings/Interface/index.tsx | 16 +--------------- ui/src/utils/request.ts | 10 +++------- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index a4c7dcd26..1e61d8741 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -116,9 +116,12 @@ export interface UserInfoRes extends UserInfoBase { bio: string; bio_html: string; create_time?: string; - /** value = 1 active; value = 2 inactivated + /** + * value = 1 active; + * value = 2 inactivated */ mail_status: number; + language: string; e_mail?: string; [prop: string]: any; } diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index c39bf0b97..fb78f608b 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -5,8 +5,6 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormDataType } from '@/common/interface'; import { useToast } from '@/hooks'; import { getLanguageOptions, updateUserInterface } from '@/services'; -import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; -import Storage from '@/utils/storage'; import { localize } from '@/utils'; import { loggedUserInfoStore } from '@/stores'; @@ -19,8 +17,7 @@ const Index = () => { const [langs, setLangs] = useState(); const [formData, setFormData] = useState({ lang: { - // FIXME: userinfo? or userInfo.language - value: loggedUserInfo, + value: loggedUserInfo.language, isInvalid: false, errorMsg: '', }, @@ -49,17 +46,6 @@ const Index = () => { useEffect(() => { getLangs(); - // TODO: get default lang by interface api - const lang = Storage.get(CURRENT_LANG_STORAGE_KEY); - if (lang) { - setFormData({ - lang: { - value: lang, - isInvalid: false, - errorMsg: '', - }, - }); - } }, []); return (
diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index 8167448dc..7859b90d6 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -3,12 +3,9 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import { Modal } from '@/components'; import { loggedUserInfoStore, toastStore } from '@/stores'; -import { - LOGGED_TOKEN_STORAGE_KEY, - CURRENT_LANG_STORAGE_KEY, - DEFAULT_LANG, -} from '@/common/constants'; +import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; +import { getCurrentLang } from '@/utils/localize'; import Storage from './storage'; import { floppyNavigation } from './floppyNavigation'; @@ -35,8 +32,7 @@ class Request { this.instance.interceptors.request.use( (requestConfig: AxiosRequestConfig) => { const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || ''; - // default lang en_US - const lang = Storage.get(CURRENT_LANG_STORAGE_KEY) || DEFAULT_LANG; + const lang = getCurrentLang(); requestConfig.headers = { Authorization: token, 'Accept-Language': lang, From 23b4a44ce33bd6b555ad2f063894cc9bfb035a4d Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 4 Nov 2022 11:30:53 +0800 Subject: [PATCH 0156/3337] feat: rewrite config file when install config --- internal/base/conf/conf.go | 11 +++++++++ internal/cli/install.go | 25 +++------------------ internal/install/install_controller.go | 14 ++++++++---- pkg/writer/writer.go | 31 ++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 pkg/writer/writer.go diff --git a/internal/base/conf/conf.go b/internal/base/conf/conf.go index 558468259..37b091c08 100644 --- a/internal/base/conf/conf.go +++ b/internal/base/conf/conf.go @@ -9,7 +9,9 @@ import ( "github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/router" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/pkg/writer" "github.com/segmentfault/pacman/contrib/conf/viper" + "sigs.k8s.io/yaml" ) // AllConfig all config @@ -48,3 +50,12 @@ func ReadConfig(configFilePath string) (c *AllConfig, err error) { } return c, nil } + +// RewriteConfig rewrite config file path +func RewriteConfig(configFilePath string, allConfig *AllConfig) error { + content, err := yaml.Marshal(allConfig) + if err != nil { + return err + } + return writer.ReplaceFile(configFilePath, string(content)) +} diff --git a/internal/cli/install.go b/internal/cli/install.go index eee7b89a6..b31abbf27 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -1,14 +1,13 @@ package cli import ( - "bufio" "fmt" - "os" "path/filepath" "github.com/answerdev/answer/configs" "github.com/answerdev/answer/i18n" "github.com/answerdev/answer/pkg/dir" + "github.com/answerdev/answer/pkg/writer" ) const ( @@ -50,7 +49,7 @@ func InstallConfigFile(configFilePath string) error { } fmt.Printf("[config-file] create directory success, config file is %s\n", configFilePath) - if err := writerFile(configFilePath, string(configs.Config)); err != nil { + if err := writer.WriteFile(configFilePath, string(configs.Config)); err != nil { fmt.Printf("[config-file] install fail %s\n", err.Error()) return fmt.Errorf("write file failed %s", err) } @@ -87,7 +86,7 @@ func installI18nBundle() { continue } fmt.Printf("[i18n] install %s bundle...\n", item.Name()) - err = writerFile(path, string(content)) + err = writer.WriteFile(path, string(content)) if err != nil { fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error()) } else { @@ -95,21 +94,3 @@ func installI18nBundle() { } } } - -func writerFile(filePath, content string) error { - file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666) - if err != nil { - return err - } - defer func() { - _ = file.Close() - }() - writer := bufio.NewWriter(file) - if _, err := writer.WriteString(content); err != nil { - return err - } - if err := writer.Flush(); err != nil { - return err - } - return nil -} diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index 42a676710..1b98db82f 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -98,8 +98,7 @@ func InitEnvironment(ctx *gin.Context) { return } - err := cli.InstallConfigFile(confPath) - if err != nil { + if err := cli.InstallConfigFile(confPath); err != nil { handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{ Success: false, CreateConfigFailed: true, @@ -112,8 +111,15 @@ func InitEnvironment(ctx *gin.Context) { c, err := conf.ReadConfig(confPath) if err != nil { log.Errorf("read config failed %s", err) - err = errors.BadRequest(reason.ReadConfigFailed) - handler.HandleResponse(ctx, err, nil) + handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) + return + } + c.Data.Database.Driver = req.DbType + c.Data.Database.Connection = req.GetConnection() + + if err := conf.RewriteConfig(confPath, c); err != nil { + log.Errorf("rewrite config failed %s", err) + handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) return } diff --git a/pkg/writer/writer.go b/pkg/writer/writer.go new file mode 100644 index 000000000..d2b6c440a --- /dev/null +++ b/pkg/writer/writer.go @@ -0,0 +1,31 @@ +package writer + +import ( + "bufio" + "os" +) + +// ReplaceFile remove old file and write new file +func ReplaceFile(filePath, content string) error { + _ = os.Remove(filePath) + return WriteFile(filePath, content) +} + +// WriteFile write file to path +func WriteFile(filePath, content string) error { + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666) + if err != nil { + return err + } + defer func() { + _ = file.Close() + }() + writer := bufio.NewWriter(file) + if _, err := writer.WriteString(content); err != nil { + return err + } + if err := writer.Flush(); err != nil { + return err + } + return nil +} From 9dd41b6248c8fd35a52d5851a6a822eb7b5f16f4 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 11:45:04 +0800 Subject: [PATCH 0157/3337] fix: install select langs need change i18n --- ui/src/common/constants.ts | 2 +- .../pages/Install/components/FirstStep/index.tsx | 4 ++-- ui/src/pages/Install/index.tsx | 15 ++++++++++++--- ui/src/utils/localize.ts | 6 ++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index b700ad542..028e56e84 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -1,5 +1,5 @@ export const DEFAULT_LANG = 'en_US'; -export const CURRENT_LANG_STORAGE_KEY = '_a_lang__'; +export const CURRENT_LANG_STORAGE_KEY = '_a_lang_'; export const LOGGED_USER_STORAGE_KEY = '_a_lui_'; export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; diff --git a/ui/src/pages/Install/components/FirstStep/index.tsx b/ui/src/pages/Install/components/FirstStep/index.tsx index 78b4aac8b..e35a0f291 100644 --- a/ui/src/pages/Install/components/FirstStep/index.tsx +++ b/ui/src/pages/Install/components/FirstStep/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormValue, FormDataType } from '@/common/interface'; import Progress from '../Progress'; -import { getInstallLangOptions } from '@/services'; +import { getLanguageOptions } from '@/services'; interface Props { data: FormValue; @@ -18,7 +18,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const [langs, setLangs] = useState(); const getLangs = async () => { - const res: LangsType[] = await getInstallLangOptions(); + const res: LangsType[] = await getLanguageOptions(); setLangs(res); changeCallback({ lang: { diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 6201f2cd2..00793fee6 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -10,6 +10,8 @@ import { installBaseInfo, checkConfigFileExists, } from '@/services'; +import { Storage } from '@/utils'; +import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import { FirstStep, @@ -170,13 +172,20 @@ const Index: FC = () => { }; const handleStep = () => { + if (step === 1) { + Storage.set(CURRENT_LANG_STORAGE_KEY, formData.lang.value); + handleNext(); + } if (step === 2) { submitDatabaseForm(); - } else if (step === 3) { + } + if (step === 3) { checkInstall(); - } else if (step === 4) { + } + if (step === 4) { submitSiteConfig(); - } else { + } + if (step > 4) { handleNext(); } }; diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts index 4a2b5c053..0c6313b66 100644 --- a/ui/src/utils/localize.ts +++ b/ui/src/utils/localize.ts @@ -2,7 +2,8 @@ import dayjs from 'dayjs'; import i18next from 'i18next'; import { interfaceStore, loggedUserInfoStore } from '@/stores'; -import { DEFAULT_LANG } from '@/common/constants'; +import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; +import { Storage } from '@/utils'; const localDayjs = (langName) => { langName = langName.replace('_', '-').toLowerCase(); @@ -12,12 +13,13 @@ const localDayjs = (langName) => { export const getCurrentLang = () => { const loggedUser = loggedUserInfoStore.getState().user; const adminInterface = interfaceStore.getState().interface; + const storageLang = Storage.get(CURRENT_LANG_STORAGE_KEY); let currentLang = loggedUser.language; // `default` mean use language value from admin interface if (/default/i.test(currentLang) && adminInterface.language) { currentLang = adminInterface.language; } - currentLang ||= DEFAULT_LANG; + currentLang ||= storageLang || DEFAULT_LANG; return currentLang; }; From 25ab726cd1ec62931ea1644a390110c432a1180a Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 11:57:36 +0800 Subject: [PATCH 0158/3337] fix: delete console --- ui/src/pages/Install/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 00793fee6..e42baaa29 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -99,7 +99,7 @@ const Index: FC = () => { }); const handleChange = (params: FormDataType) => { - console.log(params); + // console.log(params); setFormData({ ...formData, ...params }); }; From 3a235fd85b6d8f601708eb57c9bfe1649249349a Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 4 Nov 2022 12:03:42 +0800 Subject: [PATCH 0159/3337] feat(interface-language): done admin language setting --- ui/src/pages/Admin/Interface/index.tsx | 29 ++++++++------------------ ui/src/services/admin/settings.ts | 5 +++++ ui/src/stores/interface.ts | 3 ++- ui/src/stores/userInfo.ts | 10 ++++++--- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 8a477bdba..d42b60582 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -12,18 +12,19 @@ import { interfaceStore } from '@/stores'; import { UploadImg } from '@/components'; import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; import { - getLanguageOptions, + getAdminLanguageOptions, uploadAvatar, updateInterfaceSetting, useInterfaceSetting, useThemeOptions, } from '@/services'; +import { setupAppLanguage } from '@/utils/localize'; const Interface: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.interface', }); - const { update: interfaceStoreUpdate } = interfaceStore(); + const storeInterface = interfaceStore.getState().interface; const { data: themes } = useThemeOptions(); const Toast = useToast(); const [langs, setLangs] = useState(); @@ -31,17 +32,17 @@ const Interface: FC = () => { const [formData, setFormData] = useState({ logo: { - value: setting?.logo || '', + value: setting?.logo || storeInterface.logo, isInvalid: false, errorMsg: '', }, theme: { - value: setting?.theme || '', + value: setting?.theme || storeInterface.theme, isInvalid: false, errorMsg: '', }, language: { - value: setting?.language || '', + value: setting?.language || storeInterface.language, isInvalid: false, errorMsg: '', }, @@ -52,19 +53,8 @@ const Interface: FC = () => { }, }); const getLangs = async () => { - const res: LangsType[] = await getLanguageOptions(); + const res: LangsType[] = await getAdminLanguageOptions(); setLangs(res); - if (!formData.language.value) { - // set default theme value - setFormData({ - ...formData, - language: { - value: res[0].value, - isInvalid: false, - errorMsg: '', - }, - }); - } }; // set default theme value if (!formData.theme.value && Array.isArray(themes) && themes.length) { @@ -122,7 +112,8 @@ const Interface: FC = () => { msg: t('update', { keyPrefix: 'toast' }), variant: 'success', }); - interfaceStoreUpdate(reqParams); + interfaceStore.getState().update(reqParams); + setupAppLanguage(); }) .catch((err) => { if (err.isError && err.key) { @@ -172,8 +163,6 @@ const Interface: FC = () => { useEffect(() => { getLangs(); }, []); - - console.log('formData', formData); return ( <>

{t('page_title')}

diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index 5f3702608..6bae35765 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -83,3 +83,8 @@ export const useDashBoard = () => { error, }; }; + +export const getAdminLanguageOptions = () => { + const apiUrl = `/answer/admin/api/language/options`; + return request.get(apiUrl); +}; diff --git a/ui/src/stores/interface.ts b/ui/src/stores/interface.ts index eddd1db8b..e92e0f8a7 100644 --- a/ui/src/stores/interface.ts +++ b/ui/src/stores/interface.ts @@ -1,6 +1,7 @@ import create from 'zustand'; import { AdminSettingsInterface } from '@/common/interface'; +import { DEFAULT_LANG } from '@/common/constants'; interface InterfaceType { interface: AdminSettingsInterface; @@ -11,7 +12,7 @@ const interfaceSetting = create((set) => ({ interface: { logo: '', theme: '', - language: '', + language: DEFAULT_LANG, time_zone: '', }, update: (params) => diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index cf1aa8604..e82a592b8 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -25,17 +25,21 @@ const initUser: UserInfoRes = { website: '', status: '', mail_status: 1, - language: '', + language: 'Default', }; const loggedUserInfoStore = create((set) => ({ user: initUser, - update: (params) => + update: (params) => { + if (!params.language) { + params.language = 'Default'; + } set(() => { Storage.set(LOGGED_TOKEN_STORAGE_KEY, params.access_token); Storage.set(LOGGED_USER_STORAGE_KEY, params); return { user: params }; - }), + }); + }, clear: () => set(() => { Storage.remove(LOGGED_TOKEN_STORAGE_KEY); From 262b29ee28999003cf5a4188e0c3055e334d701c Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 4 Nov 2022 14:54:59 +0800 Subject: [PATCH 0160/3337] fix: delete upgrade page --- ui/src/i18n/locales/en.json | 9 ----- ui/src/pages/Upgrade/index.tsx | 72 ---------------------------------- ui/src/router/routes.ts | 4 -- ui/src/services/common.ts | 4 -- 4 files changed, 89 deletions(-) delete mode 100644 ui/src/pages/Upgrade/index.tsx diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 4f0de90bd..5b767092c 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -830,15 +830,6 @@ "installed": "Already installed", "installed_description": "You appear to have already installed. To reinstall please clear your old database tables first." }, - "upgrade": { - "title": "Answer", - "update_btn": "Update data", - "update_title": "Data update required", - "update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.<1>The update process may take a little while, so please be patient.", - "done_title": "No update required", - "done_btn": "Done", - "done_desscription": "Your Answer data is already up-to-date." - }, "page_404": { "description": "Unfortunately, this page doesn't exist.", "back_home": "Back to homepage" diff --git a/ui/src/pages/Upgrade/index.tsx b/ui/src/pages/Upgrade/index.tsx deleted file mode 100644 index c264e46c5..000000000 --- a/ui/src/pages/Upgrade/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useState } from 'react'; -import { Container, Row, Col, Card, Button, Spinner } from 'react-bootstrap'; -import { useTranslation, Trans } from 'react-i18next'; - -import { PageTitle } from '@/components'; -import { upgradSystem } from '@/services'; - -const Index = () => { - const { t } = useTranslation('translation', { - keyPrefix: 'upgrade', - }); - const [step] = useState(1); - const [loading, setLoading] = useState(false); - - const handleUpdate = async () => { - await upgradSystem(); - setLoading(true); - }; - return ( -
- - - -
-

{t('title')}

- - - {step === 1 && ( - <> -
{t('update_title')}
- }} - /> - {loading ? ( - - ) : ( - - )} - - )} - - {step === 2 && ( - <> -
{t('done_title')}
-

{t('done_desscription')}

- - - )} -
-
- - - - - ); -}; - -export default Index; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 5723a4e20..24016d0a2 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -269,9 +269,5 @@ const routes: RouteNode[] = [ path: '/maintenance', page: 'pages/Maintenance', }, - { - path: '/upgrade', - page: 'pages/Upgrade', - }, ]; export default routes; diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 7b4db7200..1ddc7f189 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -248,7 +248,3 @@ export const changeEmailVerify = (params: { code: string }) => { export const getAppSettings = () => { return request.get('/answer/api/v1/siteinfo'); }; - -export const upgradSystem = () => { - return request.post('/answer/api/v1/upgradation'); -}; From 7bea814bd410385a37634aea47dd34ce734eb122 Mon Sep 17 00:00:00 2001 From: kumfo Date: Fri, 4 Nov 2022 15:00:15 +0800 Subject: [PATCH 0161/3337] fix: change from xorm builder union query to manual build query --- internal/repo/search_common/search_repo.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index b2db83c31..72965c1ff 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -142,13 +142,22 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID, argsA = append(argsA, votes) } - b = b.Union("all", ub) + //b = b.Union("all", ub) + ubSQL, _, err := ub.ToSQL() + if err != nil { + return + } + bSQL, _, err := b.ToSQL() + if err != nil { + return + } + sql := fmt.Sprintf("(%s UNION ALL %s)", ubSQL, bSQL) - querySQL, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() + querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL() if err != nil { return } - countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() + countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL() if err != nil { return } From baec9a93004daf270653398b4ee13353e92f4c98 Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 4 Nov 2022 15:31:53 +0800 Subject: [PATCH 0162/3337] feat: Add timezone setting --- ui/src/common/constants.ts | 699 ++++++++++++------ ui/src/components/FormatTime/index.tsx | 8 +- ui/src/i18n/locales/en.json | 7 +- ui/src/i18n/locales/zh_CN.json | 2 +- ui/src/pages/Admin/Interface/index.tsx | 15 +- ui/src/pages/Questions/Ask/index.tsx | 8 +- .../Detail/components/Alert/index.tsx | 4 +- ui/src/pages/Questions/EditAnswer/index.tsx | 6 +- ui/src/pages/Tags/Edit/index.tsx | 6 +- ui/src/utils/localize.ts | 9 +- 10 files changed, 523 insertions(+), 241 deletions(-) diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 028e56e84..43f92c74c 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -56,229 +56,494 @@ export const ADMIN_NAV_MENUS = [ child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], }, ]; -// timezones + export const TIMEZONES = [ { - label: 'UTC-12', - value: 'UTC-12', - }, - { - label: 'UTC-11:30', - value: 'UTC-11.5', - }, - { - label: 'UTC-11', - value: 'UTC-11', - }, - { - label: 'UTC-10:30', - value: 'UTC-10.5', - }, - { - label: 'UTC-10', - value: 'UTC-10', - }, - { - label: 'UTC-9:30', - value: 'UTC-9.5', - }, - { - label: 'UTC-9', - value: 'UTC-9', - }, - { - label: 'UTC-8:30', - value: 'UTC-8.5', - }, - { - label: 'UTC-8', - value: 'UTC-8', - }, - { - label: 'UTC-7:30', - value: 'UTC-7.5', - }, - { - label: 'UTC-7', - value: 'UTC-7', - }, - { - label: 'UTC-6:30', - value: 'UTC-6.5', - }, - { - label: 'UTC-6', - value: 'UTC-6', - }, - { - label: 'UTC-5:30', - value: 'UTC-5.5', - }, - { - label: 'UTC-5', - value: 'UTC-5', - }, - { - label: 'UTC-4:30', - value: 'UTC-4.5', - }, - { - label: 'UTC-4', - value: 'UTC-4', - }, - { - label: 'UTC-3:30', - value: 'UTC-3.5', - }, - { - label: 'UTC-3', - value: 'UTC-3', - }, - { - label: 'UTC-2:30', - value: 'UTC-2.5', - }, - { - label: 'UTC-2', - value: 'UTC-2', - }, - { - label: 'UTC-1:30', - value: 'UTC-1.5', - }, - { - label: 'UTC-1', - value: 'UTC-1', - }, - { - label: 'UTC-0:30', - value: 'UTC-0.5', - }, - { - label: 'UTC+0', - value: 'UTC+0', - }, - { - label: 'UTC+0:30', - value: 'UTC+0.5', - }, - { - label: 'UTC+1', - value: 'UTC+1', - }, - { - label: 'UTC+1:30', - value: 'UTC+1.5', - }, - { - label: 'UTC+2', - value: 'UTC+2', - }, - { - label: 'UTC+2:30', - value: 'UTC+2.5', - }, - { - label: 'UTC+3', - value: 'UTC+3', - }, - { - label: 'UTC+3:30', + label: 'Africa', + options: [ + { value: 'Africa/Abidjan', label: 'Abidjan' }, + { value: 'Africa/Accra', label: 'Accra' }, + { value: 'Africa/Addis_Ababa', label: 'Addis Ababa' }, + { value: 'Africa/Algiers', label: 'Algiers' }, + { value: 'Africa/Asmara', label: 'Asmara' }, + { value: 'Africa/Bamako', label: 'Bamako' }, + { value: 'Africa/Bangui', label: 'Bangui' }, + { value: 'Africa/Banjul', label: 'Banjul' }, + { value: 'Africa/Bissau', label: 'Bissau' }, + { value: 'Africa/Blantyre', label: 'Blantyre' }, + { value: 'Africa/Brazzaville', label: 'Brazzaville' }, + { value: 'Africa/Bujumbura', label: 'Bujumbura' }, + { value: 'Africa/Cairo', label: 'Cairo' }, + { value: 'Africa/Casablanca', label: 'Casablanca' }, + { value: 'Africa/Ceuta', label: 'Ceuta' }, + { value: 'Africa/Conakry', label: 'Conakry' }, + { value: 'Africa/Dakar', label: 'Dakar' }, + { value: 'Africa/Dar_es_Salaam', label: 'Dar es Salaam' }, + { value: 'Africa/Djibouti', label: 'Djibouti' }, + { value: 'Africa/Douala', label: 'Douala' }, + { value: 'Africa/El_Aaiun', label: 'El Aaiun' }, + { value: 'Africa/Freetown', label: 'Freetown' }, + { value: 'Africa/Gaborone', label: 'Gaborone' }, + { value: 'Africa/Harare', label: 'Harare' }, + { value: 'Africa/Johannesburg', label: 'Johannesburg' }, + { value: 'Africa/Juba', label: 'Juba' }, + { value: 'Africa/Kampala', label: 'Kampala' }, + { value: 'Africa/Khartoum', label: 'Khartoum' }, + { value: 'Africa/Kigali', label: 'Kigali' }, + { value: 'Africa/Kinshasa', label: 'Kinshasa' }, + { value: 'Africa/Lagos', label: 'Lagos' }, + { value: 'Africa/Libreville', label: 'Libreville' }, + { value: 'Africa/Lome', label: 'Lome' }, + { value: 'Africa/Luanda', label: 'Luanda' }, + { value: 'Africa/Lubumbashi', label: 'Lubumbashi' }, + { value: 'Africa/Lusaka', label: 'Lusaka' }, + { value: 'Africa/Malabo', label: 'Malabo' }, + { value: 'Africa/Maputo', label: 'Maputo' }, + { value: 'Africa/Maseru', label: 'Maseru' }, + { value: 'Africa/Mbabane', label: 'Mbabane' }, + { value: 'Africa/Mogadishu', label: 'Mogadishu' }, + { value: 'Africa/Monrovia', label: 'Monrovia' }, + { value: 'Africa/Nairobi', label: 'Nairobi' }, + { value: 'Africa/Ndjamena', label: 'Ndjamena' }, + { value: 'Africa/Niamey', label: 'Niamey' }, + { value: 'Africa/Nouakchott', label: 'Nouakchott' }, + { value: 'Africa/Ouagadougou', label: 'Ouagadougou' }, + { value: 'Africa/Porto-Novo', label: 'Porto-Novo' }, + { value: 'Africa/Sao_Tome', label: 'Sao Tome' }, + { value: 'Africa/Tripoli', label: 'Tripoli' }, + { value: 'Africa/Tunis', label: 'Tunis' }, + { value: 'Africa/Windhoek', label: 'Windhoek' }, + ], + }, + { + label: 'America', + options: [ + { value: 'America/Adak', label: 'Adak' }, + { value: 'America/Anchorage', label: 'Anchorage' }, + { value: 'America/Anguilla', label: 'Anguilla' }, + { value: 'America/Antigua', label: 'Antigua' }, + { value: 'America/Araguaina', label: 'Araguaina' }, + { + value: 'America/Argentina/Buenos_Aires', + label: 'Argentina - Buenos Aires', + }, + { value: 'America/Argentina/Catamarca', label: 'Argentina - Catamarca' }, + { value: 'America/Argentina/Cordoba', label: 'Argentina - Cordoba' }, + { value: 'America/Argentina/Jujuy', label: 'Argentina - Jujuy' }, + { value: 'America/Argentina/La_Rioja', label: 'Argentina - La Rioja' }, + { value: 'America/Argentina/Mendoza', label: 'Argentina - Mendoza' }, + { + value: 'America/Argentina/Rio_Gallegos', + label: 'Argentina - Rio Gallegos', + }, + { value: 'America/Argentina/Salta', label: 'Argentina - Salta' }, + { value: 'America/Argentina/San_Juan', label: 'Argentina - San Juan' }, + { value: 'America/Argentina/San_Luis', label: 'Argentina - San Luis' }, + { value: 'America/Argentina/Tucuman', label: 'Argentina - Tucuman' }, + { value: 'America/Argentina/Ushuaia', label: 'Argentina - Ushuaia' }, + { value: 'America/Aruba', label: 'Aruba' }, + { value: 'America/Asuncion', label: 'Asuncion' }, + { value: 'America/Atikokan', label: 'Atikokan' }, + { value: 'America/Bahia', label: 'Bahia' }, + { value: 'America/Bahia_Banderas', label: 'Bahia Banderas' }, + { value: 'America/Barbados', label: 'Barbados' }, + { value: 'America/Belem', label: 'Belem' }, + { value: 'America/Belize', label: 'Belize' }, + { value: 'America/Blanc-Sablon', label: 'Blanc-Sablon' }, + { value: 'America/Boa_Vista', label: 'Boa Vista' }, + { value: 'America/Bogota', label: 'Bogota' }, + { value: 'America/Boise', label: 'Boise' }, + { value: 'America/Cambridge_Bay', label: 'Cambridge Bay' }, + { value: 'America/Campo_Grande', label: 'Campo Grande' }, + { value: 'America/Cancun', label: 'Cancun' }, + { value: 'America/Caracas', label: 'Caracas' }, + { value: 'America/Cayenne', label: 'Cayenne' }, + { value: 'America/Cayman', label: 'Cayman' }, + { value: 'America/Chicago', label: 'Chicago' }, + { value: 'America/Chihuahua', label: 'Chihuahua' }, + { value: 'America/Costa_Rica', label: 'Costa Rica' }, + { value: 'America/Creston', label: 'Creston' }, + { value: 'America/Cuiaba', label: 'Cuiaba' }, + { value: 'America/Curacao', label: 'Curacao' }, + { value: 'America/Danmarkshavn', label: 'Danmarkshavn' }, + { value: 'America/Dawson', label: 'Dawson' }, + { value: 'America/Dawson_Creek', label: 'Dawson Creek' }, + { value: 'America/Denver', label: 'Denver' }, + { value: 'America/Detroit', label: 'Detroit' }, + { value: 'America/Dominica', label: 'Dominica' }, + { value: 'America/Edmonton', label: 'Edmonton' }, + { value: 'America/Eirunepe', label: 'Eirunepe' }, + { value: 'America/El_Salvador', label: 'El Salvador' }, + { value: 'America/Fort_Nelson', label: 'Fort Nelson' }, + { value: 'America/Fortaleza', label: 'Fortaleza' }, + { value: 'America/Glace_Bay', label: 'Glace Bay' }, + { value: 'America/Godthab', label: 'Godthab' }, + { value: 'America/Goose_Bay', label: 'Goose Bay' }, + { value: 'America/Grand_Turk', label: 'Grand Turk' }, + { value: 'America/Grenada', label: 'Grenada' }, + { value: 'America/Guadeloupe', label: 'Guadeloupe' }, + { value: 'America/Guatemala', label: 'Guatemala' }, + { value: 'America/Guayaquil', label: 'Guayaquil' }, + { value: 'America/Guyana', label: 'Guyana' }, + { value: 'America/Halifax', label: 'Halifax' }, + { value: 'America/Havana', label: 'Havana' }, + { value: 'America/Hermosillo', label: 'Hermosillo' }, + { + value: 'America/Indiana/Indianapolis', + label: 'Indiana - Indianapolis', + }, + { value: 'America/Indiana/Knox', label: 'Indiana - Knox' }, + { value: 'America/Indiana/Marengo', label: 'Indiana - Marengo' }, + { value: 'America/Indiana/Petersburg', label: 'Indiana - Petersburg' }, + { value: 'America/Indiana/Tell_City', label: 'Indiana - Tell City' }, + { value: 'America/Indiana/Vevay', label: 'Indiana - Vevay' }, + { value: 'America/Indiana/Vincennes', label: 'Indiana - Vincennes' }, + { value: 'America/Indiana/Winamac', label: 'Indiana - Winamac' }, + { value: 'America/Inuvik', label: 'Inuvik' }, + { value: 'America/Iqaluit', label: 'Iqaluit' }, + { value: 'America/Jamaica', label: 'Jamaica' }, + { value: 'America/Juneau', label: 'Juneau' }, + { value: 'America/Kentucky/Louisville', label: 'Kentucky - Louisville' }, + { value: 'America/Kentucky/Monticello', label: 'Kentucky - Monticello' }, + { value: 'America/Kralendijk', label: 'Kralendijk' }, + { value: 'America/La_Paz', label: 'La Paz' }, + { value: 'America/Lima', label: 'Lima' }, + { value: 'America/Los_Angeles', label: 'Los Angeles' }, + { value: 'America/Lower_Princes', label: 'Lower Princes' }, + { value: 'America/Maceio', label: 'Maceio' }, + { value: 'America/Managua', label: 'Managua' }, + { value: 'America/Manaus', label: 'Manaus' }, + { value: 'America/Marigot', label: 'Marigot' }, + { value: 'America/Martinique', label: 'Martinique' }, + { value: 'America/Matamoros', label: 'Matamoros' }, + { value: 'America/Mazatlan', label: 'Mazatlan' }, + { value: 'America/Miquelon', label: 'Miquelon' }, + { value: 'America/Moncton', label: 'Moncton' }, + { value: 'America/Monterrey', label: 'Monterrey' }, + { value: 'America/Montevideo', label: 'Montevideo' }, + { value: 'America/Montserrat', label: 'Montserrat' }, + { value: 'America/Nassau', label: 'Nassau' }, + { value: 'America/New_York', label: 'New York' }, + { value: 'America/Nipigon', label: 'Nipigon' }, + { value: 'America/Nome', label: 'Nome' }, + { value: 'America/Noronha', label: 'Noronha' }, + { value: 'America/North_Dakota/Beulah', label: 'North Dakota - Beulah' }, + { value: 'America/North_Dakota/Center', label: 'North Dakota - Center' }, + { + value: 'America/North_Dakota/New_Salem', + label: 'North Dakota - New Salem', + }, + { value: 'America/Ojinaga', label: 'Ojinaga' }, + { value: 'America/Panama', label: 'Panama' }, + { value: 'America/Pangnirtung', label: 'Pangnirtung' }, + { value: 'America/Paramaribo', label: 'Paramaribo' }, + { value: 'America/Phoenix', label: 'Phoenix' }, + { value: 'America/Port-au-Prince', label: 'Port-au-Prince' }, + { value: 'America/Port_of_Spain', label: 'Port of Spain' }, + { value: 'America/Porto_Velho', label: 'Porto Velho' }, + { value: 'America/Puerto_Rico', label: 'Puerto Rico' }, + { value: 'America/Punta_Arenas', label: 'Punta Arenas' }, + { value: 'America/Rainy_River', label: 'Rainy River' }, + { value: 'America/Rankin_Inlet', label: 'Rankin Inlet' }, + { value: 'America/Recife', label: 'Recife' }, + { value: 'America/Regina', label: 'Regina' }, + { value: 'America/Resolute', label: 'Resolute' }, + { value: 'America/Rio_Branco', label: 'Rio Branco' }, + { value: 'America/Santarem', label: 'Santarem' }, + { value: 'America/Santiago', label: 'Santiago' }, + { value: 'America/Santo_Domingo', label: 'Santo Domingo' }, + { value: 'America/Sao_Paulo', label: 'Sao Paulo' }, + { value: 'America/Scoresbysund', label: 'Scoresbysund' }, + { value: 'America/Sitka', label: 'Sitka' }, + { value: 'America/St_Barthelemy', label: 'St Barthelemy' }, + { value: 'America/St_Johns', label: 'St Johns' }, + { value: 'America/St_Kitts', label: 'St Kitts' }, + { value: 'America/St_Lucia', label: 'St Lucia' }, + { value: 'America/St_Thomas', label: 'St Thomas' }, + { value: 'America/St_Vincent', label: 'St Vincent' }, + { value: 'America/Swift_Current', label: 'Swift Current' }, + { value: 'America/Tegucigalpa', label: 'Tegucigalpa' }, + { value: 'America/Thule', label: 'Thule' }, + { value: 'America/Thunder_Bay', label: 'Thunder Bay' }, + { value: 'America/Tijuana', label: 'Tijuana' }, + { value: 'America/Toronto', label: 'Toronto' }, + { value: 'America/Tortola', label: 'Tortola' }, + { value: 'America/Vancouver', label: 'Vancouver' }, + { value: 'America/Whitehorse', label: 'Whitehorse' }, + { value: 'America/Winnipeg', label: 'Winnipeg' }, + { value: 'America/Yakutat', label: 'Yakutat' }, + { value: 'America/Yellowknife', label: 'Yellowknife' }, + ], + }, + { + label: 'Antarctica', + options: [ + { value: 'Antarctica/Casey', label: 'Casey' }, + { value: 'Antarctica/Davis', label: 'Davis' }, + { value: 'Antarctica/DumontDUrville', label: 'DumontDUrville' }, + { value: 'Antarctica/Macquarie', label: 'Macquarie' }, + { value: 'Antarctica/Mawson', label: 'Mawson' }, + { value: 'Antarctica/McMurdo', label: 'McMurdo' }, + { value: 'Antarctica/Palmer', label: 'Palmer' }, + { value: 'Antarctica/Rothera', label: 'Rothera' }, + { value: 'Antarctica/Syowa', label: 'Syowa' }, + { value: 'Antarctica/Troll', label: 'Troll' }, + { value: 'Antarctica/Vostok', label: 'Vostok' }, + ], + }, + { + label: 'Arctic', + options: [{ value: 'Arctic/Longyearbyen', label: 'Longyearbyen' }], + }, + { + label: 'Asia', + options: [ + { value: 'Asia/Aden', label: 'Aden' }, + { value: 'Asia/Almaty', label: 'Almaty' }, + { value: 'Asia/Amman', label: 'Amman' }, + { value: 'Asia/Anadyr', label: 'Anadyr' }, + { value: 'Asia/Aqtau', label: 'Aqtau' }, + { value: 'Asia/Aqtobe', label: 'Aqtobe' }, + { value: 'Asia/Ashgabat', label: 'Ashgabat' }, + { value: 'Asia/Atyrau', label: 'Atyrau' }, + { value: 'Asia/Baghdad', label: 'Baghdad' }, + { value: 'Asia/Bahrain', label: 'Bahrain' }, + { value: 'Asia/Baku', label: 'Baku' }, + { value: 'Asia/Bangkok', label: 'Bangkok' }, + { value: 'Asia/Barnaul', label: 'Barnaul' }, + { value: 'Asia/Beirut', label: 'Beirut' }, + { value: 'Asia/Bishkek', label: 'Bishkek' }, + { value: 'Asia/Brunei', label: 'Brunei' }, + { value: 'Asia/Chita', label: 'Chita' }, + { value: 'Asia/Choibalsan', label: 'Choibalsan' }, + { value: 'Asia/Colombo', label: 'Colombo' }, + { value: 'Asia/Damascus', label: 'Damascus' }, + { value: 'Asia/Dhaka', label: 'Dhaka' }, + { value: 'Asia/Dili', label: 'Dili' }, + { value: 'Asia/Dubai', label: 'Dubai' }, + { value: 'Asia/Dushanbe', label: 'Dushanbe' }, + { value: 'Asia/Famagusta', label: 'Famagusta' }, + { value: 'Asia/Gaza', label: 'Gaza' }, + { value: 'Asia/Hebron', label: 'Hebron' }, + { value: 'Asia/Ho_Chi_Minh', label: 'Ho Chi Minh' }, + { value: 'Asia/Hong_Kong', label: 'Hong Kong' }, + { value: 'Asia/Hovd', label: 'Hovd' }, + { value: 'Asia/Irkutsk', label: 'Irkutsk' }, + { value: 'Asia/Jakarta', label: 'Jakarta' }, + { value: 'Asia/Jayapura', label: 'Jayapura' }, + { value: 'Asia/Jerusalem', label: 'Jerusalem' }, + { value: 'Asia/Kabul', label: 'Kabul' }, + { value: 'Asia/Kamchatka', label: 'Kamchatka' }, + { value: 'Asia/Karachi', label: 'Karachi' }, + { value: 'Asia/Kathmandu', label: 'Kathmandu' }, + { value: 'Asia/Khandyga', label: 'Khandyga' }, + { value: 'Asia/Kolkata', label: 'Kolkata' }, + { value: 'Asia/Krasnoyarsk', label: 'Krasnoyarsk' }, + { value: 'Asia/Kuala_Lumpur', label: 'Kuala Lumpur' }, + { value: 'Asia/Kuching', label: 'Kuching' }, + { value: 'Asia/Kuwait', label: 'Kuwait' }, + { value: 'Asia/Macau', label: 'Macau' }, + { value: 'Asia/Magadan', label: 'Magadan' }, + { value: 'Asia/Makassar', label: 'Makassar' }, + { value: 'Asia/Manila', label: 'Manila' }, + { value: 'Asia/Muscat', label: 'Muscat' }, + { value: 'Asia/Nicosia', label: 'Nicosia' }, + { value: 'Asia/Novokuznetsk', label: 'Novokuznetsk' }, + { value: 'Asia/Novosibirsk', label: 'Novosibirsk' }, + { value: 'Asia/Omsk', label: 'Omsk' }, + { value: 'Asia/Oral', label: 'Oral' }, + { value: 'Asia/Phnom_Penh', label: 'Phnom Penh' }, + { value: 'Asia/Pontianak', label: 'Pontianak' }, + { value: 'Asia/Pyongyang', label: 'Pyongyang' }, + { value: 'Asia/Qatar', label: 'Qatar' }, + { value: 'Asia/Qostanay', label: 'Qostanay' }, + { value: 'Asia/Qyzylorda', label: 'Qyzylorda' }, + { value: 'Asia/Riyadh', label: 'Riyadh' }, + { value: 'Asia/Sakhalin', label: 'Sakhalin' }, + { value: 'Asia/Samarkand', label: 'Samarkand' }, + { value: 'Asia/Seoul', label: 'Seoul' }, + { value: 'Asia/Shanghai', label: 'Shanghai' }, + { value: 'Asia/Singapore', label: 'Singapore' }, + { value: 'Asia/Srednekolymsk', label: 'Srednekolymsk' }, + { value: 'Asia/Taipei', label: 'Taipei' }, + { value: 'Asia/Tashkent', label: 'Tashkent' }, + { value: 'Asia/Tbilisi', label: 'Tbilisi' }, + { value: 'Asia/Tehran', label: 'Tehran' }, + { value: 'Asia/Thimphu', label: 'Thimphu' }, + { value: 'Asia/Tokyo', label: 'Tokyo' }, + { value: 'Asia/Tomsk', label: 'Tomsk' }, + { value: 'Asia/Ulaanbaatar', label: 'Ulaanbaatar' }, + { value: 'Asia/Urumqi', label: 'Urumqi' }, + { value: 'Asia/Ust-Nera', label: 'Ust-Nera' }, + { value: 'Asia/Vientiane', label: 'Vientiane' }, + { value: 'Asia/Vladivostok', label: 'Vladivostok' }, + { value: 'Asia/Yakutsk', label: 'Yakutsk' }, + { value: 'Asia/Yangon', label: 'Yangon' }, + { value: 'Asia/Yekaterinburg', label: 'Yekaterinburg' }, + { value: 'Asia/Yerevan', label: 'Yerevan' }, + ], + }, + { + label: 'Atlantic', + options: [ + { value: 'Atlantic/Azores', label: 'Azores' }, + { value: 'Atlantic/Bermuda', label: 'Bermuda' }, + { value: 'Atlantic/Canary', label: 'Canary' }, + { value: 'Atlantic/Cape_Verde', label: 'Cape Verde' }, + { value: 'Atlantic/Faroe', label: 'Faroe' }, + { value: 'Atlantic/Madeira', label: 'Madeira' }, + { value: 'Atlantic/Reykjavik', label: 'Reykjavik' }, + { value: 'Atlantic/South_Georgia', label: 'South Georgia' }, + { value: 'Atlantic/Stanley', label: 'Stanley' }, + { value: 'Atlantic/St_Helena', label: 'St Helena' }, + ], + }, + { + label: 'Australia', + options: [ + { value: 'Australia/Adelaide', label: 'Adelaide' }, + { value: 'Australia/Brisbane', label: 'Brisbane' }, + { value: 'Australia/Broken_Hill', label: 'Broken Hill' }, + { value: 'Australia/Currie', label: 'Currie' }, + { value: 'Australia/Darwin', label: 'Darwin' }, + { value: 'Australia/Eucla', label: 'Eucla' }, + { value: 'Australia/Hobart', label: 'Hobart' }, + { value: 'Australia/Lindeman', label: 'Lindeman' }, + { value: 'Australia/Lord_Howe', label: 'Lord Howe' }, + { value: 'Australia/Melbourne', label: 'Melbourne' }, + { value: 'Australia/Perth', label: 'Perth' }, + { value: 'Australia/Sydney', label: 'Sydney' }, + ], + }, + { + label: 'Europe', + options: [ + { value: 'Europe/Amsterdam', label: 'Amsterdam' }, + { value: 'Europe/Andorra', label: 'Andorra' }, + { value: 'Europe/Astrakhan', label: 'Astrakhan' }, + { value: 'Europe/Athens', label: 'Athens' }, + { value: 'Europe/Belgrade', label: 'Belgrade' }, + { value: 'Europe/Berlin', label: 'Berlin' }, + { value: 'Europe/Bratislava', label: 'Bratislava' }, + { value: 'Europe/Brussels', label: 'Brussels' }, + { value: 'Europe/Bucharest', label: 'Bucharest' }, + { value: 'Europe/Budapest', label: 'Budapest' }, + { value: 'Europe/Busingen', label: 'Busingen' }, + { value: 'Europe/Chisinau', label: 'Chisinau' }, + { value: 'Europe/Copenhagen', label: 'Copenhagen' }, + { value: 'Europe/Dublin', label: 'Dublin' }, + { value: 'Europe/Gibraltar', label: 'Gibraltar' }, + { value: 'Europe/Guernsey', label: 'Guernsey' }, + { value: 'Europe/Helsinki', label: 'Helsinki' }, + { value: 'Europe/Isle_of_Man', label: 'Isle of Man' }, + { value: 'Europe/Istanbul', label: 'Istanbul' }, + { value: 'Europe/Jersey', label: 'Jersey' }, + { value: 'Europe/Kaliningrad', label: 'Kaliningrad' }, + { value: 'Europe/Kiev', label: 'Kiev' }, + { value: 'Europe/Kirov', label: 'Kirov' }, + { value: 'Europe/Lisbon', label: 'Lisbon' }, + { value: 'Europe/Ljubljana', label: 'Ljubljana' }, + { value: 'Europe/London', label: 'London' }, + { value: 'Europe/Luxembourg', label: 'Luxembourg' }, + { value: 'Europe/Madrid', label: 'Madrid' }, + { value: 'Europe/Malta', label: 'Malta' }, + { value: 'Europe/Mariehamn', label: 'Mariehamn' }, + { value: 'Europe/Minsk', label: 'Minsk' }, + { value: 'Europe/Monaco', label: 'Monaco' }, + { value: 'Europe/Moscow', label: 'Moscow' }, + { value: 'Europe/Oslo', label: 'Oslo' }, + { value: 'Europe/Paris', label: 'Paris' }, + { value: 'Europe/Podgorica', label: 'Podgorica' }, + { value: 'Europe/Prague', label: 'Prague' }, + { value: 'Europe/Riga', label: 'Riga' }, + { value: 'Europe/Rome', label: 'Rome' }, + { value: 'Europe/Samara', label: 'Samara' }, + { value: 'Europe/San_Marino', label: 'San Marino' }, + { value: 'Europe/Sarajevo', label: 'Sarajevo' }, + { value: 'Europe/Saratov', label: 'Saratov' }, + { value: 'Europe/Simferopol', label: 'Simferopol' }, + { value: 'Europe/Skopje', label: 'Skopje' }, + { value: 'Europe/Sofia', label: 'Sofia' }, + { value: 'Europe/Stockholm', label: 'Stockholm' }, + { value: 'Europe/Tallinn', label: 'Tallinn' }, + { value: 'Europe/Tirane', label: 'Tirane' }, + { value: 'Europe/Ulyanovsk', label: 'Ulyanovsk' }, + { value: 'Europe/Uzhgorod', label: 'Uzhgorod' }, + { value: 'Europe/Vaduz', label: 'Vaduz' }, + { value: 'Europe/Vatican', label: 'Vatican' }, + { value: 'Europe/Vienna', label: 'Vienna' }, + { value: 'Europe/Vilnius', label: 'Vilnius' }, + { value: 'Europe/Volgograd', label: 'Volgograd' }, + { value: 'Europe/Warsaw', label: 'Warsaw' }, + { value: 'Europe/Zagreb', label: 'Zagreb' }, + { value: 'Europe/Zaporozhye', label: 'Zaporozhye' }, + { value: 'Europe/Zurich', label: 'Zurich' }, + ], + }, + { + label: 'Indian', + options: [ + { value: 'Indian/Antananarivo', label: 'Antananarivo' }, + { value: 'Indian/Chagos', label: 'Chagos' }, + { value: 'Indian/Christmas', label: 'Christmas' }, + { value: 'Indian/Cocos', label: 'Cocos' }, + { value: 'Indian/Comoro', label: 'Comoro' }, + { value: 'Indian/Kerguelen', label: 'Kerguelen' }, + { value: 'Indian/Mahe', label: 'Mahe' }, + { value: 'Indian/Maldives', label: 'Maldives' }, + { value: 'Indian/Mauritius', label: 'Mauritius' }, + { value: 'Indian/Mayotte', label: 'Mayotte' }, + { value: 'Indian/Reunion', label: 'Reunion' }, + ], + }, + { + label: 'Pacific', + options: [ + { value: 'Pacific/Apia', label: 'Apia' }, + { value: 'Pacific/Auckland', label: 'Auckland' }, + { value: 'Pacific/Bougainville', label: 'Bougainville' }, + { value: 'Pacific/Chatham', label: 'Chatham' }, + { value: 'Pacific/Chuuk', label: 'Chuuk' }, + { value: 'Pacific/Easter', label: 'Easter' }, + { value: 'Pacific/Efate', label: 'Efate' }, + { value: 'Pacific/Enderbury', label: 'Enderbury' }, + { value: 'Pacific/Fakaofo', label: 'Fakaofo' }, + { value: 'Pacific/Fiji', label: 'Fiji' }, + { value: 'Pacific/Funafuti', label: 'Funafuti' }, - value: 'UTC+3.5', - }, - { - label: 'UTC+4', - value: 'UTC+4', + { value: 'Pacific/Galapagos', label: 'Galapagos' }, + { value: 'Pacific/Gambier', label: 'Gambier' }, + { value: 'Pacific/Guadalcanal', label: 'Guadalcanal' }, + { value: 'Pacific/Guam', label: 'Guam' }, + { value: 'Pacific/Honolulu', label: 'Honolulu' }, + { value: 'Pacific/Kiritimati', label: 'Kiritimati' }, + { value: 'Pacific/Kosrae', label: 'Kosrae' }, + { value: 'Pacific/Kwajalein', label: 'Kwajalein' }, + { value: 'Pacific/Majuro', label: 'Majuro' }, + { value: 'Pacific/Marquesas', label: 'Marquesas' }, + { value: 'Pacific/Midway', label: 'Midway' }, + { value: 'Pacific/Nauru', label: 'Nauru' }, + { value: 'Pacific/Niue', label: 'Niue' }, + { value: 'Pacific/Norfolk', label: 'Norfolk' }, + { value: 'Pacific/Noumea', label: 'Noumea' }, + { value: 'Pacific/Pago_Pago', label: 'Pago Pago' }, + { value: 'Pacific/Palau', label: 'Palau' }, + { value: 'Pacific/Pitcairn', label: 'Pitcairn' }, + { value: 'Pacific/Pohnpei', label: 'Pohnpei' }, + { value: 'Pacific/Port_Moresby', label: 'Port Moresby' }, + { value: 'Pacific/Rarotonga', label: 'Rarotonga' }, + { value: 'Pacific/Saipan', label: 'Saipan' }, + { value: 'Pacific/Tahiti', label: 'Tahiti' }, + { value: 'Pacific/Tarawa', label: 'Tarawa' }, + { value: 'Pacific/Tongatapu', label: 'Tongatapu' }, + { value: 'Pacific/Wake', label: 'Wake' }, + { value: 'Pacific/Wallis', label: 'Wallis' }, + ], }, - { - label: 'UTC+4:30', - value: 'UTC+4.5', - }, - { - label: 'UTC+5', - value: 'UTC+5', - }, - { - label: 'UTC+5:30', - value: 'UTC+5.5', - }, - { - label: 'UTC+5:45', - value: 'UTC+5.75', - }, - { - label: 'UTC+6', - value: 'UTC+6', - }, - { - label: 'UTC+6:30', - value: 'UTC+6.5', - }, - { - label: 'UTC+7', - value: 'UTC+7', - }, - { - label: 'UTC+7:30', - value: 'UTC+7.5', - }, - { - label: 'UTC+8', - value: 'UTC+8', - }, - { - label: 'UTC+8:30', - value: 'UTC+8.5', - }, - { - label: 'UTC+8:45', - value: 'UTC+8.75', - }, - { - label: 'UTC+9', - value: 'UTC+9', - }, - { - label: 'UTC+9:30', - value: 'UTC+9.5', - }, - { - label: 'UTC+10', - value: 'UTC+10', - }, - { - label: 'UTC+10:30', - value: 'UTC+10.5', - }, - { - label: 'UTC+11', - value: 'UTC+11', - }, - { - label: 'UTC+11:30', - value: 'UTC+11.5', - }, - { - label: 'UTC+12', - value: 'UTC+12', - }, - { - label: 'UTC+12:45', - value: 'UTC+12.75', - }, - { - label: 'UTC+13', - value: 'UTC+13', - }, - { - label: 'UTC+13:45', - value: 'UTC+13.75', - }, { - label: 'UTC+14', - value: 'UTC+14', + label: 'UTC', + options: [{ value: 'UTC', label: 'UTC' }], }, ]; export const DEFAULT_TIMEZONE = 'UTC+0'; diff --git a/ui/src/components/FormatTime/index.tsx b/ui/src/components/FormatTime/index.tsx index 456490879..d3a4d0249 100644 --- a/ui/src/components/FormatTime/index.tsx +++ b/ui/src/components/FormatTime/index.tsx @@ -37,10 +37,10 @@ const Index: FC = ({ time, preFix, className }) => { between < 3600 * 24 * 366 && dayjs.unix(from).format('YYYY') === dayjs.unix(now).format('YYYY') ) { - return dayjs.unix(from).format(t('dates.long_date')); + return dayjs.unix(from).tz().format(t('dates.long_date')); } - return dayjs.unix(from).format(t('dates.long_date_with_year')); + return dayjs.unix(from).tz().format(t('dates.long_date_with_year')); }; if (!time) { @@ -50,8 +50,8 @@ const Index: FC = ({ time, preFix, className }) => { return ( diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 5b767092c..76c30c3a7 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -800,10 +800,9 @@ "label": "Contact Email", "text": "Email address of key contact responsible for this site.", "msg": { - "empty": "Contact Email cannot be empty.", + "empty": "Contact Email cannot be empty.", "incorrect": "Contact Email incorrect format." } - }, "admin_name": { "label": "Name", @@ -818,7 +817,7 @@ "label": "Email", "text": "You will need this email to log in.", "msg": { - "empty": "Email cannot be empty.", + "empty": "Email cannot be empty.", "incorrect": "Email incorrect format." } }, @@ -1029,7 +1028,7 @@ "time_zone": { "label": "Timezone", "msg": "Timezone cannot be empty.", - "text": "Choose a UTC (Coordinated Universal Time) time offset." + "text": "Choose a city in the same timezone as you." } }, "smtp": { diff --git a/ui/src/i18n/locales/zh_CN.json b/ui/src/i18n/locales/zh_CN.json index a5e25ea3f..72069e9e2 100644 --- a/ui/src/i18n/locales/zh_CN.json +++ b/ui/src/i18n/locales/zh_CN.json @@ -283,7 +283,7 @@ "btn_cancel": "取消" }, "dates": { - "long_date": "YYYY年MM月", + "long_date": "MM月DD日", "long_date_with_year": "YYYY年MM月DD日", "long_date_with_time": "YYYY年MM月DD日 HH:mm", "now": "刚刚", diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index d42b60582..e4fab6659 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -18,7 +18,7 @@ import { useInterfaceSetting, useThemeOptions, } from '@/services'; -import { setupAppLanguage } from '@/utils/localize'; +import { setupAppLanguage, setupAppTimeZone } from '@/utils/localize'; const Interface: FC = () => { const { t } = useTranslation('translation', { @@ -114,6 +114,7 @@ const Interface: FC = () => { }); interfaceStore.getState().update(reqParams); setupAppLanguage(); + setupAppTimeZone(); }) .catch((err) => { if (err.isError && err.key) { @@ -258,9 +259,15 @@ const Interface: FC = () => { }}> {TIMEZONES?.map((item) => { return ( - + + {item.options.map((option) => { + return ( + + ); + })} + ); })} diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index fe4d33199..3ed72f843 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -281,9 +281,11 @@ const Ask = () => { {revisions.map( ({ reason, create_at, user_info }, index) => { - const date = dayjs(create_at * 1000).format( - t('long_date_with_time', { keyPrefix: 'dates' }), - ); + const date = dayjs(create_at * 1000) + .tz() + .format( + t('long_date_with_time', { keyPrefix: 'dates' }), + ); return ( + From 8ddc4cf44d4287c799f1e6461cba49bdcaef2762 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 12:03:39 +0800 Subject: [PATCH 0175/3337] update: site info service reference --- cmd/answer/wire_gen.go | 11 +++++------ internal/controller/lang_controller.go | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 57f5a5f61..49e980eb7 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -93,18 +93,18 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, return nil, nil, err } siteInfoRepo := site_info.NewSiteInfo(dataData) - configRepo := config.NewConfigRepo(dataData) - emailRepo := export.NewEmailRepo(dataData) - emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) - langController := controller.NewLangController(i18nTranslator, siteInfoService) + siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) + langController := controller.NewLangController(i18nTranslator, siteInfoCommonService) authRepo := auth.NewAuthRepo(dataData) authService := auth2.NewAuthService(authRepo) + configRepo := config.NewConfigRepo(dataData) userRepo := user.NewUserRepo(dataData, configRepo) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo) userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) + emailRepo := export.NewEmailRepo(dataData) + emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) @@ -152,7 +152,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index 384d711c0..176d1d0ae 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -5,18 +5,18 @@ import ( "github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/translator" - "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { translator i18n.Translator - siteInfoService *service.SiteInfoService + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController { +func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController { return &LangController{translator: tr, siteInfoService: siteInfoService} } From d6b65c3da512f24d617d4260ca5b8d17a97d2c81 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 7 Nov 2022 12:34:08 +0800 Subject: [PATCH 0176/3337] refactor(admin): Form field validity verification --- ui/src/pages/Admin/General/index.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index cdce67bbe..2ea2f4859 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -62,7 +62,15 @@ const General: FC = () => { isInvalid: true, errorMsg: t('site_url.msg'), }; + } else if (!/^(https?):\/\/([\w.]+\/?)\S*$/.test(site_url.value)) { + ret = false; + formData.site_url = { + value: formData.site_url.value, + isInvalid: true, + errorMsg: t('site_url.validate'), + }; } + if (!contact_email.value) { ret = false; formData.contact_email = { @@ -70,6 +78,17 @@ const General: FC = () => { isInvalid: true, errorMsg: t('contact_email.msg'), }; + } else if ( + !/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test( + contact_email.value, + ) + ) { + ret = false; + formData.contact_email = { + value: formData.contact_email.value, + isInvalid: true, + errorMsg: t('contact_email.validate'), + }; } setFormData({ ...formData, From f4a2b6b614fcf97416db0070a9c00cd434b775d1 Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 7 Nov 2022 12:34:22 +0800 Subject: [PATCH 0177/3337] refactor: update en.json --- ui/src/i18n/locales/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 76c30c3a7..67a91cfa8 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -990,6 +990,7 @@ "site_url": { "label": "Site URL", "msg": "Site url cannot be empty.", + "validate": "Please enter a valid URL.", "text": "The address of your site." }, "short_description": { @@ -1005,6 +1006,7 @@ "contact_email": { "label": "Contact Email", "msg": "Contact email cannot be empty.", + "validate": "Contact email is not valid.", "text": "Email address of key contact responsible for this site." } }, From 088d3876fcddf3014caaf6e84808740d3d86499c Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 12:43:44 +0800 Subject: [PATCH 0178/3337] doc: update install document --- INSTALL_CN.md | 108 ++++----------------------------- docs/img/install-database.png | Bin 0 -> 13214 bytes docs/img/install-site-info.png | Bin 0 -> 43093 bytes 3 files changed, 13 insertions(+), 95 deletions(-) create mode 100644 docs/img/install-database.png create mode 100644 docs/img/install-site-info.png diff --git a/INSTALL_CN.md b/INSTALL_CN.md index 300851a2e..848c9ce16 100644 --- a/INSTALL_CN.md +++ b/INSTALL_CN.md @@ -1,106 +1,24 @@ # Answer 安装指引 -安装 Answer 之前,您需要先安装基本环境。 - - 数据库 - - [MySQL](http://dev.mysql.com):版本 >= 5.7 - -然后,您可以通过以下几种方式来安装 Answer: - - - 采用 Docker 部署 - - 二进制安装 - - 源码安装 - -## 使用 Docker-compose 安装 Answer +## 使用 docker 安装 +### 步骤 1: 使用 docker 命令启动项目 ```bash -$ mkdir answer && cd answer -$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml -$ docker-compose up -``` - -启动完成后使用浏览器访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/). - -你可以使用默认的用户名:( **`admin@admin.com`** ) 和密码:( **`admin`** ) 进行登录. - -## 使用Docker 安装 Answer -可以从 Docker Hub 或者 GitHub Container registry 下载最新的 tags 镜像 - -### 用法 -将配置和存储目录挂在到镜像之外 volume (/var/data -> /data),你可以修改外部挂载地址 - -``` -# 将镜像从 docker hub 拉到本地 -$ docker pull answerdev/answer:latest - -# 创建一个挂载目录 -$ mkdir -p /var/data - -# 先运行一遍镜像 -$ docker run --name=answer -p 9080:80 -v /var/data:/data answerdev/answer - -# 第一次启动后会在/var/data 目录下生成配置文件 -# /var/data/conf/config.yaml -# 需要修改配置文件中的Mysql 数据库地址 -vim /var/data/conf/config.yaml - -# 修改数据库连接 connection: [username]:[password]@tcp([host]:[port])/[DbName] -... - -# 配置好配置文件后可以再次启动镜像即可启动服务 -$ docker start answer +docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest ``` +### 步骤 2: 访问安装路径进行项目安装 +[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) -## 使用二进制 安装 Answer -可以使用编译完成的各个平台的二进制文件运行 Answer 项目 -### 用法 -从 GitHub 最新版本的tag中下载对应平台的二进制文件压缩包 +选择语言后点击下一步选择合适的数据库,如果当前只是想体验,建议直接选择 sqlite 作为数据库,如下图所示 - 1. 解压压缩包 - 2. 使用命令 cd 进入到刚刚创建的目录 - 3. 执行命令 ./answer init - 4. Answer 会在当前目录生成 ./data 目录 - 5. 进入 data 目录修改 config.yaml 文件 - 6. 将数据库连接地址修改为你的数据库连接地址 +![install-database](docs/img/install-database.png) - connection: [username]:[password]@tcp([host]:[port])/[DbName] - 7. 退出 data 目录,执行 ./answer run -c ./data/conf/config.yaml +然后点击下一步会进行配置文件创建等操作,点击下一步输入网站基本信息和管理员信息,如下图所示 -## 当前支持的命令 -用法: answer [command] +![install-site-info](docs/img/install-site-info.png) -- help: 帮助 -- init: 初始化环境 -- run: 启动 -- check: 环境依赖检查 -- dump: 备份数据 +点击下一步即可安装完成 -## 配置文件 config.yaml 参数说明 +### 步骤 3:安装完成后访问项目路径开始使用 +[http://127.0.0.1:9080/](http://127.0.0.1:9080/) -``` -server: - http: - addr: 0.0.0.0:80 #项目访问端口号 -data: - database: - connection: root:root@tcp(127.0.0.1:3306)/answer #mysql数据库连接地址 - cache: - file_path: "/tmp/cache/cache.db" #缓存文件存放路径 -i18n: - bundle_dir: "/data/i18n" #国际化文件存放目录 -swaggerui: - show: true #是否显示swaggerapi文档,地址 /swagger/index.html - protocol: http #swagger 协议头 - host: 127.0.0.1 #可被访问的ip地址或域名 - address: ':80' #可被访问的端口号 -service_config: - secret_key: "answer" #加密key - web_host: "http://127.0.0.1" #页面访问使用域名地址 - upload_path: "./upfiles" #上传目录 -``` - -## 编译镜像 -如果修改了源文件并且要重新打包镜像可以使用以下语句重新打包镜像 -``` -docker build -t answer:v1.0.0 . -``` -## 常见问题 - 1. 项目无法启动,answer 主程序启动依赖配置文件 config.yaml 、国际化翻译目录 /i18n 、上传文件存放目录 /upfiles,需要确保项目启动时加载了配置文件 answer run -c config.yaml 以及在 config.yaml 正确的指定 i18n 和 upfiles 目录的配置项 +使用刚才创建的管理员用户名密码即可登录。 diff --git a/docs/img/install-database.png b/docs/img/install-database.png new file mode 100644 index 0000000000000000000000000000000000000000..09fbf36a18bb039cfaec997dd76fc6e4b9c12599 GIT binary patch literal 13214 zcmcI~byOVBv+n`{0zpC`I0Sc>;O-D45M*(;#Vts%KycUK8c1LlSsa1|g1ZbJ90H4z z#RHG;@BZ`7d2-*CcTP>$r+d1qYO1DtS|(acLkSm~92*1z;i@RhYlA>&00@Md_7v^0 z1enm$dCXX7y?Lkb@bG{{B7dLUgZLlz4vxoWmS`DSr>3UJBHumM6c7?~@($wQ;U{`Y z?D^hbQ(Ld?d;8tp-=^jkYdgsO{r&Rt%7@_4`i3S2Wwo=jb4gi+qoX4`M>hdcnXK&G zx%q{)wY8bq*`1@i>)1WlMi1i8b0Thr)7Nh4g3`RF}$#-B(I>*FCcJv>$LHEe|tw~ zVOdRdY&*&PH;P5yX--q4(!|mVz56Bd!kE_~opd=M6wq7;7!3H$VW?;#@L!O;HU?EGPK>mecI;eE)1 zobCgo%)|2j*5Tnp$=8Rjf$61{hk@Y-9gBzOLJu>ubLrg`h@;c;;b!-$c(c+Fp=9f7 zVPPc@i04X0Ugn+e;=wcuE(nCvQ@Gl>cWvK=b}1bPB$Wtm+2e{B$|d|a!MH#%dk6Zb zJ;Gz2e&LMuMMbl^_S%+C6pTnB`2iVwL-IF8?enVj0?Maai`XxEb+W0S6usw`@IQZk z7ao6na1fWH^^}P6$8B4dL_z0mv8I1Y6k5$Rsp9TO_+dn_sC;&vC~dP#vWl z6;=Le2z%WuTm_T;=HLK?wrXyos;aBi;prqtvqWg?OhJ1D3cYc@l$C-pzqKht$W+x; z>hPve^4QX{6KttU=|wqN1gS*|Zp5B^J%R6F`8F ztPz<+!G-_A4Xj)>a;MY}C$<5Iin{PYwx3~@i>X0z55O#iz4A5=*KHeDntj@p>E9pAkl&li)PF}a8#cqUDwRy6dglV$V)1x& z&MQ8SU5qi4O&MKyly6IwP50B1V zM#v+@i|tMW;$}QNoGzngpmIgvNL%Siv>HAOv~R#T9wsbrD5q+E#iGtizQFlL_%|Za+|@-oqn%6UOo$W@q#mkx|fmJp^qoS z;n94YJ%fz+}?cOZo3f9qeK`tFPpJD$E%QuCWGDVB{~9?w12qAeUR# zd4SmR1hl6V9Qo6Q@-M5-(nr}b9b)e+^z*4 zkpM6RvE?wRZqEpo}@ z50ma`8Os?@W<~lQNm&Qi?|N)d#o&XC0VA#T324~V*a6z{^y?jYqRzF2!YkSyp_osi zP!D9pyLjPUDUa&Gi4)gj1fRS~+&g>dkLh9-kaC@A2=-$X(x_spYfP<=GaP{1lkXS! zVkMKyeGJ3*6$>93jD2DqS#)v-crV7^Gu(8AGJ6ZcK!Iv~q=Od_}O7KNy%b^11Ez zj+bu`MM>Yn-_g7*PeNTd1bN>0m@UO!vG(TSv@SRX} zsddvq$J&P@R(4-krcX%?bhd5+H~Vgu2{=u&J=tS74!x|C#(vScM(^7YZQ@W(#lfn* z#QcYX?KnG9f|e{dRE5VjoIwU*ac+Q*YbGIb1;u5V+%RK0G5V{eP)OQc>NVH+N%m7P zA{_csWH*v3iD1AG+T^!*6vfoBU^J;K_Eoe-if=H6WZQeXc!%77uWHB2$mq`E`x zLg$%XUITUQ@3$KaBE8$1LgBLXe&QW!uGznA31(UTO=N8W)eKSmOweyPpkOryPb7fj zLgn+)Y*^I_nxporSzT($MrjJa^I6f#Ur1t9QiH~TVJsW7r~qtqLz0PL$Uj|ykqJQ( zE0-;7jIQuSQF6-gJKiPAUN@Hmk`Oe*zd3k=S z#_&zL`;q7-#g99PH_YFwQuQHad+oc^TYm$oM_tNo@+jTQ;$em!xhnXa7Co&K=c$A zS?zU+oa)kg90>)(zdRO6&Agwsn{fEtjt5dJ-tTZ($-G6eIP9je#0g9%xQN(X#)^>s zORW|kcUp^MvQTf!uw|II7!w2%u$~V)a*Lj-xbRvLY20)ogYh>KCt7(BkBkawJpq%< zm4vPPo>Z*nt`-J2)tcP<^Ur^;1fI^w0iaJ|P!w!TFd7ja9D{|@8dpnrfV|KxNl<#7 z1`_f^=6|X%=6bOwjqy0?G*9Y8N_2QAL*^&Red;5qU=~XMZ7^@X!D<@gBPgnP*WNOe z+l?VsVR8pU$x&M2o7bGV!EM5NJ9Q8b4?IHjKS!N|{v&^h*Fa5y2~q?8;%Jn=MfB_f zHP5b*r}H7fkCWA#Hy&m=$TwB16mFlJAbk3^ftouuXn0;(gMlUhy`OVew=G|W&EFAZ zAR3-e%T5a$B6)t+4b^qfAPqUvfx=v4$2H{N1YM^zT_CXu9G(v$ByHgw5=XhAo6KgO7I!aG_^<3__1Z-5R8s4yNc%!=s=?JY z>VIBz>I7X@-3P;|?b=MPHsuHP|7j=;h9u+oKTFa{OhyWR80s)sinPF~c*yg9BJ0ok zyg2DqIF#9fRBq?7&$AH^$~_tZl1mlrSSZa4%@}ZT>g1G-Zke<0_Fj>{92s78=eFZk zn)DE8{m|ALlCigZp;*-(e0kzy<=qhsJgYK#@*;F$@N-yM^x#sMv7wWRzGUlBtH+_k zoC#=ZA#?ba-hOc;!&YrUoirl2Cq?ih1Da^IJDV8qQ>XdE{5|Mu8B6?b;pp7g`OFDP zPHn$wmOw#;?H;dAX#YU`7gp78r1fVMs-Irf3^Ibo2f?7>t;{!3DOBM*Gn}e=5;#34 z4!-a{BC<%guCmPpXq&CS=wQSv*!cM(M=4Z2G4InmyPitqJ@{C0Z_7!Vc2rQs+L$6R9`)Oo7$RSEEcp7y;(eNIp20b(#qG7ho0 z5)c4qdIzQV^kA__>1M#70i!Ivh4RY8pIMQgY6+20sWCp3IxxQ{Ywa26~%U!}< zYAL|XH1YFWY)M2pyevx9t#ziK;EGkCE7dYZno*@eQJkJk+96RHc--qHwn9Ci-Ik$K zR&90lvQJrUZWP z@N{&Aed$zxr)T!wW<`P?{K>A|`!3tKfMWD$A47e451s`5BE_ZHYdHu(F8w>z2@* zw31!1U`4@mf6Z5 z0O=`M9~4!aeHT$ojXC9H2T0gC*+ioHJ#$LF7AOtOYhV_?`LuJaLia+0gQ53`!wZaFBClIub7=pFc&=epQ)zGis~u;?~IU2P<-mpYP!kOs}fA zOB-lg03{GK%dx}>Q5yDli4iT7=3n760xwA)lYJ6X9wQ!Pu+0W1zS)CH&GdPU@|@q8 zCMRk!U0i77V%i3(c(Cu#cse0R{S{UuOWqtEnsoErkwrkuFX^3@2f%55!>`zU=uI-M zXZIvZ>R(|@nW=Q3CA~;h2SpQk1MX9LMQyLIsppqzv(kq@B&0C=ngh*6=EJkk{=)O= z$p$=Fx8fCPcLrV1HIuue!GUAt%Q#x&+wNBOe`z+)Dd5-S&_ur}nfpvJt za0@%Ci1lg0v73neAC`wMgt+JKMhb!+()ltTM;Bf;k1_u#A3inis+G(BGWbv@i^Df^ zqA|vw5zU#axTiB)CQ_j>0Dmn`Jfv_F*0V{(@7z@K9YXCrSBO6FJk2Re`wtCP&&eLE z-QR%mi(N`D-1>Skpi6~bSo1$|Xbyd?K4uM(fx{gjRvA7)657zwD zra8XH+=vzh31ai)+D{Uf8Qx3#x+ug(4edwlhziH64#c7^FwOIFjh(Y3AMRpxSRDj^ z|G?!OjzZdBfOkohU@k#uHQT82m8tQ5pFxr3?bq3Gwt76rT7qCL(I{t`QmW3?Ic>nnL*#T*uWdLjDgOgku2BkdIw)$H{B)=_ zpjvwcp=fY=Y2dwW(%p^p89fSl z`P&W%%vUrI#l4`oI!czX-}sYU=hkj$_VAQo`X>`&)TT9K+Tj>!N+9^nr1iyM@}Flw zu34U-ZGL=p_pHjV$f!NbR3z`KENaAp@yDjhjTT=xyZkimD;l@u_;DI;eMXV3 z%e@+(Pb=ac2$)4qSzFrMv!DqNGh~|SuQGp6Z&UWVHi?uXOY!b5WA9927c8KnP^vRc z$wI*KT(i`9Ny!^mc>nH)$Y+^(oCnA?j)XwfSFaQa@=eYpWR+c-GLRfu5xYsH7o!YQ zcNrANZx*oLzZCVQJ^Umgoj}ve6ks?cs>b(hm9GUYndO-yo@snb_jZ;6S=>Xfw$!iZU zQ@)@RRcQaj-olnCw)J)kZ*87rX+bPC^*V^i*@J}B%m{(v6rH)4^|o#-T#A-ij#;HL z2sY9!_?ag4+cATk%aA{TU=IWTb6y=|_z^=(e3zn|W4$!om9)w;rH%H#9Nw<&_`$Zwy0Ov;kIv$$^Um_u}1&-O%etTE-^yvTtj z?^(&AOWGBvzp%GnsR0Jam5|KMuT#JCl#lZBs(pYfx+=BzaO?vZ^Al*A`J;HD-e)3? zx#zPq5MjAg=pe!m!mAz!$B0=<-`q{F6?^J;rr7_q`w8k(RT2hJrpjRebr4&gz;|yO0c}8Mwj71~Lq%B;+@-B*-FY^E zf*sMQ=Y1(CE^t}V5TAi;n48U9FvDK{lb2~arNiq=LfkV8^HFatHRfVun2lKuw9@zX!b!7RF;=SWxz# zx@M05IO+O*?>2{<)?MH*@=LzGG^vCFQkZFC?Va^>@43ozg8Y33uXCUX(xmbClf?ON zt7&L0>Gr|ErFT{jNoQhaQyhQXA>~+hZC($UztK||5n`A{8rwWzv%)K?aue(nda+O1 zSL{#;E{|h8pC#E%y2lQQ{4-8T8bmw^>JkdXv6`%(g}&M?aQEb2&;Xe3s-h)9 zEbaiu;*SACH39o}Qnwe1K!E%<)K4K-Ft*XzE|2D$UKEwI<7y-{ku|E$mViVNP+bMB zc>r+)hi`yCx}aj2XJ2gF#l)Ezokv3wY`|aEoMw%dv&+iv(aa8B?)oGIsfez$ZsJdS z?0SoN+5z|+m6QXbR&%>m3ULaGzTj!e5K_N{H!S1R3^E9GyVfR)m(+wZVtehW4 z`pF3$~P#MMI? zCmF_{7*@i;Knrm_7cVich3#K*o-IZ+KXvh?2M&-xRN2obG%-IdZ!X>_tx_Z3DBGl* zf#56iTSOt7oJ1jCFIU@zLfBMZgLtWtTqwCzE-Bv2w*xk=0>q9&R>{w80XdMnZWBw= zJK~VdVCzN~jJ-d(rR?0t+u5Wlns3Y-Dc>o#n${a2KzT7iJ&m0zfOjCK*6WZ|IZF=g zA0s!G$!4qGiOSa~BX{8H92wQr6c7y>ce9Hp(lxN=iqls$X0uhKL4%{Qcd1K;261P^ z87PJT_9=oftDq{lO+3pEjQ|l}#Gfu=aiZ_AQ42-2t^Bnh}>ljUp;N zLz_<-_FPqQN#QwVtB9BP-fuJe<9y1NX$tMgzOY?>^dP^ij;-w@OU53KIxZB>PY2R$ zx!%hdcS`LMwl{WI$NgsjSg!l;hPP&vL}n>@@jKA-gx4t6ZtEN^OGDhB_C36rJx zQPEbE0CGr=KH2u%^|gP$^?DpqJaV~-tdm!(;Yr_nDOt`_Bk^ZD^Xbr5^B|S z5?Xfa#&ife`i<}@>%dKjD@UMX7Mi3`D%zkUf>n?zp$2FKUAmsX@ZvSq3?-q;+KVJA}ytq+~{`2 z%vahby~DipL)yLWBFYQWE2Kj3Q`nLglipRB!?{8bksTX9lpIQMXX;YMUUf@14cvh2 zDOD>vYFwA~ESJ3jVIdxj*fSy&=1Vv#lZg)h*kyn_#af7tFZ!DA_ymf5uJaFJ1ZT*v zO-p-P!gli-UZOy$?Op(MutIBt6&P zC_FEDmmzNV&E}PiEkxw2?S>-id3yrR|KpAS zyVtp^O&f2#n*Dl;B<4yuVypWTch;Lv5X2tv?VpPe?DgNYTkeUSZ-h4 z)c9gk>bl@$Hb@c{wV(RMY1!7P{sxxKl+ik@tB|&nIgHIJg~-qRqnV+1a}DQU=e5%I z1&RLmR{YDnE34f*aH`6NFka^o`+`Afa+2e-C2MaIv>(J(4zD<8FsUhWbG7yQzOAW%{&MM?@MP7;<7VD`#5tkgp>`3wy83hwa~hDFOI1`PBdy6-#(UfRDpuxTMr zxeorg+RiXkH5*tI#oZ(05QrW(NP}ze=RGN!%3>Ii8|^ck3%48Ab(9*@m86jee6lsw zG!UHV)JB*E#<}J$1*7<)9TudXZ?a?rA?c!k`EsWVOS2q8?8kDGMs6RCJ8h^pMa^h0 z07wotx%c3Z0LWpce>sOEesj6m$=L`!a-jutEr%+_Pl^sk-;oAzo#fr)!bn2wj$sX! znSd3DlQ#I4vP(ex+~uDN8GuwscW1FTVbpXd49U#HP-ctr-gt)QWyS5$eQR=R5Lv@fr!mkO$b*5#K%3Ybz3iHf9Y1|i?qPAk3SS4 z0n{VWaG42o?NDLck2`J?EJ@ZDpHrVyjgHt#4_ZAJmREQ`&oY&5$P-jB<}^m|MAuRn zs}ASmYb@;P?i#YB1zp&=WbPzJX9`&d=Y3%VVKZx!20{Ngii5NfSsKk}`ZuDz`#UzccS*4>;A2p{|(`2^#WBpm+iq&PpZN&h=bb zSxsoz95gVGpLIjMugpED=p0e}ESV4;{%0Rdxw>-SIIPTSjhEbSsB+@y(8Q7q3s#8D5+Qs;)%Kzc0u$B zBJr-AQ_=1j!xffY)aJ6N%^kOTJ)K=feAhP$4l6FNl@hAL78?QAQClCF0eBe9>`@rZ z9ha@+ml&hV7@9i&1u3!iMaA3C5*4W-cXbP0sQ^BiH7 z-#mF}4R5oWF+ZuXs}42%!1^qoO7#YfCCL&l2f;V;8Ys*|)iSjE=gAzLvu5~`OboAM zD%Q-^MZYjGdev}b;wr?qvR z6Q+*yz($8hrWFwtMgIA83E5$jtwk3}sa=e$E?)o?J>y_&#p$tv`YNmPb*0=zQ93h8eLu$qpqt>N6# zZ&rMRD(zpp4qF%+Nv9H#P{4*uebH5k+-E4`0LH>ELmj+qh8~?l`sptoR1jx7uWl&a z8Xy>-YS9f|BP3^Ats(*DuB67NN5^(MgqCD^A`FjOMmEr-wK-!e8P`5x9!(M{51zOU zJa$yOwKSsCcz}$4IqBb?r}~cZ9%H)c=7+{#fjbRN z|2jO^-crJ73MCg?o9Ok;`Z3BEJn{$`J`vMu4)M3dq2c8EW){eG5r*mDo8DZe7bS;P z#A@DF*+D6c2@QaM{`wnVf)|f|iviM+&(oevoJAk7rak(QIj&;6s3FKsXI9yGuA*De zIAG>>wt@dzn_T)bISO0o>2&sA+0G&7@T-?bI0qtYiYrUAoQUxi9$x+&l%9qiU)+wq zUav|ny*pz&&3!vn_Z4EfE2@IxqoL2mSHYRQ;ENhjK>&~&qePe+I#|dZt?$i)dLS4yg@DAuvxaW8m+3( zx?RD|I@G;;m%de$e%BZ5sMG82$zmAnLL5$opJEJlbe58KN3g=2iztK~q}>VZ1gS&t zEi-uSwej$~^@FdUWC|y_npnLD(sa3=WsgI%{KOvLp?Bb>3-TPs2iFR`XT$Y`6TNwy zuxAqm-|-8Z$(-t|0+I}*ST_blp2GsIO5$Sto~m*u-kkp2!Ai8Sf&bZ5;SRH7T>kL! zrOBKIYL!pCyS4+|`sMS$lkPZk^XtIN*Frr2R#hQO6}fS zPa4C_&<+Q`RvoX#cKm(x#WF45w{#;2-GJMh!f$^M-VJtD`RvQ;0HQ?9Mkn5zjQX!n z5%H>vc*7$o?7peo%*vhqCfMEH_LAmkQb?L|x?tiZ#ZQMs%~hd3x3MX5DnmI$mQ@{1l;L47w1xE`#0r2uIb_?7utEm zk7lwn(h3;e;IB5^m*O=Uu1I+e~;R^so?OUccY- z!Nys^@nEh?K`k}LZ+H<;#kt&R&0_c$1Jw>UWVjM!TyJ~+xdF_z#0tU}ZAuxi&(6nJshz>-93bx+bZtDzNhu4)o z;)*Ikm?5NjufOVVTRlOaJLN;#ZbaCzmm6JC*e5)61x(g-=|Xp$y{#jy8aWK1wkT@6 z*EAB8*|O_NP{xtBJ#l*k8>k!<^@ z##t68en|0B%6D$az#~{+&?%|uhek(~S11+@{lmX177aXdrFe`wCPyn^Bs&UBxTmP_ z0~-B8{@+Q9WdP>Pxz15xJ}&4`_lJ*QaQm<;4MXOeeI zEzSWI(-H%s9ZP(;bs9xEnpIDJZBP7pYuuI5sGwC3kERoh1Q%F#^9rt-&AKy#;B+s_Jq+yi=X!>uS{g;<11+Hni=Z4CY6m z*#1@B5|s7aSd8yUkHFI62J1V~=-jd+te({*q*a5~&PUU#BITAVm|Sga`*MrOmTeSz z14BX>`7l@gdA`~a>xHGB-{1$lKc3Pf>T|!t)QsrjN9RhL`l%hGLsj}zR$7c$Y@sD~ zzMEL6jmVv!2AAv&tyV8gm0UULdscW9`QGa6R!1&F-@ZD{7WLV!lPX`3hzR#(el0u$ zdpTDfpa@A#&woe8avLai%@z{#iGBSvDt46YuHUwE1$`E^b*f^?Whb&?xbOrVAW||p z-s*AzU)eCaA3yfT6^6Tqu~WO(iiAb}r50AvJGrbJm*nnFH_$VA5})eLj>VwLgkVS- z^k?g~1QSaP64xa>9^YHHf7UbkcK`@416jWT@FE0mVmC4MsFk9XjGe~T zXqg>mQ9o!HUh*E~?NhUO0ZfbWG!UFhWlb#U;vVRxyRF10sM%qKq@4NW!zM%KEl4w= z1bGF1?-(+DkO*WIAJ-dt?%;FXe^+vnr&*#QWzmheK>jr=98#Z*I zDs~)OtDtdOcOb;+H@WD{4++KrYPI+H&ys5>G${(8hviV6^6*d5;!o2vNDZY0Q3v zA*j4&_o*H?fv?=Ca@euN_h9=Jq(<-63W%i%Z8?=>01uJx5EDG6*@ZPi_5Nb!<}2Ct zr{MnSDSy0daRj*=t0KF@&e$yEkFc1Z)LBHc%g5Q1C5rxHXi2*xaSnZ@?_!K2-XGzt zk)O_+G~9{5x6V1$b^wA`)mGPPr0r_(USqng(I9cnnV6p-Ok*&Hl0oErlAz1$ZR;)6}QTkbM@=uyIvFhUx>|eq6 zOxIRjK!Sqr8O1+&KE7uU^R-s=;T9~|AA=Q91fOeTp*J%}V(Wf1qVWhOl6G`ht6l10 z6tc!8Jaleve`N8$^bm3Y&+Me0m@06xUS7SR2ai}t_yz1K4U+Fr1Q^-8VuTo2I_*|A zwcH?<71N>XQiJ3h1OEjw{*0^(;Xe7!f#^Rp?aDC`du>7bw3x=9;_o~SdP2BC_{X2$ zwA(@|f24mUL+%k%a^xE9AbA#+yL2GhKwSdH8V25|3OMrO5%?%*16+aRA?v0o%r?#@ zb2A{E*CzWJ=?!XjXgu?*euhA!%BejF!#}(pYzh!hS>kr_tSVp+k1W1cQ4==9=IL6p zO0EaH06QiijD42{aE{+dB?zZxZ2h9z78FAZDWrsB4E-P2-C{W!+2ihDh|E23c{N1) cpZXUOt4XkddA&E}UuQ-t3L5fNvgRNE2XLVHF8}}l literal 0 HcmV?d00001 diff --git a/docs/img/install-site-info.png b/docs/img/install-site-info.png new file mode 100644 index 0000000000000000000000000000000000000000..b8166caf426bca8f51a475b8c23da10617761ed1 GIT binary patch literal 43093 zcmce-1yG#NvnaZF2o?w$+zAc|Zo!=pH0VNbcV7q+T!On3f@^SDEVu;-5^RwLf-dZ0 zi(dY}^QvCeIq$u5?tSOps;|H4`o6B7neLhHneLfrO$|l7=dYdv002B?B{?ks01XTP zU@kmEd-_r=l~V!$poVIy>Bv7mK3-g0{5^e``n^KW{F)+CXLfeh(JKh>=5hZB_C)OB z8*+dD@K#v-^78WT?tW!;jg$8+5{bOIx!u^@+CMm)n*Hq&@MUvrySA?WOGMPw)%DTw zao^yGww~ed`Nj8IdR<-Mn7D-brRCZA#naO>Nf`xCAOG*w4Zguqf#LD7DcOnXc^1}o ztLqzkdw)-0r{QsFBO{}&ZS6k?hk&1~s%vWHmDNs8PQqf6MI_!qp}U#+rI9gl0b#Lc zXYlmg5*c~r)bz~r^YgOuiog5&)4ygMoL$?xdWS|QyZZ*~n%Z`D_kJ`rFE0I2R?~#T z5%mo}`uYZ(Ts=GkBl1fs$ERl8y?pcxjB6TO!9BhHfx&AK$mHZyaOjuZ{6Ylcys5oA zJ2yW%G1JA}du)871Ki)(+Nq|g{m}^M`$Qd;SYFdOGBMrwqbVypx3scu;@8~b@@i2@ z>9^9d!tYh_Uz7R<1~W3VXJ%(j%q*(vn?avL3{A{dH?}IOs%`9@TRMAEvI?zi9de4w zl2g)faq&i`SBObS6OvLOkd1<(;tlBE>EA2UbMp<&9jRGGdk4qcP-s*_dPirMouf;0 zOB-bS@AC4>8zHgc@|vF`<6HX|6}8PfN4I;hyMx1{!;>>FU(nU{6heCT_ia&W+OUex!#M-#Re>Y3WR3rADEY1DJJhGqP51Zl&>Q`xk1Nnz6}U7@ z)^Z;HpQ1}u7U^G#dE>UlPiIel6@Gyxt}uT{(flUufw1Q8n^ci0GJJ)L##;1xSo735 z2t!I1SS@;WAYnK{f%^6IZfIa&lk@Y`)F-fG=CVTy9)Rs8MRXKrzctL=?dz z=!j-n6Hmq$6lP#=-WRhY=FKLnT%xAB-l;^co=7$o|Lewe+fKin^mj+> z8Pes4Y#2_$A6R9NLvQLnd{2xE&k(5)eCwv@htY+D+zo39LoWB?#a5d!#q{WX=NWru z0AvUw_56c1Lo)nKM_!~)OC43{V|EmgqV45LEX&%Q>2K`TG*-ghPW6dJ%)k$0A0Gy2 z-hKe<8W6@ZCMSC3VFoqzyAf}6ql1?^!!p?Vc@wtpHX`>hiP+P*Fj}?SGCO}Npm*$) z>eYnJv~+xt_9_qi)WL+kS=1G+9FTHC9oj2Z_?dGzaBpodFK?7Yv`bsOKzq64aHkV7 zHG!yI$b+$xX%vUH~Fg^&?|4qe0?*)%CRLh@g{P~=M|-r&i<-QG57H2 z%uEkX?mok^Qe;FK5_9o;L--Ph2sX*R4i{bwBl()P;mp|OS!50P*8UZMC89-UV%G1A z+{)8b%g}&bT@>h)^={L};C-h?B_ahSjr3Eh&#P(u%Y3V1Y4tchVFF^{gIn`{(v-eUSD0V5) z%s=x9TrPUH3f4}(+whHhn)YX3Lf)Lr96%Jez6W^yWP(I!_&fk-#%-u0>41%H0734gO0`=2~03&hCieOjAoisyzKKWG#Pp-^!3t+@|Nc0|CKg53@`0CSS|m+Ioga zgB>)VG;i)?rQucfx`FwI+iluN3V5WYQ!7;Gz1jKKgt~KLiC+*J4Bq9*fbkZHTbfAC zmGQoX1Y4T%8@_DP+Zaj5;7f&tN4=gIJL8H1x>N2`S`Z}9R1O2`_j&J_-6E0a5b=! zqV2Y4Iz%*{Jx(IO2*VaEUyO|KocLiJ%Eu0z3FOm{{fnFr*R?ou)3RwoFNCf$E}zg2 z*=yZ+CcFe>yCm6y>5tDi9hBmx^gV|KX>Y4c9O4@=JCbmDuE=xx?G6SncbYxy{>2s3 zGe_^y&(L3N#t%UROx(nGgV#;=TN14hUI9|~=irC}8 zXM7bOs^YSI{41(*qzp+Ky-TTSfJv;aQgPo_f;jHt7o@wiOHFE< zTC#tyz?|}JJSDC>g5Ib2@O0JW@8BiMzx8ddxH-fe=j|&K@)CHrTFLBS@B3Fmi62|O zD%F(t@Oh6d@#l)YEna!y;m&3Jk8pFj3^RC-iazhyd4i`iU)#Ic0{iF((W`}si5wyq zs`e*Zol$@NGf(BxD>_HmW^OPobin1mZcwU3m8-D#th{p%QLs#9e%tC=o9VRv4a|)E zr<=TQngo+BG8HUUDDz+}FPo!rd}zC_PscIzL*FQuVU?-b5N&z<{JFKd6S;C2{~Jr+ z{vRpMrF%IHscUH{EADlAju~T-XO<#Jr&vXQoc_)lfA~Q1kE(es%^C&aQi^-Ac~K&y z>Abx1Hds@*d9SZ)d54`z`2L~MOt2G3vXS>*%4Y?xe$Bkq*ZPTPc<(2yN?1dwRQ0Ar z9AEaI{-;V9&W(65g{AC`k&)?VF0_dgKu~H9U*I&3Xatp<~%^lak>v@<|2^Sr~*y2Ff=9A*q7Y-c-S;<8BabAvvH+RphtC&}Htm7*|`Tr#|u zT+AgOhIB59n07Ya+?5Mp=xS!yX+3gI>fc)z;43}3YykTG=e2})QX}c$`cfXpeH^2k z{{K#NsFpfM@P)iIu?g!->>q`w#P=UDguPk5JlB+e{(11Ex1QhEXCs$}`zsb&GZg2) z`3ai#nYbV_!^-eVkb;Wkqj;ZF!FB`G;Wt6KWE+q?T1Xw!;uSn^^xgx-8{zhTR}5*I z;%694ooDU7mJd?`lbYGQp~yi5?w(xspJ-f(x9*A&edeL+Rpb}k75l;6i&C9Mi7@dn z3Px+QTFZnnTcF^ZK^yrXKMm3FH*+1Qd{FSE!J4Z7zn{a%yj!H*tGI392La@lxiL1% z=r}7iqT%Q`Gjr=NnYw0Ve~1Yfc_a7|pJ7SZS`uM1MTGe?+6%ve(Wu>_-Ahg{H|H67 z$&J{9>(C5}RuELaUn3tfp&W9S%3v9}^#6b?2<+W%xTZePrd06}6%!DS`E2$5GPdH> z$bV(_P*~*(K9#T_{^8dlxzDMol>8zvN(ua?I4#gWwpw^p|F|H>xq2q8jZy= z>~seD2h+Fk_=3z+omOSBFn)!=%8+=?D3OgD#2SDj>BVOrBIM9_!X||=rjO~V`pn*E zb?_Q#KK?&^9fot_hxTzzU(K|cT*+r z&T5Ca*2h8up&r`(we>2nGC4E!ecVzT%A#6+69%sXl}eVQF*a8gCDhls zU)S_Q$iN=q%y*agq|-?T0qF^9OUi_aXV}tc1w2`1ti;6WE`z~kAF(H|ywIFnI-xnX zy~9hF@H`nh$XC~_A62$1v@AQ97T3hXGKl*Ql!Bv}4A{5mmK>&^8+)B7dBecH&QCID z_RmL!%HaXv*XYbE$o>qMUwfZi6^@8ehRu;IjnCNPS`Y$gXgGY=NYMspklrT~M9s(a z+7dg11uTZ_kVY?7zbcf{%Sh1pxX1Rbrgao4mjjk#Hqddp8!&z?oYh1lJNh#axeGq- zqH)<1d$eEk9uUwjIN_Am9^Z3fD;rPqaG;4XG&4$HzfDxkzSwh+^=lF2hclyDjg9KR zR&#RBmkyAX!3BX|PH*r+1g;ds5Xa(h(6b$^*HVQ#G6)k^()ZG53F5FsJ&ZR9sMUr1 z&Y{E{hePy7NvuJtGX?#*Y_y0aV(vn5Y4P!pTYFAcTZI) z_Vjh#zofMLIREg>n=~CJY$v2v@2Lo8)a0D~^v1007_>K108@)g6??Eb3SHa8a)?*3 zunTR%kvLvQ<_A-7d@IGP&p4qAXZ>~@9LP$KDFsPDlVa<_9`(`jvPGW~P3woNYmSpH z*(-v*H>ePtsgWAsVd4>=+@`Ka`GQa`O-KHB1e*fW%*t}6z=wS<1h-pvYFz13 zkh|q8I7rZjK^$Vvo3e~szcNMVGit5`4wYvCav=}Mmp@E{3buX7=rf$Y>>8ox(CuQH#;^VDNg~0#U29!Y9ebV6((e_K&KNvjd3%%Z_imp#MjNtkBDJ{dSL?zkn2<5fPl|g} zZ(^Slo_r?(k)3^R77P7I_3D*qk0-dbSLI6CjeX+7BXarS9_R@bTTOC#UQK#k9oqV@ zYMvu&8C~IC*OZxA&!7Rjw;XndQ0J6%q4}XwL|!m_>9I({CS$~B*k@X2?G(O-Rpklo zd~TK1*qyF3p?0=UMAI6 z^K~}Y#SW?V zGybf^7w8I}V4_BDEgA4P(wW#JW#W|D{|vAr8Gy`IOo?p3)eqlXYvF@ce85y{Et)&mJl6spWvVH{yPtAzC)zPuz>44Pex)F)}!7B>qsJ;)`JZ_{lZ`|3Th)%}cc^D03uTlngcYXy8WJ#5ul zSrr`DAUu`9cR4Ico%pZS+_Rg<4(gB~P2kepe~7-&U+xE^KheDV^|*Na>^$s$Vx^~g zv^MGRQkrV$tl)a%bfW0?NCdf%781Y03#3+w9oaa7hH#C2n`-j`FL>>$p3}meP}weG-7Qdi zyRIKtfZ}Fgxkq}4$cC~?)9*KCHwAF{JS35cFIWVidlrxOQI~?JbDTlDRt%4--Tuh! za5DU&;V&qUat*o5pSC-^(&Ecs>kxY^Sxa;H{$>%TEA?kqum$|9x>F`q#|tdIe$ z1B3*ZyP`xL(I>vN3dCAoyEQ?sdz`-%&#enYpVsBk!Wn^5v&mAl>wFR9%%6Ap<-HKj zmbEpa|GXE+NK|g`_EkvQ0|Y^BigqfR3;X>7l@{|JbhX(~o3?>)?`{a~1;1d{;zW|7 zq*^n;C!q-ZUn6^qS`3GP-1rn)48Y3@2qNuO@cSYVf#J(Nh<*WzFdVL{f^*9ZNVoUz z>A-o^UEv0>b&3CR1*{SlGsQ4NB-iG5EriI%gu1-a@-e^PhAnfLAQKFFRazJ!x>%Wb z{y{|0&a&Eys^iEb3xM`P2cYovSR95Lyo=>uEg8$gnEqLEUI3@Y%#EWdweO9s1zmjC zZ(xLYcAGYo?z{=Eo>~O@Q1@2&@L&n+ERqz`FY^ITfFBqJQ|fTz7*5|(=hbENEjNfs zTnB(fjU2I%5rDr4xJlwh?QO9HI-DWUKcWmUf%5@%FsW{#r^Q@)rHeKT_WYE2+ipkf zS01b|TP#p2w@yNE!`Em--k`}y6aoWCT8U$r=|>T<)q$6*YKrZJ5&%yQ%Wh(AJUi-HFegk0-j(LCV>-0}cVei^k$wZ;z zv}rK>U#YgDz~el$rzwS;LSu}bO+L(mkphgOlDQHgAJ!aR(OUp zE7GPfojPs6Zvg4xY=5>!e8_v-t`7h$VsI1epe+&fQ;T zYMNcix)C*K2Bi_D%9L26kdypt903hz`gnBrC$6&ffg1Yj-ii!}`Pv)g_ub7Z;ci5? zflT3$I;YBaUwGR5jJKH0vG!;&URUECXy@p)VQ$!K?AHLJ+4Oug)BO*DSSp(dAA94X z)X-X2I9lXao~S$7xL!L=~8Mh=YDj3?e1;T@RM8423s^L zmszo~h=(&^yV}!X{CpR+*^q(3Ny97d{ETTs8lPN$w9)yKu-QeSx&BxC%Z^C(JG53r z4pLog#f_2~bLbw7k6wD;#MG5XVSDPN^7_As;R1S3yitoYp-KikRcheZH0~yor z>!u3enu=7@D|RpGcAd%#B6wg87O<52#OmOJ8G7q*kib^T{a52)QFY|%y@Ze6!Ipc4 z6c0T_|MdqIlaB}U%+Q=!P+G?eqvYKx9T_{Y3Um4s^69^h+|Dm|$W`!^AWhSeGw{_B z89mTG->UkDgIEv@NwX!4L-fJbwGmD+O*Jyo+WIGKQ=|FjGp5R{T%X&`x82`v`0^Dn zA&8`z=0P{w$f(T`S}c5WlJI~cxzf8(yK>?C_XX%1|B&J@qDAjhZ@`G1mfy(39=H<< zd5Vh@jv1&Kv4pYe!}9mO4_P=m^*FY;zX(dZ=o#@bvacl^6*}3)9c^s}sO7w`s(LA$ zq=^9ing6%{bX-L?k7+IoA=pbvE)F=W24{JwDZpf&*zCIJcRX+_9hq@K2uKpb972W! z_2|U+`x1U1$VDJYVaFwkgI7cyoj&KbE+566kRXM)bP!lKt`sE6m4BI&Hs%*#X@9vl zP*R=STZkSJLh|_9Q(0dU7^<~iyBw_qKGC!Q|5z0B){9^unAcr3a3q`_8krjT_HnaZ z3wy)|HRz=S3RQ|r6^_-}jxl*srL^#bs2Ob|o-y(ZU&>1hxa3fS{&+O?D8ONBAJe+q zHfX>XOsHW6L$QPcT7JH=cR+N|Mq(KBva5LZ?{*&dyWIeh_;sJ!c20|CjeIe0ts1}B z6slJim#CN^K{f|<)>8cyqxuU)bl?JY9R?|sRhw)HfobfzN4oHxYKd%g;EQ@R%A>@~ znJye+_7#lYV}deF$rJIZ>;ksyG4W;4GB$9%h15%IMe<*8fY$QE~^bCN)DDVH?ELK@< zMCfp3>dkWBvFXkMocad6Ouxz1x1&1#sgULCz*ie98+4cfl|VXTG`=A5#p+iLGhj5X z+Li9zr$5DYT&MrsZcJON^3uGi{H;5!@4snWsg^ZJ2h3`)nJ(c?B25~lo785ThabPR z!1BKr%ZqAgZxd$Xe-_Iyi{H(=M+;eN@(H2xwtld~Y@Z_O=k7 z@Oe{CVyqoC%Gva+=UOU(z2BoJQ##3LV@W4O;xjvs-tGjmrfuwat6TrBgL8lWWa~PS zaNiGS>__a6MDNLdk^a+@*75N8jo)WVj$n7d&cYd{oHGR5 zolT(Dd}{l@YFP{rBWYR$j$e@y-^sdv?at=fm0g4}d(HK^6*W9dKkzNDg6E zh*!l4o4H{GvK;vlGIlTVN)4Y4#zaEBmAu92H0Cwwzg)#|2=-Lk>MD zc?{px8KF*V{&rf(TBm}epe{~25=u3)0+OiGOwV;-NaxP*w`oAWlJ)4s?`>Lyu{e%q zi9or3aB`fE0=*SbsTmn5y(>Rh14Jp|@mb~5LIJd^l+s|u;M=p&nxw577NlU@0f&$d z5!@uqv>jVQs{qDE+$F?ZA&MnRdm9|>Xbjf7xslk&`n)5~bvy5sG|UUZ%6SMs%=|4^ zb`v{zNObTeB?4r@;s_q1BzyiDiOhfZnlV58J2Lx2X>yv2H!9rXlsEawragFlOsU*E zeTTaCj!zQE(M?dLr5nnec}DPahsN0DmcDI&dZ!b$i_8I6uLS8Fy%gOBisx4-DELds zXPpgoP)t+N0lPoD>N{>kM$mTeWN)Z7lzbxDkah$cRfXkza+DqoLc`6!!P=5J&A9JP zCC^%uDPrdLq7+W)W#WA~*~)bGQV>{TiJ-=08T4@KQU>#ts4&w(t}d%2l3`-vd6&Al z1`VVkYR2HsjqHzb?g{k!Kb$gE&l|weL+L;uF}tN)ZxaHlN}KV#uAjAkJn+mHFs(0WA-hCu2;)EyC}_xiA)g zt}N>MkW-PaET*F0*ieaga&7Tx7ahGM15Vre(wypu`_OQq4Q68RhX`O>eczmjlMqy{ z)o|{WgBI~OgQzo8N6JmFE35KdUOM$Af-0^SGYm`HkAC&f(f%Of>C{|b&ll^V=evo7 zDX?aD+85{cg%4QhTy9en645Z|~&-3q2NY0U~5s6^s02&;)#1AaqioJF^ zNP3G+X0ufb26D*iuD?28zj~xK9)!WWnO4H}NErio zcq0_YYLsj&nJ1O`zwiV8@3!`d|LKW}|C@mMa=(DNh&N&Dnqtb=#gHAHtcxAWf`KIH z>uVIOF@7HZjQ&~Vs?0Z6+vh-4Sy+AKVKY@IQD@(PM8VD#<hEyyktu_@+z;qB#8cv> zrWQ{pELXI_L@pnoISdB!e1;9He$LR)8FEX0VOd5i9VhMf<3tNtQIDPjD#;zeMl-tYp1F^g`Rg*`o#s0eVDM7VF zNxN#@FI*vG7$Q4=zq4D!H-FD}A+?UlhzXg1`qG0QX&oz1W;nQIKjw(%x;PC}?e(l2 z2DyY(!_zEdnIJ(}X4`X`+=(u{lb(3?$g*V)iQNE{6Pz86QBq~F_x7pnjm3=%G9ftU zLa&0w_%ao?iqBgY#;Dw83vZUmVlCT01&wQ79%~TXX@i?*!_3JF!Nnlv%_kwG08M;O z9YN0~eJFw~X>~(nzGkqD`wI!O5P4yN48&XvlgH zcJ}jKqlSbO4RCw|R%dS)oFrNlIF8C_juh3bZ6e`FQ$8e*;0Q=}FCt&+SVn=5=@aq9 zfN-?6??c}2gFb=->YX;T>s_YTQRd%)@878+Rkq}lU~ESi&YJmHh#KEm(zjfz^ru7l zZ-Zs%fVbGI>_FuXHaIm(<-#TD(#1|EZbA1Tp{q^)(a)dqL!!_g#dp81Sd0KY`uT{ha3re?ed)goLP`iG))d#4YXg4C!kv@-!!|g9{B=lOwtUa zi|IrAnS7tYVcY1IBz$W_B?$ROpqg%k^x6S9V361Z^GqL?= zXNBP^)Sqt~=i3787>X6O1%7VQ~4mzSdExP|Z>btNLz--caTH4LJSP3eC+Mt0iQ^XzMPO{y+ zD==GCc=w!;naXrG10;Q^o7e9Bawv#rm~8jL_{mgzIQD*H+zSN(d)`c}Ah<_-EYZGO zagGSV!=(~O$YTDZK?Po(?3UJWCJbErZ1m|m*ZSy`^Drs!8elMASZT`e&0D{DWkE&) zERG88uqBJAl(#GLoblGTR>`osG$Gz4$nDBKl-P#~T4l=5u1Oj6SbsgD8Eokw+>pNN|h^5$zk8WVj5u?Vd~d9tgXH7R5J@hlkfp zA2|9t2nhW3Hr?lJNitl@Muw=b_T6LIUz~P`cSvy<4Z3lY?Aw@Rz{J0uOO5o(9J*d- zYzx=YPKx8zvFP|5G%d~K9XcJTNVDbXzwJ5}mJh7T{yr6eyjK@2n*z+fckg)FQ%vuI zb<`^+e`CM{?M#`5Eo~FHnbldfX`ta&m=6pnLEZo@jWS@2U`7gR{kPBSYh2V&`aBf= zvIKBk<6Hh@AEj!U01y1Flc=Ra;ZB9b86P4nrGKoONX%F)Q@S^(62vsmQi3+KBPqr8Ir~`UZwxbb zJG~$2e9Ak{EE}HW+sy zJ|ZLNyQQRh(54lUh8KT>1fz>9E1RjR%l76Zs<#Bo3h0`d3ZV5izmmj){Fx3W)BaZH zWd$C6>lHnnOtU#|P?8!lAMJ6_6_)u*utze{%L!|MKboKsE+N5HZby)rWVJ2|GNH5D zecuZ^=+d$ls-=TaYz$E~Me5x?1sXMi-2`@u%CphfM%nsnUo8}sD}6p30ufMUtIR|sO{$eRs$`Wly10JQ!$Le#l)#ExGrcFrh2FN7 z({4+kPU==_^K8}Z$*E0;u{JucMAkibWdUO3f2s2vTQmtx+YO6d13+bjM$Ii3PVA+U z8ZeKtgi;z~&fA?Q7G2CSv>@Y)(G?ZXCn2lStk-Kn(k<3gQ$r}ch{8Y3+DI#xd?r}OvB^@l=48W66f zNe)9xZS{UI!+otHT)@DT;P6}FNM3OQ$Cs1s-sd?#6I7QS_ zdeNY6_x<9A@M~(~_zX&dgudTrw4uSJDB&!i!H%m1P-IA)$Z`R?6vTUrK1Z1M&&N0W zWfP16r4i!3bIIJBYDwTl93=SaJ#D}QfOx2MiMd{f^6lRZT$SvwH>0%6JGUghReJAh zdb)VuVju2W4DVYh&tcE0kHmPvYLM)@xv$QqFGGzWn=Xay&1-U4y<4spXCn1)p;Ap% zA4`0+kt;WK|5Ra$no85-K=Eb+r7jl#;Z}-dL)FzOr$`-)2AIl2lPY~WmOK;tV5BVm z*>49he@b$=ehli7Sbe<%kpiL<^{6-reYg|o-9P!buzgb6kOMH)sgkE#D;Gx*k-stJ zfl{t~=U(KuF3cV*zp10mN^Zgv8O1$@2P((5vItvS8l$FE8VzO;0I^Rb#DVdS+c~SL z<}S0u9InHv6?XoF1MQ1ZR8btWR~_6C&ZEpTzUrzI>1K4O;Sg?hM-_f=jotZ`GD}9O z@M)|i-caLHkb=}hhNnLh=E&_YWq5dg>$klHf{}8bv*XTz+0^ zmtBoAr;F=nyq8TEt+%P@kLlbQCu{DCaYv;@$50%T#(6HI;=Q&Yws8BaH(2}+reMd! z)!h9tx&Jxzqz>~u!^QpTjNISTt>@J*b1=`zBCKeo;BMglPjDMDzic0lxYC$Il6Ra- zs;^$iLqRBsJTFL*sOCxkzR*hn;UH0a-jzy&JAKWm$^~s*5@FSnbwq$i``=I7B>6u8 zoiW2#(43zq6y*X+71H3A#yiyJMm>il7j{vK;ODGqznOq5ay{=%x|gff!7WNz2p-om zdezwGvBegKoutr^96XEkG3#S^bJO$Qyah`xxWqTB^;p2_wmHX)<0{ig8W`4V6t2V$Dj6V2KXrnhb9?Ox+FZ5yHoavaTEt zwGp0T*fTPpotuSrYQWpB<9w3o(+D;|!nm8lI9YX{i_%5{?qmCAp<;|>zKXYtWlvx~I; zAz~It%8*X^yYdo0%~0fP``m#FsS8#3pewLtj3&9+QIDoX|H>TNXfT~E1pmW{3nW-B zQ~skrba#X)60O*L@2YpXDIA*z>r4E$9DH_`*`R=eUBhBwv%>)n38r)Zv=b1vL^{gPAjEP{uS=68)>!=+*XwJ{Ui7Vbh~yTRU&M%yJ4QO^ z9|cFX+OGWf(^r^+0l?8kuvfF9_lrNeP%Po8{@q*bhCn!JFIs!}l44_6YwZZ>FM%|r zTAF$mkwbST2z`@mBY{)O^HWKgS?k(8~PLP$ikyo~&=?xjNjwc;ai zdtcd%n_LOyij!!}Om~Uz;;qXg(B9RCOGGH0;nmwnVE$Ook!T?1o!uuOGirlz>&WWE@NJ<8A3OULzlesk|xh=KKesh$r;_bxPb2) zoJ_^YwEo5S%iyw*0EDJSwslijH3=5nLQ2;qYoF|5^T3Ag&o@Z)^AJSBwFl@AUjEZ@ z@;AY7ZR|%KyvEHn*et1J;qN}i*p?yLtnd7Yp~nRJQ)`pmA*Y) z@bLy?!o|hX;2T9taDTpIH)ZJSU zX-So}tr{nv6eNA&{H>9=6)l9=FMWK)pc?qWJ_>?~`7lX}#OB{fc8b5aJadS^A9YYM zhO)*&5M#T`!`=B_{_SPmk5go^EWS`o3g4)}#9Gqv%$Q+VGK?%)`q97A6U?HpE+OHS zIidetbGUn1DJkGxt(>5L__RKg{)P8(P}RKtcef~Tu2}5 zWgC~8R_7+$uss~j7Gv6Vnz%JKWzi`otDBfz*ChBbc_$EWl$H)#3EUaIWj>Uc-jZ9# zRJbZ2%awmR^fq=CQ~iz-PSkJ|@#&jEn3I^_fe!LEocwBYpG zce#rc6qkr7-B4dG43nRFWu>XdnoAZ(!+>QJimkLRhVj>kLF3!+QY>0hOUnO8n%UX8C&Y28^tjCUDvKOd5b`PhcNdx5TbzWR#lk`q-HhgcTl88LSJ zFHSb-rz=+2=$TYXT;^iXb^NjEE2M(>kgrQU6LKn-`~4!*0y89Q<+{VAo(1_ZdUaGq zHJ%Kc@RD+_e+n|EgXmSkiOj%}WFE^=vZr|SQ`Gr??KAD=$wryo)9t)#S$J$&tr5mc zR!b`uO4eGy0wLR~o%^{Sqexfa+jo?T^6tuoCzM3v_af(}Ok29@-5Ik01>FP7} zpaNA|75z4l_GJr>+``jcz5h*cui-9Z8T$<3b*P#)d6&5aCH(av$DdR0Me(DFC32lx zIv^+G^GN(fh3VNmKwR1^`3U!0_0d$KQSCfTlP zdK4Q(mudIN^wpFkj0IUs1 zCUY^2`uN_I^%%Xu0O;x4`y)}8@pi32`vvh3yTG$Nn;GcK;e(A_$r{WPIltOMB+-a<|)K?q^&1*jW zAQG2Pl?`N$9t`nN908^j!AODAs$tlJ8tzo{a5FFAw6SG84awT2%rqOf?nQ_t{ET3@gu^_bq)@PKa` zb5aYatEeY3IA6+xfx(%%N%%(d8o!P45bDtjfRntGx?83XYZ~FmDS$2T01SIDU#uKm zyqntY)~yYn1LZ~jxY%LIzV(h5|AZB+^>;kuynnpd~>OE}znZNX5tHB_w zb>R*+$c)OQY%nr8oMA)=my5o~5D!gYH)O~K?#1eAFH>hM@>v98(2O;a;YcD6K4~xD z&qi(hBn-&z8NDKZ=U1Ah)+*EwQUt6%bRzKs01K){IjZ9ZyQ-Jb48RH$hlFz|8JW;| zH@$unqX2|F7seKxE;;IuR)_Dg58GXl3=}gl16vj-!JPoMO8THzOJsibhI|cMXY$DO zLGceCh@GS+^vB*QH~rCIb%L9;D}ZT(@vz(PwzD{imBG%WryxGV-RKeG#6Xr1*2?m1 zM3EjV2d5hGsY_L`%xUi5^VB;-ME8+Qa+__H9R@6;YV_) z*6--h_6nvV!@u4m{TuSvLXh~< zSx@)r+;_67i9dj6LyCCXi;65fop1|s>flAU(IrWoO*$MCt*~!(#@NEs(?MJ1?}N<1 zvtP~vLy*aV#bf$+*tQJ`j1hC1D6bC-0#uD-i%UBRScat_b4ZnwG_rJY&tCzthBf7)!6?Sv$O_K<9s%P8fj1q zHl7|ab9Nr;`dcnalp*O{eAD<@pXExemfp1I>?a`uw0!g zZ}&Ft8|^J7$X3dqKi>;!s=UYzvV)B_vqM;se}e1onWnQrE1EnLmtxDwAlNxx=B!8E zNGr29kJ@rd3ms(qEZk3GF%&dSMajEA@|56RgyAACyKvyte}e5OaNzNvO#&u(c=3No z#%^Zhb^6&c#-j;8yafA>SrV1}QWz~y{G(k?F2GEbZu>Ta1$CG z(YaYS35P`bdcemeDOW^S981%mwYg8HNGi)>#3gZ0ApaXXtn~3kH_ZR<*Mh~g59qQo zZKd`*Wh9?lE()RzLG9vEwq!~;Ou_b56w=L~IB|TU|E9dOOL&UT?$EM!y`n0gV< zXtvlL=8ZYZIOX%|dP9}T8=EtF8#f^`(}g|BWfdILuYxXZ=^`EwpPlwWR z@bk^L)%aXIoV6sq|GRibSI?S(7b-u)QjAcU-xa>qSSy=j+VHRnjaevt6VIsIJ}CX5uIx$ zp^(WCWmnGfUd+%#JN1Lo4G+o|L(t2Iw|N!l%_3|+z z>D##lC6Da6-QEU8>&B@Lm$4){B+awsc@;W_z73Z&Vst0k5sRN}MS*#iTPf+;xSJ}- zU7G7LFEVJEHX(zaHF)-%aWUuu6@ukIu!PhV6Q6?>f>;~V8V5v5gdpvfi@vEd{7Jhp zldahUzTJ@**2cNR$9+31n7bLyq};)8Zg26k2#|;Fo=zQj#*;MpH7GB)Se-<*0X#>u zT~P%yl%~$iArg6YL_rQsfzf`}YuZg}QLGHuOZ-209ZFZE3@Zt9c)si#TG6!C|KRL_ zrV8Rq;4!eQpquE_<{%kO_0qK|Oy00Vj>qP{HzyW3evp>uy? z0iLRSZc2HV{HdZvM}d0QZJ6wjL0tk2n> zOAF;y2LZuzz8sjt%t5^ikI$Ru`4;@(?(XjHF2P*` z1RV$(+;t$3-~ocW1t+-sK!6~@32uXHut5hxvh)4Dt=ir9-qw5j-&WnJTh()KPj{d0 zp6=74cXsY+d9pI*KHei_N;iXm3fR@gtWLJXy&WpAqCex$Wc;dMPB=R#WFj)X%iW04a z8!2`_brl zQ-E*Jj#0@d>&dPm3S3|4!FBU+VO~7REM`oNK1^m8Vz9{Daebb*N69ATfg9T>WQ)}# z=nl3|iQ$(cFO<4m!aMD{;zS1)zcU}H@=XLEqwPT4Mu@|F5K3ZDfevYJc9P%g1Z?gc z5RyQJc2*icD~*pRglkMAYvyWU|5+tL>~PyYgog^_ZiDhB+avp?|a2<6s>Oxsq4fLfn~e zD^GH^@HfyOUd{>}@%Xd#88=JnZvY$$Ob2#1d#;EUcpBHte^(s|Qa)fJ< zIdiImaRfXbP!X3|^S!k8UKw}WXwufaU#Dg``1>!U(aw=n zUgZ066G8}od`&P>#&l<)w<4=D>)qs9@uiX?U+u%>iD>joZ-5=VdTo@#Z@T4C)M>KP#(P&-*oxK0hhaZ9}xB z+iTh8IkF9;XbEBH-1~Kjzo%v#Ai!dlO;UPdE>zcUPyxbt0LCDRzCq|so2(^*xj*8w z*$2OZo%8Ch|5=wIet zlHF`{;8GWeD1_t1aj`!#ngra~SH({(QzU~l_4Iu7zbf~G_#$X5bx@O(I0$99>Hw~B21s8gIFsKMZ)G&N z{nC;omEP7?pae|$+DQRoK|$*W5mJkObLT80x*kp>D#?GW*P?RFXPn(=g=)yiQo zq->@`2&q&++j3OicR|hegGs{G7~BK;&z7lR7%oeM)-!Vf@}aAtK3@?)RA~ai&cCJ* zMsMo@{=-at8a3BF?P2H785<{vtj8XTb&}W6i}2$)!ym!vQ>qwSEpb;~KNa8r2qp>l z?sXKmf3_J9L{D}hLAWp5qmi#zL=NPFH>Dmlk;mpBEfktzipSLz^d8~qUN&A}F4ITp zYgqbafn;Flv@g-w^F$a-b{t>gMmY!JN5ENf#~w_+Oqx|@C9@2HlL0@i zu!=AQy^UD6oa$bbthZ%5W|z-?uh6pAiMWu;pMN_J=Z1bJv`UJ-#de%7J7On;(nSD za1S=VIUC{-)CZJ4z!;qUYZi)jYh3@)-jj0`Hp)U{%R(;Hyn;hnZ zxEnDke~+K?_{#c^>SErKc?PHy3g$>SZ+zwD6Br>_Q|YeBXy1_d1C zoom7-)ov+S9_dB^_ts3c$%m3#dk;j5{0|=KbB&`H?kKrBii-%->mPPfigU$@HlIi-(RtE~Nm(OjIgbesQA^yGbBcqK zf9{II1d?vnrzZIMZU*Jxn;Q^OTmeZnsKGF{Op7v3IR;%_GS9rAn*MMpwZU*!u<>4f z3yD3Bfjb{-P7tjo>r_ehxo25g~ zQ$CIzgeV?@druv1LQj1`Z;~quxLjYw6Hh~7r0V`HkoNZ#mu|J~yQ$?ck-c}8FxkJD z)-yCr-mt=CCP3a)hiZ2t{WzAZqR+x86o$G_1z4B+`5~vVt~4(tm?s{v-Y3=f@q8YWtaC6ac~Ro5sRNPdfBNO<8n7Vk5&X`_P>lO+j2jy zHn-AkXvFpu#*n?8BqiwdpLK4^kKlYc|9TizwETPalX` z5ScHpnmnBjW1&tm5d>wm+sOt)=bGmc8$st;7_TPm1@j^P zeUKqC{{g~34N;7k}=*I)wSj;eyFN-C>N^M(B3kob-ZeK*{>)) zE~0bh(=!=Bov_@DKmce77RPN#UzW{8$m#?x|F|A2wxc|G z^t<_|U?%IFJv4Xj!#@GGZQ_Wmy$c8k)yi|yPf;ldvP5&=@ZabRh0(BIG;{HPJY5C& zKV8PSZHr^$@MHN7Uh$Rl&0dfhC$7sM@jh2$v_e5h?+VM^=_2IU z>bz}7O(*)`>m&Il&p&(B!IxcNc^66@bNOrf`DJx!LrgMxIqZkn3jmzqu#QSzJB2Iz zx#4D5n#z=t>!Lw(58xCJ){#!ACkfqi{o1)T-t$~k#)s-M@A|q@OUXxV>mrcvXGD`! zkIOc%Is|3%jlY5)!8h3=ndZ7~gvofBK^0$734R;wJ3zhX$s-=L>sdoozWCLJdo@=Lkv$_VN z<&f>lGjp5L#|~=iv3}}vouqR0C6iyRCj8fD;Sq-43fxkR&jxf(rNb{MFuIklMJnL3 z#(3flcOS__og2Dn>vJg!Jpb{oE;qQ|sP0o{I}KD|hLu)373^1&`XxK-?);i<<8|t^ z#tRZ|=tLtegE(aj7w5;A+9_jRQc2MtM$5B6WTUgw zv*A11N23|=S@kT#r{g>-K8FZ|KSq$#Bi4=x=v!AeKhE9hU^xAdYYfFWd!&*$#gI^? zNz8BiSUHrZM?XZ3jC)wdQ%89DQVuEndmQ4k+GLblAobnahb0q7Co(7S*GnG0m>IOs z=_J12q0NKm%<^3wVA^^2aFVt>A-H0f$OkF1wjnLBu3PBchn2*W+Yi`VGg z#%KQ$R>9Mg{KVeR45iQs!1Mt=DTtV=2o1*w#*nuAlz~B7{^Jkv18r^(o=-IV*dTj$fSkfMX)=&VfqB6s}n-NjqZtaUD_D zi>=;Qdzn2pHLG+w?$W^uf>QSuH^|bief>pXKRqK+raRE?CZ;vYcGVE$8&>#fI(K3G zhL(-|bl;R^YcT)-N)q(RDDz0JY9PWZb;$tlk+BYkYzLh7U|6i!p^ZPeU#X_ow18jk z5`qByvm_co(v}N?4hjOxRrMjlU#AArF7e)!KGxxT%%MRkJhYzAnqg@zqIDlLn|us# zKFHM~e#h1~DJ=x(n9=?&L8x+&dx zGRSg5+TA7+f9^BVN4wB>PW|jY7D#7jixIn1kAWENMkY?7h6wyUw;>Mzok!uLk%rf2 z@}GP;D(u3ae0yb(FZ$lkEt@0OCCKn8mI60x5r(j5^kumWZUMd_%S=PSFKT|_Ys;_$ zZ1id1d;s?LmwNm^ybQ($>FJI4}X7z5vL(CWnW?o7r=grtgh#J zF*)YiSdcZ*sAs&#D;Ef zqq@|q6W=QZg^vEAPaE)9y*H_3)(Tv&Tiq8+4Sg%R7P#wy>q@D8#(Jiy`Z-#)R7ffP z*%gpo(fS~2t^ouSrfrFe3doXi`8}uIm9`a>V;BqTY>^rS>4W^+7#u)nPsU9g?bn`B zX@Pz|7~_L`gL;Ja{0xZ++&?{?9f)MphwkI*GTZC$1Fx9M1sA~m4KLKp*+^?7)5e%i?PJE(w*iB zF9bxlS{B!O&jy(prwAPxMFSrXh@V(kgfUULRxmSDA%nFx1vt8XOa4Z^Hrv)+rTkQ& zc|=HP$&HRF=>HZZOX<(Bq7}(zEY+5uenQxE2W-%j&XQ$j_!I1XD&T!Gb(L26Gb+yG z#8vX@ueC-~F=^IjD&#|tAz4^A)<>f};)ArPBcgYg)*=BjTp5Jx_vhtQ-+3cPNp-4% zVT01pg6fsSk1v}OGWU{DOYD6q!_D`C=AB*qFkVz>fgd=AZ+t7j?(ZLS#GA2zie?F5 zc=Z?=0ERdM-@(H7tq%Vm-d4vSoMdqSj1tE5-)fD1!I$JYX zb;Ck4ZdcA$I^B%zt4cE3&v0=pUp>oXsVIxdZ^sNdJGDqjkUX+GPD<*_pA1lq!7Z#m z8)JGO04_>8QB(AnDuz3ceAQ8nPNl(-=#o`4Q;er8J905sOj-=fyL6=Cs2n4cTkR@Z zV*MbVuqU_k&9FUB1hbt3#>8Bxp_$j3%4kq`S7LPbYOW&wO%?6!>{}yl%=wSObzfAk zFJ4`tNVZ53If_;53#@%R@|hRuX-f9+_jhbZyJm*?=vjSg{US|Jim8g_4z9=J0Xtc zBQ+;7>#2DjJ}w8nIAWCRc@m#iQeCrbHEz9}0>XQS3XKV7xCIEp$)K z!|lZ~KfYgXq+Ocov!1CV2I<4MHV=nXlb^iLNz?IC+X@p3lt;ewrVLZ3vyV5}_LIpf zniIH*d=PZ=AF9(!Z^Ur()&qMeuYYWxl$u#6VuMWw3^^-O_&C^##S`XD?MtN)xuKdV zzQrq^yglsUYdX?9WIjuJHE)h*n6m9>RgRKdJ zJ!CTL>4q8dtS4C#`6}S)kg_%Ht+d*a6lasV6Uq^PiwsQkyVENSmOApB>uwA$&DPC@ zr?W+za?YQ6^=n9xQqFrODKr6-JY0GjNKnhvVO#cx96p+X&mgj5i^s5%b<%R|a^9so zUvN!ocr#Pn`pZoyS9F(HhA5J~37jfMGRM2hYH40^(#n3P4hsI&Xab;`~4vngg5v zB9ss6m@9o(X|yz)ORWQDxpm6h@YJ`c7)vVC?#ep$pHV;5jG6X?_QeSsPIsRyuFz_L zFK1n*a3iQ*+>`q=TB%!0$QfEcj6W2R6Wo+TJRfiR0B+Y!DChMsSEi#@p-51`7v#@% zJ$fa!4&(bF0+{FiA${y7^LzWvYk>-7xb_kvnYh1D;HVa7Yw+y^XzU^Qkl}^pIfT#b z0`d6MH-6~U_w#c{clf-DS#Lb`GL=CifplIab=Kmb(;{Zr(P6a~D#{}8WC+WQ9NSR@ zPfZfL)E{j8ZenXxz!(|};ON13Spa1bf9ac!%M^1-D?d@Pv!l(G@ULhqdi$5K-{A>Sp@S^gRjzd9T@mZZbMa!tN zkCoDNe*4GB4b)bhsy}a{M9J0;=cK>1>vZtb1BDh=?ECj6f5X5rP_3oi0`AaK#vS|H$BFSlD$;6Ow@T8|`6 z*Kmwv-+2W#nQDIdbsS_?)*gO2@?1UE@+h==^vg^DT?NM|$MeR^Ym}wDL40VR-NpJU8SX0LrhusLz*PDPk` zg5?|iuz}tNXf(`V3H~0@kB^h<@)$}S(@S3#YZ!T?+Pp^#ocscSG2Y+(rEO>4ot1P- znSG&=dPpdARHmS)%YKrzB;{HTOdM7+@0OK)moq&BOs`*O8D4TWXsQf)>824lENJ-b z*)$B|IG;U|QginX?U7;mI~G?OvUu+O%3zwO;dQ^X92>_DD5POcmO|sl_=&R!Fuh1v zQ0{#%t5Wn|7$Lz83tZ<8Wc2v|<%7clVS9DeNN<@)pN-E5WrP?4{L5=FUq{CFj+1Fl zX0cBdfzDL3`Sj!EV7s_CEE?BvxZcH7M{-dY#1w}mg)d7HV_+t7ZW?rPN zIqgjxsbzku8w*B8w0Ajkc?f{o(w<{>Yf+J-)4SPuD?+>2WqPiWRo)kyV zuTs;BfX#FfNF1?$wH}-)swCl0WjBagUNwILElU;{7z;rn&}_g!;hVre^Zpj(ny1rj z%SC`ZAVr!3*rr`;7nB8{vH|m;eC2<=jo4nEXstT-F*-q@`mhE;@6EK>vun-l`_3^j zNjJQB%w8Pm3kql=N}7C(Jk1?E6&l#oKK9_`t%N%?WIZ%T&`8m#T*wk&JqIn%FbLt?xDEW#@664F{wRidOLS9*&|sHfY7eHIzBwO(Ny-sIamaN1iOFGq3K9fPj?U8eKxcZJ z8LhJP5)j;3cMY=;JF+GLt{*0O?&%%`+i!gj6uiMugx-g9>?@^_Y@_{}Ja%B^tFnF8 zqxIXkHn&)NEoYdOq9qp6mGDmqw5#AB^4jwZ`JCIYfDwC-G5MRg{(Fh`lXU(U+351* z0BdVD6;y2BK_pE47-w!@*NZo5a?$z%b|9n1Kx4 z{}82@Q$bP3W~rA&O`;0;yY0-7eh)5c+0n#9!pYCCV$D-Qf5ArH_+paurjhs^EU@-# zKBy(D1SW&KI@E2iXkRLSLYN2LOEdKn=m;e#*;5C@(Whnw*&grhn*Ez>zQu|sp(tf_ zESFww`c!n7I4F4Hh))Ar*^<&e_(QxLzLXiqw>2XTdFP|^_x#;0M`00ddT5r7=0Ym1 z;5^ z-4&<=J^GF@r}O#f*(s8TF1k-4>3ICqf7*Y=8#8^Zc3ig&oy7q&v~w)8&lrq%d=R1p zT+@OA?^b(@E48gxH1-_8dB{$ z+mCbaD+&>nQ$;!*2JKk)51tXtB3dtp{2cPye=;vW?Kp=OdH4h5mFgJiknktc6eB=L zpNL8lx28K9EDBqTL5x8&#Droq>1taDr_9e^lqT#|O^lzLO-bAy$zcSZxv2k#5HT>7DZ<*}>sR}pM{ z3O1zc`yqzIqzOh$rl;UjQM|GnlPK8v@Z5$`d*=4=>6GK`@HpIu8PoO7CuKg&;)sgL zG{(mAATP3Ap&s@Q?FNVU-=x#^aas5`h>0Isfj?S7=&qGbPM1t3pSm-P5rn-z=Cd7( zU?>Ts0?#>T7J3!17+xhj!s^>+mVx>ETx;-#XLD`V2FF^=k7eExdt(g+PiT88EsLQp zkT;S}c6j8Qj7-gmAkUzUoA^fQOe}wK`Qvi^w%=D9y!Pw6aiIo=-oatYlKBNb%xT=h zGM822gd>aXECOWoWzxe9FKLNN5<--Uw==E=F5&Vu1EUBEpq2l90wuOQFmrjbjO$8x zV{q<%3iXkA_cEGRU(~=+xX!@N=L@!F{`sdE&OuhFJfLFB`m(rwfVHosxxzFfBrSX( zl3Ql>4SPK5462=GN**H#gdg~l=fOuQZb9bdTQ11#fgg;ND}%`?&FV9oUFt^k8O;z= zE6*k3!Ww_8gJ2NBSir(ez(-aHLn?0iC(_2*oW z071p5RYbcEH1V$#b+~Ar-9+!5B6tot@v%M2Xk9!6_SRr~xN>nUuOiuaIm8406VSHm9CBW50OGVG!cIK(U)NU=rYy>(t$r17Yozz(#Ub&w-D#OF}bmGCVezS}rl8JKREMr~_&z#MUwf zV(Lk`ZW5i2t`m^A1Pk|E3EpS&UGa_mdj`_rzcY#We~KgZzodz4S{+aUzgcJCoTdaaf~KnuXq4Q1(Z}(alav z81k~JSQ6U=zRwed8>(nT)cBQTo`Jf=B)>$@neY5w%ioJX`5Z z4LlR>Th`h9)bHGnw`{Lf;>Qdj&|yEhd;=cI&jNkSe1EzO6*aL+%P?5N080b?YPamLy ztH@ysQOemA;U0@h3QOKN7oEOpi8g({kt0g)wH<)mr9Xt#66wwAsvGh^;fxs6zh97r z2JxgW$AF>{q`m7W4RPO&AVp$t;{x%Vl&)_G>if#@A-p(OBWRZ+cGi%Ry)7~$rBAPj z$Z|e01IP};G95{B5RErde-A=Mvg9jo&d1@6SsxrKSUP4>thxpWCON5DR;)(!I1oO& z>;2Ts+p&WCO5_Lihji#bQa%`PzYFphhoPQ?DGUD0Y)}VJAFj(;pmIu%H&na~v?pmJ ziqO{KjF|tW)3@oXU_~ zb6>uHfN;px&zX4_(;I`Fqu61QRebS2xQJW!02&Iqr4Ze>=LV*MXeO4W>I2-5MBq(< z9b49cNuliE`GaZGJK9>OZ#MRxxJ-LYmBd%OQ$sA(i8SqsHOy1j`*hd(3g`bTBLMfa#cc&2=4bqc;HRpn0~1s1z8Y- zZ~y03GK4T9TGz-uXpJQ3iYB@d8h$H)n6^66Yb%L!5ZiT+qyUle8?-GTZD@f$n58o( zQDbB9sAnB~#+moAa$XX-C4NTDWI+%M-;TM1$bmQr=`||LCKi3CpCrV+c!llVZr`S# zJ#T~%Ts3la`@JVz9KwZ=9MU9%4=^+!+8$Z^CXR1M!hy7>s@gTl2`xKD72VLZth_P8 zD3&fnnum9jUZEdiMhv19oggDMBCO0sZ~67X(mhf+u6exlgS(7(HK)sw#d}>7JUmBY@0TXlvs-hlfBM^B<7niNz+93U>Q@7yfIW0vYn5?!nyrC)on`0Dyj zvlkz}K5-AY6n%Bkfr$Q!0{%@BEx$Ak-I)lZ$pD}a=n<1xA>r5w5go?>N%^Z7SQEZU z(D1H*Ocl~q1-ra`P?*cC#er=l@huc|Q9)&z`Q+ycOBw$0_wX~{iUu`s%bV6mgP4Gi zuNZ;k8eslRkPSC%@C`JoX&=jWoNOeOFM$O}t_cSHT6?95_=9D-~5aXQ@+XE9uK=+5U+MY@Zh)A94-uL(TLz zaNS2!!bE^Cvl2&26-dlgif^I-L54viM(N-Tv7I{uVcS$89W(mi>=#m&LOO2r0oV5! zZ==}xZ>^v>gGd-5i!n7ks*vV`Tu0L6n}AjKKAS11UxxRvzRX{HkW=60_4~@v<&3Y9 zrPT{jY85Nv#wL+tde;-nC$@}aC?Fr~b2DYw8l3%xzvgdmPi8AsauDt=-aN2)2*@=d zWEA?&3;7|+IHWx%i~nxw28Z9qgM@$4YO=$4XrT+Ag2Nvsi@;tK`g{p^A~Zlhc>V@3 zOXt%Q$HNRpB1iqh6rT1Sgm5|F(RVWb`Hfvg=fV1I_|IdC-(MYNyWimT1%cDl>ZlbW zB7*^(pJ2f_7;OlT3U(!(2YJ=Ynn_MffEq?B0NAefkq;8#6atV{Zjs&&VgU9n{1~Xm z&tZ3m{b}-e;KV_;A&%DdYQ!92F@GFY!TO1jN=(3_t<{_%mq%<)PVfOCP0|S+UEvCM z7^|`4263}sH9334-(uQOM`#gC2n6C%dm4zmjc=sQgtc2QjAJAablpfn2opFc6qrBf zPj&=0Thh|5f%?bD>`*5_EmF8}|Msn5FhXstbhH|6SC&VJse!lM!QXc!UVTI%Er_L?PL|%7>P)c=3FXwA$7b&Q%bVUHD`k zl*CIM5}sglV&u;4#7mno@B%4vqgc!%y}X5 zUo_#5d4r8uRdgF@$zbg{xiMDr_5aFBAuT7@@kV*H)To;CpWjE}rOQFTm{*%gfsSg4 zxIe9$%~7nz5F{&-3s9_%KFEZ}CfLxV`o!+1guoE<4iZb-((00ck+I@M4YBON6p$g9 z@oy&d#ij$-nI8EAeZ0;*F8M2QMFeW`I)upjSXp1rpqU|ElHpXo|La9L{*b~~3l2tw zzBuz0zgredzBD>Q%$@E=g@&G4$^W#xln1K@&CIsdBt^j0Nxuh+UL@Ni@KHr`Y@h<>>BSiMFg0fYT@-#nbh!)WWCP;#-1HA< zhIGU)Y?zxV1O(fQ_?&}#tz4n z{HHg%XJ0J7#b4UdxjcGf@*mlYqW5HTXXaEhTL-Re1DDJO`0 zS!A)fjT>fZx3u}-;D2N;P2qmzrFkF?mW;<{)gpdpH)|}7rl)_3I8b09CGQW(P%!KM zg9k1T^{_jel=~xHOAi*PwvumECr$Y#qK;FW(tzNq z)>hl@I+yPI!fL8mKQ@`wKJLdoll=QbR6OzGPE=v*IMu48F7bp#U~jnvercZ7S+M4!N7KB^_N9rFVBTyFRx%Y08d8?%GhvClIVD24*NT7a+!81;IAI`(N?3h zMo^jtnNo*bDVzTlfDL_eUs{S&ngsGeYKNYeLQAP!zwe_e+TGmk^5ODvMh->dmeZJ?Qrkp~J}R=ie0}^#Z=^DCiU`@(h*uB)CVl+g;BJmo z!IRmGwH3&)ynH*cNrW*_@Qb=*C{Y&Kcd|-5p4AE(lM231r`J@{g5x$_g#A49>hAX+ zNPhhEXrQSq>ha7%c~vBh`C)ah`D!od#ZUj~QJKfF$8H&cSD+ z(mU(z0*)k0uv(;b(N<2w-+E00w}D`uep;;=%N1AK_}#v)!|W&a@BVqMVD}-8_vL<% zZ~b4hhBQXoakt$g91AH|v*G+`-_2ujz)g1s!?Y#AC-bv(pl|Y+@iKzYf^)L{;9h#L zs-4KDgHUvUfuCgvL;epi)t35b1XX>}TgIT4BC9Xbq3dqh!54#W1-RcQ_PzcxN(U6U zi&TWlmbC?;G%alOcN#33cX7zb-+TTFiE2Onz7PdV+~$X&YK=OpRMj%oo?(PO^@k;-GYcSTmgQ){<{b@M$wcMT_&8lYvsuV04Pgx7Aa zbI%+$WM{mGB+3-DGf7LPQHvR}qknEvOW1Ip@_er?rG1D=WEOS4i&;|2orpNMT;o_A z9fo>;KY7G7&~B5ctQtDi7Y=Lj5v{9c)+d*(-+~9c7F{MS;UbyW*V<5KR6&n$vatK2 zu+7W(!BpuLpBm|){IH*3vgN+}Q2?%`-&3hUuIME8{`6!IM!BWY(b-A!*Sud|gPj~? zMUIVyV`;R%>S&?X-j7{50qx#cUH!%Qgv73_M(ym5WD+rt)a zm(($EEx2vL#>NGRI)5aMka!BGurv4OcOlwolQ(5r$6;r6LuFj-fW zE*y5P-Hm*^)p01Vua6b;jc-f{Nq~nK7sc~jdzp4C?H~4zuXe>7ZDQB_L$nj z{QZXHfCDIEM=~u8&>e?&EwgM4p{6H=jbol(Uk3n|qgpac<3&)deRD0PIKNS3^_lKe z&R!6hHFkuINiVLSgfd;)XzgWaAgLL1E}?>U62c~y5vUD|39oV|ULS^EAriSgqvLx- z`RW2bB|dgTI-V-cM)^MRX#-RN6#i4-Yfx+Zt|pEtx)*|Vc?pOn(W9`YqV|ut0Helh zqppRBirykDYhWUL^-l!}_GOuVOnVjXL8g%T9dqpXv9Ow2UCs=Yr6z2*+oE;-EoFTH zj$`|G!8ek3MDllRc^|2?6FZZ413;VkhpVdVoE& zyyHH+3){eQ%yo@@Jh%Tr?9Nc+TeYjIIrylb%f}i?OcH!J`z)V!z83AVwC;W@Hy~DM z=r{4x0v^fF>=lE|*dd^k5hw?us*sO)L;Z_Jy7#S0LhohLyEsvJ(_*Q#q^fIF<) z?EEua1t#kIyZg}rVFhh6GwTPnXD1D$mpg+@SP}|0kBL>?omkwJuNqEUzKB-z;~kQ? zzDaokKpayMlgNQaba*EVs35-I<~~WF@1uV(mw)SU_bD3*?>oLg^fPs=#s#JJ5DiLUOqjP4DUBU#sK`=}fEY^Q@*k^qbvh@A(& z)tBgA)Xj#WA85{ZNFD^CU@0(7gJf>rH4Q%3kNdCpEw1pOQ8+aHykOybjVI(PR82h} zDKJM6S*?v$JC!(xZynlaEH6-?wtZ9!Fk4xreGuf*CMGyLI$2Vd*qOs znEX(o%bq>WyesV%GDNCW_-+7IG0Hc3+g?*0CBRz)0+FU(;Xe3Mjf`iKMrz2$>ncE&AcKw zMEE_i-oLWEP1n!tS6Z`)E#WBwU>S6a83TQeN4F`Pq`-$;%lQtOnWr=JmEQJbH~KYt zO$ddu{Z#_%GTTeKy3ub`8asl=R>h!y$gb-EFz6&r6Nl3*FBT>?+iw> zzJ|$Uggcr_8HP#L?rGvywEKF*29fRQ?$prC61P+~FSyal=Jlm0F21H+W<`F%yVBm+ z{NA@d^CkLn?15`f=ctiNFlT8q3h4H(ue{wGY30=YZ&`L1D(N4nAg&^@TNg^KkC7x| z?1@E=BHKV4O#64iFoPCQ|htCXf6%`w;7kB55F0(^tA9x?$@&Yd{Pw)-X(k-9XSn!R$|Tz$5roRna69 zI{jZ$(7Jrbn5udbV>vO~owDw==)IVx*X9xo?NJt~!K?lyGn$XHxd%ECoy-9?F8VBE z7hSI#{OK4Kf4-^{FyxJ0JRqUM8t%k4k@s6t4}_kMQ=jmK4e;Z9$-a8-2S5a zGvKXYlyKq+MnpF(UAq)X3xTN#Le@O85MGND>u{}7ez?Y|#Krj&hgK#V#pl==l3l2c zu-_R=dx2HQ5}p)%9BmNtc7gj2q1G(R+yqF)-T@A!LlU38Ch0S-pS;AY4IiQB$g8R{ zDVUsTgz&ec`x@~C1?>jF^aYY7o+4?r~;nY~3U-|;2`RmzLe zCi1n}D`6si;XVG*u~lzj@(r?O0=}d1<66(^NCsV~g|um6-K)}i-zWA`J%SP5O2?hY zUQ*)A-Mol7J*up3A-*F_-o@uiKcPFvn;% z2Hl^$nfJ$8%Jv17@>CL0X)wBYmgCh4HzUNl)O2Bi?E%Kf;9n#)&g!@6&PD?w{^-Uh z3OLh*S;O!_u=&p*2ta~hTd)KSKNMq@!#lrzJKlg<`9=+0=i<-nMX9iZzDts;Uom#v zcBFlHoPrw@u0(43Zxs0^4U6m0kw<=l-l40wYb-vkw$HuI)!yQBOIicyloy({X^p_*T>X4%N^aq2b`MKa~Y^jNR zKA8rxOZEJn_gJyu7nwQ{pZ+TI=NWezCwHL3eZo?VzhmndtpmZec(S9XOiFfg)c+~SeXu&O_R{C2ya7eevrRp*2zO0zalx_pwYBdBAxkfgk4 zfXs*G%5vtA-K$KwQ(w9cAF^EYT735!cNI$FSqZzYc)EkNi3@zPDi1c-w8Ma@FrjVJ zs%BC=S{6lT_N-D*tL9CP)uNcfZQH&rH35?Xu4TeCoEZ%~!L}cKZg??hnQto34?aiL zqy?SbB!>1OhkzYMcccVOSQ(=ZrBK9lep;e!dNSvS6m7`{coxu9uTY8B7^0aws7{gQ zi{_0&pql`*f<%2PUJY4b=c8WV3wCgKy<*ucs3y{o0*Ij9!bFd=?v~r;b>N8z6aKqo zG}f^{SiRU0yvm15dE=@n^JvFtU=3d87xX~4?WfBV6PKR6%NjUCfmO`%H(;J~L)`jR zXT=CO_#sH*g8!qm?+%LM>$V*-5*5KA=VT)193*E!5D7z2keq}8W`-b=B}+y!D9IrR zLu^1m$w`u=K_q7x(hQ()e&2gluj;;AukLs6KV97??0vdV_vxy$*Io-r1q-q82q$J% zUfi2WmFHB=Il=vk5N4T?$)JqpwelEjib*)mN=-g zeW`LD4av2$2arYX**7H8k^R09#BB`s#|xVVme>f~x@D-dn-6eXxr0*hJ|v}VJ@+r< zoH#$bBcgms`=>~~FuTwwit8nOK%uOIlGatGWjkeeHd|w%Z3g4`Srj1q3j>s5L?667vvv_6t)zUp(2uPw0V#+8?Am6?Xa8x1k+d<@wK*dA!2soHQbA+ z6`?v~nSyLIbcb7lxKc`=^7u>ChfxC%_A6CbqzhK@7FHQX0Y>0T^g7dQ>#8K8L};jr>+$?x9)y&Oks0NC;2P&c{_8 zh*4CVdhhN`Uf2tdolN#JZAwFpZOY-{iVx7~S9sVWo~`h&g+7+%!LDv;6FYkI(F8Q= zmxh5_ByMQ0;z9UZw-)?%za;j62o>QR__g)zr=KoPea@_3Hk(&lKwWs7@4S$4U9a9G z%(e@9kxSz7kjxko37D8PLDtJ3*G)ur+}x6fPW}P7pEm2+!X~TOvjBA*Kib^zVH9cC z-H$aZATrk`0zad7MztY+b>Azr?<{7qb}IP`S9bl2b?q&cD<{J zavqI@QQ!*{Ap451=+>+l&;}RrlQC<#`;1E(DOkEPF0>bWV#qM+Wc+m!mhQR+-oOAF z&fJQB-)51(O+h5mo-aFX7xzGlo7j{$l2fqsn9!H#P8{iX!8a1Ns^%;C~*%|i2(Nb0uMl(oZrGz`RnLwY3RK!8l#R zwgu4(vdTLI;H$h%>8ewxj!G|_0Q=P*)ic`)epb2jdqEZ8=>gBPfJ*glYa2)|I*;A{|cdn3%+-r|Af8%-rS2lpo-@B z?v5Tj_QW-8ME7vGltUNRM~DU5hOr0f>ql=ypJuEFq&sEpb)s={sNcskA?b$BZ4aQ6 z`a`ES=XChI5ZFvpk>J~3-u)+G(fZ4A0@5<0D_rlG9dnGvxrir!u(Q^`5@IBzj?)Yu z;McPgnW5}g)dRysm{OCI$IRZtke}88Q_^M2)$|+K`L5o&dCrxpiPW_R^iYF`o;?M+s$5x%y;=p4C9y`lIZ ze11mTHA>gI8(wDgoG~}ei;ibD?ZzI@<^v3yO8tj|LMjW){%PT>nCUQ7aU)w-B`>m< zZ;gm^wG5TCILcPn^}4=u7?r0+F>kC1Sz8EuT*N@@w`RD$gNV;#Si{b6vKe@2H??r} z&K2TGA}bqmgT<9asZ;KbkfQAFp@*wR!-g8emkUR+WmQQ5N6o`>2ltVcBK-Cq?ZHJ#XS3xsmoBj-4WMXd(2Bho4938-wrH_GHC96KT?Bl=8 z*#s|`L8;U5nj00?yT^{mq^ll6-F?fPq`1BptNg~k({dEcjO$)+J06RoE%JkDY>&)A zD;x1}wn2qwU3VGsh?)!Ej-48RtJ%N4L739v}?eX;mVg3bKp6r5-@3IEfqA z)PjfL^1Q?01UC%2EJtSf?D@yk+k92y2}}%1H50<#Py{;+mwJQOfKiL#ly6%p#-yX> z7;0^DP`rewFz>dDjCG7fE{%?3OB7_=(eN~b5>JRW_Zi-?cJHkx$U4?Mr*!HV?oU;x z^sO46?WM2p9>QG=Y)!4-7&c|G>1f9e{NiOK^ge;T#f_=yN{vj{uC*UpdOXG5TJgLuIL+OMU3#x-0FrCQn!% z>4%2;PL*8j4K#uz+9GCbljB7FIr(f>aacLm{4?T}>~~g$ZpV^%gd0=x@@>p>sc$O! zxBH|B(ukn-E7Rg$rivNVZ0;>nu4kCQ(ldciT^X;L)Jj` z;lm+!IuHY-uU-=XsE5)k>Lgy%(@iu2Ed$}K;$)L=p2jpDpAsgC86dLHe-Q3G;_-R8 zAxSXwF9TlSETh=|-OV5EAQ8XVE3TP=%+cv!3lxDF*i79?x*xqjiaT@F@>ulkG}?ZK z6>^`u?JuC2W<)`k_+%*|RmrCcyyC+Tam3SMCc6Oew)WWz_Pr(tf-^Zh!>Q{OjN;on zpOW11*$%sqMWb%k)8~b+nuz9h*DggnEsyE5FYLhC0U?<gLh4g|oE| zlYaMJT3`wPzW#UgD$yJPqbmYFe2Kr54beRkDshB20ts)|m9BcklIpyTV0E_bOM-B9)IjhE(mW2)H!SuUa50-f20@!dzBqBv%% z!wh8zT0h~B*4gcojp|8S%ko+#ZtVBdMhcQ#iGU)z3E}ZujC3YCF_he`;MLn51W+~O zEbmJrm4f9AGosB5xgwGkTzu3>?Ot!87t>8SXMfPu1LH+p{#oz=?ddh8Oc9EYfLy+` z}1t(Uf7wmQ>1Q}sI9)nDAFG3oC-x$cLB;)GrtSiXlj|d_En*HX7%)TE7&mXs>>DmGHr2Gn-jQC0zb>{0^ zVh_0Lw)BKJTOS3fOf|QMQk73~m(%vsP7&v)&ylnb?smzFdf4e}*zxG1O;_rlbVC!< zv6Uq|)sxa>!zBi(;@y2F+=X65sp3EW*wcp1aXJ|!ut3bL~h%}rlj3j}FN1*oT9MOd_*P|I*9P+5tnMV0ZeBerif z^HdTor+ds6y^K6*ypET6c*+5pT1GrWXrR^Y$Op=u9i3QG^Yyzl2y~`X&(t16uW;s^ zE$8!{QO`?HMvmP9g1#)ceo^xi+;YVqT^e4~A2Hms*+?&ur}hX765it53Vq_8fS7wX zc{a<$kd{7WZJd|eYhZX_rx%@i8%A>8^SKn27{Jm_6?^hq%(gw=-FVG}cRhYWx zH5(alszDgI#QkkG9VRn{!s0;3u5WX9D`~$ZPGvIZvu1$21(01OSX_o#NY!9yI`P;= zQ@E9jUz$u1g`Gwd94Yqnq~?BIiN)K`fMN`lS4WyNZ(XnotZCds2T{;`?x%8O!tbjR z_mSl76o&arDo%5;m4fVksTYHHJ5K?Kh`W{So{kVlS{nXs z0!I_BSj@6=OU>5jz#4HFqeDR5(%-Hz3~bg=pNRFoRh*y+3umv^+_?mc!nz*qx~cB4 ztfk8TW+Y%ULVtCS3E?wJLXnMnIVS=??MO}ycC;KQWD9pAgY-xgO1%WtqF(SwA>u*4 zB>wBhjU;`b08rlgKr-FJhYbDvIA+H{rA zWU_~0#^rWy9X+9yd{&LDZz53`$r-t%Vk10YMf7x2>{t=<^Zq!cG2qln{hkaIkuzl^ z%$=0|+{0BX3cIwbh(AM99rbTJYs<2?Y&0JzAoNOoGmiMjZqk8nxMGK>i`{JAGyBaV z{6Y|VcI=~ZYrh!74m@}d6~<2X^}5pmd6XfKKo}Mm6~Qot>MJ1{xZ!niqQGw$eL}6- zzt#4p_ru|U``#=;K*6D*>;&#H1GO%Quk{Pjhr>oJ&K4&$N^`Ym%%VhmUf#L)pj^0x zMuBvk*Cf^8uP0ae68R|p5U zhH2pu@zkB~nGIg%z9}Q~)h0HXL;0mIQqAs6W>z#pNx}vJ<;y>1xno+pFw(-H<97zq zo4YJD=Ei8sl^kB@_HOAwB?(-Jg2Sw|2;VgYRV*%;CYXH+%D~9s$K4RV$Hx3Q1_)mM zc$Y-8z_v4b&EC-jk*$B9%vvGzE~njRz`sy`_+>tRh9frtxdo{Xr>aE~M-LW+GCO|J zblPJXhf9Ua-M**=Hkr#3gbo z7kAwESaZ8g=g)$6wnN>`3Sw4H{R2r>Aay&0a14JzCodMYct!*=E*}N z&yig1A@x0+SeuIHl$QlTv+J<6OPLi>`xKO-Q6VVo@cEtT%p4Uj9(i`v8AG!$OoOi* zVIj}qL6-}}IyMt`MzGjO!;Sa!vB_MSmVjQnfv|91sHRS};w9oR?5j?2O`OH42Pk3T z&yZc@iGf7e$mMUQ=%aY)(SB85oH#CkKq1B6^5P+`oYeSv><_x5Em`4{Mgr?G(j1zH z)m|F}aS1C#-Gd*bcnu{#BKKvzIm(1j`)CW)h19ud1fQW3>GWAU%Mb5ndDqgnhIo`$ z38qqQywN)yvr;bcVz(ZueSm*+i@bKs83(}uPAt|&4LYJnUvG(nCL0zMJX|^gTJ^9pa(OmE;vrVW!co@x=V-GzhDQ0-M z;Wzi?mTU22-k$oKNq~Lzs;<&cVW>keiR2UO?F*|WlxLjzii~*;M~HBp5%RH>4aRb)`v7LmgVVpE@l-T)IJ!$DJW2>SrDAmGfCVp3w&MDuYr%=7hV^=`R| zv|GIeNPF7)wl0PL1M}zHB=|oi|NI}7as})$HrjKN_k3jTCMRQ=+z|oPJTt`CsMI1& zN^~Q;Be`0P$is=j;f*!gZ3D3VDGp_QJRrI+RhS+xR*v5s!(R-vy0EUIsDDijfH$U3 z>)rur!oyoP_2lr3nBsl){8+V=h0C9ukSq=G(C!+g-xS?MMAE??1;2H^aDg;rmE9a^(DJnjSFn2{-Y{W8n~kacV$Temfgd?~{j}AFfI; ze)GBdgiNhZBc@p$W9&Du-4 zmwGxju)x2`*wfO4Gl`l!g^WX(j^4uWzBG86MEZqh(|)DTCy(@77YF%izk(vtv0_d23Ota(ASx02E$`T&1De#4K(&*zMB=E z+m+}8f8qIp9?g@rtWni_PStW_kq@mr+D1|#29dZEiQJQC={uJzhvqNlufU?)bGbvv zU1)$K@EfGH8@?MdYu@h|?)Lgn;2Fp>Et6}c2Yh`OuGqPmM3_z=m@jSYVt*_a;zgu5 zwwu!j)e(!uL$!93HFo!i(ZA}PKrE%!m6Bv=_OLB5P{JA@puY^-VnC~mM|g6=!5>4v zK;x~uiP0r%q+uS{LGrp6LQZ7E7H%DHbbzt` z7ofwSnkSBnX(h={D^<*(Rz85Iw-JHLMh+LHd5{?t)|kzne;-3Y0`kEXpOMgDy<^^|&EQgGdS}S(ZjIh}})G5ax^h0`iA<@-b2do1E zxxyU03zW%~m?v$Z#T$TppKmp@N+}x|0)(c{R}j{T!YCSiViW$2P*>8-)A&@HH zd=E1+w|bf7Wu^Q?B8_clX@h(#3%>eNM=a0F9YduHM{jJxAnqNSISraZ>N(+Fj9&Y- zE4jw|PWwAWPry2cf^aa3X!A0>onQn7+Ao}Voxd!(rR2;@c_Y^kU5h<2n2Q|_(e_zKHR9VWUW;9YL@Fj5{h{Zn%w9y=E z!0Z#noYZ;kd|o7XvB67~J*3q9-I0i}n)wDC_W#mueWaxI*oJ@1@92qA>d9J5(IAP` z3Mzb~@Z#sV4B*>SXaY|%TXFmS*r#`BtV-Yz!q-CKVh&WA9I+A1nYt_q>hht&wMnp> zSV9k0NQ(_x5Ew1MF7t74x<$AYLk)rO8Y~U#wE$~=+QZ(Sy{i-mRRK>SQ`T3DvwVY9 z813ck;15qjYi_J>f{t4;R0wdjz&v4J-P3H#<~Se?c4gQ$?G#!qZn8u@=s$L&gOG(D zkOom{NLxsI6@*D}K&B_))3(^pCZ)8&Z8(^@QXi@R6GqG+r-T0XW^`qe;6l@bpscu~bh0-&Jr?XY@4?r9FQ)Rs`L_QWKGD6{-s3tfDn(6!kPc_Dhs& z9Nb=AQUjL65S}PuNKE$d$ zJC6LkM(HvDo=g=k<%4MNa$s#Ky$NT3!e;~TtPtnd(QLH!!UzmkU9lp^7AH z!!3TxUj*$K>bOh$mpW?&w`Ik;AfM;u0R!FMuc6t5y+l|NW!c#TSB2hZ(AEW#@gbi7 zWPEsCwEtTXvhab3?w>fQ>R$+oXPkED-wAI18*NFnLhse@3om#5<4Z@w@X;qVyVw5< DY$>gM literal 0 HcmV?d00001 From bf730b954abf3dfafde2487af9b9cd678705738d Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 7 Nov 2022 12:45:01 +0800 Subject: [PATCH 0179/3337] add version --- cmd/answer/main.go | 4 +- docs/docs.go | 4 +- docs/swagger.json | 4 +- docs/swagger.yaml | 4 +- internal/base/constant/constant.go | 2 + internal/schema/dashboard_schema.go | 37 +++++++++++++------ .../service/dashboard/dashboard_service.go | 32 ++++++++++++++++ 7 files changed, 68 insertions(+), 19 deletions(-) diff --git a/cmd/answer/main.go b/cmd/answer/main.go index 082142440..e93084427 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/answerdev/answer/internal/base/conf" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/cli" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman" @@ -40,7 +41,6 @@ func main() { func runApp() { log.SetLogger(zap.NewLogger( log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) - c, err := readConfig() if err != nil { panic(err) @@ -50,6 +50,8 @@ func runApp() { if err != nil { panic(err) } + constant.Version = Version + defer cleanup() if err := app.Run(); err != nil { panic(err) diff --git a/docs/docs.go b/docs/docs.go index 5a2096868..5c31c2fb3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2717,14 +2717,14 @@ const docTemplate = `{ }, "/answer/api/v1/siteinfo": { "get": { - "description": "Get siteinfo", + "description": "get site info", "produces": [ "application/json" ], "tags": [ "site" ], - "summary": "Get siteinfo", + "summary": "get site info", "responses": { "200": { "description": "OK", diff --git a/docs/swagger.json b/docs/swagger.json index 362039a03..f75456160 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2705,14 +2705,14 @@ }, "/answer/api/v1/siteinfo": { "get": { - "description": "Get siteinfo", + "description": "get site info", "produces": [ "application/json" ], "tags": [ "site" ], - "summary": "Get siteinfo", + "summary": "get site info", "responses": { "200": { "description": "OK", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 76c5217dd..8fab90531 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -3043,7 +3043,7 @@ paths: - Search /answer/api/v1/siteinfo: get: - description: Get siteinfo + description: get site info produces: - application/json responses: @@ -3056,7 +3056,7 @@ paths: data: $ref: '#/definitions/schema.SiteGeneralResp' type: object - summary: Get siteinfo + summary: get site info tags: - site /answer/api/v1/tag: diff --git a/internal/base/constant/constant.go b/internal/base/constant/constant.go index 5a1827161..e41d50ea6 100644 --- a/internal/base/constant/constant.go +++ b/internal/base/constant/constant.go @@ -27,6 +27,8 @@ const ( // object TagID AnswerList // key equal database's table name var ( + Version string = "" + ObjectTypeStrMapping = map[string]int{ QuestionObjectType: 1, AnswerObjectType: 2, diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index 74eb8ca95..bba29da1b 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -1,16 +1,29 @@ package schema type DashboardInfo struct { - QuestionCount int64 `json:"question_count"` - AnswerCount int64 `json:"answer_count"` - CommentCount int64 `json:"comment_count"` - VoteCount int64 `json:"vote_count"` - UserCount int64 `json:"user_count"` - ReportCount int64 `json:"report_count"` - UploadingFiles bool `json:"uploading_files"` - SMTP bool `json:"smtp"` - HTTPS bool `json:"https"` - TimeZone string `json:"time_zone"` - OccupyingStorageSpace string `json:"occupying_storage_space"` - AppStartTime string `json:"app_start_time"` + QuestionCount int64 `json:"question_count"` + AnswerCount int64 `json:"answer_count"` + CommentCount int64 `json:"comment_count"` + VoteCount int64 `json:"vote_count"` + UserCount int64 `json:"user_count"` + ReportCount int64 `json:"report_count"` + UploadingFiles bool `json:"uploading_files"` + SMTP bool `json:"smtp"` + HTTPS bool `json:"https"` + TimeZone string `json:"time_zone"` + OccupyingStorageSpace string `json:"occupying_storage_space"` + AppStartTime string `json:"app_start_time"` + VersionInfo DashboardInfoVersion `json:"version_info"` +} + +type DashboardInfoVersion struct { + Version string `json:"version"` + RemoteVersion string `json:"remote_version"` +} + +type RemoteVersion struct { + Release struct { + Version string `json:"version"` + URL string `json:"url"` + } `json:"release"` } diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index fcea03dd1..e642a85c5 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -2,7 +2,11 @@ package dashboard import ( "context" + "encoding/json" + "io/ioutil" + "net/http" + "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity_common" answercommon "github.com/answerdev/answer/internal/service/answer_common" @@ -12,6 +16,7 @@ import ( "github.com/answerdev/answer/internal/service/report_common" "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/segmentfault/pacman/log" ) type DashboardService struct { @@ -112,5 +117,32 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.OccupyingStorageSpace = "1MB" dashboardInfo.AppStartTime = "102" dashboardInfo.TimeZone = siteInfoInterface.TimeZone + dashboardInfo.VersionInfo.Version = constant.Version + dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx) return dashboardInfo, nil } + +func (ds *DashboardService) RemoteVersion(ctx context.Context) string { + url := "https://answer.dev/getlatest" + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("User-Agent", "Answer/"+constant.Version) + resp, err := (&http.Client{}).Do(req) + if err != nil { + log.Error("http.Client error", err) + return "" + } + defer resp.Body.Close() + + respByte, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Error("http.Client error", err) + return "" + } + remoteVersion := &schema.RemoteVersion{} + err = json.Unmarshal(respByte, remoteVersion) + if err != nil { + log.Error("json.Unmarshal error", err) + return "" + } + return remoteVersion.Release.Version +} From 8f567e0abd22b0f86245595b81d93ab2a11a818f Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 7 Nov 2022 14:27:54 +0800 Subject: [PATCH 0180/3337] feat: get text excerpt --- go.mod | 1 + go.sum | 2 ++ pkg/htmltext/htmltext.go | 44 ++++++++++++++++++++++++++++++ pkg/htmltext/htmltext_test.go | 51 +++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 pkg/htmltext/htmltext.go create mode 100644 pkg/htmltext/htmltext_test.go diff --git a/go.mod b/go.mod index d6d85117f..a3a2470ca 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/goccy/go-json v0.9.11 github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 + github.com/grokify/html-strip-tags-go v0.0.1 github.com/jinzhu/copier v0.3.5 github.com/jinzhu/now v1.1.5 github.com/lib/pq v1.10.7 diff --git a/go.sum b/go.sum index be85073b8..2e07c8ffe 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go new file mode 100644 index 000000000..0694d9906 --- /dev/null +++ b/pkg/htmltext/htmltext.go @@ -0,0 +1,44 @@ +package htmltext + +import ( + "github.com/grokify/html-strip-tags-go" + "regexp" + "strings" +) + +// ClearText clear HTML, get the clear text +func ClearText(html string) (text string) { + var ( + re *regexp.Regexp + codeReg = `(?ism)<(pre)>.*<\/pre>` + codeRepl = "{code...}" + linkReg = `(?ism).*?<\/a>` + linkRepl = "[link]" + spaceReg = ` +` + spaceRepl = " " + ) + re = regexp.MustCompile(codeReg) + html = re.ReplaceAllString(html, codeRepl) + + re = regexp.MustCompile(linkReg) + html = re.ReplaceAllString(html, linkRepl) + + text = strings.NewReplacer( + "\n", " ", + "\r", " ", + "\t", " ", + ).Replace(strip.StripTags(html)) + + // replace multiple spaces to one space + re = regexp.MustCompile(spaceReg) + text = strings.TrimSpace(re.ReplaceAllString(text, spaceRepl)) + return +} + +// FetchExcerpt return the excerpt from the HTML string +func FetchExcerpt(html, trimMarker string, limit int) (text string) { + text = ClearText(html) + runeText := []rune(text) + text = string(runeText[0:limit]) + return +} diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go new file mode 100644 index 000000000..353ca20b9 --- /dev/null +++ b/pkg/htmltext/htmltext_test.go @@ -0,0 +1,51 @@ +package htmltext + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestClearText(t *testing.T) { + var ( + expected, + clearedText string + ) + + // test code clear text + expected = "hello{code...}" + clearedText = ClearText("

hello

var a = \"good\"

") + assert.Equal(t, expected, clearedText) + + // test link clear text + expected = "hello[link]" + clearedText = ClearText("

helloexample.com

") + assert.Equal(t, expected, clearedText) + clearedText = ClearText("

helloexample.com

") + assert.Equal(t, expected, clearedText) + + expected = "hello world" + clearedText = ClearText("
hello
\n
world
") + assert.Equal(t, expected, clearedText) +} + +func TestFetchExcerpt(t *testing.T) { + var ( + expected, + text string + ) + + // test english string + expected = "hello" + text = FetchExcerpt("

hello world

", "...", 5) + assert.Equal(t, expected, text) + + // test mixed string + expected = "hello你好" + text = FetchExcerpt("

hello你好world

", "...", 7) + assert.Equal(t, expected, text) + + // test mixed string with emoticon + expected = "hello你好😂" + text = FetchExcerpt("

hello你好😂world

", "...", 8) + assert.Equal(t, expected, text) +} From ad8cb70d49773210e33968cb7e227fac2df04f6f Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 7 Nov 2022 14:29:49 +0800 Subject: [PATCH 0181/3337] feat: get text excerpt --- pkg/htmltext/htmltext.go | 2 +- pkg/htmltext/htmltext_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go index 0694d9906..fa93ff2d0 100644 --- a/pkg/htmltext/htmltext.go +++ b/pkg/htmltext/htmltext.go @@ -39,6 +39,6 @@ func ClearText(html string) (text string) { func FetchExcerpt(html, trimMarker string, limit int) (text string) { text = ClearText(html) runeText := []rune(text) - text = string(runeText[0:limit]) + text = string(runeText[0:limit]) + trimMarker return } diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go index 353ca20b9..71aafbb6e 100644 --- a/pkg/htmltext/htmltext_test.go +++ b/pkg/htmltext/htmltext_test.go @@ -35,17 +35,17 @@ func TestFetchExcerpt(t *testing.T) { ) // test english string - expected = "hello" + expected = "hello..." text = FetchExcerpt("

hello world

", "...", 5) assert.Equal(t, expected, text) // test mixed string - expected = "hello你好" + expected = "hello你好..." text = FetchExcerpt("

hello你好world

", "...", 7) assert.Equal(t, expected, text) // test mixed string with emoticon - expected = "hello你好😂" + expected = "hello你好😂..." text = FetchExcerpt("

hello你好😂world

", "...", 8) assert.Equal(t, expected, text) } From c7ee44d33c718bc3ed83c7b7726eb35429857e4c Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 7 Nov 2022 14:32:17 +0800 Subject: [PATCH 0182/3337] feat: get text excerpt --- pkg/htmltext/htmltext.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go index fa93ff2d0..31ae1a223 100644 --- a/pkg/htmltext/htmltext.go +++ b/pkg/htmltext/htmltext.go @@ -8,6 +8,11 @@ import ( // ClearText clear HTML, get the clear text func ClearText(html string) (text string) { + if len(html) == 0 { + text = html + return + } + var ( re *regexp.Regexp codeReg = `(?ism)<(pre)>.*<\/pre>` @@ -37,8 +42,19 @@ func ClearText(html string) (text string) { // FetchExcerpt return the excerpt from the HTML string func FetchExcerpt(html, trimMarker string, limit int) (text string) { + if len(html) == 0 { + text = html + return + } + text = ClearText(html) runeText := []rune(text) - text = string(runeText[0:limit]) + trimMarker + if len(runeText) <= limit { + text = string(runeText) + } else { + text = string(runeText[0:limit]) + } + + text += trimMarker return } From dfaa926e252bc85d79de8485da8002a2f1a6a71e Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 7 Nov 2022 15:49:43 +0800 Subject: [PATCH 0183/3337] refactor(admin): Display version updates --- ui/src/common/interface.ts | 5 +++- .../components/HealthStatus/index.tsx | 23 +++++++++++++++---- ui/src/pages/Admin/Dashboard/index.tsx | 1 + 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 1e61d8741..9ea391c78 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -342,7 +342,10 @@ export interface AdminDashboard { time_zone: string; occupying_storage_space: string; app_start_time: number; - app_version: string; https: boolean; + version_info: { + remote_version: string; + version: string; + }; }; } diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx index 1b1c7ced3..f36e02a52 100644 --- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -11,7 +11,8 @@ interface IProps { const HealthStatus: FC = ({ data }) => { const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); - + const { version, remote_version } = data.version_info || {}; + const isLatest = version === remote_version; return ( @@ -19,10 +20,22 @@ const HealthStatus: FC = ({ data }) => {
{t('version')} - 90 - - {t('update_to')} {data.app_version} - + {version} + {isLatest && ( + + {t('latest')} + + )} + {!isLatest && remote_version && ( + + {t('update_to')} {remote_version} + + )} + {!isLatest && !remote_version && ( + + {t('check_failed')} + + )} {t('https')} diff --git a/ui/src/pages/Admin/Dashboard/index.tsx b/ui/src/pages/Admin/Dashboard/index.tsx index 2037016e9..c20f250cb 100644 --- a/ui/src/pages/Admin/Dashboard/index.tsx +++ b/ui/src/pages/Admin/Dashboard/index.tsx @@ -18,6 +18,7 @@ const Dashboard: FC = () => { if (!data) { return null; } + return ( <>

{t('title')}

From 8b312cde7e62deb8261a3c420afd31ebe1bf4e37 Mon Sep 17 00:00:00 2001 From: kumfo Date: Mon, 7 Nov 2022 16:05:01 +0800 Subject: [PATCH 0184/3337] feat: unified processing excerpt --- internal/repo/search_common/search_repo.go | 16 ++++------------ .../service/report_backyard/report_backyard.go | 18 ++++-------------- internal/service/tag/tag_service.go | 17 ++++------------- 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index b2db83c31..22a4cbb5a 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -3,6 +3,7 @@ package search_common import ( "context" "fmt" + "github.com/answerdev/answer/pkg/htmltext" "strings" "time" @@ -25,7 +26,7 @@ var ( "`question`.`id`", "`question`.`id` as `question_id`", "`title`", - "`original_text`", + "`parsed_text`", "`question`.`created_at`", "`user_id`", "`vote_count`", @@ -38,7 +39,7 @@ var ( "`answer`.`id` as `id`", "`question_id`", "`question`.`title` as `title`", - "`answer`.`original_text` as `original_text`", + "`answer`.`parsed_text` as `parsed_text`", "`answer`.`created_at`", "`answer`.`user_id` as `user_id`", "`answer`.`vote_count` as `vote_count`", @@ -412,7 +413,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte) object = schema.SearchObject{ ID: string(r["id"]), Title: string(r["title"]), - Excerpt: cutOutParsedText(string(r["original_text"])), + Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240), CreatedAtParsed: tp.Unix(), UserInfo: userInfo, Tags: tags, @@ -443,15 +444,6 @@ func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.Us } } -func cutOutParsedText(parsedText string) string { - parsedText = strings.TrimSpace(parsedText) - idx := strings.Index(parsedText, "\n") - if idx >= 0 { - parsedText = parsedText[0:idx] - } - return parsedText -} - func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) { relevanceRes := []string{} args = []interface{}{} diff --git a/internal/service/report_backyard/report_backyard.go b/internal/service/report_backyard/report_backyard.go index 947e71972..69b87c852 100644 --- a/internal/service/report_backyard/report_backyard.go +++ b/internal/service/report_backyard/report_backyard.go @@ -2,9 +2,8 @@ package report_backyard import ( "context" - "strings" - "github.com/answerdev/answer/internal/service/config" + "github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" @@ -180,20 +179,20 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem case "question": r.QuestionID = questionId r.Title = question.Title - r.Excerpt = rs.cutOutTagParsedText(question.OriginalText) + r.Excerpt = htmltext.FetchExcerpt(question.ParsedText, "...", 240) case "answer": r.QuestionID = questionId r.AnswerID = answerId r.Title = question.Title - r.Excerpt = rs.cutOutTagParsedText(answer.OriginalText) + r.Excerpt = htmltext.FetchExcerpt(answer.ParsedText, "...", 240) case "comment": r.QuestionID = questionId r.AnswerID = answerId r.CommentID = commentId r.Title = question.Title - r.Excerpt = rs.cutOutTagParsedText(cmt.OriginalText) + r.Excerpt = htmltext.FetchExcerpt(cmt.ParsedText, "...", 240) } // parse reason @@ -214,12 +213,3 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem } resp = &res } - -func (rs *ReportBackyardService) cutOutTagParsedText(parsedText string) string { - parsedText = strings.TrimSpace(parsedText) - idx := strings.Index(parsedText, "\n") - if idx >= 0 { - parsedText = parsedText[0:idx] - } - return parsedText -} diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index cf6fc02e9..6384eb822 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -3,9 +3,8 @@ package tag import ( "context" "encoding/json" - "strings" - "github.com/answerdev/answer/internal/service/revision_common" + "github.com/answerdev/answer/pkg/htmltext" "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" @@ -344,12 +343,13 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith resp := make([]*schema.GetTagPageResp, 0) for _, tag := range tags { + excerpt := htmltext.FetchExcerpt(tag.ParsedText, "...", 240) resp = append(resp, &schema.GetTagPageResp{ TagID: tag.ID, SlugName: tag.SlugName, DisplayName: tag.DisplayName, - OriginalText: cutOutTagParsedText(tag.OriginalText), - ParsedText: cutOutTagParsedText(tag.ParsedText), + OriginalText: excerpt, + ParsedText: excerpt, FollowCount: tag.FollowCount, QuestionCount: tag.QuestionCount, IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID), @@ -371,12 +371,3 @@ func (ts *TagService) checkTagIsFollow(ctx context.Context, userID, tagID string } return followed } - -func cutOutTagParsedText(parsedText string) string { - parsedText = strings.TrimSpace(parsedText) - idx := strings.Index(parsedText, "\n") - if idx >= 0 { - parsedText = parsedText[0:idx] - } - return parsedText -} From 82ac07ae675a79be9ecf7b7b34a36a942c3e23dc Mon Sep 17 00:00:00 2001 From: robin Date: Mon, 7 Nov 2022 16:27:35 +0800 Subject: [PATCH 0185/3337] refactor(admin): remove jsx --- ui/src/pages/Admin/Dashboard/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ui/src/pages/Admin/Dashboard/index.tsx b/ui/src/pages/Admin/Dashboard/index.tsx index c20f250cb..0b6834f90 100644 --- a/ui/src/pages/Admin/Dashboard/index.tsx +++ b/ui/src/pages/Admin/Dashboard/index.tsx @@ -37,12 +37,6 @@ const Dashboard: FC = () => { - {process.env.REACT_APP_VERSION && ( -

- {`${t('version')} `} - {process.env.REACT_APP_VERSION} -

- )} ); }; From aa1d434b28d2f7602c4c3dcc526db19b6a0c0b20 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 16:38:06 +0800 Subject: [PATCH 0186/3337] doc: update install document, add docker-compose and binary installation --- INSTALL_CN.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/INSTALL_CN.md b/INSTALL_CN.md index 848c9ce16..4a93964a6 100644 --- a/INSTALL_CN.md +++ b/INSTALL_CN.md @@ -22,3 +22,44 @@ docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:lat [http://127.0.0.1:9080/](http://127.0.0.1:9080/) 使用刚才创建的管理员用户名密码即可登录。 + +## 使用 docker-compose 安装 +### 步骤 1: 使用 docker-compose 命令启动项目 +```bash +mkdir answer && cd answer +wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml +docker-compose up +``` + +### 步骤 2: 访问安装路径进行项目安装 +[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) + +具体配置与 docker 使用时相同 + +### 步骤 3:安装完成后访问项目路径开始使用 +[http://127.0.0.1:9080/](http://127.0.0.1:9080/) + +## 使用 二进制 安装 +### 步骤 1: 下载二进制文件 +[https://github.com/answerdev/answer/releases](https://github.com/answerdev/answer/releases) +请下载您当下系统所需要的对应版本 + +### 步骤 2: 使用命令行安装 +> 以下命令中 -C 指定的是 answer 所需的数据目录,您可以根据实际需要进行修改 + +```bash +./answer init -C ./answer-data/ +``` + +然后访问:[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install) 进行安装,具体配置与使用 docker 安装相同 + +### 步骤 3: 使用命令行启动 +安装完成之后程序会退出,请使用命令正式启动项目 +```bash +./answer run -C ./answer-data/ +``` + +正常启动后可以访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/) 使用安装时指定的管理员用户名密码进行登录 + +## 安装常见问题 +- 使用 docker 重新安装遇到问题?默认我们给出的命令是使用 `answer-data` 命名卷,所以如果重新不需要原来的数据,请主动进行删除 `docker volume rm answer-data` From 530532ab1c84e006571ed977b8f09859bce14c50 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 16:44:48 +0800 Subject: [PATCH 0187/3337] feat: change the default smtp configuration to empty --- internal/migrations/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/migrations/init.go b/internal/migrations/init.go index 509bf516d..cebdc1c79 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -189,7 +189,7 @@ func initConfigTable(engine *xorm.Engine) error { {ID: 30, Key: "answer.vote_up", Value: `0`}, {ID: 31, Key: "answer.vote_up_cancel", Value: `0`}, {ID: 32, Key: "question.follow", Value: `0`}, - {ID: 33, Key: "email.config", Value: `{"from_name":"answer","from_email":"answer@answer.com","smtp_host":"smtp.answer.org","smtp_port":465,"smtp_password":"answer","smtp_username":"answer@answer.com","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`}, + {ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}

\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}

\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].

\n\nIf it was not you, you can safely ignore this email.

\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:

\n\n{{.ChangeEmailUrl}}

\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`}, {ID: 35, Key: "tag.follow", Value: `0`}, {ID: 36, Key: "rank.question.add", Value: `0`}, {ID: 37, Key: "rank.question.edit", Value: `0`}, From f43e2e93a125b4bde1ac5fae0c13c7d836903a4b Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Mon, 7 Nov 2022 16:45:23 +0800 Subject: [PATCH 0188/3337] add dashboard smtp verification --- .../service/dashboard/dashboard_service.go | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index e642a85c5..fda3d6466 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -7,15 +7,18 @@ import ( "net/http" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity_common" answercommon "github.com/answerdev/answer/internal/service/answer_common" "github.com/answerdev/answer/internal/service/comment_common" "github.com/answerdev/answer/internal/service/config" + "github.com/answerdev/answer/internal/service/export" questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/report_common" "github.com/answerdev/answer/internal/service/siteinfo_common" usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -112,7 +115,13 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.ReportCount = reportCount dashboardInfo.UploadingFiles = true - dashboardInfo.SMTP = true + emailconfig, err := ds.GetEmailConfig() + if err != nil { + return dashboardInfo, err + } + if emailconfig.SMTPHost != "" { + dashboardInfo.SMTP = true + } dashboardInfo.HTTPS = true dashboardInfo.OccupyingStorageSpace = "1MB" dashboardInfo.AppStartTime = "102" @@ -146,3 +155,16 @@ func (ds *DashboardService) RemoteVersion(ctx context.Context) string { } return remoteVersion.Release.Version } + +func (ds *DashboardService) GetEmailConfig() (ec *export.EmailConfig, err error) { + emailConf, err := ds.configRepo.GetString("email.config") + if err != nil { + return nil, err + } + ec = &export.EmailConfig{} + err = json.Unmarshal([]byte(emailConf), ec) + if err != nil { + return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return ec, nil +} From a29eca4a4de902961151c44708de3fb89ed3a66a Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 17:45:36 +0800 Subject: [PATCH 0189/3337] feat: add timezone validation --- internal/schema/siteinfo_schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 446b986db..70be00ba3 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -12,7 +12,7 @@ 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"` + TimeZone string `validate:"required,gt=1,lte=128,timezone" form:"time_zone" json:"time_zone"` } // SiteGeneralResp site general response From d78df49928ebc22b05f118cb9a55e75c78741680 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 7 Nov 2022 17:59:07 +0800 Subject: [PATCH 0190/3337] fix: install translation error --- ui/src/i18n/locales/en.json | 5 +++-- ui/src/pages/Install/index.tsx | 15 ++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 76c30c3a7..29f7ff912 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -824,8 +824,9 @@ "ready_title": "Your Answer is Ready!", "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", "good_luck": "Have fun, and good luck!", - "warning": "Warning", - "warning_description": "The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now.", + "warn_title": "Warning", + "warn_description": "The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first.", + "install_now": "You may try <1>installing now.", "installed": "Already installed", "installed_description": "You appear to have already installed. To reinstall please clear your old database tables first." }, diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index c0737654d..b15ed1450 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable prettier/prettier */ import { FC, useState, useEffect } from 'react'; import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; import { useTranslation, Trans } from 'react-i18next'; @@ -265,16 +266,12 @@ const Index: FC = () => { {step === 6 && (
-
{t('warning')}
+
{t('warn_title')}

- - The file config.yaml already exists. If you - need to reset any of the configuration items in this - file, please delete it first. You may try{' '} - handleInstallNow(e)}> - installing now - - . + }} /> + {' '} + + You may try handleInstallNow(e)}>installing now.

From d5a57589e6d421308aeef8df8d889a2ce9e658ef Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Mon, 7 Nov 2022 18:03:51 +0800 Subject: [PATCH 0191/3337] feat: add site_url and contact_email validation --- internal/schema/siteinfo_schema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index da78327ef..9a0a4839a 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -5,8 +5,8 @@ type SiteGeneralReq struct { Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` - SiteUrl string `validate:"required,gt=1,lte=128" form:"site_url" json:"site_url"` - ContactEmail string `validate:"required,gt=1,lte=128" form:"contact_email" json:"contact_email"` + SiteUrl string `validate:"required,gt=1,lte=512,url" form:"site_url" json:"site_url"` + ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` } // SiteInterfaceReq site interface request From ae38b2d1fc8491d43afe304ed6a131ee18dbcc5d Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 7 Nov 2022 18:10:10 +0800 Subject: [PATCH 0192/3337] fix: install page cannot fetch init config data --- ui/src/router/routes.ts | 2 +- ui/src/utils/guard.ts | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 24016d0a2..c6b8d3593 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -262,7 +262,7 @@ const routes: RouteNode[] = [ ], }, { - path: 'install', + path: '/install', page: 'pages/Install', }, { diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index c9517d7a8..2e123bf75 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -190,6 +190,15 @@ export const initAppSettingsStore = async () => { } }; +export const shouldInitAppFetchData = () => { + const { pathname } = window.location; + if (pathname === '/install') { + return false; + } + + return true; +}; + export const setupApp = async () => { /** * WARN: @@ -197,7 +206,10 @@ export const setupApp = async () => { * 2. must pre init app settings for app render */ // TODO: optimize `initAppSettingsStore` by server render - await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]); - setupAppLanguage(); - setupAppTimeZone(); + + if (shouldInitAppFetchData()) { + await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]); + setupAppLanguage(); + setupAppTimeZone(); + } }; From 79ae9636abf3d39c220de6f3208eaf0d446da1f6 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 10:03:39 +0800 Subject: [PATCH 0193/3337] fix: install page input add type password --- ui/src/pages/Install/components/FourthStep/index.tsx | 2 ++ ui/src/pages/Install/components/SecondStep/index.tsx | 1 + ui/src/pages/Install/index.tsx | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index fae0c53ce..3807ac690 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -171,6 +171,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {t('contact_email.label')} { @@ -215,6 +216,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {t('admin_password.label')} { diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx index 3e67913ac..30e48930c 100644 --- a/ui/src/pages/Install/components/SecondStep/index.tsx +++ b/ui/src/pages/Install/components/SecondStep/index.tsx @@ -148,6 +148,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {t('db_password.label')} { if (tableExist) { setStep(7); } else { - setStep(4); + setStep(2); } }; From 607573d3860d1385fae8443e92e8cef5c25decd9 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Tue, 8 Nov 2022 11:33:48 +0800 Subject: [PATCH 0194/3337] fix startTime --- cmd/answer/main.go | 3 +++ internal/schema/dashboard_schema.go | 4 ++++ internal/service/dashboard/dashboard_service.go | 5 ++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/answer/main.go b/cmd/answer/main.go index e93084427..da836a5dc 100644 --- a/cmd/answer/main.go +++ b/cmd/answer/main.go @@ -3,10 +3,12 @@ package main import ( "os" "path/filepath" + "time" "github.com/answerdev/answer/internal/base/conf" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/cli" + "github.com/answerdev/answer/internal/schema" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman" "github.com/segmentfault/pacman/contrib/conf/viper" @@ -51,6 +53,7 @@ func runApp() { panic(err) } constant.Version = Version + schema.AppStartTime = time.Now() defer cleanup() if err := app.Run(); err != nil { diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index bba29da1b..2f5b36220 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -1,5 +1,9 @@ package schema +import "time" + +var AppStartTime time.Time + type DashboardInfo struct { QuestionCount int64 `json:"question_count"` AnswerCount int64 `json:"answer_count"` diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index fda3d6466..aa8765071 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -3,8 +3,10 @@ package dashboard import ( "context" "encoding/json" + "fmt" "io/ioutil" "net/http" + "time" "github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/reason" @@ -124,7 +126,8 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI } dashboardInfo.HTTPS = true dashboardInfo.OccupyingStorageSpace = "1MB" - dashboardInfo.AppStartTime = "102" + startTime := time.Now().Unix() - schema.AppStartTime.Unix() + dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime) dashboardInfo.TimeZone = siteInfoInterface.TimeZone dashboardInfo.VersionInfo.Version = constant.Version dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx) From 41f83ab1751a223d7d9e6579684d7ad007268d66 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 15:08:21 +0800 Subject: [PATCH 0195/3337] fix: change avatar size --- ui/src/components/UserCard/index.tsx | 21 +++++++++++++++------ ui/src/i18n/locales/en.json | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/ui/src/components/UserCard/index.tsx b/ui/src/components/UserCard/index.tsx index 16be5b0d4..169354ec4 100644 --- a/ui/src/components/UserCard/index.tsx +++ b/ui/src/components/UserCard/index.tsx @@ -33,12 +33,21 @@ const Index: FC = ({ data, time, preFix, className = '' }) => { /> ) : ( - + <> + + + + )}
diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 93b6a8ace..e2aaa72f0 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -859,7 +859,7 @@ }, "dashboard": { "title": "Dashboard", - "welcome": "Welcome to Answer Admin !", + "welcome": "Welcome to Answer Admin!", "site_statistics": "Site Statistics", "questions": "Questions:", "answers": "Answers:", From 4a123b933d42587966f4338c3db2d950ec00f6f9 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 17:11:19 +0800 Subject: [PATCH 0196/3337] fix: remove redundant code --- ui/src/components/FollowingTags/index.tsx | 4 ++-- ui/src/pages/Users/AccountForgot/index.tsx | 7 +------ ui/src/pages/Users/Login/index.tsx | 2 -- ui/src/pages/Users/PasswordReset/index.tsx | 6 +----- ui/src/pages/Users/Register/index.tsx | 7 +------ ui/src/services/client/notification.ts | 4 ++-- ui/src/services/client/tag.ts | 4 ++-- ui/src/utils/common.ts | 1 + ui/src/utils/guard.ts | 16 +++++++++++++--- 9 files changed, 23 insertions(+), 28 deletions(-) diff --git a/ui/src/components/FollowingTags/index.tsx b/ui/src/components/FollowingTags/index.tsx index 06fec6ecb..9e3156b0a 100644 --- a/ui/src/components/FollowingTags/index.tsx +++ b/ui/src/components/FollowingTags/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; import { TagSelector, Tag } from '@/components'; -import { tryNormalLogged } from '@/utils/guard'; +import { tryLoggedAndActicevated } from '@/utils/guard'; import { useFollowingTags, followTags } from '@/services'; const Index: FC = () => { @@ -32,7 +32,7 @@ const Index: FC = () => { }); }; - if (!tryNormalLogged()) { + if (!tryLoggedAndActicevated().ok) { return null; } diff --git a/ui/src/pages/Users/AccountForgot/index.tsx b/ui/src/pages/Users/AccountForgot/index.tsx index e7a77ea5e..7af609cc0 100644 --- a/ui/src/pages/Users/AccountForgot/index.tsx +++ b/ui/src/pages/Users/AccountForgot/index.tsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Container, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { tryNormalLogged } from '@/utils/guard'; import { PageTitle } from '@/components'; import SendEmail from './components/sendEmail'; @@ -17,10 +16,6 @@ const Index: React.FC = () => { setEmail(mail); }; - useEffect(() => { - tryNormalLogged(); - }, []); - return ( <> diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index b8794ff4e..7a499e640 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -158,8 +158,6 @@ const Index: React.FC = () => { if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { setStep(2); - } else { - guard.tryNormalLogged(); } }, []); diff --git a/ui/src/pages/Users/PasswordReset/index.tsx b/ui/src/pages/Users/PasswordReset/index.tsx index b97bd7077..ab00de301 100644 --- a/ui/src/pages/Users/PasswordReset/index.tsx +++ b/ui/src/pages/Users/PasswordReset/index.tsx @@ -1,4 +1,4 @@ -import React, { FormEvent, useState, useEffect } from 'react'; +import React, { FormEvent, useState } from 'react'; import { Container, Col, Form, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -7,7 +7,6 @@ import { loggedUserInfoStore } from '@/stores'; import { getQueryString } from '@/utils'; import type { FormDataType } from '@/common/interface'; import { replacementPassword } from '@/services'; -import { tryNormalLogged } from '@/utils/guard'; import { PageTitle } from '@/components'; const Index: React.FC = () => { @@ -115,9 +114,6 @@ const Index: React.FC = () => { }); }; - useEffect(() => { - tryNormalLogged(); - }, []); return ( <> diff --git a/ui/src/pages/Users/Register/index.tsx b/ui/src/pages/Users/Register/index.tsx index e4d94b2db..811de77d1 100644 --- a/ui/src/pages/Users/Register/index.tsx +++ b/ui/src/pages/Users/Register/index.tsx @@ -1,9 +1,8 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Container } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { PageTitle, Unactivate } from '@/components'; -import { tryNormalLogged } from '@/utils/guard'; import SignUpForm from './components/SignUpForm'; @@ -15,10 +14,6 @@ const Index: React.FC = () => { setShowForm((bol) => !bol); }; - useEffect(() => { - tryNormalLogged(); - }, []); - return (

{t('page_title')}

diff --git a/ui/src/services/client/notification.ts b/ui/src/services/client/notification.ts index a849db0ac..6d34022bc 100644 --- a/ui/src/services/client/notification.ts +++ b/ui/src/services/client/notification.ts @@ -3,7 +3,7 @@ import qs from 'qs'; import request from '@/utils/request'; import type * as Type from '@/common/interface'; -import { tryNormalLogged } from '@/utils/guard'; +import { tryLoggedAndActicevated } from '@/utils/guard'; export const useQueryNotifications = (params) => { const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, { @@ -33,7 +33,7 @@ export const useQueryNotificationStatus = () => { const apiUrl = '/answer/api/v1/notification/status'; return useSWR<{ inbox: number; achievement: number }>( - tryNormalLogged() ? apiUrl : null, + tryLoggedAndActicevated().ok ? apiUrl : null, request.instance.get, { refreshInterval: 3000, diff --git a/ui/src/services/client/tag.ts b/ui/src/services/client/tag.ts index 42b9f1ac1..b1ec3a9a7 100644 --- a/ui/src/services/client/tag.ts +++ b/ui/src/services/client/tag.ts @@ -2,7 +2,7 @@ import useSWR from 'swr'; import request from '@/utils/request'; import type * as Type from '@/common/interface'; -import { tryNormalLogged } from '@/utils/guard'; +import { tryLoggedAndActicevated } from '@/utils/guard'; export const deleteTag = (id) => { return request.delete('/answer/api/v1/tag', { @@ -24,7 +24,7 @@ export const saveSynonymsTags = (params) => { export const useFollowingTags = () => { let apiUrl = ''; - if (tryNormalLogged()) { + if (tryLoggedAndActicevated().ok) { apiUrl = '/answer/api/v1/tags/following'; } const { data, error, mutate } = useSWR(apiUrl, request.instance.get); diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index dcc2d0840..31bb0a40d 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -85,6 +85,7 @@ function formatUptime(value) { return `< 1 ${t('dates.hour')}`; } + export { getQueryString, thousandthDivision, diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index 2e123bf75..7f926fba8 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -154,9 +154,9 @@ export const admin = () => { /** * try user was logged and all state ok - * @param autoLogin + * @param canNavigate // if true, will navigate to login page if not logged */ -export const tryNormalLogged = (autoLogin: boolean = false) => { +export const tryNormalLogged = (canNavigate: boolean = false) => { const us = deriveLoginState(); if (us.isNormal) { @@ -164,7 +164,7 @@ export const tryNormalLogged = (autoLogin: boolean = false) => { } // must assert logged state first and return if (!us.isLogged) { - if (autoLogin) { + if (canNavigate) { floppyNavigation.navigateToLogin(); } return false; @@ -182,6 +182,16 @@ export const tryNormalLogged = (autoLogin: boolean = false) => { return false; }; +export const tryLoggedAndActicevated = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + console.log('tryLogged', us); + if (!us.isLogged || !us.isActivated) { + gr.ok = false; + } + return gr; +}; + export const initAppSettingsStore = async () => { const appSettings = await getAppSettings(); if (appSettings) { From 7abbff7278924cd34e179ad71248dbf0bbf9f6b2 Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 17:16:50 +0800 Subject: [PATCH 0197/3337] fix: change email page need login --- ui/src/router/routes.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index c6b8d3593..c745ba1d9 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -154,7 +154,9 @@ const routes: RouteNode[] = [ { path: 'users/change-email', page: 'pages/Users/ChangeEmail', - // TODO: guard this (change email when user not activated) ? + guard: async () => { + return guard.notLogged(); + }, }, { path: 'users/password-reset', From 64ef0140305284855a8998b8c6c639dfa71980c0 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 8 Nov 2022 17:42:48 +0800 Subject: [PATCH 0198/3337] feat(i18n): language resource migrate done! --- ui/.eslintrc.js | 4 +- ui/config-overrides.js | 15 + ui/package.json | 5 +- ui/pnpm-lock.yaml | 17 + ui/scripts/i18n-locale-tool.js | 59 + ui/src/common/constants.ts | 1 + ui/src/i18n/DO_NOT_EDIT_dir_locales | 0 ui/src/i18n/init.ts | 9 +- ui/src/i18n/locales/en.json | 1094 ---------------- ui/src/i18n/locales/en_US.yaml | 1119 +++++++++++++++++ ui/src/i18n/locales/i18n.yaml | 6 + ui/src/i18n/locales/it_IT.yaml | 170 +++ ui/src/i18n/locales/zh_CN.json | 914 -------------- ui/src/i18n/locales/zh_CN.yaml | 919 ++++++++++++++ ui/src/pages/Admin/Interface/index.tsx | 5 +- .../pages/Users/Settings/Interface/index.tsx | 4 +- ui/src/react-app-env.d.ts | 1 + ui/src/services/client/settings.ts | 4 +- ui/src/utils/common.ts | 1 + ui/src/utils/localize.ts | 75 +- ui/tsconfig.json | 2 +- 21 files changed, 2392 insertions(+), 2032 deletions(-) create mode 100644 ui/scripts/i18n-locale-tool.js create mode 100644 ui/src/i18n/DO_NOT_EDIT_dir_locales delete mode 100644 ui/src/i18n/locales/en.json create mode 100644 ui/src/i18n/locales/en_US.yaml create mode 100644 ui/src/i18n/locales/i18n.yaml create mode 100644 ui/src/i18n/locales/it_IT.yaml delete mode 100644 ui/src/i18n/locales/zh_CN.json create mode 100644 ui/src/i18n/locales/zh_CN.yaml diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 98edc4083..6ecb4b49e 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + root: true, env: { browser: true, es2021: true, @@ -19,7 +20,8 @@ module.exports = { }, ecmaVersion: 'latest', sourceType: 'module', - project: './tsconfig.json', + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], }, plugins: ['react', '@typescript-eslint'], rules: { diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 35510f123..e8d2dceb8 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -1,10 +1,23 @@ const path = require('path'); +const i18nLocaleTool = require('./scripts/i18n-locale-tool'); module.exports = { webpack: function (config, env) { if (env === 'production') { config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH; + i18nLocaleTool.resolvePresetLocales(); } + + for (let _rule of config.module.rules) { + if (_rule.oneOf) { + _rule.oneOf.unshift({ + test: /\.ya?ml$/, + use: 'yaml-loader' + }); + break; + } + } + config.resolve.alias = { ...config.resolve.alias, '@': path.resolve(__dirname, 'src'), @@ -14,6 +27,8 @@ module.exports = { }, devServer: function (configFunction) { + i18nLocaleTool.autoSync(); + return function (proxy, allowedHost) { const config = configFunction(proxy, allowedHost); config.proxy = { diff --git a/ui/package.json b/ui/package.json index 069456ef6..fd2a34709 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,7 +10,6 @@ "build:prod": "env-cmd -f .env.production react-app-rewired build", "build": "env-cmd -f .env react-app-rewired build", "test": "react-app-rewired test", - "eject": "react-scripts eject", "lint": "eslint . --cache --fix --ext .ts,.tsx", "prepare": "cd .. && husky install", "cz": "cz", @@ -74,6 +73,7 @@ "@types/react-helmet": "^6.1.5", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.33.0", + "chokidar": "^3.5.3", "commitizen": "^4.2.5", "conventional-changelog-cli": "^2.2.2", "customize-cra": "^1.0.0", @@ -101,7 +101,8 @@ "sass": "^1.54.4", "tsconfig-paths-webpack-plugin": "^4.0.0", "typescript": "*", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "yaml-loader": "^0.8.0" }, "packageManager": "pnpm@7.9.5", "engines": { diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 505ff7383..a4408b969 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -26,6 +26,7 @@ specifiers: axios: ^0.27.2 bootstrap: ^5.2.0 bootstrap-icons: ^1.9.1 + chokidar: ^3.5.3 classnames: ^2.3.1 codemirror: 5.65.0 commitizen: ^4.2.5 @@ -77,6 +78,7 @@ specifiers: tsconfig-paths-webpack-plugin: ^4.0.0 typescript: '*' web-vitals: ^2.1.4 + yaml-loader: ^0.8.0 zustand: ^4.1.1 dependencies: @@ -131,6 +133,7 @@ devDependencies: '@types/react-helmet': 6.1.5 '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha + chokidar: 3.5.3 commitizen: 4.2.5 conventional-changelog-cli: 2.2.2 customize-cra: 1.0.0 @@ -159,6 +162,7 @@ devDependencies: tsconfig-paths-webpack-plugin: 4.0.0 typescript: 4.8.3 web-vitals: 2.1.4 + yaml-loader: 0.8.0 packages: @@ -7040,6 +7044,10 @@ packages: filelist: 1.0.4 minimatch: 3.1.2 + /javascript-stringify/2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + dev: true + /jest-changed-files/27.5.1: resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11682,6 +11690,15 @@ packages: /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yaml-loader/0.8.0: + resolution: {integrity: sha512-LjeKnTzVBKWiQBeE2L9ssl6WprqaUIxCSNs5tle8PaDydgu3wVFXTbMfsvF2MSErpy9TDVa092n4q6adYwJaWg==} + engines: {node: '>= 12.13'} + dependencies: + javascript-stringify: 2.1.0 + loader-utils: 2.0.2 + yaml: 2.1.1 + dev: true + /yaml/1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} diff --git a/ui/scripts/i18n-locale-tool.js b/ui/scripts/i18n-locale-tool.js new file mode 100644 index 000000000..456b117b9 --- /dev/null +++ b/ui/scripts/i18n-locale-tool.js @@ -0,0 +1,59 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const path = require('node:path'); +const fs = require('node:fs'); + +const chokidar = require('chokidar'); + +const SRC_PATH = path.resolve(__dirname, '../../i18n'); +const DEST_PATH = path.resolve(__dirname, '../src/i18n/locales'); +const PRESET_LANG = ['en_US', 'zh_CN']; + +const cleanLocales = () => { + fs.readdirSync(DEST_PATH).forEach((fp) => { + fs.rmSync(path.resolve(DEST_PATH, fp), { force: true, recursive: true }); + }); +}; + +const copyLocaleFile = (filePath) => { + const targetFilePath = path.resolve(DEST_PATH, path.basename(filePath)); + fs.copyFile( + filePath, + targetFilePath, + fs.constants.COPYFILE_FICLONE, + (err) => { + if (err) { + throw err; + } + }, + ); +}; + +const watchAndSync = () => { + chokidar + .watch(path.resolve(SRC_PATH, '*.yaml'), { + awaitWriteFinish: true, + }) + .on('all', (evt, filePath) => { + copyLocaleFile(filePath); + }); +}; + +const autoSync = () => { + cleanLocales(); + watchAndSync(); +}; + +const resolvePresetLocales = () => { + PRESET_LANG.forEach((lng) => { + const sp = path.resolve(SRC_PATH, `${lng}.yaml`); + const tp = path.resolve(DEST_PATH, `${lng}.yaml`); + if (fs.existsSync(tp) === false) { + copyLocaleFile(sp); + } + }); +}; + +module.exports = { + autoSync, + resolvePresetLocales, +}; diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 028e56e84..a4db79adc 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -1,5 +1,6 @@ export const DEFAULT_LANG = 'en_US'; export const CURRENT_LANG_STORAGE_KEY = '_a_lang_'; +export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_'; export const LOGGED_USER_STORAGE_KEY = '_a_lui_'; export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_'; export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_'; diff --git a/ui/src/i18n/DO_NOT_EDIT_dir_locales b/ui/src/i18n/DO_NOT_EDIT_dir_locales new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index d60413bb4..6d75ddc0b 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -4,9 +4,8 @@ import i18next from 'i18next'; import Backend from 'i18next-http-backend'; import { DEFAULT_LANG } from '@/common/constants'; - -import en from './locales/en.json'; -import zh from './locales/zh_CN.json'; +import en_US from '@/i18n/locales/en_US.yaml'; +import zh_CN from '@/i18n/locales/zh_CN.yaml'; i18next // load translation using http @@ -16,10 +15,10 @@ i18next .init({ resources: { en_US: { - translation: en, + translation: en_US.ui, }, zh_CN: { - translation: zh, + translation: zh_CN.ui, }, }, // debug: process.env.NODE_ENV === 'development', diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json deleted file mode 100644 index 4f0de90bd..000000000 --- a/ui/src/i18n/locales/en.json +++ /dev/null @@ -1,1094 +0,0 @@ -{ - "how_to_format": { - "title": "How to Format", - "description": "
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
" - }, - "pagination": { - "prev": "Prev", - "next": "Next" - }, - "page_title": { - "question": "Question", - "questions": "Questions", - "tag": "Tag", - "tags": "Tags", - "tag_wiki": "tag wiki", - "edit_tag": "Edit Tag", - "ask_a_question": "Add Question", - "edit_question": "Edit Question", - "edit_answer": "Edit Answer", - "search": "Search", - "posts_containing": "Posts containing", - "settings": "Settings", - "notifications": "Notifications", - "login": "Log In", - "sign_up": "Sign Up", - "account_recovery": "Account Recovery", - "account_activation": "Account Activation", - "confirm_email": "Confirm Email", - "account_suspended": "Account Suspended", - "admin": "Admin", - "change_email": "Modify Email", - "install": "Answer Installation", - "upgrade": "Answer Upgrade", - "maintenance": "Webite Maintenance" - }, - "notifications": { - "title": "Notifications", - "inbox": "Inbox", - "achievement": "Achievements", - "all_read": "Mark all as read", - "show_more": "Show more" - }, - "suspended": { - "title": "Your Account has been Suspended", - "until_time": "Your account was suspended until {{ time }}.", - "forever": "This user was suspended forever.", - "end": "You don't meet a community guideline." - }, - "editor": { - "blockquote": { - "text": "Blockquote" - }, - "bold": { - "text": "Strong" - }, - "chart": { - "text": "Chart", - "flow_chart": "Flow chart", - "sequence_diagram": "Sequence diagram", - "class_diagram": "Class diagram", - "state_diagram": "State diagram", - "entity_relationship_diagram": "Entity relationship diagram", - "user_defined_diagram": "User defined diagram", - "gantt_chart": "Gantt chart", - "pie_chart": "Pie chart" - }, - "code": { - "text": "Code Sample", - "add_code": "Add code sample", - "form": { - "fields": { - "code": { - "label": "Code", - "msg": { - "empty": "Code cannot be empty." - } - }, - "language": { - "label": "Language (optional)", - "placeholder": "Automatic detection" - } - } - }, - "btn_cancel": "Cancel", - "btn_confirm": "Add" - }, - "formula": { - "text": "Formula", - "options": { - "inline": "Inline formula", - "block": "Block formula" - } - }, - "heading": { - "text": "Heading", - "options": { - "h1": "Heading 1", - "h2": "Heading 2", - "h3": "Heading 3", - "h4": "Heading 4", - "h5": "Heading 5", - "h6": "Heading 6" - } - }, - "help": { - "text": "Help" - }, - "hr": { - "text": "Horizontal Rule" - }, - "image": { - "text": "Image", - "add_image": "Add image", - "tab_image": "Upload image", - "form_image": { - "fields": { - "file": { - "label": "Image File", - "btn": "Select image", - "msg": { - "empty": "File cannot be empty.", - "only_image": "Only image files are allowed.", - "max_size": "File size cannot exceed 4MB." - } - }, - "description": { - "label": "Description (optional)" - } - } - }, - "tab_url": "Image URL", - "form_url": { - "fields": { - "url": { - "label": "Image URL", - "msg": { - "empty": "Image URL cannot be empty." - } - }, - "name": { - "label": "Description (optional)" - } - } - }, - "btn_cancel": "Cancel", - "btn_confirm": "Add", - "uploading": "Uploading" - }, - "indent": { - "text": "Indent" - }, - "outdent": { - "text": "Outdent" - }, - "italic": { - "text": "Emphasis" - }, - "link": { - "text": "Hyperlink", - "add_link": "Add hyperlink", - "form": { - "fields": { - "url": { - "label": "URL", - "msg": { - "empty": "URL cannot be empty." - } - }, - "name": { - "label": "Description (optional)" - } - } - }, - "btn_cancel": "Cancel", - "btn_confirm": "Add" - }, - "ordered_list": { - "text": "Numbered List" - }, - "unordered_list": { - "text": "Bulleted List" - }, - "table": { - "text": "Table", - "heading": "Heading", - "cell": "Cell" - } - }, - "close_modal": { - "title": "I am closing this post as...", - "btn_cancel": "Cancel", - "btn_submit": "Submit", - "remark": { - "empty": "Cannot be empty." - }, - "msg": { - "empty": "Please select a reason." - } - }, - "report_modal": { - "flag_title": "I am flagging to report this post as...", - "close_title": "I am closing this post as...", - "review_question_title": "Review question", - "review_answer_title": "Review answer", - "review_comment_title": "Review comment", - "btn_cancel": "Cancel", - "btn_submit": "Submit", - "remark": { - "empty": "Cannot be empty." - }, - "msg": { - "empty": "Please select a reason." - } - }, - "tag_modal": { - "title": "Create new tag", - "form": { - "fields": { - "display_name": { - "label": "Display Name", - "msg": { - "empty": "Display name cannot be empty.", - "range": "Display name up to 35 characters." - } - }, - "slug_name": { - "label": "URL Slug", - "description": "Must use the character set \"a-z\", \"0-9\", \"+ # - .\"", - "msg": { - "empty": "URL slug cannot be empty.", - "range": "URL slug up to 35 characters.", - "character": "URL slug contains unallowed character set." - } - }, - "description": { - "label": "Description (optional)" - } - } - }, - "btn_cancel": "Cancel", - "btn_submit": "Submit" - }, - "tag_info": { - "created_at": "Created", - "edited_at": "Edited", - "synonyms": { - "title": "Synonyms", - "text": "The following tags will be remapped to", - "empty": "No synonyms found.", - "btn_add": "Add a synonym", - "btn_edit": "Edit", - "btn_save": "Save" - }, - "synonyms_text": "The following tags will be remapped to", - "delete": { - "title": "Delete this tag", - "content": "

We do not allowed deleting tag with posts.

Please remove this tag from the posts first.

", - "content2": "Are you sure you wish to delete?", - "close": "Close" - } - }, - "edit_tag": { - "title": "Edit Tag", - "default_reason": "Edit tag", - "form": { - "fields": { - "revision": { - "label": "Revision" - }, - "display_name": { - "label": "Display Name" - }, - "slug_name": { - "label": "URL Slug", - "info": "Must use the character set \"a-z\", \"0-9\", \"+ # - .\"" - }, - "description": { - "label": "Description" - }, - "edit_summary": { - "label": "Edit Summary", - "placeholder": "Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)" - } - } - }, - "btn_save_edits": "Save edits", - "btn_cancel": "Cancel" - }, - "dates": { - "long_date": "MMM D", - "long_date_with_year": "MMM D, YYYY", - "long_date_with_time": "MMM D, YYYY [at] HH:mm", - "now": "now", - "x_seconds_ago": "{{count}}s ago", - "x_minutes_ago": "{{count}}m ago", - "x_hours_ago": "{{count}}h ago", - "hour": "hour", - "day": "day" - }, - "comment": { - "btn_add_comment": "Add comment", - "reply_to": "Reply to", - "btn_reply": "Reply", - "btn_edit": "Edit", - "btn_delete": "Delete", - "btn_flag": "Flag", - "btn_save_edits": "Save edits", - "btn_cancel": "Cancel", - "show_more": "Show more comment", - "tip_question": "Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.", - "tip_answer": "Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting." - }, - "edit_answer": { - "title": "Edit Answer", - "default_reason": "Edit answer", - "form": { - "fields": { - "revision": { - "label": "Revision" - }, - "answer": { - "label": "Answer" - }, - "edit_summary": { - "label": "Edit Summary", - "placeholder": "Briefly explain your changes (corrected spelling, fixed grammar, improved formatting)" - } - } - }, - "btn_save_edits": "Save edits", - "btn_cancel": "Cancel" - }, - "tags": { - "title": "Tags", - "sort_buttons": { - "popular": "Popular", - "name": "Name", - "newest": "newest" - }, - "button_follow": "Follow", - "button_following": "Following", - "tag_label": "questions", - "search_placeholder": "Filter by tag name", - "no_description": "The tag has no description.", - "more": "More" - }, - "ask": { - "title": "Add Question", - "edit_title": "Edit Question", - "default_reason": "Edit question", - "similar_questions": "Similar questions", - "form": { - "fields": { - "revision": { - "label": "Revision" - }, - "title": { - "label": "Title", - "placeholder": "Be specific and imagine you're asking a question to another person", - "msg": { - "empty": "Title cannot be empty.", - "range": "Title up to 150 characters" - } - }, - "body": { - "label": "Body", - "msg": { - "empty": "Body cannot be empty." - } - }, - "tags": { - "label": "Tags", - "msg": { - "empty": "Tags cannot be empty." - } - }, - "answer": { - "label": "Answer", - "msg": { - "empty": "Answer cannot be empty." - } - } - } - }, - "btn_post_question": "Post your question", - "btn_save_edits": "Save edits", - "answer_question": "Answer your own question", - "post_question&answer": "Post your question and answer" - }, - "tag_selector": { - "add_btn": "Add tag", - "create_btn": "Create new tag", - "search_tag": "Search tag", - "hint": "Describe what your question is about, at least one tag is required.", - "no_result": "No tags matched" - }, - "header": { - "nav": { - "question": "Questions", - "tag": "Tags", - "user": "Users", - "profile": "Profile", - "setting": "Settings", - "logout": "Log out", - "admin": "Admin" - }, - "search": { - "placeholder": "Search" - } - }, - "footer": { - "build_on": "Built on <1> Answer - the open-source software that power Q&A communities
Made with love © 2022 Answer" - }, - "upload_img": { - "name": "Change", - "loading": "loading..." - }, - "pic_auth_code": { - "title": "Captcha", - "placeholder": "Type the text above", - "msg": { - "empty": "Captcha cannot be empty." - } - }, - "inactive": { - "first": "You're almost done! We sent an activation mail to {{mail}}. Please follow the instructions in the mail to activate your account.", - "info": "If it doesn't arrive, check your spam folder.", - "another": "We sent another activation email to you at {{mail}}. It might take a few minutes for it to arrive; be sure to check your spam folder.", - "btn_name": "Resend activation email", - "change_btn_name": "Change email", - "msg": { - "empty": "Cannot be empty." - } - }, - "login": { - "page_title": "Welcome to Answer", - "info_sign": "Don't have an account? <1>Sign up", - "info_login": "Already have an account? <1>Log in", - "forgot_pass": "Forgot password?", - "name": { - "label": "Name", - "msg": { - "empty": "Name cannot be empty.", - "range": "Name up to 30 characters." - } - }, - "email": { - "label": "Email", - "msg": { - "empty": "Email cannot be empty." - } - }, - "password": { - "label": "Password", - "msg": { - "empty": "Password cannot be empty.", - "different": "The passwords entered on both sides are inconsistent" - } - } - }, - "account_forgot": { - "page_title": "Forgot Your Password", - "btn_name": "Send me recovery email", - "send_success": "If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly.", - "email": { - "label": "Email", - "msg": { - "empty": "Email cannot be empty." - } - } - }, - "change_email": { - "page_title": "Welcome to Answer", - "btn_cancel": "Cancel", - "btn_update": "Update email address", - "send_success": "If an account matches {{mail}}, you should receive an email with instructions on how to reset your password shortly.", - "email": { - "label": "New Email", - "msg": { - "empty": "Email cannot be empty." - } - } - }, - "password_reset": { - "page_title": "Password Reset", - "btn_name": "Reset my password", - "reset_success": "You successfully changed your password; you will be redirected to the log in page.", - "link_invalid": "Sorry, this password reset link is no longer valid. Perhaps your password is already reset?", - "to_login": "Continue to log in page", - "password": { - "label": "Password", - "msg": { - "empty": "Password cannot be empty.", - "length": "The length needs to be between 8 and 32", - "different": "The passwords entered on both sides are inconsistent" - } - }, - "password_confirm": { - "label": "Confirm New Password" - } - }, - "settings": { - "page_title": "Settings", - "nav": { - "profile": "Profile", - "notification": "Notifications", - "account": "Account", - "interface": "Interface" - }, - "profile": { - "btn_name": "Update profile", - "display_name": { - "label": "Display Name", - "msg": "Display name cannot be empty.", - "msg_range": "Display name up to 30 characters" - }, - "username": { - "label": "Username", - "caption": "People can mention you as \"@username\".", - "msg": "Username cannot be empty.", - "msg_range": "Username up to 30 characters", - "character": "Must use the character set \"a-z\", \"0-9\", \" - . _\"" - }, - "avatar": { - "label": "Profile Image", - "gravatar": "Gravatar", - "gravatar_text": "You can change image on <1>gravatar.com", - "custom": "Custom", - "btn_refresh": "Refresh", - "custom_text": "You can upload your image.", - "default": "Default", - "msg": "Please upload an avatar" - }, - "bio": { - "label": "About Me (optional)" - }, - "website": { - "label": "Website (optional)", - "placeholder": "https://example.com", - "msg": "Website incorrect format" - }, - "location": { - "label": "Location (optional)", - "placeholder": "City, Country" - } - }, - "notification": { - "email": { - "label": "Email Notifications", - "radio": "Answers to your questions, comments, and more" - } - }, - "account": { - "change_email_btn": "Change email", - "change_pass_btn": "Change password", - "change_email_info": "We've sent an email to that address. Please follow the confirmation instructions.", - "email": { - "label": "Email", - "msg": "Email cannot be empty." - }, - "password_title": "Password", - "current_pass": { - "label": "Current Password", - "msg": { - "empty": "Current Password cannot be empty.", - "length": "The length needs to be between 8 and 32.", - "different": "The two entered passwords do not match." - } - }, - "new_pass": { - "label": "New Password" - }, - "pass_confirm": { - "label": "Confirm New Password" - } - }, - "interface": { - "lang": { - "label": "Interface Language", - "text": "User interface language. It will change when you refresh the page." - } - } - }, - "toast": { - "update": "update success", - "update_password": "Password changed successfully.", - "flag_success": "Thanks for flagging." - }, - "related_question": { - "title": "Related Questions", - "btn": "Add question", - "answers": "answers" - }, - "question_detail": { - "Asked": "Asked", - "asked": "asked", - "update": "Modified", - "edit": "edited", - "Views": "Viewed", - "Follow": "Follow", - "Following": "Following", - "answered": "answered", - "closed_in": "Closed in", - "show_exist": "Show existing question.", - "answers": { - "title": "Answers", - "score": "Score", - "newest": "Newest", - "btn_accept": "Accept", - "btn_accepted": "Accepted" - }, - "write_answer": { - "title": "Your Answer", - "btn_name": "Post your answer", - "confirm_title": "Continue to answer", - "continue": "Continue", - "confirm_info": "

Are you sure you want to add another answer?

You could use the edit link to refine and improve your existing answer, instead.

", - "empty": "Answer cannot be empty." - } - }, - "delete": { - "title": "Delete this post", - "question": "We do not recommend deleting questions with answers because doing so deprives future readers of this knowledge.

Repeated deletion of answered questions can result in your account being blocked from asking. Are you sure you wish to delete?", - "answer_accepted": "

We do not recommend deleting accepted answer because doing so deprives future readers of this knowledge.

Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?", - "other": "Are you sure you wish to delete?", - "tip_question_deleted": "This post has been deleted", - "tip_answer_deleted": "This answer has been deleted" - }, - "btns": { - "confirm": "Confirm", - "cancel": "Cancel", - "save": "Save", - "delete": "Delete", - "login": "Log in", - "signup": "Sign up", - "logout": "Log out", - "verify": "Verify", - "add_question": "Add question" - }, - "search": { - "title": "Search Results", - "keywords": "Keywords", - "options": "Options", - "follow": "Follow", - "following": "Following", - "counts": "{{count}} Results", - "more": "More", - "sort_btns": { - "relevance": "Relevance", - "newest": "Newest", - "active": "Active", - "score": "Score" - }, - "tips": { - "title": "Advanced Search Tips", - "tag": "<1>[tag] search withing a tag", - "user": "<1>user:username search by author", - "answer": "<1>answers:0 unanswered questions", - "score": "<1>score:3 posts with a 3+ score", - "question": "<1>is:question search questions", - "is_answer": "<1>is:answer search answers" - }, - "empty": "We couldn't find anything.
Try different or less specific keywords." - }, - "share": { - "name": "Share", - "copy": "Copy link", - "via": "Share post via...", - "copied": "Copied", - "facebook": "Share to Facebook", - "twitter": "Share to Twitter" - }, - "cannot_vote_for_self": "You can't vote for your own post", - "modal_confirm": { - "title": "Error..." - }, - "account_result": { - "page_title": "Welcome to Answer", - "success": "Your new account is confirmed; you will be redirected to the home page.", - "link": "Continue to homepage", - "invalid": "Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?", - "confirm_new_email": "Your email has been updated.", - "confirm_new_email_invalid": "Sorry, this confirmation link is no longer valid. Perhaps your email was already changed?" - }, - "question": { - "following_tags": "Following Tags", - "edit": "Edit", - "save": "Save", - "follow_tag_tip": "Follow tags to curate your list of questions.", - "hot_questions": "Hot Questions", - "all_questions": "All Questions", - "x_questions": "{{ count }} Questions", - "x_answers": "{{ count }} answers", - "questions": "Questions", - "answers": "Answers", - "newest": "Newest", - "active": "Active", - "frequent": "Frequent", - "score": "Score", - "unanswered": "Unanswered", - "modified": "modified", - "answered": "answered", - "asked": "asked", - "closed": "closed", - "follow_a_tag": "Follow a tag", - "more": "More" - }, - "personal": { - "overview": "Overview", - "answers": "Answers", - "answer": "answer", - "questions": "Questions", - "question": "question", - "bookmarks": "Bookmarks", - "reputation": "Reputation", - "comments": "Comments", - "votes": "Votes", - "newest": "Newest", - "score": "Score", - "edit_profile": "Edit Profile", - "visited_x_days": "Visited {{ count }} days", - "viewed": "Viewed", - "joined": "Joined", - "last_login": "Seen", - "about_me": "About Me", - "about_me_empty": "// Hello, World !", - "top_answers": "Top Answers", - "top_questions": "Top Questions", - "stats": "Stats", - "list_empty": "No posts found.
Perhaps you'd like to select a different tab?", - "accepted": "Accepted", - "answered": "answered", - "asked": "asked", - "upvote": "upvote", - "downvote": "downvote", - "mod_short": "Mod", - "mod_long": "Moderators", - "x_reputation": "reputation", - "x_votes": "votes received", - "x_answers": "answers", - "x_questions": "questions" - }, - "install": { - "title": "Answer", - "next": "Next", - "done": "Done", - "config_yaml_error": "Can’t create the config.yaml file.", - "lang": { - "label": "Please choose a language" - }, - "db_type": { - "label": "Database Engine" - }, - "db_username": { - "label": "Username", - "placeholder": "root", - "msg": "Username cannot be empty." - }, - "db_password": { - "label": "Password", - "placeholder": "root", - "msg": "Password cannot be empty." - }, - "db_host": { - "label": "Database Host", - "placeholder": "db:3306", - "msg": "Database Host cannot be empty." - }, - "db_name": { - "label": "Database Name", - "placeholder": "answer", - "msg": "Database Name cannot be empty." - }, - "db_file": { - "label": "Database File", - "placeholder": "/data/answer.db", - "msg": "Database File cannot be empty." - }, - "config_yaml": { - "title": "Create config.yaml", - "label": "The config.yaml file created.", - "description": "You can create the <1>config.yaml file manually in the <1>/var/wwww/xxx/ directory and paste the following text into it.", - "info": "After you’ve done that, click “Next” button." - }, - "site_information": "Site Information", - "admin_account": "Admin Account", - "site_name": { - "label": "Site Name", - "msg": "Site Name cannot be empty." - }, - "site_url": { - "label": "Site URL", - "text": "The address of your site.", - "msg": { - "empty": "Site URL cannot be empty.", - "incorrect": "Site URL incorrect format." - } - }, - "contact_email": { - "label": "Contact Email", - "text": "Email address of key contact responsible for this site.", - "msg": { - "empty": "Contact Email cannot be empty.", - "incorrect": "Contact Email incorrect format." - } - - }, - "admin_name": { - "label": "Name", - "msg": "Name cannot be empty." - }, - "admin_password": { - "label": "Password", - "text": "You will need this password to log in. Please store it in a secure location.", - "msg": "Password cannot be empty." - }, - "admin_email": { - "label": "Email", - "text": "You will need this email to log in.", - "msg": { - "empty": "Email cannot be empty.", - "incorrect": "Email incorrect format." - } - }, - "ready_title": "Your Answer is Ready!", - "ready_description": "If you ever feel like changing more settings, visit <1>admin section; find it in the site menu.", - "good_luck": "Have fun, and good luck!", - "warning": "Warning", - "warning_description": "The file <1>config.yaml already exists. If you need to reset any of the configuration items in this file, please delete it first. You may try <2>installing now.", - "installed": "Already installed", - "installed_description": "You appear to have already installed. To reinstall please clear your old database tables first." - }, - "upgrade": { - "title": "Answer", - "update_btn": "Update data", - "update_title": "Data update required", - "update_description": "<1>Answer has been updated! Before you continue, we have to update your data to the newest version.<1>The update process may take a little while, so please be patient.", - "done_title": "No update required", - "done_btn": "Done", - "done_desscription": "Your Answer data is already up-to-date." - }, - "page_404": { - "description": "Unfortunately, this page doesn't exist.", - "back_home": "Back to homepage" - }, - "page_50X": { - "description": "The server encountered an error and could not complete your request.", - "back_home": "Back to homepage" - }, - "page_maintenance": { - "description": "We are under maintenance, we’ll be back soon." - }, - "admin": { - "admin_header": { - "title": "Admin" - }, - "nav_menus": { - "dashboard": "Dashboard", - "contents": "Contents", - "questions": "Questions", - "answers": "Answers", - "users": "Users", - "flags": "Flags", - "settings": "Settings", - "general": "General", - "interface": "Interface", - "smtp": "SMTP" - }, - "dashboard": { - "title": "Dashboard", - "welcome": "Welcome to Answer Admin !", - "site_statistics": "Site Statistics", - "questions": "Questions:", - "answers": "Answers:", - "comments": "Comments:", - "votes": "Votes:", - "active_users": "Active users:", - "flags": "Flags:", - "site_health_status": "Site Health Status", - "version": "Version:", - "https": "HTTPS:", - "uploading_files": "Uploading files:", - "smtp": "SMTP:", - "timezone": "Timezone:", - "system_info": "System Info", - "storage_used": "Storage used:", - "uptime": "Uptime:", - "answer_links": "Answer Links", - "documents": "Documents", - "feedback": "Feedback", - "review": "Review", - "config": "Config", - "update_to": "Update to", - "latest": "Latest", - "check_failed": "Check failed", - "yes": "Yes", - "no": "No", - "not_allowed": "Not allowed", - "allowed": "Allowed", - "enabled": "Enabled", - "disabled": "Disabled" - }, - "flags": { - "title": "Flags", - "pending": "Pending", - "completed": "Completed", - "flagged": "Flagged", - "created": "Created", - "action": "Action", - "review": "Review" - }, - "change_modal": { - "title": "Change user status to...", - "btn_cancel": "Cancel", - "btn_submit": "Submit", - "normal_name": "normal", - "normal_description": "A normal user can ask and answer questions.", - "suspended_name": "suspended", - "suspended_description": "A suspended user can't log in.", - "deleted_name": "deleted", - "deleted_description": "Delete profile, authentication associations.", - "inactive_name": "inactive", - "inactive_description": "An inactive user must re-validate their email.", - "confirm_title": "Delete this user", - "confirm_content": "Are you sure you want to delete this user? This is permanent!", - "confirm_btn": "Delete", - "msg": { - "empty": "Please select a reason." - } - }, - "status_modal": { - "title": "Change {{ type }} status to...", - "normal_name": "normal", - "normal_description": "A normal post available to everyone.", - "closed_name": "closed", - "closed_description": "A closed question can't answer, but still can edit, vote and comment.", - "deleted_name": "deleted", - "deleted_description": "All reputation gained and lost will be restored.", - "btn_cancel": "Cancel", - "btn_submit": "Submit", - "btn_next": "Next" - }, - "users": { - "title": "Users", - "name": "Name", - "email": "Email", - "reputation": "Reputation", - "created_at": "Created Time", - "delete_at": "Deleted Time", - "suspend_at": "Suspended Time", - "status": "Status", - "action": "Action", - "change": "Change", - "all": "All", - "inactive": "Inactive", - "suspended": "Suspended", - "deleted": "Deleted", - "normal": "Normal", - "filter": { - "placeholder": "Filter by name, user:id" - } - }, - "questions": { - "page_title": "Questions", - "normal": "Normal", - "closed": "Closed", - "deleted": "Deleted", - "post": "Post", - "votes": "Votes", - "answers": "Answers", - "created": "Created", - "status": "Status", - "action": "Action", - "change": "Change", - "filter": { - "placeholder": "Filter by title, question:id" - } - }, - "answers": { - "page_title": "Answers", - "normal": "Normal", - "deleted": "Deleted", - "post": "Post", - "votes": "Votes", - "created": "Created", - "status": "Status", - "action": "Action", - "change": "Change", - "filter": { - "placeholder": "Filter by title, answer:id" - } - }, - "general": { - "page_title": "General", - "name": { - "label": "Site Name", - "msg": "Site name cannot be empty.", - "text": "The name of this site, as used in the title tag." - }, - "site_url": { - "label": "Site URL", - "msg": "Site url cannot be empty.", - "text": "The address of your site." - }, - "short_description": { - "label": "Short Site Description (optional)", - "msg": "Short site description cannot be empty.", - "text": "Short description, as used in the title tag on homepage." - }, - "description": { - "label": "Site Description (optional)", - "msg": "Site description cannot be empty.", - "text": "Describe this site in one sentence, as used in the meta description tag." - }, - "contact_email": { - "label": "Contact Email", - "msg": "Contact email cannot be empty.", - "text": "Email address of key contact responsible for this site." - } - }, - "interface": { - "page_title": "Interface", - "logo": { - "label": "Logo (optional)", - "msg": "Site logo cannot be empty.", - "text": "You can upload your image or <1>reset it to the site title text." - }, - "theme": { - "label": "Theme", - "msg": "Theme cannot be empty.", - "text": "Select an existing theme." - }, - "language": { - "label": "Interface Language", - "msg": "Interface language cannot be empty.", - "text": "User interface language. It will change when you refresh the page." - }, - "time_zone": { - "label": "Timezone", - "msg": "Timezone cannot be empty.", - "text": "Choose a UTC (Coordinated Universal Time) time offset." - } - }, - "smtp": { - "page_title": "SMTP", - "from_email": { - "label": "From Email", - "msg": "From email cannot be empty.", - "text": "The email address which emails are sent from." - }, - "from_name": { - "label": "From Name", - "msg": "From name cannot be empty.", - "text": "The name which emails are sent from." - }, - "smtp_host": { - "label": "SMTP Host", - "msg": "SMTP host cannot be empty.", - "text": "Your mail server." - }, - "encryption": { - "label": "Encryption", - "msg": "Encryption cannot be empty.", - "text": "For most servers SSL is the recommended option.", - "ssl": "SSL", - "none": "None" - }, - "smtp_port": { - "label": "SMTP Port", - "msg": "SMTP port must be number 1 ~ 65535.", - "text": "The port to your mail server." - }, - "smtp_username": { - "label": "SMTP Username", - "msg": "SMTP username cannot be empty." - }, - "smtp_password": { - "label": "SMTP Password", - "msg": "SMTP password cannot be empty." - }, - "test_email_recipient": { - "label": "Test Email Recipients", - "text": "Provide email address that will receive test sends.", - "msg": "Test email recipients is invalid" - }, - "smtp_authentication": { - "label": "SMTP Authentication", - "msg": "SMTP authentication cannot be empty.", - "yes": "Yes", - "no": "No" - } - } - } -} diff --git a/ui/src/i18n/locales/en_US.yaml b/ui/src/i18n/locales/en_US.yaml new file mode 100644 index 000000000..27300e929 --- /dev/null +++ b/ui/src/i18n/locales/en_US.yaml @@ -0,0 +1,1119 @@ +base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." + +email: + other: "Email" +password: + other: "Password" + +email_or_password_wrong_error: &email_or_password_wrong + other: "Email and password do not match." + +error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Answer do not found." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + theme: + not_found: + other: "Theme not found." + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + other: "Username is already in use." + set_avatar: + other: "Avatar set failed." + +report: + spam: + name: + other: "spam" + description: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + description: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" + description: + other: "This question has been asked before and already has an answer." + not_answer: + name: + other: "not an answer" + description: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: + name: + other: "no longer needed" + description: + other: "This comment is outdated, conversational or not relevant to this post." + other: + name: + other: "something else" + description: + other: "This post requires staff attention for another reason not listed above." + +question: + close: + duplicate: + name: + other: "spam" + description: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + description: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + description: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + description: + other: "This post requires another reason not listed above." + +notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + adopt_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + description: >- +
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by + placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Webite Maintenance + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: 'Your account was suspended until {{ time }}.' + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4MB. + description: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + description: 'Must use the character set "a-z", "0-9", "+ # - ."' + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + description: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allowed deleting tag with posts.

Please remove this tag + from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: 'Must use the character set "a-z", "0-9", "+ # - ."' + description: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: 'MMM D, YYYY' + long_date_with_time: 'MMM D, YYYY [at] HH:mm' + now: now + x_seconds_ago: '{{count}}s ago' + x_minutes_ago: '{{count}}m ago' + x_hours_ago: '{{count}}h ago' + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid + answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are + adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_description: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: 'Describe what your question is about, at least one tag is required.' + no_result: No tags matched + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. + Please follow the instructions in the mail to activate your account. + info: 'If it doesn''t arrive, check your spam folder.' + another: >- + We sent another activation email to you at {{mail}}. It might + take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to Answer + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name up to 30 characters. + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in + page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is + already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + btn_name: Update profile + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: Default + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: 'https://example.com' + msg: Website incorrect format + location: + label: Location (optional) + placeholder: 'City, Country' + notification: + email: + label: Email Notifications + radio: 'Answers to your questions, comments, and more' + account: + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation + instructions. + email: + label: Email + msg: Email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the + edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because + doing so deprives future readers of this knowledge.

Repeated deletion + of answered questions can result in your account being blocked from asking. + Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because + doing so deprives future readers of this knowledge.

Repeated deletion + of accepted answers can result in your account being blocked from answering. + Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: '{{count}} Results' + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + tips: + title: Advanced Search Tips + tag: '<1>[tag] search withing a tag' + user: '<1>user:username search by author' + answer: '<1>answers:0 unanswered questions' + score: '<1>score:3 posts with a 3+ score' + question: '<1>is:question search questions' + is_answer: '<1>is:answer search answers' + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to Twitter + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your + account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was + already changed? + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: '{{ count }} Questions' + x_answers: '{{ count }} answers' + questions: Questions + answers: Answers + newest: Newest + active: Active + frequent: Frequent + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: '// Hello, World !' + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Answer + next: Next + done: Done + config_yaml_error: Can’t create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: 'db:3306' + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + description: >- + You can create the <1>config.yaml file manually in the + <1>/var/wwww/xxx/ directory and paste the following text into it. + info: 'After you’ve done that, click “Next” button.' + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure + location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your Answer is Ready! + ready_description: >- + If you ever feel like changing more settings, visit <1>admin section; + find it in the site menu. + good_luck: 'Have fun, and good luck!' + warning: Warning + warning_description: >- + The file <1>config.yaml already exists. If you need to reset any of the + configuration items in this file, please delete it first. You may try + <2>installing now. + installed: Already installed + installed_description: >- + You appear to have already installed. To reinstall please clear your old + database tables first. + upgrade: + title: Answer + update_btn: Update data + update_title: Data update required + update_description: >- + <1>Answer has been updated! Before you continue, we have to update your data + to the newest version.<1>The update process may take a little while, so + please be patient. + done_title: No update required + done_btn: Done + done_desscription: Your Answer data is already up-to-date. + page_404: + description: 'Unfortunately, this page doesn''t exist.' + back_home: Back to homepage + page_50X: + description: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + description: 'We are under maintenance, we’ll be back soon.' + admin: + admin_header: + title: Admin + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + dashboard: + title: Dashboard + welcome: Welcome to Answer Admin ! + site_statistics: Site Statistics + questions: 'Questions:' + answers: 'Answers:' + comments: 'Comments:' + votes: 'Votes:' + active_users: 'Active users:' + flags: 'Flags:' + site_health_status: Site Health Status + version: 'Version:' + https: 'HTTPS:' + uploading_files: 'Uploading files:' + smtp: 'SMTP:' + timezone: 'Timezone:' + system_info: System Info + storage_used: 'Storage used:' + uptime: 'Uptime:' + answer_links: Answer Links + documents: Documents + feedback: Feedback + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + 'yes': 'Yes' + 'no': 'No' + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_description: A normal user can ask and answer questions. + suspended_name: suspended + suspended_description: A suspended user can't log in. + deleted_name: deleted + deleted_description: 'Delete profile, authentication associations.' + inactive_name: inactive + inactive_description: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: 'Change {{ type }} status to...' + normal_name: normal + normal_description: A normal post available to everyone. + closed_name: closed + closed_description: 'A closed question can''t answer, but still can edit, vote and comment.' + deleted_name: deleted + deleted_description: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + action: Action + change: Change + all: All + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + filter: + placeholder: 'Filter by name, user:id' + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, question:id' + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, answer:id' + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: 'The name of this site, as used in the title tag.' + site_url: + label: Site URL + msg: Site url cannot be empty. + text: The address of your site. + short_description: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: 'Short description, as used in the title tag on homepage.' + description: + label: Site Description (optional) + msg: Site description cannot be empty. + text: 'Describe this site in one sentence, as used in the meta description tag.' + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a UTC (Coordinated Universal Time) time offset. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: SMTP Authentication + msg: SMTP authentication cannot be empty. + 'yes': 'Yes' + 'no': 'No' diff --git a/ui/src/i18n/locales/i18n.yaml b/ui/src/i18n/locales/i18n.yaml new file mode 100644 index 000000000..13a6c5228 --- /dev/null +++ b/ui/src/i18n/locales/i18n.yaml @@ -0,0 +1,6 @@ +# all support language +language_options: + - label: "简体中文(CN)" + value: "zh_CN" + - label: "English(US)" + value: "en_US" diff --git a/ui/src/i18n/locales/it_IT.yaml b/ui/src/i18n/locales/it_IT.yaml new file mode 100644 index 000000000..0b9439412 --- /dev/null +++ b/ui/src/i18n/locales/it_IT.yaml @@ -0,0 +1,170 @@ +base: + success: + other: "Successo" + unknown: + other: "Errore sconosciuto" + request_format_error: + other: "Il formato della richiesta non è valido" + unauthorized_error: + other: "Non autorizzato" + database_error: + other: "Errore server dati" + +email: + other: "email" +password: + other: "password" + +email_or_password_wrong_error: &email_or_password_wrong + other: "Email o password errati" + +error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Risposta non trovata" + comment: + edit_without_permission: + other: "Non si hanno di privilegi sufficienti per modificare il commento" + not_found: + other: "Commento non trovato" + email: + duplicate: + other: "email già esistente" + need_to_be_verified: + other: "email deve essere verificata" + verify_url_expired: + other: "l'url di verifica email è scaduto, si prega di reinviare la email" + lang: + not_found: + other: "lingua non trovata" + object: + captcha_verification_failed: + other: "captcha errato" + disallow_follow: + other: "Non sei autorizzato a seguire" + disallow_vote: + other: "non sei autorizzato a votare" + disallow_vote_your_self: + other: "Non puoi votare un tuo post!" + not_found: + other: "oggetto non trovato" + verification_failed: + other: "verifica fallita" + email_or_password_incorrect: + other: "email o password incorretti" + old_password_verification_failed: + other: "la verifica della vecchia password è fallita" + new_password_same_as_previous_setting: + other: "La nuova password è identica alla precedente" + question: + not_found: + other: "domanda non trovata" + rank: + fail_to_meet_the_condition: + other: "Condizioni non valide per il grado" + report: + handle_failed: + other: "Gestione del report fallita" + not_found: + other: "Report non trovato" + tag: + not_found: + other: "Etichetta non trovata" + theme: + not_found: + other: "tema non trovato" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "utente non trovato" + suspended: + other: "utente sospeso" + username_invalid: + other: "utente non valido" + username_duplicate: + other: "utente già in uso" + +report: + spam: + name: + other: "spam" + description: + other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" + rude: + name: + other: "scortese o violento" + description: + other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" + duplicate: + name: + other: "duplicato" + description: + other: "Questa domanda è già stata posta e ha già una risposta." + not_answer: + name: + other: "non è una risposta" + description: + other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." + not_need: + name: + other: "non più necessario" + description: + other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." + other: + name: + other: "altro" + description: + other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." + +question: + close: + duplicate: + name: + other: "spam" + description: + other: "Questa domanda è già stata chiesta o ha già una risposta." + guideline: + name: + other: "motivo legato alla community" + description: + other: "Questa domanda non soddisfa le linee guida della comunità." + multiple: + name: + other: "richiede maggiori dettagli o chiarezza" + description: + other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: + name: + other: "altro" + description: + other: "Questo articolo richiede un'altro motivo non listato sopra." + +notification: + action: + update_question: + other: "domanda aggiornata" + answer_the_question: + other: "domanda risposta" + update_answer: + other: "risposta aggiornata" + adopt_answer: + other: "risposta accettata" + comment_question: + other: "domanda commentata" + comment_answer: + other: "risposta commentata" + reply_to_you: + other: "hai ricevuto risposta" + mention_you: + other: "sei stato menzionato" + your_question_is_closed: + other: "la tua domanda è stata chiusa" + your_question_was_deleted: + other: "la tua domanda è stata rimossa" + your_answer_was_deleted: + other: "la tua risposta è stata rimossa" + your_comment_was_deleted: + other: "il tuo commento è stato rimosso" diff --git a/ui/src/i18n/locales/zh_CN.json b/ui/src/i18n/locales/zh_CN.json deleted file mode 100644 index a5e25ea3f..000000000 --- a/ui/src/i18n/locales/zh_CN.json +++ /dev/null @@ -1,914 +0,0 @@ -{ - "how_to_format": { - "title": "如何设定文本格式", - "description": "
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 **粗体**

  • 使用 4 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 `像 _这样_`

  • 使用```创建代码块

    ```
    // 这是代码
    ```
" - }, - "pagination": { - "prev": "上一页", - "next": "下一页" - }, - "page_title": { - "question": "问题", - "questions": "问题", - "tag": "标签", - "tags": "标签", - "tag_wiki": "标签 wiki", - "edit_tag": "编辑标签", - "ask_a_question": "提问题", - "edit_question": "编辑问题", - "edit_answer": "编辑回答", - "search": "搜索", - "posts_containing": "包含", - "settings": "设定", - "notifications": "通知", - "login": "登录", - "sign_up": "注册", - "account_recovery": "账号恢复", - "account_activation": "账号激活", - "confirm_email": "确认电子邮件", - "account_suspended": "账号已封禁", - "admin": "后台管理" - }, - "notifications": { - "title": "通知", - "inbox": "收件箱", - "achievement": "成就", - "all_read": "全部标记为已读", - "show_more": "显示更多" - }, - "suspended": { - "title": "账号已封禁", - "until_time": "你的账号被封禁至{{ time }}。", - "forever": "你的账号已被永久封禁。", - "end": "违反了我们的社区准则。" - }, - "editor": { - "blockquote": { - "text": "引用" - }, - "bold": { - "text": "粗体" - }, - "chart": { - "text": "图表", - "flow_chart": "流程图", - "sequence_diagram": "时序图", - "class_diagram": "类图", - "state_diagram": "状态图", - "entity_relationship_diagram": "ER 图", - "user_defined_diagram": "User defined diagram", - "gantt_chart": "甘特图", - "pie_chart": "饼图" - }, - "code": { - "text": "代码块", - "add_code": "添加代码块", - "form": { - "fields": { - "code": { - "label": "代码块", - "msg": { - "empty": "代码块不能为空" - } - }, - "language": { - "label": "语言 (可选)", - "placeholder": "自动识别" - } - } - }, - "btn_cancel": "取消", - "btn_confirm": "添加" - }, - "formula": { - "text": "公式", - "options": { - "inline": "行内公式", - "block": "公式块" - } - }, - "heading": { - "text": "标题", - "options": { - "h1": "标题 1", - "h2": "标题 2", - "h3": "标题 3", - "h4": "标题 4", - "h5": "标题 5", - "h6": "标题 6" - } - }, - "help": { - "text": "帮助" - }, - "hr": { - "text": "水平分割线" - }, - "image": { - "text": "图片", - "add_image": "添加图片", - "tab_image": "上传图片", - "form_image": { - "fields": { - "file": { - "label": "图片文件", - "btn": "选择图片", - "msg": { - "empty": "请选择图片文件。", - "only_image": "只能上传图片文件。", - "max_size": "图片文件大小不能超过 4 MB。" - } - }, - "description": { - "label": "图片描述(可选)" - } - } - }, - "tab_url": "网络图片", - "form_url": { - "fields": { - "url": { - "label": "图片地址", - "msg": { - "empty": "图片地址不能为空" - } - }, - "name": { - "label": "图片描述(可选)" - } - } - }, - "btn_cancel": "取消", - "btn_confirm": "添加", - "uploading": "上传中..." - }, - "indent": { - "text": "添加缩进" - }, - "outdent": { - "text": "减少缩进" - }, - "italic": { - "text": "斜体" - }, - "link": { - "text": "超链接", - "add_link": "添加超链接", - "form": { - "fields": { - "url": { - "label": "链接", - "msg": { - "empty": "链接不能为空。" - } - }, - "name": { - "label": "链接描述(可选)" - } - } - }, - "btn_cancel": "取消", - "btn_confirm": "添加" - }, - "ordered_list": { - "text": "有编号列表" - }, - "unordered_list": { - "text": "无编号列表" - }, - "table": { - "text": "表格", - "heading": "表头", - "cell": "单元格" - } - }, - "close_modal": { - "title": "关闭原因是...", - "btn_cancel": "取消", - "btn_submit": "提交", - "remark": { - "empty": "不能为空。" - }, - "msg": { - "empty": "请选择一个原因。" - } - }, - "report_modal": { - "flag_title": "举报原因是...", - "close_title": "关闭原因是...", - "review_question_title": "审查问题", - "review_answer_title": "审查回答", - "review_comment_title": "审查评论", - "btn_cancel": "取消", - "btn_submit": "提交", - "remark": { - "empty": "不能为空" - }, - "msg": { - "empty": "请选择一个原因。" - } - }, - "tag_modal": { - "title": "创建新标签", - "form": { - "fields": { - "display_name": { - "label": "显示名称(别名)", - "msg": { - "empty": "不能为空", - "range": "不能超过 35 个字符" - } - }, - "slug_name": { - "label": "URL 固定链接", - "description": "必须由 \"a-z\", \"0-9\", \"+ # - .\" 组成", - "msg": { - "empty": "不能为空", - "range": "不能超过 35 个字符", - "character": "包含非法字符" - } - }, - "description": { - "label": "标签描述(可选)" - } - } - }, - "btn_cancel": "取消", - "btn_submit": "提交" - }, - "tag_info": { - "created_at": "创建于", - "edited_at": "编辑于", - "synonyms": { - "title": "同义词", - "text": "以下标签等同于", - "empty": "此标签目前没有同义词。", - "btn_add": "添加同义词", - "btn_edit": "编辑", - "btn_save": "保存" - }, - "synonyms_text": "以下标签等同于", - "delete": { - "title": "删除标签", - "content": "

不允许删除有关联问题的标签。

请先从关联的问题中删除此标签的引用。

", - "content2": "确定要删除吗?", - "close": "关闭" - } - }, - "edit_tag": { - "title": "编辑标签", - "default_reason": "编辑标签", - "form": { - "fields": { - "revision": { - "label": "编辑历史" - }, - "display_name": { - "label": "名称" - }, - "slug_name": { - "label": "URL 固定链接", - "info": "必须由 \"a-z\", \"0-9\", \"+ # - .\" 组成" - }, - "description": { - "label": "描述" - }, - "edit_summary": { - "label": "编辑概要", - "placeholder": "简单描述更改原因 (错别字、文字表达、格式等等)" - } - } - }, - "btn_save_edits": "保存更改", - "btn_cancel": "取消" - }, - "dates": { - "long_date": "YYYY年MM月", - "long_date_with_year": "YYYY年MM月DD日", - "long_date_with_time": "YYYY年MM月DD日 HH:mm", - "now": "刚刚", - "x_seconds_ago": "{{count}} 秒前", - "x_minutes_ago": "{{count}} 分钟前", - "x_hours_ago": "{{count}} 小时前" - }, - "comment": { - "btn_add_comment": "添加评论", - "reply_to": "回复", - "btn_reply": "回复", - "btn_edit": "编辑", - "btn_delete": "删除", - "btn_flag": "举报", - "btn_save_edits": "保存", - "btn_cancel": "取消", - "show_more": "显示更多评论", - "tip_question": "使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。", - "tip_answer": "使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。" - }, - "edit_answer": { - "title": "编辑回答", - "default_reason": "编辑回答", - "form": { - "fields": { - "revision": { - "label": "编辑历史" - }, - "answer": { - "label": "回答内容" - }, - "edit_summary": { - "label": "编辑概要", - "placeholder": "简单描述更改原因 (错别字、文字表达、格式等等)" - } - } - }, - "btn_save_edits": "保存更改", - "btn_cancel": "取消" - }, - "tags": { - "title": "标签", - "sort_buttons": { - "popular": "热门", - "name": "名称", - "newest": "最新" - }, - "button_follow": "关注", - "button_following": "已关注", - "tag_label": "个问题", - "search_placeholder": "通过标签名过滤", - "no_description": "此标签无描述。", - "more": "更多" - }, - "ask": { - "title": "提交新的问题", - "edit_title": "编辑问题", - "default_reason": "编辑问题", - "similar_questions": "相似的问题", - "form": { - "fields": { - "revision": { - "label": "编辑历史" - }, - "title": { - "label": "标题", - "placeholder": "请详细描述你的问题", - "msg": { - "empty": "标题不能为空", - "range": "标题最多 150 个字符" - } - }, - "body": { - "label": "内容", - "msg": { - "empty": "内容不能为空" - } - }, - "tags": { - "label": "标签", - "msg": { - "empty": "必须选择一个标签" - } - }, - "answer": { - "label": "回答内容", - "msg": { - "empty": "回答内容不能为空" - } - } - } - }, - "btn_post_question": "提交问题", - "btn_save_edits": "保存更改", - "answer_question": "直接发表回答", - "post_question&answer": "提交问题和回答" - }, - "tag_selector": { - "add_btn": "添加标签", - "create_btn": "创建新标签", - "search_tag": "搜索标签", - "hint": "选择至少一个与问题相关的标签。", - "no_result": "没有匹配的标签" - }, - "header": { - "nav": { - "question": "问题", - "tag": "标签", - "user": "用户", - "profile": "用户主页", - "setting": "账号设置", - "logout": "退出登录", - "admin": "后台管理" - }, - "search": { - "placeholder": "搜索" - } - }, - "footer": { - "build_on": "Built on <1> Answer - the open-source software that power Q&A communities
Made with love © 2022 Answer" - }, - "upload_img": { - "name": "更改图片", - "loading": "加载中..." - }, - "pic_auth_code": { - "title": "验证码", - "placeholder": "输入图片中的文字", - "msg": { - "empty": "不能为空" - } - }, - "inactive": { - "first": "马上就好了!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活您的帐户。", - "info": "如果没有收到,请检查您的垃圾邮件文件夹。", - "another": "我们向您发送了另一封激活电子邮件,地址为 {{mail}}。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。", - "btn_name": "重新发送激活邮件", - "msg": { - "empty": "不能为空" - } - }, - "login": { - "page_title": "欢迎来到 Answer", - "info_sign": "没有帐户?<1>注册", - "info_login": "已经有一个帐户?<1>登录", - "forgot_pass": "忘记密码?", - "name": { - "label": "昵称", - "msg": { - "empty": "昵称不能为空", - "range": "昵称最多 30 个字符" - } - }, - "email": { - "label": "邮箱", - "msg": { - "empty": "邮箱不能为空" - } - }, - "password": { - "label": "密码", - "msg": { - "empty": "密码不能为空", - "different": "两次输入密码不一致" - } - } - }, - "account_forgot": { - "page_title": "忘记密码", - "btn_name": "发送恢复邮件", - "send_success": "如无意外,你的邮箱 {{mail}} 将会收到一封重置密码的邮件,请根据指引重置你的密码。", - "email": { - "label": "邮箱", - "msg": { - "empty": "邮箱不能为空" - } - } - }, - "password_reset": { - "page_title": "密码重置", - "btn_name": "重置我的密码", - "reset_success": "你已经成功更改密码,将返回登录页面", - "link_invalid": "抱歉,此密码重置链接已失效。也许是你已经重置过密码了?", - "to_login": "前往登录页面", - "password": { - "label": "密码", - "msg": { - "empty": "密码不能为空", - "length": "密码长度在8-32个字符之间", - "different": "两次输入密码不一致" - } - }, - "password_confirm": { - "label": "确认新密码" - } - }, - "settings": { - "page_title": "设置", - "nav": { - "profile": "我的资料", - "notification": "通知", - "account": "账号", - "interface": "界面" - }, - "profile": { - "btn_name": "保存更改", - "display_name": { - "label": "昵称", - "msg": "昵称不能为空", - "msg_range": "昵称不能超过 30 个字符" - }, - "username": { - "label": "用户名", - "caption": "用户之间可以通过 \"@用户名\" 进行交互。", - "msg": "用户名不能为空", - "msg_range": "用户名不能超过 30 个字符", - "character": "用户名只能由 \"a-z\", \"0-9\", \" - . _\" 组成" - }, - "avatar": { - "label": "头像", - "text": "您可以上传图片作为头像,也可以 <1>重置 为" - }, - "bio": { - "label": "关于我 (可选)" - }, - "website": { - "label": "网站 (可选)", - "placeholder": "https://example.com", - "msg": "格式不正确" - }, - "location": { - "label": "位置 (可选)", - "placeholder": "城市, 国家" - } - }, - "notification": { - "email": { - "label": "邮件通知", - "radio": "你的提问有新的回答,评论,和其他" - } - }, - "account": { - "change_email_btn": "更改邮箱", - "change_pass_btn": "更改密码", - "change_email_info": "邮件已发送。请根据指引完成验证。", - "email": { - "label": "邮箱", - "msg": "邮箱不能为空" - }, - "password_title": "密码", - "current_pass": { - "label": "当前密码", - "msg": { - "empty": "当前密码不能为空", - "length": "密码长度必须在 8 至 32 之间", - "different": "两次输入的密码不匹配" - } - }, - "new_pass": { - "label": "新密码" - }, - "pass_confirm": { - "label": "确认新密码" - } - }, - "interface": { - "lang": { - "label": "界面语言", - "text": "设置用户界面语言,在刷新页面后生效。" - } - } - }, - "toast": { - "update": "更新成功", - "update_password": "更改密码成功。", - "flag_success": "感谢您的标记,我们会尽快处理。" - }, - "related_question": { - "title": "相关问题", - "btn": "我要提问", - "answers": "个回答" - }, - "question_detail": { - "Asked": "提问于", - "asked": "提问于", - "update": "修改于", - "edit": "最后编辑于", - "Views": "阅读次数", - "Follow": "关注此问题", - "Following": "已关注", - "answered": "回答于", - "closed_in": "关闭于", - "show_exist": "查看相关问题。", - "answers": { - "title": "个回答", - "score": "评分", - "newest": "最新", - "btn_accept": "采纳", - "btn_accepted": "已被采纳" - }, - "write_answer": { - "title": "你的回答", - "btn_name": "提交你的回答", - "confirm_title": "继续回答", - "continue": "继续", - "confirm_info": "

您确定要提交一个新的回答吗?

您可以直接编辑和改善您之前的回答的。

", - "empty": "回答内容不能为空。" - } - }, - "delete": { - "title": "删除", - "question": "我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗?", - "answer_accepted": "

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗?", - "other": "你确定要删除?", - "tip_question_deleted": "此问题已被删除", - "tip_answer_deleted": "此回答已被删除" - }, - "btns": { - "confirm": "确认", - "cancel": "取消", - "save": "保存", - "delete": "删除", - "login": "登录", - "signup": "注册", - "logout": "退出登录", - "verify": "验证", - "add_question": "我要提问" - }, - "search": { - "title": "搜索结果", - "keywords": "关键词", - "options": "选项", - "follow": "关注", - "following": "已关注", - "counts": "{{count}} 个结果", - "more": "更多", - "sort_btns": { - "relevance": "相关性", - "newest": "最新的", - "active": "活跃的", - "score": "评分" - }, - "tips": { - "title": "高级搜索提示", - "tag": "<1>[tag] 在指定标签中搜索", - "user": "<1>user:username 根据作者搜索", - "answer": "<1>answers:0 搜索未回答的问题", - "score": "<1>score:3 评分 3 分或以上", - "question": "<1>is:question 只搜索问题", - "is_answer": "<1>is:answer 只搜索回答" - }, - "empty": "找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。" - }, - "share": { - "name": "分享", - "copy": "复制链接", - "via": "分享在...", - "copied": "已复制", - "facebook": "分享到 Facebook", - "twitter": "分享到 Twitter" - }, - "cannot_vote_for_self": "不能给自己投票", - "modal_confirm": { - "title": "发生错误..." - }, - "account_result": { - "page_title": "欢迎来到 Answer", - "success": "你的账号已通过验证,即将返回首页。", - "link": "返回首页", - "invalid": "抱歉,此验证链接已失效。也许是你的账号已经通过验证了?", - "confirm_new_email": "你的电子邮箱已更新", - "confirm_new_email_invalid": "抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了?" - }, - "question": { - "following_tags": "已关注的标签", - "edit": "编辑", - "save": "保存", - "follow_tag_tip": "按照标签整理您的问题列表。", - "hot_questions": "热点问题", - "all_questions": "全部问题", - "x_questions": "{{ count }} 个问题", - "x_answers": "{{ count }} 个回答", - "questions": "个问题", - "answers": "回答", - "newest": "最新", - "active": "活跃", - "frequent": "浏览量", - "score": "评分", - "unanswered": "未回答", - "modified": "修改于", - "answered": "回答于", - "asked": "提问于", - "closed": "已关闭", - "follow_a_tag": "关注一个标签", - "more": "更多" - }, - "personal": { - "overview": "概览", - "answers": "回答", - "answer": "回答", - "questions": "问题", - "question": "问题", - "bookmarks": "收藏", - "reputation": "声望", - "comments": "评论", - "votes": "得票", - "newest": "最新", - "score": "评分", - "edit_profile": "编辑我的资料", - "visited_x_days": "Visited {{ count }} days", - "viewed": "Viewed", - "joined": "加入于", - "last_login": "上次登录", - "about_me": "关于我", - "about_me_empty": "// Hello, World !", - "top_answers": "热门回答", - "top_questions": "热门问题", - "stats": "状态", - "list_empty": "没有找到相关的内容。
试试看其他标签?", - "accepted": "已采纳", - "answered": "回答于", - "asked": "提问于", - "upvote": "赞同", - "downvote": "反对", - "mod_short": "管理员", - "mod_long": "管理员", - "x_reputation": "声望", - "x_votes": "得票", - "x_answers": "个回答", - "x_questions": "个问题" - }, - "page_404": { - "description": "页面不存在", - "back_home": "回到主页" - }, - "page_50X": { - "description": "服务器遇到了一个错误,无法完成你的请求。", - "back_home": "回到主页" - }, - "admin": { - "admin_header": { - "title": "后台管理" - }, - "nav_menus": { - "dashboard": "后台管理", - "contents": "内容管理", - "questions": "问题", - "answers": "回答", - "users": "用户管理", - "flags": "举报管理", - "settings": "站点设置", - "general": "一般", - "interface": "界面", - "smtp": "SMTP" - }, - "dashboard": { - "title": "后台管理", - "welcome": "欢迎来到 Answer 后台管理!", - "version": "版本" - }, - "flags": { - "title": "举报", - "pending": "等待处理", - "completed": "已完成", - "flagged": "被举报内容", - "created": "创建于", - "action": "操作", - "review": "审查" - }, - "change_modal": { - "title": "更改用户状态为...", - "btn_cancel": "取消", - "btn_submit": "提交", - "normal_name": "正常", - "normal_description": "正常状态的用户可以提问和回答。", - "suspended_name": "封禁", - "suspended_description": "被封禁的用户将无法登录。", - "deleted_name": "删除", - "deleted_description": "删除用户的个人信息,认证等等。", - "inactive_name": "不活跃", - "inactive_description": "不活跃的用户必须重新验证邮箱。", - "confirm_title": "删除此用户", - "confirm_content": "确定要删除此用户?此操作无法撤销!", - "confirm_btn": "删除", - "msg": { - "empty": "请选择一个原因" - } - }, - "status_modal": { - "title": "更改 {{ type }} 状态为...", - "normal_name": "正常", - "normal_description": "所有用户都可以访问", - "closed_name": "关闭", - "closed_description": "不能回答,但仍然可以编辑、投票和评论。", - "deleted_name": "删除", - "deleted_description": "所有获得/损失的声望将会恢复。", - "btn_cancel": "取消", - "btn_submit": "提交", - "btn_next": "下一步" - }, - "users": { - "title": "用户", - "name": "名称", - "email": "邮箱", - "reputation": "声望", - "created_at": "创建时间", - "delete_at": "删除时间", - "suspend_at": "封禁时间", - "status": "状态", - "action": "操作", - "change": "更改", - "all": "全部", - "inactive": "不活跃", - "suspended": "已封禁", - "deleted": "已删除", - "normal": "正常" - }, - "questions": { - "page_title": "问题", - "normal": "正常", - "closed": "已关闭", - "deleted": "已删除", - "post": "标题", - "votes": "得票数", - "answers": "回答数", - "created": "创建于", - "status": "状态", - "action": "操作", - "change": "更改" - }, - "answers": { - "page_title": "回答", - "normal": "正常", - "deleted": "已删除", - "post": "标题", - "votes": "得票数", - "created": "创建于", - "status": "状态", - "action": "操作", - "change": "更改" - }, - "general": { - "page_title": "一般", - "name": { - "label": "站点名称", - "msg": "不能为空", - "text": "站点的名称,作为站点的标题(HTML 的 title 标签)。" - }, - "short_description": { - "label": "简短的站点标语 (可选)", - "msg": "不能为空", - "text": "简短的标语,作为网站主页的标题(HTML 的 title 标签)。" - }, - "description": { - "label": "网站描述 (可选)", - "msg": "不能为空", - "text": "使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。" - } - }, - "interface": { - "page_title": "界面", - "logo": { - "label": "Logo (可选)", - "msg": "不能为空", - "text": "可以上传图片,或者<1>重置为站点标题。" - }, - "theme": { - "label": "主题", - "msg": "不能为空", - "text": "选择一个主题" - }, - "language": { - "label": "界面语言", - "msg": "不能为空", - "text": "设置用户界面语言,在刷新页面后生效。" - } - }, - "smtp": { - "page_title": "SMTP", - "from_email": { - "label": "发件人地址", - "msg": "不能为空", - "text": "用于发送邮件的地址。" - }, - "from_name": { - "label": "发件人名称", - "msg": "不能为空", - "text": "发件人的名称" - }, - "smtp_host": { - "label": "SMTP 主机", - "msg": "不能为空", - "text": "邮件服务器" - }, - "encryption": { - "label": "加密", - "msg": "不能为空", - "text": "对于大多数服务器而言,SSL 是推荐开启的。", - "ssl": "SSL", - "none": "无加密" - }, - "smtp_port": { - "label": "SMTP 端口", - "msg": "SMTP 端口必须在 1 ~ 65535 之间。", - "text": "邮件服务器的端口号。" - }, - "smtp_username": { - "label": "SMTP 用户名", - "msg": "不能为空" - }, - "smtp_password": { - "label": "SMTP 密码", - "msg": "不能为空" - }, - "test_email_recipient": { - "label": "测试邮件收件人", - "text": "提供用于接收测试邮件的邮箱地址。", - "msg": "地址无效" - }, - "smtp_authentication": { - "label": "SMTP 认证", - "msg": "不能为空", - "yes": "是", - "no": "否" - } - } - } -} diff --git a/ui/src/i18n/locales/zh_CN.yaml b/ui/src/i18n/locales/zh_CN.yaml new file mode 100644 index 000000000..34fd9dc98 --- /dev/null +++ b/ui/src/i18n/locales/zh_CN.yaml @@ -0,0 +1,919 @@ +base: + success: + other: "成功" + unknown: + other: "未知错误" + request_format_error: + other: "请求格式错误" + unauthorized_error: + other: "未登录" + database_error: + other: "数据服务异常" + +email: + other: "邮箱" +password: + other: "密码" + +email_or_password_wrong_error: &email_or_password_wrong + other: "邮箱或密码错误" + +error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "答案未找到" + comment: + edit_without_permission: + other: "不允许编辑评论" + not_found: + other: "评论未找到" + email: + duplicate: + other: "邮箱已经存在" + need_to_be_verified: + other: "邮箱需要验证" + verify_url_expired: + other: "邮箱验证的网址已过期,请重新发送邮件" + lang: + not_found: + other: "语言未找到" + object: + captcha_verification_failed: + other: "验证码错误" + disallow_follow: + other: "你不能关注" + disallow_vote: + other: "你不能投票" + disallow_vote_your_self: + other: "你不能为自己的帖子投票!" + not_found: + other: "对象未找到" + verification_failed: + other: "验证失败" + email_or_password_incorrect: + other: "邮箱或密码不正确" + old_password_verification_failed: + other: "旧密码验证失败" + new_password_same_as_previous_setting: + other: "新密码与之前的设置相同" + question: + not_found: + other: "问题未找到" + rank: + fail_to_meet_the_condition: + other: "级别不符合条件" + report: + handle_failed: + other: "报告处理失败" + not_found: + other: "报告未找到" + tag: + not_found: + other: "标签未找到" + theme: + not_found: + other: "主题未找到" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "用户未找到" + suspended: + other: "用户已被暂停" + username_invalid: + other: "用户名无效" + username_duplicate: + other: "用户名已被使用" + set_avatar: + other: "头像设置错误" + +report: + spam: + name: + other: "垃圾信息" + description: + other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" + rude: + name: + other: "粗鲁或辱骂的" + description: + other: "有理智的人都会发现此内容不适合进行尊重的讨论。" + duplicate: + name: + other: "重复信息" + description: + other: "此问题以前就有人问过,而且已经有了答案。" + not_answer: + name: + other: "不是答案" + description: + other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" + not_need: + name: + other: "不再需要" + description: + other: "此条评论是过时的,对话性的或与本帖无关。" + other: + name: + other: "其他原因" + description: + other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" + +question: + close: + duplicate: + name: + other: "垃圾信息" + description: + other: "此问题以前就有人问过,而且已经有了答案。" + guideline: + name: + other: "社区特定原因" + description: + other: "此问题不符合社区准则。" + multiple: + name: + other: "需要细节或澄清" + description: + other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: + name: + other: "其他原因" + description: + other: "此帖子需要上述所列以外的其他理由。" + +notification: + action: + update_question: + other: "更新了问题" + answer_the_question: + other: "回答了问题" + update_answer: + other: "更新了答案" + adopt_answer: + other: "接受了答案" + comment_question: + other: "评论了问题" + comment_answer: + other: "评论了答案" + reply_to_you: + other: "回复了你" + mention_you: + other: "提到了你" + your_question_is_closed: + other: "你的问题已被关闭" + your_question_was_deleted: + other: "你的问题已被删除" + your_answer_was_deleted: + other: "你的答案已被删除" + your_comment_was_deleted: + other: "你的评论已被删除" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何设定文本格式 + description: >- +
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 + **粗体**

  • 使用 4 + 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 + `像 _这样_`

  • 使用```创建代码块

    ```
    // + 这是代码
    ```
+ pagination: + prev: 上一页 + next: 下一页 + page_title: + question: 问题 + questions: 问题 + tag: 标签 + tags: 标签 + tag_wiki: 标签 wiki + edit_tag: 编辑标签 + ask_a_question: 提问题 + edit_question: 编辑问题 + edit_answer: 编辑回答 + search: 搜索 + posts_containing: 包含 + settings: 设定 + notifications: 通知 + login: 登录 + sign_up: 注册 + account_recovery: 账号恢复 + account_activation: 账号激活 + confirm_email: 确认电子邮件 + account_suspended: 账号已封禁 + admin: 后台管理 + notifications: + title: 通知 + inbox: 收件箱 + achievement: 成就 + all_read: 全部标记为已读 + show_more: 显示更多 + suspended: + title: 账号已封禁 + until_time: '你的账号被封禁至{{ time }}。' + forever: 你的账号已被永久封禁。 + end: 违反了我们的社区准则。 + editor: + blockquote: + text: 引用 + bold: + text: 粗体 + chart: + text: 图表 + flow_chart: 流程图 + sequence_diagram: 时序图 + class_diagram: 类图 + state_diagram: 状态图 + entity_relationship_diagram: ER 图 + user_defined_diagram: User defined diagram + gantt_chart: 甘特图 + pie_chart: 饼图 + code: + text: 代码块 + add_code: 添加代码块 + form: + fields: + code: + label: 代码块 + msg: + empty: 代码块不能为空 + language: + label: 语言 (可选) + placeholder: 自动识别 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 行内公式 + block: 公式块 + heading: + text: 标题 + options: + h1: 标题 1 + h2: 标题 2 + h3: 标题 3 + h4: 标题 4 + h5: 标题 5 + h6: 标题 6 + help: + text: 帮助 + hr: + text: 水平分割线 + image: + text: 图片 + add_image: 添加图片 + tab_image: 上传图片 + form_image: + fields: + file: + label: 图片文件 + btn: 选择图片 + msg: + empty: 请选择图片文件。 + only_image: 只能上传图片文件。 + max_size: 图片文件大小不能超过 4 MB。 + description: + label: 图片描述(可选) + tab_url: 网络图片 + form_url: + fields: + url: + label: 图片地址 + msg: + empty: 图片地址不能为空 + name: + label: 图片描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上传中... + indent: + text: 添加缩进 + outdent: + text: 减少缩进 + italic: + text: 斜体 + link: + text: 超链接 + add_link: 添加超链接 + form: + fields: + url: + label: 链接 + msg: + empty: 链接不能为空。 + name: + label: 链接描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: 有编号列表 + unordered_list: + text: 无编号列表 + table: + text: 表格 + heading: 表头 + cell: 单元格 + close_modal: + title: 关闭原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空。 + msg: + empty: 请选择一个原因。 + report_modal: + flag_title: 举报原因是... + close_title: 关闭原因是... + review_question_title: 审查问题 + review_answer_title: 审查回答 + review_comment_title: 审查评论 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空 + msg: + empty: 请选择一个原因。 + tag_modal: + title: 创建新标签 + form: + fields: + display_name: + label: 显示名称(别名) + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + slug_name: + label: URL 固定链接 + description: '必须由 "a-z", "0-9", "+ # - ." 组成' + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + character: 包含非法字符 + description: + label: 标签描述(可选) + btn_cancel: 取消 + btn_submit: 提交 + tag_info: + created_at: 创建于 + edited_at: 编辑于 + synonyms: + title: 同义词 + text: 以下标签等同于 + empty: 此标签目前没有同义词。 + btn_add: 添加同义词 + btn_edit: 编辑 + btn_save: 保存 + synonyms_text: 以下标签等同于 + delete: + title: 删除标签 + content:

不允许删除有关联问题的标签。

请先从关联的问题中删除此标签的引用。

+ content2: 确定要删除吗? + close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + form: + fields: + revision: + label: 编辑历史 + display_name: + label: 名称 + slug_name: + label: URL 固定链接 + info: '必须由 "a-z", "0-9", "+ # - ." 组成' + description: + label: 描述 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: YYYY年MM月 + long_date_with_year: YYYY年MM月DD日 + long_date_with_time: 'YYYY年MM月DD日 HH:mm' + now: 刚刚 + x_seconds_ago: '{{count}} 秒前' + x_minutes_ago: '{{count}} 分钟前' + x_hours_ago: '{{count}} 小时前' + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存 + btn_cancel: 取消 + show_more: 显示更多评论 + tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。 + tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名过滤 + no_description: 此标签无描述。 + more: 更多 + ask: + title: 提交新的问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + similar_questions: 相似的问题 + form: + fields: + revision: + label: 编辑历史 + title: + label: 标题 + placeholder: 请详细描述你的问题 + msg: + empty: 标题不能为空 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 直接发表回答 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: 选择至少一个与问题相关的标签。 + no_result: 没有匹配的标签 + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + profile: 用户主页 + setting: 账号设置 + logout: 退出登录 + admin: 后台管理 + search: + placeholder: 搜索 + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: 更改图片 + loading: 加载中... + pic_auth_code: + title: 验证码 + placeholder: 输入图片中的文字 + msg: + empty: 不能为空 + inactive: + first: '马上就好了!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活您的帐户。' + info: 如果没有收到,请检查您的垃圾邮件文件夹。 + another: '我们向您发送了另一封激活电子邮件,地址为 {{mail}}。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。' + btn_name: 重新发送激活邮件 + msg: + empty: 不能为空 + login: + page_title: 欢迎来到 Answer + info_sign: 没有帐户?<1>注册 + info_login: 已经有一个帐户?<1>登录 + forgot_pass: 忘记密码? + name: + label: 昵称 + msg: + empty: 昵称不能为空 + range: 昵称最多 30 个字符 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password: + label: 密码 + msg: + empty: 密码不能为空 + different: 两次输入密码不一致 + account_forgot: + page_title: 忘记密码 + btn_name: 发送恢复邮件 + send_success: '如无意外,你的邮箱 {{mail}} 将会收到一封重置密码的邮件,请根据指引重置你的密码。' + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password_reset: + page_title: 密码重置 + btn_name: 重置我的密码 + reset_success: 你已经成功更改密码,将返回登录页面 + link_invalid: 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? + to_login: 前往登录页面 + password: + label: 密码 + msg: + empty: 密码不能为空 + length: 密码长度在8-32个字符之间 + different: 两次输入密码不一致 + password_confirm: + label: 确认新密码 + settings: + page_title: 设置 + nav: + profile: 我的资料 + notification: 通知 + account: 账号 + interface: 界面 + profile: + btn_name: 保存更改 + display_name: + label: 昵称 + msg: 昵称不能为空 + msg_range: 昵称不能超过 30 个字符 + username: + label: 用户名 + caption: 用户之间可以通过 "@用户名" 进行交互。 + msg: 用户名不能为空 + msg_range: 用户名不能超过 30 个字符 + character: '用户名只能由 "a-z", "0-9", " - . _" 组成' + avatar: + label: 头像 + text: 您可以上传图片作为头像,也可以 <1>重置 为 + bio: + label: 关于我 (可选) + website: + label: 网站 (可选) + placeholder: 'https://example.com' + msg: 格式不正确 + location: + label: 位置 (可选) + placeholder: '城市, 国家' + notification: + email: + label: 邮件通知 + radio: 你的提问有新的回答,评论,和其他 + account: + change_email_btn: 更改邮箱 + change_pass_btn: 更改密码 + change_email_info: 邮件已发送。请根据指引完成验证。 + email: + label: 邮箱 + msg: 邮箱不能为空 + password_title: 密码 + current_pass: + label: 当前密码 + msg: + empty: 当前密码不能为空 + length: 密码长度必须在 8 至 32 之间 + different: 两次输入的密码不匹配 + new_pass: + label: 新密码 + pass_confirm: + label: 确认新密码 + interface: + lang: + label: 界面语言 + text: 设置用户界面语言,在刷新页面后生效。 + toast: + update: 更新成功 + update_password: 更改密码成功。 + flag_success: 感谢您的标记,我们会尽快处理。 + related_question: + title: 相关问题 + btn: 我要提问 + answers: 个回答 + question_detail: + Asked: 提问于 + asked: 提问于 + update: 修改于 + edit: 最后编辑于 + Views: 阅读次数 + Follow: 关注此问题 + Following: 已关注 + answered: 回答于 + closed_in: 关闭于 + show_exist: 查看相关问题。 + answers: + title: 个回答 + score: 评分 + newest: 最新 + btn_accept: 采纳 + btn_accepted: 已被采纳 + write_answer: + title: 你的回答 + btn_name: 提交你的回答 + confirm_title: 继续回答 + continue: 继续 + confirm_info:

您确定要提交一个新的回答吗?

您可以直接编辑和改善您之前的回答的。

+ empty: 回答内容不能为空。 + delete: + title: 删除 + question: >- + 我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_question_deleted: 此问题已被删除 + tip_answer_deleted: 此回答已被删除 + btns: + confirm: 确认 + cancel: 取消 + save: 保存 + delete: 删除 + login: 登录 + signup: 注册 + logout: 退出登录 + verify: 验证 + add_question: 我要提问 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: '{{count}} 个结果' + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + tips: + title: 高级搜索提示 + tag: '<1>[tag] 在指定标签中搜索' + user: '<1>user:username 根据作者搜索' + answer: '<1>answers:0 搜索未回答的问题' + score: '<1>score:3 评分 3 分或以上' + question: '<1>is:question 只搜索问题' + is_answer: '<1>is:answer 只搜索回答' + empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 + share: + name: 分享 + copy: 复制链接 + via: 分享在... + copied: 已复制 + facebook: 分享到 Facebook + twitter: 分享到 Twitter + cannot_vote_for_self: 不能给自己投票 + modal_confirm: + title: 发生错误... + account_result: + page_title: 欢迎来到 Answer + success: 你的账号已通过验证,即将返回首页。 + link: 返回首页 + invalid: 抱歉,此验证链接已失效。也许是你的账号已经通过验证了? + confirm_new_email: 你的电子邮箱已更新 + confirm_new_email_invalid: 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? + question: + following_tags: 已关注的标签 + edit: 编辑 + save: 保存 + follow_tag_tip: 按照标签整理您的问题列表。 + hot_questions: 热点问题 + all_questions: 全部问题 + x_questions: '{{ count }} 个问题' + x_answers: '{{ count }} 个回答' + questions: 个问题 + answers: 回答 + newest: 最新 + active: 活跃 + frequent: 浏览量 + score: 评分 + unanswered: 未回答 + modified: 修改于 + answered: 回答于 + asked: 提问于 + closed: 已关闭 + follow_a_tag: 关注一个标签 + more: 更多 + personal: + overview: 概览 + answers: 回答 + answer: 回答 + questions: 问题 + question: 问题 + bookmarks: 收藏 + reputation: 声望 + comments: 评论 + votes: 得票 + newest: 最新 + score: 评分 + edit_profile: 编辑我的资料 + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: 加入于 + last_login: 上次登录 + about_me: 关于我 + about_me_empty: '// Hello, World !' + top_answers: 热门回答 + top_questions: 热门问题 + stats: 状态 + list_empty: 没有找到相关的内容。
试试看其他标签? + accepted: 已采纳 + answered: 回答于 + asked: 提问于 + upvote: 赞同 + downvote: 反对 + mod_short: 管理员 + mod_long: 管理员 + x_reputation: 声望 + x_votes: 得票 + x_answers: 个回答 + x_questions: 个问题 + page_404: + description: 页面不存在 + back_home: 回到主页 + page_50X: + description: 服务器遇到了一个错误,无法完成你的请求。 + back_home: 回到主页 + admin: + admin_header: + title: 后台管理 + nav_menus: + dashboard: 后台管理 + contents: 内容管理 + questions: 问题 + answers: 回答 + users: 用户管理 + flags: 举报管理 + settings: 站点设置 + general: 一般 + interface: 界面 + smtp: SMTP + dashboard: + title: 后台管理 + welcome: 欢迎来到 Answer 后台管理! + version: 版本 + flags: + title: 举报 + pending: 等待处理 + completed: 已完成 + flagged: 被举报内容 + created: 创建于 + action: 操作 + review: 审查 + change_modal: + title: 更改用户状态为... + btn_cancel: 取消 + btn_submit: 提交 + normal_name: 正常 + normal_description: 正常状态的用户可以提问和回答。 + suspended_name: 封禁 + suspended_description: 被封禁的用户将无法登录。 + deleted_name: 删除 + deleted_description: 删除用户的个人信息,认证等等。 + inactive_name: 不活跃 + inactive_description: 不活跃的用户必须重新验证邮箱。 + confirm_title: 删除此用户 + confirm_content: 确定要删除此用户?此操作无法撤销! + confirm_btn: 删除 + msg: + empty: 请选择一个原因 + status_modal: + title: '更改 {{ type }} 状态为...' + normal_name: 正常 + normal_description: 所有用户都可以访问 + closed_name: 关闭 + closed_description: 不能回答,但仍然可以编辑、投票和评论。 + deleted_name: 删除 + deleted_description: 所有获得/损失的声望将会恢复。 + btn_cancel: 取消 + btn_submit: 提交 + btn_next: 下一步 + users: + title: 用户 + name: 名称 + email: 邮箱 + reputation: 声望 + created_at: 创建时间 + delete_at: 删除时间 + suspend_at: 封禁时间 + status: 状态 + action: 操作 + change: 更改 + all: 全部 + inactive: 不活跃 + suspended: 已封禁 + deleted: 已删除 + normal: 正常 + questions: + page_title: 问题 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + post: 标题 + votes: 得票数 + answers: 回答数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + answers: + page_title: 回答 + normal: 正常 + deleted: 已删除 + post: 标题 + votes: 得票数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + general: + page_title: 一般 + name: + label: 站点名称 + msg: 不能为空 + text: 站点的名称,作为站点的标题(HTML 的 title 标签)。 + short_description: + label: 简短的站点标语 (可选) + msg: 不能为空 + text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。 + description: + label: 网站描述 (可选) + msg: 不能为空 + text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。 + interface: + page_title: 界面 + logo: + label: Logo (可选) + msg: 不能为空 + text: 可以上传图片,或者<1>重置为站点标题。 + theme: + label: 主题 + msg: 不能为空 + text: 选择一个主题 + language: + label: 界面语言 + msg: 不能为空 + text: 设置用户界面语言,在刷新页面后生效。 + smtp: + page_title: SMTP + from_email: + label: 发件人地址 + msg: 不能为空 + text: 用于发送邮件的地址。 + from_name: + label: 发件人名称 + msg: 不能为空 + text: 发件人的名称 + smtp_host: + label: SMTP 主机 + msg: 不能为空 + text: 邮件服务器 + encryption: + label: 加密 + msg: 不能为空 + text: 对于大多数服务器而言,SSL 是推荐开启的。 + ssl: SSL + none: 无加密 + smtp_port: + label: SMTP 端口 + msg: SMTP 端口必须在 1 ~ 65535 之间。 + text: 邮件服务器的端口号。 + smtp_username: + label: SMTP 用户名 + msg: 不能为空 + smtp_password: + label: SMTP 密码 + msg: 不能为空 + test_email_recipient: + label: 测试邮件收件人 + text: 提供用于接收测试邮件的邮箱地址。 + msg: 地址无效 + smtp_authentication: + label: SMTP 认证 + msg: 不能为空 + 'yes': 是 + 'no': 否 diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index d42b60582..2d305bb2e 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -12,13 +12,12 @@ import { interfaceStore } from '@/stores'; import { UploadImg } from '@/components'; import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; import { - getAdminLanguageOptions, uploadAvatar, updateInterfaceSetting, useInterfaceSetting, useThemeOptions, } from '@/services'; -import { setupAppLanguage } from '@/utils/localize'; +import { setupAppLanguage, loadLanguageOptions } from '@/utils/localize'; const Interface: FC = () => { const { t } = useTranslation('translation', { @@ -53,7 +52,7 @@ const Interface: FC = () => { }, }); const getLangs = async () => { - const res: LangsType[] = await getAdminLanguageOptions(); + const res: LangsType[] = await loadLanguageOptions(true); setLangs(res); }; // set default theme value diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index fb78f608b..7d60dea85 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import type { LangsType, FormDataType } from '@/common/interface'; import { useToast } from '@/hooks'; -import { getLanguageOptions, updateUserInterface } from '@/services'; +import { updateUserInterface } from '@/services'; import { localize } from '@/utils'; import { loggedUserInfoStore } from '@/stores'; @@ -24,7 +24,7 @@ const Index = () => { }); const getLangs = async () => { - const res: LangsType[] = await getLanguageOptions(); + const res: LangsType[] = await localize.loadLanguageOptions(); setLangs(res); }; diff --git a/ui/src/react-app-env.d.ts b/ui/src/react-app-env.d.ts index 6431bc5fc..c40b86b3c 100644 --- a/ui/src/react-app-env.d.ts +++ b/ui/src/react-app-env.d.ts @@ -1 +1,2 @@ /// +declare module '*.yaml'; diff --git a/ui/src/services/client/settings.ts b/ui/src/services/client/settings.ts index a47262eb0..27fbe1a9c 100644 --- a/ui/src/services/client/settings.ts +++ b/ui/src/services/client/settings.ts @@ -1,9 +1,7 @@ -// import useSWR from 'swr'; - import request from '@/utils/request'; import type * as Type from '@/common/interface'; -export const loadLang = () => { +export const getLanguageConfig = () => { return request.get('/answer/api/v1/language/config'); }; diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index dcc2d0840..31bb0a40d 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -85,6 +85,7 @@ function formatUptime(value) { return `< 1 ${t('dates.hour')}`; } + export { getQueryString, thousandthDivision, diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts index 0c6313b66..a9260905f 100644 --- a/ui/src/utils/localize.ts +++ b/ui/src/utils/localize.ts @@ -2,10 +2,68 @@ import dayjs from 'dayjs'; import i18next from 'i18next'; import { interfaceStore, loggedUserInfoStore } from '@/stores'; -import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; +import { + CURRENT_LANG_STORAGE_KEY, + DEFAULT_LANG, + LANG_RESOURCE_STORAGE_KEY, +} from '@/common/constants'; import { Storage } from '@/utils'; +import { + getAdminLanguageOptions, + getLanguageConfig, + getLanguageOptions, +} from '@/services'; -const localDayjs = (langName) => { +export const loadLanguageOptions = async (forAdmin = false) => { + const languageOptions = forAdmin + ? await getAdminLanguageOptions() + : await getLanguageOptions(); + if (process.env.NODE_ENV === 'development') { + const { default: optConf } = await import('@/i18n/locales/i18n.yaml'); + optConf?.language_options.forEach((opt) => { + if (!languageOptions.find((_) => opt.label === _.label)) { + languageOptions.push(opt); + } + }); + } + return languageOptions; +}; + +const addI18nResource = async (langName) => { + const res = { lng: langName, resources: undefined }; + if (process.env.NODE_ENV === 'development') { + try { + const { default: resConf } = await import( + `@/i18n/locales/${langName}.yaml` + ); + res.resources = resConf.ui; + } catch (ex) { + console.log('ex: ', ex); + } + } else { + const storageResource = Storage.get(LANG_RESOURCE_STORAGE_KEY); + if (storageResource?.lng === res.lng) { + res.resources = storageResource.resources; + } else { + const langConf = await getLanguageConfig(); + if (langConf) { + res.resources = langConf; + } + } + } + if (res.resources) { + i18next.addResourceBundle( + res.lng, + 'translation', + res.resources, + true, + true, + ); + Storage.set(LANG_RESOURCE_STORAGE_KEY, res); + } +}; + +const localeDayjs = (langName) => { langName = langName.replace('_', '-').toLowerCase(); dayjs.locale(langName); }; @@ -13,19 +71,22 @@ const localDayjs = (langName) => { export const getCurrentLang = () => { const loggedUser = loggedUserInfoStore.getState().user; const adminInterface = interfaceStore.getState().interface; - const storageLang = Storage.get(CURRENT_LANG_STORAGE_KEY); + const fallbackLang = Storage.get(CURRENT_LANG_STORAGE_KEY) || DEFAULT_LANG; let currentLang = loggedUser.language; // `default` mean use language value from admin interface - if (/default/i.test(currentLang) && adminInterface.language) { + if (/default/i.test(currentLang)) { currentLang = adminInterface.language; } - currentLang ||= storageLang || DEFAULT_LANG; + currentLang ||= fallbackLang; return currentLang; }; -export const setupAppLanguage = () => { +export const setupAppLanguage = async () => { const lang = getCurrentLang(); - localDayjs(lang); + if (!i18next.getDataByLanguage(lang)) { + await addI18nResource(lang); + } + localeDayjs(lang); i18next.changeLanguage(lang); }; diff --git a/ui/tsconfig.json b/ui/tsconfig.json index f270b720b..000cc227a 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -23,5 +23,5 @@ "@/*": ["src/*"] } }, - "include": ["src", "node_modules/@testing-library/jest-dom"] + "include": ["src", "node_modules/@testing-library/jest-dom", "scripts"] } From 31d6a980416534c93b8084dfe304ec77b89ad2ff Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 8 Nov 2022 17:44:32 +0800 Subject: [PATCH 0199/3337] feat(i18n): merge i18n files done! --- i18n/en_US.yaml | 947 ++++++++++++++++++++++++++++++++++++++++++++++++ i18n/zh_CN.yaml | 747 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 1694 insertions(+) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 824c92e66..27300e929 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -170,3 +170,950 @@ notification: other: "Your answer has been deleted" your_comment_was_deleted: other: "Your comment has been deleted" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + description: >- +
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by + placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Webite Maintenance + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: 'Your account was suspended until {{ time }}.' + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4MB. + description: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + description: 'Must use the character set "a-z", "0-9", "+ # - ."' + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + description: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allowed deleting tag with posts.

Please remove this tag + from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: 'Must use the character set "a-z", "0-9", "+ # - ."' + description: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: 'MMM D, YYYY' + long_date_with_time: 'MMM D, YYYY [at] HH:mm' + now: now + x_seconds_ago: '{{count}}s ago' + x_minutes_ago: '{{count}}m ago' + x_hours_ago: '{{count}}h ago' + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid + answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are + adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_description: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: 'Describe what your question is about, at least one tag is required.' + no_result: No tags matched + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. + Please follow the instructions in the mail to activate your account. + info: 'If it doesn''t arrive, check your spam folder.' + another: >- + We sent another activation email to you at {{mail}}. It might + take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to Answer + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name up to 30 characters. + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in + page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is + already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + btn_name: Update profile + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: Default + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: 'https://example.com' + msg: Website incorrect format + location: + label: Location (optional) + placeholder: 'City, Country' + notification: + email: + label: Email Notifications + radio: 'Answers to your questions, comments, and more' + account: + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation + instructions. + email: + label: Email + msg: Email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the + edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because + doing so deprives future readers of this knowledge.

Repeated deletion + of answered questions can result in your account being blocked from asking. + Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because + doing so deprives future readers of this knowledge.

Repeated deletion + of accepted answers can result in your account being blocked from answering. + Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: '{{count}} Results' + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + tips: + title: Advanced Search Tips + tag: '<1>[tag] search withing a tag' + user: '<1>user:username search by author' + answer: '<1>answers:0 unanswered questions' + score: '<1>score:3 posts with a 3+ score' + question: '<1>is:question search questions' + is_answer: '<1>is:answer search answers' + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to Twitter + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your + account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was + already changed? + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: '{{ count }} Questions' + x_answers: '{{ count }} answers' + questions: Questions + answers: Answers + newest: Newest + active: Active + frequent: Frequent + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: '// Hello, World !' + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Answer + next: Next + done: Done + config_yaml_error: Can’t create the config.yaml file. + lang: + label: Please choose a language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: 'db:3306' + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + description: >- + You can create the <1>config.yaml file manually in the + <1>/var/wwww/xxx/ directory and paste the following text into it. + info: 'After you’ve done that, click “Next” button.' + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure + location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your Answer is Ready! + ready_description: >- + If you ever feel like changing more settings, visit <1>admin section; + find it in the site menu. + good_luck: 'Have fun, and good luck!' + warning: Warning + warning_description: >- + The file <1>config.yaml already exists. If you need to reset any of the + configuration items in this file, please delete it first. You may try + <2>installing now. + installed: Already installed + installed_description: >- + You appear to have already installed. To reinstall please clear your old + database tables first. + upgrade: + title: Answer + update_btn: Update data + update_title: Data update required + update_description: >- + <1>Answer has been updated! Before you continue, we have to update your data + to the newest version.<1>The update process may take a little while, so + please be patient. + done_title: No update required + done_btn: Done + done_desscription: Your Answer data is already up-to-date. + page_404: + description: 'Unfortunately, this page doesn''t exist.' + back_home: Back to homepage + page_50X: + description: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + description: 'We are under maintenance, we’ll be back soon.' + admin: + admin_header: + title: Admin + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + dashboard: + title: Dashboard + welcome: Welcome to Answer Admin ! + site_statistics: Site Statistics + questions: 'Questions:' + answers: 'Answers:' + comments: 'Comments:' + votes: 'Votes:' + active_users: 'Active users:' + flags: 'Flags:' + site_health_status: Site Health Status + version: 'Version:' + https: 'HTTPS:' + uploading_files: 'Uploading files:' + smtp: 'SMTP:' + timezone: 'Timezone:' + system_info: System Info + storage_used: 'Storage used:' + uptime: 'Uptime:' + answer_links: Answer Links + documents: Documents + feedback: Feedback + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + 'yes': 'Yes' + 'no': 'No' + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_description: A normal user can ask and answer questions. + suspended_name: suspended + suspended_description: A suspended user can't log in. + deleted_name: deleted + deleted_description: 'Delete profile, authentication associations.' + inactive_name: inactive + inactive_description: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: 'Change {{ type }} status to...' + normal_name: normal + normal_description: A normal post available to everyone. + closed_name: closed + closed_description: 'A closed question can''t answer, but still can edit, vote and comment.' + deleted_name: deleted + deleted_description: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + action: Action + change: Change + all: All + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + filter: + placeholder: 'Filter by name, user:id' + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, question:id' + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, answer:id' + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: 'The name of this site, as used in the title tag.' + site_url: + label: Site URL + msg: Site url cannot be empty. + text: The address of your site. + short_description: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: 'Short description, as used in the title tag on homepage.' + description: + label: Site Description (optional) + msg: Site description cannot be empty. + text: 'Describe this site in one sentence, as used in the meta description tag.' + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a UTC (Coordinated Universal Time) time offset. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: SMTP Authentication + msg: SMTP authentication cannot be empty. + 'yes': 'Yes' + 'no': 'No' diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 7d76cc061..34fd9dc98 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -170,3 +170,750 @@ notification: other: "你的答案已被删除" your_comment_was_deleted: other: "你的评论已被删除" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何设定文本格式 + description: >- +
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 + **粗体**

  • 使用 4 + 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 + `像 _这样_`

  • 使用```创建代码块

    ```
    // + 这是代码
    ```
+ pagination: + prev: 上一页 + next: 下一页 + page_title: + question: 问题 + questions: 问题 + tag: 标签 + tags: 标签 + tag_wiki: 标签 wiki + edit_tag: 编辑标签 + ask_a_question: 提问题 + edit_question: 编辑问题 + edit_answer: 编辑回答 + search: 搜索 + posts_containing: 包含 + settings: 设定 + notifications: 通知 + login: 登录 + sign_up: 注册 + account_recovery: 账号恢复 + account_activation: 账号激活 + confirm_email: 确认电子邮件 + account_suspended: 账号已封禁 + admin: 后台管理 + notifications: + title: 通知 + inbox: 收件箱 + achievement: 成就 + all_read: 全部标记为已读 + show_more: 显示更多 + suspended: + title: 账号已封禁 + until_time: '你的账号被封禁至{{ time }}。' + forever: 你的账号已被永久封禁。 + end: 违反了我们的社区准则。 + editor: + blockquote: + text: 引用 + bold: + text: 粗体 + chart: + text: 图表 + flow_chart: 流程图 + sequence_diagram: 时序图 + class_diagram: 类图 + state_diagram: 状态图 + entity_relationship_diagram: ER 图 + user_defined_diagram: User defined diagram + gantt_chart: 甘特图 + pie_chart: 饼图 + code: + text: 代码块 + add_code: 添加代码块 + form: + fields: + code: + label: 代码块 + msg: + empty: 代码块不能为空 + language: + label: 语言 (可选) + placeholder: 自动识别 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 行内公式 + block: 公式块 + heading: + text: 标题 + options: + h1: 标题 1 + h2: 标题 2 + h3: 标题 3 + h4: 标题 4 + h5: 标题 5 + h6: 标题 6 + help: + text: 帮助 + hr: + text: 水平分割线 + image: + text: 图片 + add_image: 添加图片 + tab_image: 上传图片 + form_image: + fields: + file: + label: 图片文件 + btn: 选择图片 + msg: + empty: 请选择图片文件。 + only_image: 只能上传图片文件。 + max_size: 图片文件大小不能超过 4 MB。 + description: + label: 图片描述(可选) + tab_url: 网络图片 + form_url: + fields: + url: + label: 图片地址 + msg: + empty: 图片地址不能为空 + name: + label: 图片描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上传中... + indent: + text: 添加缩进 + outdent: + text: 减少缩进 + italic: + text: 斜体 + link: + text: 超链接 + add_link: 添加超链接 + form: + fields: + url: + label: 链接 + msg: + empty: 链接不能为空。 + name: + label: 链接描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: 有编号列表 + unordered_list: + text: 无编号列表 + table: + text: 表格 + heading: 表头 + cell: 单元格 + close_modal: + title: 关闭原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空。 + msg: + empty: 请选择一个原因。 + report_modal: + flag_title: 举报原因是... + close_title: 关闭原因是... + review_question_title: 审查问题 + review_answer_title: 审查回答 + review_comment_title: 审查评论 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空 + msg: + empty: 请选择一个原因。 + tag_modal: + title: 创建新标签 + form: + fields: + display_name: + label: 显示名称(别名) + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + slug_name: + label: URL 固定链接 + description: '必须由 "a-z", "0-9", "+ # - ." 组成' + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + character: 包含非法字符 + description: + label: 标签描述(可选) + btn_cancel: 取消 + btn_submit: 提交 + tag_info: + created_at: 创建于 + edited_at: 编辑于 + synonyms: + title: 同义词 + text: 以下标签等同于 + empty: 此标签目前没有同义词。 + btn_add: 添加同义词 + btn_edit: 编辑 + btn_save: 保存 + synonyms_text: 以下标签等同于 + delete: + title: 删除标签 + content:

不允许删除有关联问题的标签。

请先从关联的问题中删除此标签的引用。

+ content2: 确定要删除吗? + close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + form: + fields: + revision: + label: 编辑历史 + display_name: + label: 名称 + slug_name: + label: URL 固定链接 + info: '必须由 "a-z", "0-9", "+ # - ." 组成' + description: + label: 描述 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: YYYY年MM月 + long_date_with_year: YYYY年MM月DD日 + long_date_with_time: 'YYYY年MM月DD日 HH:mm' + now: 刚刚 + x_seconds_ago: '{{count}} 秒前' + x_minutes_ago: '{{count}} 分钟前' + x_hours_ago: '{{count}} 小时前' + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存 + btn_cancel: 取消 + show_more: 显示更多评论 + tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。 + tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名过滤 + no_description: 此标签无描述。 + more: 更多 + ask: + title: 提交新的问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + similar_questions: 相似的问题 + form: + fields: + revision: + label: 编辑历史 + title: + label: 标题 + placeholder: 请详细描述你的问题 + msg: + empty: 标题不能为空 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 直接发表回答 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: 选择至少一个与问题相关的标签。 + no_result: 没有匹配的标签 + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + profile: 用户主页 + setting: 账号设置 + logout: 退出登录 + admin: 后台管理 + search: + placeholder: 搜索 + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: 更改图片 + loading: 加载中... + pic_auth_code: + title: 验证码 + placeholder: 输入图片中的文字 + msg: + empty: 不能为空 + inactive: + first: '马上就好了!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活您的帐户。' + info: 如果没有收到,请检查您的垃圾邮件文件夹。 + another: '我们向您发送了另一封激活电子邮件,地址为 {{mail}}。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。' + btn_name: 重新发送激活邮件 + msg: + empty: 不能为空 + login: + page_title: 欢迎来到 Answer + info_sign: 没有帐户?<1>注册 + info_login: 已经有一个帐户?<1>登录 + forgot_pass: 忘记密码? + name: + label: 昵称 + msg: + empty: 昵称不能为空 + range: 昵称最多 30 个字符 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password: + label: 密码 + msg: + empty: 密码不能为空 + different: 两次输入密码不一致 + account_forgot: + page_title: 忘记密码 + btn_name: 发送恢复邮件 + send_success: '如无意外,你的邮箱 {{mail}} 将会收到一封重置密码的邮件,请根据指引重置你的密码。' + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password_reset: + page_title: 密码重置 + btn_name: 重置我的密码 + reset_success: 你已经成功更改密码,将返回登录页面 + link_invalid: 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? + to_login: 前往登录页面 + password: + label: 密码 + msg: + empty: 密码不能为空 + length: 密码长度在8-32个字符之间 + different: 两次输入密码不一致 + password_confirm: + label: 确认新密码 + settings: + page_title: 设置 + nav: + profile: 我的资料 + notification: 通知 + account: 账号 + interface: 界面 + profile: + btn_name: 保存更改 + display_name: + label: 昵称 + msg: 昵称不能为空 + msg_range: 昵称不能超过 30 个字符 + username: + label: 用户名 + caption: 用户之间可以通过 "@用户名" 进行交互。 + msg: 用户名不能为空 + msg_range: 用户名不能超过 30 个字符 + character: '用户名只能由 "a-z", "0-9", " - . _" 组成' + avatar: + label: 头像 + text: 您可以上传图片作为头像,也可以 <1>重置 为 + bio: + label: 关于我 (可选) + website: + label: 网站 (可选) + placeholder: 'https://example.com' + msg: 格式不正确 + location: + label: 位置 (可选) + placeholder: '城市, 国家' + notification: + email: + label: 邮件通知 + radio: 你的提问有新的回答,评论,和其他 + account: + change_email_btn: 更改邮箱 + change_pass_btn: 更改密码 + change_email_info: 邮件已发送。请根据指引完成验证。 + email: + label: 邮箱 + msg: 邮箱不能为空 + password_title: 密码 + current_pass: + label: 当前密码 + msg: + empty: 当前密码不能为空 + length: 密码长度必须在 8 至 32 之间 + different: 两次输入的密码不匹配 + new_pass: + label: 新密码 + pass_confirm: + label: 确认新密码 + interface: + lang: + label: 界面语言 + text: 设置用户界面语言,在刷新页面后生效。 + toast: + update: 更新成功 + update_password: 更改密码成功。 + flag_success: 感谢您的标记,我们会尽快处理。 + related_question: + title: 相关问题 + btn: 我要提问 + answers: 个回答 + question_detail: + Asked: 提问于 + asked: 提问于 + update: 修改于 + edit: 最后编辑于 + Views: 阅读次数 + Follow: 关注此问题 + Following: 已关注 + answered: 回答于 + closed_in: 关闭于 + show_exist: 查看相关问题。 + answers: + title: 个回答 + score: 评分 + newest: 最新 + btn_accept: 采纳 + btn_accepted: 已被采纳 + write_answer: + title: 你的回答 + btn_name: 提交你的回答 + confirm_title: 继续回答 + continue: 继续 + confirm_info:

您确定要提交一个新的回答吗?

您可以直接编辑和改善您之前的回答的。

+ empty: 回答内容不能为空。 + delete: + title: 删除 + question: >- + 我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_question_deleted: 此问题已被删除 + tip_answer_deleted: 此回答已被删除 + btns: + confirm: 确认 + cancel: 取消 + save: 保存 + delete: 删除 + login: 登录 + signup: 注册 + logout: 退出登录 + verify: 验证 + add_question: 我要提问 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: '{{count}} 个结果' + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + tips: + title: 高级搜索提示 + tag: '<1>[tag] 在指定标签中搜索' + user: '<1>user:username 根据作者搜索' + answer: '<1>answers:0 搜索未回答的问题' + score: '<1>score:3 评分 3 分或以上' + question: '<1>is:question 只搜索问题' + is_answer: '<1>is:answer 只搜索回答' + empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 + share: + name: 分享 + copy: 复制链接 + via: 分享在... + copied: 已复制 + facebook: 分享到 Facebook + twitter: 分享到 Twitter + cannot_vote_for_self: 不能给自己投票 + modal_confirm: + title: 发生错误... + account_result: + page_title: 欢迎来到 Answer + success: 你的账号已通过验证,即将返回首页。 + link: 返回首页 + invalid: 抱歉,此验证链接已失效。也许是你的账号已经通过验证了? + confirm_new_email: 你的电子邮箱已更新 + confirm_new_email_invalid: 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? + question: + following_tags: 已关注的标签 + edit: 编辑 + save: 保存 + follow_tag_tip: 按照标签整理您的问题列表。 + hot_questions: 热点问题 + all_questions: 全部问题 + x_questions: '{{ count }} 个问题' + x_answers: '{{ count }} 个回答' + questions: 个问题 + answers: 回答 + newest: 最新 + active: 活跃 + frequent: 浏览量 + score: 评分 + unanswered: 未回答 + modified: 修改于 + answered: 回答于 + asked: 提问于 + closed: 已关闭 + follow_a_tag: 关注一个标签 + more: 更多 + personal: + overview: 概览 + answers: 回答 + answer: 回答 + questions: 问题 + question: 问题 + bookmarks: 收藏 + reputation: 声望 + comments: 评论 + votes: 得票 + newest: 最新 + score: 评分 + edit_profile: 编辑我的资料 + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: 加入于 + last_login: 上次登录 + about_me: 关于我 + about_me_empty: '// Hello, World !' + top_answers: 热门回答 + top_questions: 热门问题 + stats: 状态 + list_empty: 没有找到相关的内容。
试试看其他标签? + accepted: 已采纳 + answered: 回答于 + asked: 提问于 + upvote: 赞同 + downvote: 反对 + mod_short: 管理员 + mod_long: 管理员 + x_reputation: 声望 + x_votes: 得票 + x_answers: 个回答 + x_questions: 个问题 + page_404: + description: 页面不存在 + back_home: 回到主页 + page_50X: + description: 服务器遇到了一个错误,无法完成你的请求。 + back_home: 回到主页 + admin: + admin_header: + title: 后台管理 + nav_menus: + dashboard: 后台管理 + contents: 内容管理 + questions: 问题 + answers: 回答 + users: 用户管理 + flags: 举报管理 + settings: 站点设置 + general: 一般 + interface: 界面 + smtp: SMTP + dashboard: + title: 后台管理 + welcome: 欢迎来到 Answer 后台管理! + version: 版本 + flags: + title: 举报 + pending: 等待处理 + completed: 已完成 + flagged: 被举报内容 + created: 创建于 + action: 操作 + review: 审查 + change_modal: + title: 更改用户状态为... + btn_cancel: 取消 + btn_submit: 提交 + normal_name: 正常 + normal_description: 正常状态的用户可以提问和回答。 + suspended_name: 封禁 + suspended_description: 被封禁的用户将无法登录。 + deleted_name: 删除 + deleted_description: 删除用户的个人信息,认证等等。 + inactive_name: 不活跃 + inactive_description: 不活跃的用户必须重新验证邮箱。 + confirm_title: 删除此用户 + confirm_content: 确定要删除此用户?此操作无法撤销! + confirm_btn: 删除 + msg: + empty: 请选择一个原因 + status_modal: + title: '更改 {{ type }} 状态为...' + normal_name: 正常 + normal_description: 所有用户都可以访问 + closed_name: 关闭 + closed_description: 不能回答,但仍然可以编辑、投票和评论。 + deleted_name: 删除 + deleted_description: 所有获得/损失的声望将会恢复。 + btn_cancel: 取消 + btn_submit: 提交 + btn_next: 下一步 + users: + title: 用户 + name: 名称 + email: 邮箱 + reputation: 声望 + created_at: 创建时间 + delete_at: 删除时间 + suspend_at: 封禁时间 + status: 状态 + action: 操作 + change: 更改 + all: 全部 + inactive: 不活跃 + suspended: 已封禁 + deleted: 已删除 + normal: 正常 + questions: + page_title: 问题 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + post: 标题 + votes: 得票数 + answers: 回答数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + answers: + page_title: 回答 + normal: 正常 + deleted: 已删除 + post: 标题 + votes: 得票数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + general: + page_title: 一般 + name: + label: 站点名称 + msg: 不能为空 + text: 站点的名称,作为站点的标题(HTML 的 title 标签)。 + short_description: + label: 简短的站点标语 (可选) + msg: 不能为空 + text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。 + description: + label: 网站描述 (可选) + msg: 不能为空 + text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。 + interface: + page_title: 界面 + logo: + label: Logo (可选) + msg: 不能为空 + text: 可以上传图片,或者<1>重置为站点标题。 + theme: + label: 主题 + msg: 不能为空 + text: 选择一个主题 + language: + label: 界面语言 + msg: 不能为空 + text: 设置用户界面语言,在刷新页面后生效。 + smtp: + page_title: SMTP + from_email: + label: 发件人地址 + msg: 不能为空 + text: 用于发送邮件的地址。 + from_name: + label: 发件人名称 + msg: 不能为空 + text: 发件人的名称 + smtp_host: + label: SMTP 主机 + msg: 不能为空 + text: 邮件服务器 + encryption: + label: 加密 + msg: 不能为空 + text: 对于大多数服务器而言,SSL 是推荐开启的。 + ssl: SSL + none: 无加密 + smtp_port: + label: SMTP 端口 + msg: SMTP 端口必须在 1 ~ 65535 之间。 + text: 邮件服务器的端口号。 + smtp_username: + label: SMTP 用户名 + msg: 不能为空 + smtp_password: + label: SMTP 密码 + msg: 不能为空 + test_email_recipient: + label: 测试邮件收件人 + text: 提供用于接收测试邮件的邮箱地址。 + msg: 地址无效 + smtp_authentication: + label: SMTP 认证 + msg: 不能为空 + 'yes': 是 + 'no': 否 From 5952114feb24965f92eec19f11e9d441ba0209c0 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Tue, 8 Nov 2022 18:10:51 +0800 Subject: [PATCH 0200/3337] refactor(i18n): remove fs.existsSync in resolvePresetLocales --- ui/scripts/i18n-locale-tool.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ui/scripts/i18n-locale-tool.js b/ui/scripts/i18n-locale-tool.js index 456b117b9..05d55a50e 100644 --- a/ui/scripts/i18n-locale-tool.js +++ b/ui/scripts/i18n-locale-tool.js @@ -46,10 +46,7 @@ const autoSync = () => { const resolvePresetLocales = () => { PRESET_LANG.forEach((lng) => { const sp = path.resolve(SRC_PATH, `${lng}.yaml`); - const tp = path.resolve(DEST_PATH, `${lng}.yaml`); - if (fs.existsSync(tp) === false) { - copyLocaleFile(sp); - } + copyLocaleFile(sp); }); }; From 8dda61d6855af817f809cc98fee30122ad12612d Mon Sep 17 00:00:00 2001 From: shuai Date: Tue, 8 Nov 2022 18:17:08 +0800 Subject: [PATCH 0201/3337] fix: install page add title and install page forms add default value --- ui/src/components/PageTitle/index.tsx | 2 +- ui/src/i18n/locales/en_US.yaml | 49 ++++++++----------- ui/src/i18n/locales/zh_CN.yaml | 25 +++++----- .../Install/components/SecondStep/index.tsx | 2 - ui/src/pages/Install/index.tsx | 8 +-- ui/src/utils/guard.ts | 1 - 6 files changed, 38 insertions(+), 49 deletions(-) diff --git a/ui/src/components/PageTitle/index.tsx b/ui/src/components/PageTitle/index.tsx index a11ea9928..a581af2e9 100644 --- a/ui/src/components/PageTitle/index.tsx +++ b/ui/src/components/PageTitle/index.tsx @@ -18,7 +18,7 @@ const PageTitle: FC = ({ title = '', suffix = '' }) => { if (!suffix) { suffix = `${siteInfo.name}`; } - title = title ? `${title} - ${suffix}` : suffix; + title = title ? `${title}${suffix ? ` - ${suffix}` : ''}` : suffix; return <>{setPageTitle(title)}; }; diff --git a/ui/src/i18n/locales/en_US.yaml b/ui/src/i18n/locales/en_US.yaml index 27300e929..9f2a06abc 100644 --- a/ui/src/i18n/locales/en_US.yaml +++ b/ui/src/i18n/locales/en_US.yaml @@ -173,17 +173,17 @@ notification: # The following fields are used for interface presentation(Front-end) ui: how_to_format: - title: How to Format - description: >- -
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by - placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ title: How to Format + description: >- +
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by + placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next @@ -816,7 +816,7 @@ ui: done: Done config_yaml_error: Can’t create the config.yaml file. lang: - label: Please choose a language + label: Please Choose a Language db_type: label: Database Engine db_username: @@ -883,26 +883,15 @@ ui: If you ever feel like changing more settings, visit <1>admin section; find it in the site menu. good_luck: 'Have fun, and good luck!' - warning: Warning - warning_description: >- + warn_title: Warning + warn_description: >- The file <1>config.yaml already exists. If you need to reset any of the - configuration items in this file, please delete it first. You may try - <2>installing now. + configuration items in this file, please delete it first. + install_now: You may try <1>installing now. installed: Already installed installed_description: >- You appear to have already installed. To reinstall please clear your old database tables first. - upgrade: - title: Answer - update_btn: Update data - update_title: Data update required - update_description: >- - <1>Answer has been updated! Before you continue, we have to update your data - to the newest version.<1>The update process may take a little while, so - please be patient. - done_title: No update required - done_btn: Done - done_desscription: Your Answer data is already up-to-date. page_404: description: 'Unfortunately, this page doesn''t exist.' back_home: Back to homepage @@ -927,7 +916,7 @@ ui: smtp: SMTP dashboard: title: Dashboard - welcome: Welcome to Answer Admin ! + welcome: Welcome to Answer Admin! site_statistics: Site Statistics questions: 'Questions:' answers: 'Answers:' @@ -1047,6 +1036,7 @@ ui: site_url: label: Site URL msg: Site url cannot be empty. + validate: Please enter a valid URL. text: The address of your site. short_description: label: Short Site Description (optional) @@ -1059,6 +1049,7 @@ ui: contact_email: label: Contact Email msg: Contact email cannot be empty. + validate: Contact email is not valid. text: Email address of key contact responsible for this site. interface: page_title: Interface @@ -1077,7 +1068,7 @@ ui: time_zone: label: Timezone msg: Timezone cannot be empty. - text: Choose a UTC (Coordinated Universal Time) time offset. + text: Choose a city in the same timezone as you. smtp: page_title: SMTP from_email: diff --git a/ui/src/i18n/locales/zh_CN.yaml b/ui/src/i18n/locales/zh_CN.yaml index 34fd9dc98..a02f42193 100644 --- a/ui/src/i18n/locales/zh_CN.yaml +++ b/ui/src/i18n/locales/zh_CN.yaml @@ -173,17 +173,17 @@ notification: # The following fields are used for interface presentation(Front-end) ui: how_to_format: - title: 如何设定文本格式 - description: >- -
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 - **粗体**

  • 使用 4 - 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 - `像 _这样_`

  • 使用```创建代码块

    ```
    // - 这是代码
    ```
+ title: 如何设定文本格式 + description: >- +
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 + **粗体**

  • 使用 4 + 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 + `像 _这样_`

  • 使用```创建代码块

    ```
    // + 这是代码
    ```
pagination: prev: 上一页 next: 下一页 @@ -396,7 +396,7 @@ ui: btn_save_edits: 保存更改 btn_cancel: 取消 dates: - long_date: YYYY年MM月 + long_date: MM月DD日 long_date_with_year: YYYY年MM月DD日 long_date_with_time: 'YYYY年MM月DD日 HH:mm' now: 刚刚 @@ -917,3 +917,4 @@ ui: msg: 不能为空 'yes': 是 'no': 否 + diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx index 30e48930c..6c7125f86 100644 --- a/ui/src/pages/Install/components/SecondStep/index.tsx +++ b/ui/src/pages/Install/components/SecondStep/index.tsx @@ -148,8 +148,6 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { {t('db_password.label')} { diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 84a350bb2..ae8f037b4 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -43,22 +43,22 @@ const Index: FC = () => { errorMsg: '', }, db_username: { - value: '', + value: 'root', isInvalid: false, errorMsg: '', }, db_password: { - value: '', + value: 'root', isInvalid: false, errorMsg: '', }, db_host: { - value: '', + value: 'db:3306', isInvalid: false, errorMsg: '', }, db_name: { - value: '', + value: 'answer', isInvalid: false, errorMsg: '', }, diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index 7f926fba8..4fb214965 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -185,7 +185,6 @@ export const tryNormalLogged = (canNavigate: boolean = false) => { export const tryLoggedAndActicevated = () => { const gr: TGuardResult = { ok: true }; const us = deriveLoginState(); - console.log('tryLogged', us); if (!us.isLogged || !us.isActivated) { gr.ok = false; } From b474d3ade30a8cde24a1b11f37d358d56e3a3741 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Tue, 8 Nov 2022 18:28:15 +0800 Subject: [PATCH 0202/3337] feat: set install router in release mode. remove install mock router. --- internal/install/install_main.go | 2 + internal/install/install_server.go | 2 +- internal/router/ui.go | 67 ------------------------------ 3 files changed, 3 insertions(+), 68 deletions(-) diff --git a/internal/install/install_main.go b/internal/install/install_main.go index a961c2806..6586b9d36 100644 --- a/internal/install/install_main.go +++ b/internal/install/install_main.go @@ -1,6 +1,7 @@ package install import ( + "fmt" "os" "github.com/answerdev/answer/internal/base/translator" @@ -24,6 +25,7 @@ func Run(configPath string) { if len(port) == 0 { port = "80" } + fmt.Printf("[SUCCESS] answer installation service will run at: http://localhost:%s/install/ \n", port) if err = installServer.Run(":" + port); err != nil { panic(err) } diff --git a/internal/install/install_server.go b/internal/install/install_server.go index a301c1978..b2ada51eb 100644 --- a/internal/install/install_server.go +++ b/internal/install/install_server.go @@ -26,8 +26,8 @@ func (r *_resource) Open(name string) (fs.File, error) { // NewInstallHTTPServer new install http server. func NewInstallHTTPServer() *gin.Engine { + gin.SetMode(gin.ReleaseMode) r := gin.New() - gin.SetMode(gin.DebugMode) r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") }) r.StaticFS("/static", http.FS(&_resource{ fs: ui.Build, diff --git a/internal/router/ui.go b/internal/router/ui.go index 84231b0c0..3498265c6 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -4,16 +4,11 @@ import ( "embed" "fmt" "io/fs" - "math/rand" "net/http" "os" - "github.com/answerdev/answer/i18n" - "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" - "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" ) @@ -45,7 +40,6 @@ func (r *_resource) Open(name string) (fs.File, error) { // Register a new static resource which generated by ui directory func (a *UIRouter) Register(r *gin.Engine) { staticPath := os.Getenv("ANSWER_STATIC_PATH") - r.StaticFS("/i18n/", http.FS(i18n.I18n)) // if ANSWER_STATIC_PATH is set and not empty, ignore embed resource if staticPath != "" { @@ -72,67 +66,6 @@ func (a *UIRouter) Register(r *gin.Engine) { fs: ui.Build, })) - // Install godoc - // @Summary Install - // @Description Install - // @Tags Install - // @Accept json - // @Produce json - // @Success 200 {object} handler.RespBody{} - // @Router /install [get] - r.GET("/install", func(c *gin.Context) { - filePath := "" - var file []byte - var err error - filePath = "build/index.html" - c.Header("content-type", "text/html;charset=utf-8") - file, err = ui.Build.ReadFile(filePath) - if err != nil { - log.Error(err) - c.Status(http.StatusNotFound) - return - } - c.String(http.StatusOK, string(file)) - }) - - r.GET("/installation/language/options", func(c *gin.Context) { - handler.HandleResponse(c, nil, translator.LanguageOptions) - }) - - r.POST("/installation/db/check", func(c *gin.Context) { - num := rand.Intn(10) - if num > 5 { - err := errors.BadRequest("connection error") - handler.HandleResponse(c, err, gin.H{}) - } else { - handler.HandleResponse(c, nil, gin.H{ - "connection_success": true, - }) - } - }) - - r.POST("/installation/config-file/check", func(c *gin.Context) { - num := rand.Intn(10) - if num > 5 { - handler.HandleResponse(c, nil, gin.H{ - "exist": true, - }) - } else { - handler.HandleResponse(c, nil, gin.H{ - "exist": false, - }) - } - - }) - - r.POST("/installation/init", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) - - r.POST("/installation/base-info", func(c *gin.Context) { - handler.HandleResponse(c, nil, gin.H{}) - }) - // specify the not router for default routes and redirect r.NoRoute(func(c *gin.Context) { name := c.Request.URL.Path From 6626f966145a4ce82ca56704c5a0fd88b37d8939 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 10:22:52 +0800 Subject: [PATCH 0203/3337] fix: style adjustment --- ui/src/components/QueryGroup/index.tsx | 6 +++-- ui/src/components/QuestionList/index.tsx | 32 +++++++++++------------- ui/src/index.scss | 5 ++++ 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/ui/src/components/QueryGroup/index.tsx b/ui/src/components/QueryGroup/index.tsx index 15c3ad493..f69e35960 100644 --- a/ui/src/components/QueryGroup/index.tsx +++ b/ui/src/components/QueryGroup/index.tsx @@ -12,6 +12,7 @@ interface Props { sortKey?: string; className?: string; pathname?: string; + wrapClassName?: string; } const MAX_BUTTON_COUNT = 3; const Index: FC = ({ @@ -21,6 +22,7 @@ const Index: FC = ({ i18nKeyPrefix = '', className = '', pathname = '', + wrapClassName = '', }) => { const [searchParams, setUrlSearchParams] = useSearchParams(); const navigate = useNavigate(); @@ -51,7 +53,7 @@ const Index: FC = ({ return (typeof btn === 'string' ? btn : btn.name) === currentSort; }); return ( - + {data.map((btn, index) => { const key = typeof btn === 'string' ? btn : btn.sort; const name = typeof btn === 'string' ? btn : btn.name; @@ -62,7 +64,7 @@ const Index: FC = ({ variant="outline-secondary" active={currentSort === name} className={classNames( - 'text-capitalize', + 'text-capitalize fit-content', data.length > MAX_BUTTON_COUNT && index > MAX_BUTTON_COUNT - 2 && 'd-none d-md-block', diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 469a6e584..a45cd4019 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { Row, Col, ListGroup } from 'react-bootstrap'; +import { ListGroup } from 'react-bootstrap'; import { NavLink, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -103,23 +103,19 @@ const QuestionList: FC = ({ source }) => { return (
- -
-
- {source === 'questions' - ? t('all_questions') - : t('x_questions', { count })} -
- - - - - +
+
+ {source === 'questions' + ? t('all_questions') + : t('x_questions', { count })} +
+ +
{listData?.list?.map((li) => { return ( diff --git a/ui/src/index.scss b/ui/src/index.scss index b225a9240..097dafa0f 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -141,6 +141,11 @@ a { background-color: #fff3cd80; } +.fit-content { + height: fit-content; + flex: none; +} + // fix bug for React-Bootstrap Form.Text .form-text { display: inline-block; From 38c8f80849f5fd6534c7474ebdb6a0b26c37ef55 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 10:24:27 +0800 Subject: [PATCH 0204/3337] feat: remove user status api --- internal/controller/user_controller.go | 16 -------------- internal/router/answer_api_router.go | 1 - internal/service/user_service.go | 29 -------------------------- 3 files changed, 46 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 028f9302f..3d4d2d3ac 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -89,22 +89,6 @@ func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// GetUserStatus get user status info -// @Summary get user status info -// @Description get user status info -// @Tags User -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Success 200 {object} handler.RespBody{data=schema.GetUserResp} -// @Router /answer/api/v1/user/status [get] -func (uc *UserController) GetUserStatus(ctx *gin.Context) { - userID := middleware.GetLoginUserIDFromContext(ctx) - token := middleware.ExtractToken(ctx) - resp, err := uc.userService.GetUserStatus(ctx, userID, token) - handler.HandleResponse(ctx, err, resp) -} - // UserEmailLogin godoc // @Summary UserEmailLogin // @Description UserEmailLogin diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 522eac09b..fc7fab95a 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -91,7 +91,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // user r.GET("/user/info", a.userController.GetUserInfoByUserID) - r.GET("/user/status", a.userController.GetUserStatus) r.GET("/user/action/record", a.userController.ActionRecord) r.POST("/user/login/email", a.userController.UserEmailLogin) r.POST("/user/register/email", a.userController.UserRegisterByEmail) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 76ce0d8a0..07594c90f 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -71,35 +71,6 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st return resp, nil } -// GetUserStatus get user info by user id -func (us *UserService) GetUserStatus(ctx context.Context, userID, token string) (resp *schema.GetUserStatusResp, err error) { - resp = &schema.GetUserStatusResp{} - if len(userID) == 0 { - return resp, nil - } - userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID) - if err != nil { - return nil, err - } - if !exist { - return nil, errors.BadRequest(reason.UserNotFound) - } - - userCacheInfo := &entity.UserCacheInfo{ - UserID: userID, - UserStatus: userInfo.Status, - EmailStatus: userInfo.MailStatus, - } - err = us.authService.UpdateUserCacheInfo(ctx, token, userCacheInfo) - if err != nil { - return nil, err - } - resp = &schema.GetUserStatusResp{ - Status: schema.UserStatusShow[userInfo.Status], - } - return resp, nil -} - func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) ( resp *schema.GetOtherUserInfoResp, err error, ) { From 6800cb43b06e7fae493383045fb039083232b46c Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 10:29:15 +0800 Subject: [PATCH 0205/3337] fix: add i18n key --- i18n/en_US.yaml | 1 + ui/src/i18n/locales/en_US.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 9f2a06abc..fa6a1de34 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -724,6 +724,7 @@ ui: newest: Newest active: Active score: Score + more: More tips: title: Advanced Search Tips tag: '<1>[tag] search withing a tag' diff --git a/ui/src/i18n/locales/en_US.yaml b/ui/src/i18n/locales/en_US.yaml index 9f2a06abc..fa6a1de34 100644 --- a/ui/src/i18n/locales/en_US.yaml +++ b/ui/src/i18n/locales/en_US.yaml @@ -724,6 +724,7 @@ ui: newest: Newest active: Active score: Score + more: More tips: title: Advanced Search Tips tag: '<1>[tag] search withing a tag' From 9509a9a964e3a278f389c48b511e740c8ef8d618 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 11:00:34 +0800 Subject: [PATCH 0206/3337] feat: change site url from config file to database --- internal/install/install_controller.go | 6 ------ .../service/service_config/service_config.go | 1 - internal/service/uploader/upload.go | 10 ++++++++-- internal/service/user_service.go | 18 ++++++++++++++---- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index c732f562c..1bc0984f3 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -156,12 +156,6 @@ func InitBaseInfo(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) return } - c.ServiceConfig.WebHost = req.SiteURL - if err := conf.RewriteConfig(confPath, c); err != nil { - log.Errorf("rewrite config failed %s", err) - handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) - return - } err = migrations.UpdateInstallInfo(c.Data.Database, req.Language, req.SiteName, req.SiteURL, req.ContactEmail, req.AdminName, req.AdminPassword, req.AdminEmail) diff --git a/internal/service/service_config/service_config.go b/internal/service/service_config/service_config.go index 92d46393b..b5c1f56b1 100644 --- a/internal/service/service_config/service_config.go +++ b/internal/service/service_config/service_config.go @@ -2,6 +2,5 @@ package service_config type ServiceConfig struct { SecretKey string `json:"secret_key" mapstructure:"secret_key" yaml:"secret_key"` - WebHost string `json:"web_host" mapstructure:"web_host" yaml:"web_host"` UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"` } diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index fab199e36..f4a8e61dc 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -12,6 +12,7 @@ import ( "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/service/service_config" + "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/uid" "github.com/disintegration/imaging" @@ -27,7 +28,8 @@ const ( // UploaderService user service type UploaderService struct { - serviceConfig *service_config.ServiceConfig + serviceConfig *service_config.ServiceConfig + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewUploaderService new upload service @@ -122,10 +124,14 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.File func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) ( url string, err error) { + siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + return "", err + } filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath) if err := ctx.SaveUploadedFile(file, filePath); err != nil { return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() } - url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, fileSubPath) + url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath) return url, nil } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 07594c90f..a4fc1fad5 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -144,7 +144,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet UserID: userInfo.ID, } code := uuid.NewString() - verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.serviceConfig.WebHost, code) + verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL) if err != nil { return "", err @@ -308,7 +308,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo UserID: userInfo.ID, } code := uuid.NewString() - verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return nil, err @@ -351,7 +351,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e UserID: userInfo.ID, } code := uuid.NewString() - verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) + verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return err @@ -500,7 +500,7 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. } code := uuid.NewString() var title, body string - verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.serviceConfig.WebHost, code) + verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.getSiteUrl(ctx), code) if userInfo.MailStatus == entity.EmailStatusToBeVerified { title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL) } else { @@ -548,3 +548,13 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string } return nil } + +// getSiteUrl get site url +func (us *UserService) getSiteUrl(ctx context.Context) string { + siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get site general failed: %s", err) + return "" + } + return siteGeneral.SiteUrl +} From c7fa313df8965a5d2665772a98d7671d5dd30b6a Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Wed, 9 Nov 2022 11:49:44 +0800 Subject: [PATCH 0207/3337] refactor(ts): remove downlevelIteration flag --- ui/src/pages/Questions/Ask/index.tsx | 2 +- ui/tsconfig.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index 3ed72f843..f8f1ea1e5 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -123,7 +123,7 @@ const Ask = () => { isInvalid: true, errorMsg: t('form.fields.title.msg.empty'), }; - } else if ([...title.value].length > 150) { + } else if (Array.from(title.value).length > 150) { bol = false; formData.title = { value: title.value, diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 000cc227a..3cfbea320 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -7,7 +7,6 @@ "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, - "downlevelIteration": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", From 5d3ee17a255de1d709711ee9a28b504310fda427 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 12:28:19 +0800 Subject: [PATCH 0208/3337] fix: i18n file parsing error --- go.mod | 2 +- go.sum | 2 ++ internal/base/translator/provider.go | 40 +++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d6d85117f..0da95f1c6 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index be85073b8..8a6c2d9bb 100644 --- a/go.sum +++ b/go.sum @@ -596,6 +596,8 @@ github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd143 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A= diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index e047ceb28..527f91379 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -8,7 +8,7 @@ import ( "github.com/google/wire" myTran "github.com/segmentfault/pacman/contrib/i18n" "github.com/segmentfault/pacman/i18n" - "sigs.k8s.io/yaml" + "gopkg.in/yaml.v3" ) // ProviderSet is providers. @@ -31,18 +31,52 @@ var ( // NewTranslator new a translator func NewTranslator(c *I18n) (tr i18n.Translator, err error) { - GlobalTrans, err = myTran.NewTranslator(c.BundleDir) + entries, err := os.ReadDir(c.BundleDir) if err != nil { return nil, err } + // read the Bundle resources file from entries + for _, file := range entries { + // ignore directory + if file.IsDir() { + continue + } + // ignore non-YAML file + if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" { + continue + } + buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name())) + if err != nil { + return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err) + } + + // only parse the backend translation + translation := struct { + Content map[string]interface{} `yaml:"backend"` + }{} + if err = yaml.Unmarshal(buf, &translation); err != nil { + return nil, err + } + content, err := yaml.Marshal(translation.Content) + if err != nil { + return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err) + } + + // add translator use backend translation + if err = myTran.AddTranslator(content, file.Name()); err != nil { + return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err) + } + } + GlobalTrans = myTran.GlobalTrans + i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml")) if err != nil { return nil, fmt.Errorf("read i18n file failed: %s", err) } s := struct { - LangOption []*LangOption `json:"language_options"` + LangOption []*LangOption `yaml:"language_options"` }{} err = yaml.Unmarshal(i18nFile, &s) if err != nil { From afb3ecd63b919d0f301e96de6793b95b2c2aef06 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 13:19:44 +0800 Subject: [PATCH 0209/3337] doc: update i18n file --- i18n/en_US.yaml | 330 ++++++++++++++++++++++++------------------------ i18n/it_IT.yaml | 310 +++++++++++++++++++++++---------------------- i18n/zh_CN.yaml | 311 ++++++++++++++++++++++----------------------- 3 files changed, 478 insertions(+), 473 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index f287444f6..0ae83bf3e 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1,184 +1,186 @@ -base: - success: - other: "Success." - unknown: - other: "Unknown error." - request_format_error: - other: "Request format is not valid." - unauthorized_error: - other: "Unauthorized." - database_error: - other: "Data server error." +# The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." -email: - other: "Email" -password: - other: "Password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email and password do not match." - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Answer do not found." - comment: - edit_without_permission: - other: "Comment are not allowed to edit." - not_found: - other: "Comment not found." email: - duplicate: - other: "Email already exists." - need_to_be_verified: - other: "Email should be verified." - verify_url_expired: - other: "Email verified URL has expired, please resend the email." - lang: - not_found: - other: "Language file not found." - object: - captcha_verification_failed: - other: "Captcha wrong." - disallow_follow: - other: "You are not allowed to follow." - disallow_vote: - other: "You are not allowed to vote." - disallow_vote_your_self: - other: "You can't vote for your own post." - not_found: - other: "Object not found." - verification_failed: - other: "Verification failed." - email_or_password_incorrect: - other: "Email and password do not match." - old_password_verification_failed: - other: "The old password verification failed" - new_password_same_as_previous_setting: - other: "The new password is the same as the previous one." - question: - not_found: - other: "Question not found." - rank: - fail_to_meet_the_condition: - other: "Rank fail to meet the condition." - report: - handle_failed: - other: "Report handle failed." - not_found: - other: "Report not found." - tag: - not_found: - other: "Tag not found." - theme: - not_found: - other: "Theme not found." - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "User not found." - suspended: - other: "User has been suspended." - username_invalid: - other: "Username is invalid." - username_duplicate: - other: "Username is already in use." - set_avatar: - other: "Avatar set failed." + other: "Email" + password: + other: "Password" - config: - read_config_failed: - other: "Read config failed" - database: - connection_failed: - other: "Database connection failed" - install: - create_config_failed: - other: "Can’t create the config.yaml file." -report: - spam: - name: - other: "spam" - description: - other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." - rude: - name: - other: "rude or abusive" - description: - other: "A reasonable person would find this content inappropriate for respectful discourse." - duplicate: - name: - other: "a duplicate" - description: - other: "This question has been asked before and already has an answer." - not_answer: - name: - other: "not an answer" - description: - other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." - not_need: - name: - other: "no longer needed" - description: - other: "This comment is outdated, conversational or not relevant to this post." - other: - name: - other: "something else" - description: - other: "This post requires staff attention for another reason not listed above." + email_or_password_wrong_error: &email_or_password_wrong + other: "Email and password do not match." -question: - close: - duplicate: + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Answer do not found." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + theme: + not_found: + other: "Theme not found." + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + 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" + install: + create_config_failed: + other: "Can’t create the config.yaml file." + report: + spam: name: other: "spam" + description: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + description: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" description: other: "This question has been asked before and already has an answer." - guideline: + not_answer: name: - other: "a community-specific reason" + other: "not an answer" description: - other: "This question doesn't meet a community guideline." - multiple: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: name: - other: "needs details or clarity" + other: "no longer needed" description: - other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" description: - other: "This post requires another reason not listed above." + other: "This post requires staff attention for another reason not listed above." + + question: + close: + duplicate: + name: + other: "spam" + description: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + description: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + description: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + description: + other: "This post requires another reason not listed above." -notification: - action: - update_question: - other: "updated question" - answer_the_question: - other: "answered question" - update_answer: - other: "updated answer" - adopt_answer: - other: "accepted answer" - comment_question: - other: "commented question" - comment_answer: - other: "commented answer" - reply_to_you: - other: "replied to you" - mention_you: - other: "mentioned you" - your_question_is_closed: - other: "Your question has been closed" - your_question_was_deleted: - other: "Your question has been deleted" - your_answer_was_deleted: - other: "Your answer has been deleted" - your_comment_was_deleted: - other: "Your comment has been deleted" + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + adopt_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" # The following fields are used for interface presentation(Front-end) ui: how_to_format: diff --git a/i18n/it_IT.yaml b/i18n/it_IT.yaml index 0b9439412..61de40f89 100644 --- a/i18n/it_IT.yaml +++ b/i18n/it_IT.yaml @@ -1,170 +1,172 @@ -base: - success: - other: "Successo" - unknown: - other: "Errore sconosciuto" - request_format_error: - other: "Il formato della richiesta non è valido" - unauthorized_error: - other: "Non autorizzato" - database_error: - other: "Errore server dati" +# The following fields are used for back-end +backend: + base: + success: + other: "Successo" + unknown: + other: "Errore sconosciuto" + request_format_error: + other: "Il formato della richiesta non è valido" + unauthorized_error: + other: "Non autorizzato" + database_error: + other: "Errore server dati" -email: - other: "email" -password: - other: "password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email o password errati" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Risposta non trovata" - comment: - edit_without_permission: - other: "Non si hanno di privilegi sufficienti per modificare il commento" - not_found: - other: "Commento non trovato" email: - duplicate: - other: "email già esistente" - need_to_be_verified: - other: "email deve essere verificata" - verify_url_expired: - other: "l'url di verifica email è scaduto, si prega di reinviare la email" - lang: - not_found: - other: "lingua non trovata" - object: - captcha_verification_failed: - other: "captcha errato" - disallow_follow: - other: "Non sei autorizzato a seguire" - disallow_vote: - other: "non sei autorizzato a votare" - disallow_vote_your_self: - other: "Non puoi votare un tuo post!" - not_found: - other: "oggetto non trovato" - verification_failed: - other: "verifica fallita" - email_or_password_incorrect: - other: "email o password incorretti" - old_password_verification_failed: - other: "la verifica della vecchia password è fallita" - new_password_same_as_previous_setting: - other: "La nuova password è identica alla precedente" - question: - not_found: - other: "domanda non trovata" - rank: - fail_to_meet_the_condition: - other: "Condizioni non valide per il grado" - report: - handle_failed: - other: "Gestione del report fallita" - not_found: - other: "Report non trovato" - tag: - not_found: - other: "Etichetta non trovata" - theme: - not_found: - other: "tema non trovato" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "utente non trovato" - suspended: - other: "utente sospeso" - username_invalid: - other: "utente non valido" - username_duplicate: - other: "utente già in uso" + other: "email" + password: + other: "password" -report: - spam: - name: - other: "spam" - description: - other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" - rude: - name: - other: "scortese o violento" - description: - other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" - duplicate: - name: - other: "duplicato" - description: - other: "Questa domanda è già stata posta e ha già una risposta." - not_answer: - name: - other: "non è una risposta" - description: - other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." - not_need: - name: - other: "non più necessario" - description: - other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." - other: - name: - other: "altro" - description: - other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." + email_or_password_wrong_error: &email_or_password_wrong + other: "Email o password errati" -question: - close: - duplicate: + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Risposta non trovata" + comment: + edit_without_permission: + other: "Non si hanno di privilegi sufficienti per modificare il commento" + not_found: + other: "Commento non trovato" + email: + duplicate: + other: "email già esistente" + need_to_be_verified: + other: "email deve essere verificata" + verify_url_expired: + other: "l'url di verifica email è scaduto, si prega di reinviare la email" + lang: + not_found: + other: "lingua non trovata" + object: + captcha_verification_failed: + other: "captcha errato" + disallow_follow: + other: "Non sei autorizzato a seguire" + disallow_vote: + other: "non sei autorizzato a votare" + disallow_vote_your_self: + other: "Non puoi votare un tuo post!" + not_found: + other: "oggetto non trovato" + verification_failed: + other: "verifica fallita" + email_or_password_incorrect: + other: "email o password incorretti" + old_password_verification_failed: + other: "la verifica della vecchia password è fallita" + new_password_same_as_previous_setting: + other: "La nuova password è identica alla precedente" + question: + not_found: + other: "domanda non trovata" + rank: + fail_to_meet_the_condition: + other: "Condizioni non valide per il grado" + report: + handle_failed: + other: "Gestione del report fallita" + not_found: + other: "Report non trovato" + tag: + not_found: + other: "Etichetta non trovata" + theme: + not_found: + other: "tema non trovato" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "utente non trovato" + suspended: + other: "utente sospeso" + username_invalid: + other: "utente non valido" + username_duplicate: + other: "utente già in uso" + + report: + spam: name: other: "spam" description: - other: "Questa domanda è già stata chiesta o ha già una risposta." - guideline: + other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" + rude: name: - other: "motivo legato alla community" + other: "scortese o violento" description: - other: "Questa domanda non soddisfa le linee guida della comunità." - multiple: + other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" + duplicate: + name: + other: "duplicato" + description: + other: "Questa domanda è già stata posta e ha già una risposta." + not_answer: name: - other: "richiede maggiori dettagli o chiarezza" + other: "non è una risposta" description: - other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." + not_need: + name: + other: "non più necessario" + description: + other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." other: name: other: "altro" description: - other: "Questo articolo richiede un'altro motivo non listato sopra." + other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." + + question: + close: + duplicate: + name: + other: "spam" + description: + other: "Questa domanda è già stata chiesta o ha già una risposta." + guideline: + name: + other: "motivo legato alla community" + description: + other: "Questa domanda non soddisfa le linee guida della comunità." + multiple: + name: + other: "richiede maggiori dettagli o chiarezza" + description: + other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: + name: + other: "altro" + description: + other: "Questo articolo richiede un'altro motivo non listato sopra." -notification: - action: - update_question: - other: "domanda aggiornata" - answer_the_question: - other: "domanda risposta" - update_answer: - other: "risposta aggiornata" - adopt_answer: - other: "risposta accettata" - comment_question: - other: "domanda commentata" - comment_answer: - other: "risposta commentata" - reply_to_you: - other: "hai ricevuto risposta" - mention_you: - other: "sei stato menzionato" - your_question_is_closed: - other: "la tua domanda è stata chiusa" - your_question_was_deleted: - other: "la tua domanda è stata rimossa" - your_answer_was_deleted: - other: "la tua risposta è stata rimossa" - your_comment_was_deleted: - other: "il tuo commento è stato rimosso" + notification: + action: + update_question: + other: "domanda aggiornata" + answer_the_question: + other: "domanda risposta" + update_answer: + other: "risposta aggiornata" + adopt_answer: + other: "risposta accettata" + comment_question: + other: "domanda commentata" + comment_answer: + other: "risposta commentata" + reply_to_you: + other: "hai ricevuto risposta" + mention_you: + other: "sei stato menzionato" + your_question_is_closed: + other: "la tua domanda è stata chiusa" + your_question_was_deleted: + other: "la tua domanda è stata rimossa" + your_answer_was_deleted: + other: "la tua risposta è stata rimossa" + your_comment_was_deleted: + other: "il tuo commento è stato rimosso" diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index a02f42193..65e0c5ca4 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1,175 +1,176 @@ -base: - success: - other: "成功" - unknown: - other: "未知错误" - request_format_error: - other: "请求格式错误" - unauthorized_error: - other: "未登录" - database_error: - other: "数据服务异常" +backend: + base: + success: + other: "成功" + unknown: + other: "未知错误" + request_format_error: + other: "请求格式错误" + unauthorized_error: + other: "未登录" + database_error: + other: "数据服务异常" -email: - other: "邮箱" -password: - other: "密码" - -email_or_password_wrong_error: &email_or_password_wrong - other: "邮箱或密码错误" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "答案未找到" - comment: - edit_without_permission: - other: "不允许编辑评论" - not_found: - other: "评论未找到" email: - duplicate: - other: "邮箱已经存在" - need_to_be_verified: - other: "邮箱需要验证" - verify_url_expired: - other: "邮箱验证的网址已过期,请重新发送邮件" - lang: - not_found: - other: "语言未找到" - object: - captcha_verification_failed: - other: "验证码错误" - disallow_follow: - other: "你不能关注" - disallow_vote: - other: "你不能投票" - disallow_vote_your_self: - other: "你不能为自己的帖子投票!" - not_found: - other: "对象未找到" - verification_failed: - other: "验证失败" - email_or_password_incorrect: - other: "邮箱或密码不正确" - old_password_verification_failed: - other: "旧密码验证失败" - new_password_same_as_previous_setting: - other: "新密码与之前的设置相同" - question: - not_found: - other: "问题未找到" - rank: - fail_to_meet_the_condition: - other: "级别不符合条件" - report: - handle_failed: - other: "报告处理失败" - not_found: - other: "报告未找到" - tag: - not_found: - other: "标签未找到" - theme: - not_found: - other: "主题未找到" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "用户未找到" - suspended: - other: "用户已被暂停" - username_invalid: - other: "用户名无效" - username_duplicate: - other: "用户名已被使用" - set_avatar: - other: "头像设置错误" + other: "邮箱" + password: + other: "密码" -report: - spam: - name: - other: "垃圾信息" - description: - other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" - rude: - name: - other: "粗鲁或辱骂的" - description: - other: "有理智的人都会发现此内容不适合进行尊重的讨论。" - duplicate: - name: - other: "重复信息" - description: - other: "此问题以前就有人问过,而且已经有了答案。" - not_answer: - name: - other: "不是答案" - description: - other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" - not_need: - name: - other: "不再需要" - description: - other: "此条评论是过时的,对话性的或与本帖无关。" - other: - name: - other: "其他原因" - description: - other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" + email_or_password_wrong_error: &email_or_password_wrong + other: "邮箱或密码错误" -question: - close: - duplicate: + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "答案未找到" + comment: + edit_without_permission: + other: "不允许编辑评论" + not_found: + other: "评论未找到" + email: + duplicate: + other: "邮箱已经存在" + need_to_be_verified: + other: "邮箱需要验证" + verify_url_expired: + other: "邮箱验证的网址已过期,请重新发送邮件" + lang: + not_found: + other: "语言未找到" + object: + captcha_verification_failed: + other: "验证码错误" + disallow_follow: + other: "你不能关注" + disallow_vote: + other: "你不能投票" + disallow_vote_your_self: + other: "你不能为自己的帖子投票!" + not_found: + other: "对象未找到" + verification_failed: + other: "验证失败" + email_or_password_incorrect: + other: "邮箱或密码不正确" + old_password_verification_failed: + other: "旧密码验证失败" + new_password_same_as_previous_setting: + other: "新密码与之前的设置相同" + question: + not_found: + other: "问题未找到" + rank: + fail_to_meet_the_condition: + other: "级别不符合条件" + report: + handle_failed: + other: "报告处理失败" + not_found: + other: "报告未找到" + tag: + not_found: + other: "标签未找到" + theme: + not_found: + other: "主题未找到" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "用户未找到" + suspended: + other: "用户已被暂停" + username_invalid: + other: "用户名无效" + username_duplicate: + other: "用户名已被使用" + set_avatar: + other: "头像设置错误" + + report: + spam: name: other: "垃圾信息" + description: + other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" + rude: + name: + other: "粗鲁或辱骂的" + description: + other: "有理智的人都会发现此内容不适合进行尊重的讨论。" + duplicate: + name: + other: "重复信息" description: other: "此问题以前就有人问过,而且已经有了答案。" - guideline: + not_answer: name: - other: "社区特定原因" + other: "不是答案" description: - other: "此问题不符合社区准则。" - multiple: + other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" + not_need: name: - other: "需要细节或澄清" + other: "不再需要" description: - other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: "此条评论是过时的,对话性的或与本帖无关。" other: name: other: "其他原因" description: - other: "此帖子需要上述所列以外的其他理由。" + other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" + + question: + close: + duplicate: + name: + other: "垃圾信息" + description: + other: "此问题以前就有人问过,而且已经有了答案。" + guideline: + name: + other: "社区特定原因" + description: + other: "此问题不符合社区准则。" + multiple: + name: + other: "需要细节或澄清" + description: + other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: + name: + other: "其他原因" + description: + other: "此帖子需要上述所列以外的其他理由。" -notification: - action: - update_question: - other: "更新了问题" - answer_the_question: - other: "回答了问题" - update_answer: - other: "更新了答案" - adopt_answer: - other: "接受了答案" - comment_question: - other: "评论了问题" - comment_answer: - other: "评论了答案" - reply_to_you: - other: "回复了你" - mention_you: - other: "提到了你" - your_question_is_closed: - other: "你的问题已被关闭" - your_question_was_deleted: - other: "你的问题已被删除" - your_answer_was_deleted: - other: "你的答案已被删除" - your_comment_was_deleted: - other: "你的评论已被删除" + notification: + action: + update_question: + other: "更新了问题" + answer_the_question: + other: "回答了问题" + update_answer: + other: "更新了答案" + adopt_answer: + other: "接受了答案" + comment_question: + other: "评论了问题" + comment_answer: + other: "评论了答案" + reply_to_you: + other: "回复了你" + mention_you: + other: "提到了你" + your_question_is_closed: + other: "你的问题已被关闭" + your_question_was_deleted: + other: "你的问题已被删除" + your_answer_was_deleted: + other: "你的答案已被删除" + your_comment_was_deleted: + other: "你的评论已被删除" # The following fields are used for interface presentation(Front-end) ui: how_to_format: From 68fce365c8f684fa62f43bcd93d49058b9282a40 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 12:28:19 +0800 Subject: [PATCH 0210/3337] fix: i18n file parsing error --- go.mod | 2 +- go.sum | 2 ++ internal/base/translator/provider.go | 40 +++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d6d85117f..0da95f1c6 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/segmentfault/pacman v1.0.1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 - github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 + github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index be85073b8..8a6c2d9bb 100644 --- a/go.sum +++ b/go.sum @@ -596,6 +596,8 @@ github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd143 github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0= +github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A= diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index e047ceb28..527f91379 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -8,7 +8,7 @@ import ( "github.com/google/wire" myTran "github.com/segmentfault/pacman/contrib/i18n" "github.com/segmentfault/pacman/i18n" - "sigs.k8s.io/yaml" + "gopkg.in/yaml.v3" ) // ProviderSet is providers. @@ -31,18 +31,52 @@ var ( // NewTranslator new a translator func NewTranslator(c *I18n) (tr i18n.Translator, err error) { - GlobalTrans, err = myTran.NewTranslator(c.BundleDir) + entries, err := os.ReadDir(c.BundleDir) if err != nil { return nil, err } + // read the Bundle resources file from entries + for _, file := range entries { + // ignore directory + if file.IsDir() { + continue + } + // ignore non-YAML file + if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" { + continue + } + buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name())) + if err != nil { + return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err) + } + + // only parse the backend translation + translation := struct { + Content map[string]interface{} `yaml:"backend"` + }{} + if err = yaml.Unmarshal(buf, &translation); err != nil { + return nil, err + } + content, err := yaml.Marshal(translation.Content) + if err != nil { + return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err) + } + + // add translator use backend translation + if err = myTran.AddTranslator(content, file.Name()); err != nil { + return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err) + } + } + GlobalTrans = myTran.GlobalTrans + i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml")) if err != nil { return nil, fmt.Errorf("read i18n file failed: %s", err) } s := struct { - LangOption []*LangOption `json:"language_options"` + LangOption []*LangOption `yaml:"language_options"` }{} err = yaml.Unmarshal(i18nFile, &s) if err != nil { From fa3838b6048e1f4964c2dadfdd792ea3bef6a2e9 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 13:19:44 +0800 Subject: [PATCH 0211/3337] doc: update i18n file --- i18n/en_US.yaml | 1268 +++++++++++++++++++++++++++++++++++++++++------ i18n/it_IT.yaml | 310 ++++++------ i18n/zh_CN.yaml | 1059 +++++++++++++++++++++++++++++++++------ 3 files changed, 2164 insertions(+), 473 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 84afa373d..0ae83bf3e 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1,182 +1,1122 @@ -base: - success: - other: "Success." - unknown: - other: "Unknown error." - request_format_error: - other: "Request format is not valid." - unauthorized_error: - other: "Unauthorized." - database_error: - other: "Data server error." +# The following fields are used for back-end +backend: + base: + success: + other: "Success." + unknown: + other: "Unknown error." + request_format_error: + other: "Request format is not valid." + unauthorized_error: + other: "Unauthorized." + database_error: + other: "Data server error." -email: - other: "Email" -password: - other: "Password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email and password do not match." - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Answer do not found." - comment: - edit_without_permission: - other: "Comment are not allowed to edit." - not_found: - other: "Comment not found." email: - duplicate: - other: "Email already exists." - need_to_be_verified: - other: "Email should be verified." - verify_url_expired: - other: "Email verified URL has expired, please resend the email." - lang: - not_found: - other: "Language file not found." - object: - captcha_verification_failed: - other: "Captcha wrong." - disallow_follow: - other: "You are not allowed to follow." - disallow_vote: - other: "You are not allowed to vote." - disallow_vote_your_self: - other: "You can't vote for your own post." - not_found: - other: "Object not found." - verification_failed: - other: "Verification failed." - email_or_password_incorrect: - other: "Email and password do not match." - old_password_verification_failed: - other: "The old password verification failed" - new_password_same_as_previous_setting: - other: "The new password is the same as the previous one." - question: - not_found: - other: "Question not found." - rank: - fail_to_meet_the_condition: - other: "Rank fail to meet the condition." - report: - handle_failed: - other: "Report handle failed." - not_found: - other: "Report not found." - tag: - not_found: - other: "Tag not found." - theme: - not_found: - other: "Theme not found." - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "User not found." - suspended: - other: "User has been suspended." - username_invalid: - other: "Username is invalid." - username_duplicate: - other: "Username is already in use." - set_avatar: - other: "Avatar set failed." + other: "Email" + password: + other: "Password" - config: - read_config_failed: - other: "Read config failed" - database: - connection_failed: - other: "Database connection failed" - install: - create_config_failed: - other: "Can’t create the config.yaml file." -report: - spam: - name: - other: "spam" - description: - other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." - rude: - name: - other: "rude or abusive" - description: - other: "A reasonable person would find this content inappropriate for respectful discourse." - duplicate: - name: - other: "a duplicate" - description: - other: "This question has been asked before and already has an answer." - not_answer: - name: - other: "not an answer" - description: - other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." - not_need: - name: - other: "no longer needed" - description: - other: "This comment is outdated, conversational or not relevant to this post." - other: - name: - other: "something else" - description: - other: "This post requires staff attention for another reason not listed above." + email_or_password_wrong_error: &email_or_password_wrong + other: "Email and password do not match." -question: - close: - duplicate: + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Answer do not found." + comment: + edit_without_permission: + other: "Comment are not allowed to edit." + not_found: + other: "Comment not found." + email: + duplicate: + other: "Email already exists." + need_to_be_verified: + other: "Email should be verified." + verify_url_expired: + other: "Email verified URL has expired, please resend the email." + lang: + not_found: + other: "Language file not found." + object: + captcha_verification_failed: + other: "Captcha wrong." + disallow_follow: + other: "You are not allowed to follow." + disallow_vote: + other: "You are not allowed to vote." + disallow_vote_your_self: + other: "You can't vote for your own post." + not_found: + other: "Object not found." + verification_failed: + other: "Verification failed." + email_or_password_incorrect: + other: "Email and password do not match." + old_password_verification_failed: + other: "The old password verification failed" + new_password_same_as_previous_setting: + other: "The new password is the same as the previous one." + question: + not_found: + other: "Question not found." + rank: + fail_to_meet_the_condition: + other: "Rank fail to meet the condition." + report: + handle_failed: + other: "Report handle failed." + not_found: + other: "Report not found." + tag: + not_found: + other: "Tag not found." + theme: + not_found: + other: "Theme not found." + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "User not found." + suspended: + other: "User has been suspended." + username_invalid: + other: "Username is invalid." + username_duplicate: + 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" + install: + create_config_failed: + other: "Can’t create the config.yaml file." + report: + spam: name: other: "spam" + description: + other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." + rude: + name: + other: "rude or abusive" + description: + other: "A reasonable person would find this content inappropriate for respectful discourse." + duplicate: + name: + other: "a duplicate" description: other: "This question has been asked before and already has an answer." - guideline: + not_answer: name: - other: "a community-specific reason" + other: "not an answer" description: - other: "This question doesn't meet a community guideline." - multiple: + other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." + not_need: name: - other: "needs details or clarity" + other: "no longer needed" description: - other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: "This comment is outdated, conversational or not relevant to this post." other: name: other: "something else" description: - other: "This post requires another reason not listed above." + other: "This post requires staff attention for another reason not listed above." -notification: - action: - update_question: - other: "updated question" - answer_the_question: - other: "answered question" - update_answer: - other: "updated answer" - adopt_answer: - other: "accepted answer" - comment_question: - other: "commented question" - comment_answer: - other: "commented answer" - reply_to_you: - other: "replied to you" - mention_you: - other: "mentioned you" - your_question_is_closed: - other: "Your question has been closed" - your_question_was_deleted: - other: "Your question has been deleted" - your_answer_was_deleted: - other: "Your answer has been deleted" - your_comment_was_deleted: - other: "Your comment has been deleted" + question: + close: + duplicate: + name: + other: "spam" + description: + other: "This question has been asked before and already has an answer." + guideline: + name: + other: "a community-specific reason" + description: + other: "This question doesn't meet a community guideline." + multiple: + name: + other: "needs details or clarity" + description: + other: "This question currently includes multiple questions in one. It should focus on one problem only." + other: + name: + other: "something else" + description: + other: "This post requires another reason not listed above." + notification: + action: + update_question: + other: "updated question" + answer_the_question: + other: "answered question" + update_answer: + other: "updated answer" + adopt_answer: + other: "accepted answer" + comment_question: + other: "commented question" + comment_answer: + other: "commented answer" + reply_to_you: + other: "replied to you" + mention_you: + other: "mentioned you" + your_question_is_closed: + other: "Your question has been closed" + your_question_was_deleted: + other: "Your question has been deleted" + your_answer_was_deleted: + other: "Your answer has been deleted" + your_comment_was_deleted: + other: "Your comment has been deleted" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: How to Format + description: >- +
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by + placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ pagination: + prev: Prev + next: Next + page_title: + question: Question + questions: Questions + tag: Tag + tags: Tags + tag_wiki: tag wiki + edit_tag: Edit Tag + ask_a_question: Add Question + edit_question: Edit Question + edit_answer: Edit Answer + search: Search + posts_containing: Posts containing + settings: Settings + notifications: Notifications + login: Log In + sign_up: Sign Up + account_recovery: Account Recovery + account_activation: Account Activation + confirm_email: Confirm Email + account_suspended: Account Suspended + admin: Admin + change_email: Modify Email + install: Answer Installation + upgrade: Answer Upgrade + maintenance: Webite Maintenance + notifications: + title: Notifications + inbox: Inbox + achievement: Achievements + all_read: Mark all as read + show_more: Show more + suspended: + title: Your Account has been Suspended + until_time: 'Your account was suspended until {{ time }}.' + forever: This user was suspended forever. + end: You don't meet a community guideline. + editor: + blockquote: + text: Blockquote + bold: + text: Strong + chart: + text: Chart + flow_chart: Flow chart + sequence_diagram: Sequence diagram + class_diagram: Class diagram + state_diagram: State diagram + entity_relationship_diagram: Entity relationship diagram + user_defined_diagram: User defined diagram + gantt_chart: Gantt chart + pie_chart: Pie chart + code: + text: Code Sample + add_code: Add code sample + form: + fields: + code: + label: Code + msg: + empty: Code cannot be empty. + language: + label: Language (optional) + placeholder: Automatic detection + btn_cancel: Cancel + btn_confirm: Add + formula: + text: Formula + options: + inline: Inline formula + block: Block formula + heading: + text: Heading + options: + h1: Heading 1 + h2: Heading 2 + h3: Heading 3 + h4: Heading 4 + h5: Heading 5 + h6: Heading 6 + help: + text: Help + hr: + text: Horizontal Rule + image: + text: Image + add_image: Add image + tab_image: Upload image + form_image: + fields: + file: + label: Image File + btn: Select image + msg: + empty: File cannot be empty. + only_image: Only image files are allowed. + max_size: File size cannot exceed 4MB. + description: + label: Description (optional) + tab_url: Image URL + form_url: + fields: + url: + label: Image URL + msg: + empty: Image URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + uploading: Uploading + indent: + text: Indent + outdent: + text: Outdent + italic: + text: Emphasis + link: + text: Hyperlink + add_link: Add hyperlink + form: + fields: + url: + label: URL + msg: + empty: URL cannot be empty. + name: + label: Description (optional) + btn_cancel: Cancel + btn_confirm: Add + ordered_list: + text: Numbered List + unordered_list: + text: Bulleted List + table: + text: Table + heading: Heading + cell: Cell + close_modal: + title: I am closing this post as... + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + report_modal: + flag_title: I am flagging to report this post as... + close_title: I am closing this post as... + review_question_title: Review question + review_answer_title: Review answer + review_comment_title: Review comment + btn_cancel: Cancel + btn_submit: Submit + remark: + empty: Cannot be empty. + msg: + empty: Please select a reason. + tag_modal: + title: Create new tag + form: + fields: + display_name: + label: Display Name + msg: + empty: Display name cannot be empty. + range: Display name up to 35 characters. + slug_name: + label: URL Slug + description: 'Must use the character set "a-z", "0-9", "+ # - ."' + msg: + empty: URL slug cannot be empty. + range: URL slug up to 35 characters. + character: URL slug contains unallowed character set. + description: + label: Description (optional) + btn_cancel: Cancel + btn_submit: Submit + tag_info: + created_at: Created + edited_at: Edited + synonyms: + title: Synonyms + text: The following tags will be remapped to + empty: No synonyms found. + btn_add: Add a synonym + btn_edit: Edit + btn_save: Save + synonyms_text: The following tags will be remapped to + delete: + title: Delete this tag + content: >- +

We do not allowed deleting tag with posts.

Please remove this tag + from the posts first.

+ content2: Are you sure you wish to delete? + close: Close + edit_tag: + title: Edit Tag + default_reason: Edit tag + form: + fields: + revision: + label: Revision + display_name: + label: Display Name + slug_name: + label: URL Slug + info: 'Must use the character set "a-z", "0-9", "+ # - ."' + description: + label: Description + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + dates: + long_date: MMM D + long_date_with_year: 'MMM D, YYYY' + long_date_with_time: 'MMM D, YYYY [at] HH:mm' + now: now + x_seconds_ago: '{{count}}s ago' + x_minutes_ago: '{{count}}m ago' + x_hours_ago: '{{count}}h ago' + hour: hour + day: day + comment: + btn_add_comment: Add comment + reply_to: Reply to + btn_reply: Reply + btn_edit: Edit + btn_delete: Delete + btn_flag: Flag + btn_save_edits: Save edits + btn_cancel: Cancel + show_more: Show more comment + tip_question: >- + Use comments to ask for more information or suggest improvements. Avoid + answering questions in comments. + tip_answer: >- + Use comments to reply to other users or notify them of changes. If you are + adding new information, edit your post instead of commenting. + edit_answer: + title: Edit Answer + default_reason: Edit answer + form: + fields: + revision: + label: Revision + answer: + label: Answer + edit_summary: + label: Edit Summary + placeholder: >- + Briefly explain your changes (corrected spelling, fixed grammar, + improved formatting) + btn_save_edits: Save edits + btn_cancel: Cancel + tags: + title: Tags + sort_buttons: + popular: Popular + name: Name + newest: newest + button_follow: Follow + button_following: Following + tag_label: questions + search_placeholder: Filter by tag name + no_description: The tag has no description. + more: More + ask: + title: Add Question + edit_title: Edit Question + default_reason: Edit question + similar_questions: Similar questions + form: + fields: + revision: + label: Revision + title: + label: Title + placeholder: Be specific and imagine you're asking a question to another person + msg: + empty: Title cannot be empty. + range: Title up to 150 characters + body: + label: Body + msg: + empty: Body cannot be empty. + tags: + label: Tags + msg: + empty: Tags cannot be empty. + answer: + label: Answer + msg: + empty: Answer cannot be empty. + btn_post_question: Post your question + btn_save_edits: Save edits + answer_question: Answer your own question + post_question&answer: Post your question and answer + tag_selector: + add_btn: Add tag + create_btn: Create new tag + search_tag: Search tag + hint: 'Describe what your question is about, at least one tag is required.' + no_result: No tags matched + header: + nav: + question: Questions + tag: Tags + user: Users + profile: Profile + setting: Settings + logout: Log out + admin: Admin + search: + placeholder: Search + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: Change + loading: loading... + pic_auth_code: + title: Captcha + placeholder: Type the text above + msg: + empty: Captcha cannot be empty. + inactive: + first: >- + You're almost done! We sent an activation mail to {{mail}}. + Please follow the instructions in the mail to activate your account. + info: 'If it doesn''t arrive, check your spam folder.' + another: >- + We sent another activation email to you at {{mail}}. It might + take a few minutes for it to arrive; be sure to check your spam folder. + btn_name: Resend activation email + change_btn_name: Change email + msg: + empty: Cannot be empty. + login: + page_title: Welcome to Answer + info_sign: Don't have an account? <1>Sign up + info_login: Already have an account? <1>Log in + forgot_pass: Forgot password? + name: + label: Name + msg: + empty: Name cannot be empty. + range: Name up to 30 characters. + email: + label: Email + msg: + empty: Email cannot be empty. + password: + label: Password + msg: + empty: Password cannot be empty. + different: The passwords entered on both sides are inconsistent + account_forgot: + page_title: Forgot Your Password + btn_name: Send me recovery email + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: Email + msg: + empty: Email cannot be empty. + change_email: + page_title: Welcome to Answer + btn_cancel: Cancel + btn_update: Update email address + send_success: >- + If an account matches {{mail}}, you should receive an email + with instructions on how to reset your password shortly. + email: + label: New Email + msg: + empty: Email cannot be empty. + password_reset: + page_title: Password Reset + btn_name: Reset my password + reset_success: >- + You successfully changed your password; you will be redirected to the log in + page. + link_invalid: >- + Sorry, this password reset link is no longer valid. Perhaps your password is + already reset? + to_login: Continue to log in page + password: + label: Password + msg: + empty: Password cannot be empty. + length: The length needs to be between 8 and 32 + different: The passwords entered on both sides are inconsistent + password_confirm: + label: Confirm New Password + settings: + page_title: Settings + nav: + profile: Profile + notification: Notifications + account: Account + interface: Interface + profile: + btn_name: Update profile + display_name: + label: Display Name + msg: Display name cannot be empty. + msg_range: Display name up to 30 characters + username: + label: Username + caption: People can mention you as "@username". + msg: Username cannot be empty. + msg_range: Username up to 30 characters + character: 'Must use the character set "a-z", "0-9", " - . _"' + avatar: + label: Profile Image + gravatar: Gravatar + gravatar_text: You can change image on <1>gravatar.com + custom: Custom + btn_refresh: Refresh + custom_text: You can upload your image. + default: Default + msg: Please upload an avatar + bio: + label: About Me (optional) + website: + label: Website (optional) + placeholder: 'https://example.com' + msg: Website incorrect format + location: + label: Location (optional) + placeholder: 'City, Country' + notification: + email: + label: Email Notifications + radio: 'Answers to your questions, comments, and more' + account: + change_email_btn: Change email + change_pass_btn: Change password + change_email_info: >- + We've sent an email to that address. Please follow the confirmation + instructions. + email: + label: Email + msg: Email cannot be empty. + password_title: Password + current_pass: + label: Current Password + msg: + empty: Current Password cannot be empty. + length: The length needs to be between 8 and 32. + different: The two entered passwords do not match. + new_pass: + label: New Password + pass_confirm: + label: Confirm New Password + interface: + lang: + label: Interface Language + text: User interface language. It will change when you refresh the page. + toast: + update: update success + update_password: Password changed successfully. + flag_success: Thanks for flagging. + related_question: + title: Related Questions + btn: Add question + answers: answers + question_detail: + Asked: Asked + asked: asked + update: Modified + edit: edited + Views: Viewed + Follow: Follow + Following: Following + answered: answered + closed_in: Closed in + show_exist: Show existing question. + answers: + title: Answers + score: Score + newest: Newest + btn_accept: Accept + btn_accepted: Accepted + write_answer: + title: Your Answer + btn_name: Post your answer + confirm_title: Continue to answer + continue: Continue + confirm_info: >- +

Are you sure you want to add another answer?

You could use the + edit link to refine and improve your existing answer, instead.

+ empty: Answer cannot be empty. + delete: + title: Delete this post + question: >- + We do not recommend deleting questions with answers because + doing so deprives future readers of this knowledge.

Repeated deletion + of answered questions can result in your account being blocked from asking. + Are you sure you wish to delete? + answer_accepted: >- +

We do not recommend deleting accepted answer because + doing so deprives future readers of this knowledge.

Repeated deletion + of accepted answers can result in your account being blocked from answering. + Are you sure you wish to delete? + other: Are you sure you wish to delete? + tip_question_deleted: This post has been deleted + tip_answer_deleted: This answer has been deleted + btns: + confirm: Confirm + cancel: Cancel + save: Save + delete: Delete + login: Log in + signup: Sign up + logout: Log out + verify: Verify + add_question: Add question + search: + title: Search Results + keywords: Keywords + options: Options + follow: Follow + following: Following + counts: '{{count}} Results' + more: More + sort_btns: + relevance: Relevance + newest: Newest + active: Active + score: Score + more: More + tips: + title: Advanced Search Tips + tag: '<1>[tag] search withing a tag' + user: '<1>user:username search by author' + answer: '<1>answers:0 unanswered questions' + score: '<1>score:3 posts with a 3+ score' + question: '<1>is:question search questions' + is_answer: '<1>is:answer search answers' + empty: We couldn't find anything.
Try different or less specific keywords. + share: + name: Share + copy: Copy link + via: Share post via... + copied: Copied + facebook: Share to Facebook + twitter: Share to Twitter + cannot_vote_for_self: You can't vote for your own post + modal_confirm: + title: Error... + account_result: + page_title: Welcome to Answer + success: Your new account is confirmed; you will be redirected to the home page. + link: Continue to homepage + invalid: >- + Sorry, this account confirmation link is no longer valid. Perhaps your + account is already active? + confirm_new_email: Your email has been updated. + confirm_new_email_invalid: >- + Sorry, this confirmation link is no longer valid. Perhaps your email was + already changed? + question: + following_tags: Following Tags + edit: Edit + save: Save + follow_tag_tip: Follow tags to curate your list of questions. + hot_questions: Hot Questions + all_questions: All Questions + x_questions: '{{ count }} Questions' + x_answers: '{{ count }} answers' + questions: Questions + answers: Answers + newest: Newest + active: Active + frequent: Frequent + score: Score + unanswered: Unanswered + modified: modified + answered: answered + asked: asked + closed: closed + follow_a_tag: Follow a tag + more: More + personal: + overview: Overview + answers: Answers + answer: answer + questions: Questions + question: question + bookmarks: Bookmarks + reputation: Reputation + comments: Comments + votes: Votes + newest: Newest + score: Score + edit_profile: Edit Profile + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: Joined + last_login: Seen + about_me: About Me + about_me_empty: '// Hello, World !' + top_answers: Top Answers + top_questions: Top Questions + stats: Stats + list_empty: No posts found.
Perhaps you'd like to select a different tab? + accepted: Accepted + answered: answered + asked: asked + upvote: upvote + downvote: downvote + mod_short: Mod + mod_long: Moderators + x_reputation: reputation + x_votes: votes received + x_answers: answers + x_questions: questions + install: + title: Answer + next: Next + done: Done + config_yaml_error: Can’t create the config.yaml file. + lang: + label: Please Choose a Language + db_type: + label: Database Engine + db_username: + label: Username + placeholder: root + msg: Username cannot be empty. + db_password: + label: Password + placeholder: root + msg: Password cannot be empty. + db_host: + label: Database Host + placeholder: 'db:3306' + msg: Database Host cannot be empty. + db_name: + label: Database Name + placeholder: answer + msg: Database Name cannot be empty. + db_file: + label: Database File + placeholder: /data/answer.db + msg: Database File cannot be empty. + config_yaml: + title: Create config.yaml + label: The config.yaml file created. + description: >- + You can create the <1>config.yaml file manually in the + <1>/var/wwww/xxx/ directory and paste the following text into it. + info: 'After you’ve done that, click “Next” button.' + site_information: Site Information + admin_account: Admin Account + site_name: + label: Site Name + msg: Site Name cannot be empty. + site_url: + label: Site URL + text: The address of your site. + msg: + empty: Site URL cannot be empty. + incorrect: Site URL incorrect format. + contact_email: + label: Contact Email + text: Email address of key contact responsible for this site. + msg: + empty: Contact Email cannot be empty. + incorrect: Contact Email incorrect format. + admin_name: + label: Name + msg: Name cannot be empty. + admin_password: + label: Password + text: >- + You will need this password to log in. Please store it in a secure + location. + msg: Password cannot be empty. + admin_email: + label: Email + text: You will need this email to log in. + msg: + empty: Email cannot be empty. + incorrect: Email incorrect format. + ready_title: Your Answer is Ready! + ready_description: >- + If you ever feel like changing more settings, visit <1>admin section; + find it in the site menu. + good_luck: 'Have fun, and good luck!' + warn_title: Warning + warn_description: >- + The file <1>config.yaml already exists. If you need to reset any of the + configuration items in this file, please delete it first. + install_now: You may try <1>installing now. + installed: Already installed + installed_description: >- + You appear to have already installed. To reinstall please clear your old + database tables first. + page_404: + description: 'Unfortunately, this page doesn''t exist.' + back_home: Back to homepage + page_50X: + description: The server encountered an error and could not complete your request. + back_home: Back to homepage + page_maintenance: + description: 'We are under maintenance, we’ll be back soon.' + admin: + admin_header: + title: Admin + nav_menus: + dashboard: Dashboard + contents: Contents + questions: Questions + answers: Answers + users: Users + flags: Flags + settings: Settings + general: General + interface: Interface + smtp: SMTP + dashboard: + title: Dashboard + welcome: Welcome to Answer Admin! + site_statistics: Site Statistics + questions: 'Questions:' + answers: 'Answers:' + comments: 'Comments:' + votes: 'Votes:' + active_users: 'Active users:' + flags: 'Flags:' + site_health_status: Site Health Status + version: 'Version:' + https: 'HTTPS:' + uploading_files: 'Uploading files:' + smtp: 'SMTP:' + timezone: 'Timezone:' + system_info: System Info + storage_used: 'Storage used:' + uptime: 'Uptime:' + answer_links: Answer Links + documents: Documents + feedback: Feedback + review: Review + config: Config + update_to: Update to + latest: Latest + check_failed: Check failed + 'yes': 'Yes' + 'no': 'No' + not_allowed: Not allowed + allowed: Allowed + enabled: Enabled + disabled: Disabled + flags: + title: Flags + pending: Pending + completed: Completed + flagged: Flagged + created: Created + action: Action + review: Review + change_modal: + title: Change user status to... + btn_cancel: Cancel + btn_submit: Submit + normal_name: normal + normal_description: A normal user can ask and answer questions. + suspended_name: suspended + suspended_description: A suspended user can't log in. + deleted_name: deleted + deleted_description: 'Delete profile, authentication associations.' + inactive_name: inactive + inactive_description: An inactive user must re-validate their email. + confirm_title: Delete this user + confirm_content: Are you sure you want to delete this user? This is permanent! + confirm_btn: Delete + msg: + empty: Please select a reason. + status_modal: + title: 'Change {{ type }} status to...' + normal_name: normal + normal_description: A normal post available to everyone. + closed_name: closed + closed_description: 'A closed question can''t answer, but still can edit, vote and comment.' + deleted_name: deleted + deleted_description: All reputation gained and lost will be restored. + btn_cancel: Cancel + btn_submit: Submit + btn_next: Next + users: + title: Users + name: Name + email: Email + reputation: Reputation + created_at: Created Time + delete_at: Deleted Time + suspend_at: Suspended Time + status: Status + action: Action + change: Change + all: All + inactive: Inactive + suspended: Suspended + deleted: Deleted + normal: Normal + filter: + placeholder: 'Filter by name, user:id' + questions: + page_title: Questions + normal: Normal + closed: Closed + deleted: Deleted + post: Post + votes: Votes + answers: Answers + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, question:id' + answers: + page_title: Answers + normal: Normal + deleted: Deleted + post: Post + votes: Votes + created: Created + status: Status + action: Action + change: Change + filter: + placeholder: 'Filter by title, answer:id' + general: + page_title: General + name: + label: Site Name + msg: Site name cannot be empty. + text: 'The name of this site, as used in the title tag.' + site_url: + label: Site URL + msg: Site url cannot be empty. + validate: Please enter a valid URL. + text: The address of your site. + short_description: + label: Short Site Description (optional) + msg: Short site description cannot be empty. + text: 'Short description, as used in the title tag on homepage.' + description: + label: Site Description (optional) + msg: Site description cannot be empty. + text: 'Describe this site in one sentence, as used in the meta description tag.' + contact_email: + label: Contact Email + msg: Contact email cannot be empty. + validate: Contact email is not valid. + text: Email address of key contact responsible for this site. + interface: + page_title: Interface + logo: + label: Logo (optional) + msg: Site logo cannot be empty. + text: You can upload your image or <1>reset it to the site title text. + theme: + label: Theme + msg: Theme cannot be empty. + text: Select an existing theme. + language: + label: Interface Language + msg: Interface language cannot be empty. + text: User interface language. It will change when you refresh the page. + time_zone: + label: Timezone + msg: Timezone cannot be empty. + text: Choose a city in the same timezone as you. + smtp: + page_title: SMTP + from_email: + label: From Email + msg: From email cannot be empty. + text: The email address which emails are sent from. + from_name: + label: From Name + msg: From name cannot be empty. + text: The name which emails are sent from. + smtp_host: + label: SMTP Host + msg: SMTP host cannot be empty. + text: Your mail server. + encryption: + label: Encryption + msg: Encryption cannot be empty. + text: For most servers SSL is the recommended option. + ssl: SSL + none: None + smtp_port: + label: SMTP Port + msg: SMTP port must be number 1 ~ 65535. + text: The port to your mail server. + smtp_username: + label: SMTP Username + msg: SMTP username cannot be empty. + smtp_password: + label: SMTP Password + msg: SMTP password cannot be empty. + test_email_recipient: + label: Test Email Recipients + text: Provide email address that will receive test sends. + msg: Test email recipients is invalid + smtp_authentication: + label: SMTP Authentication + msg: SMTP authentication cannot be empty. + 'yes': 'Yes' + 'no': 'No' diff --git a/i18n/it_IT.yaml b/i18n/it_IT.yaml index 0b9439412..61de40f89 100644 --- a/i18n/it_IT.yaml +++ b/i18n/it_IT.yaml @@ -1,170 +1,172 @@ -base: - success: - other: "Successo" - unknown: - other: "Errore sconosciuto" - request_format_error: - other: "Il formato della richiesta non è valido" - unauthorized_error: - other: "Non autorizzato" - database_error: - other: "Errore server dati" +# The following fields are used for back-end +backend: + base: + success: + other: "Successo" + unknown: + other: "Errore sconosciuto" + request_format_error: + other: "Il formato della richiesta non è valido" + unauthorized_error: + other: "Non autorizzato" + database_error: + other: "Errore server dati" -email: - other: "email" -password: - other: "password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email o password errati" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Risposta non trovata" - comment: - edit_without_permission: - other: "Non si hanno di privilegi sufficienti per modificare il commento" - not_found: - other: "Commento non trovato" email: - duplicate: - other: "email già esistente" - need_to_be_verified: - other: "email deve essere verificata" - verify_url_expired: - other: "l'url di verifica email è scaduto, si prega di reinviare la email" - lang: - not_found: - other: "lingua non trovata" - object: - captcha_verification_failed: - other: "captcha errato" - disallow_follow: - other: "Non sei autorizzato a seguire" - disallow_vote: - other: "non sei autorizzato a votare" - disallow_vote_your_self: - other: "Non puoi votare un tuo post!" - not_found: - other: "oggetto non trovato" - verification_failed: - other: "verifica fallita" - email_or_password_incorrect: - other: "email o password incorretti" - old_password_verification_failed: - other: "la verifica della vecchia password è fallita" - new_password_same_as_previous_setting: - other: "La nuova password è identica alla precedente" - question: - not_found: - other: "domanda non trovata" - rank: - fail_to_meet_the_condition: - other: "Condizioni non valide per il grado" - report: - handle_failed: - other: "Gestione del report fallita" - not_found: - other: "Report non trovato" - tag: - not_found: - other: "Etichetta non trovata" - theme: - not_found: - other: "tema non trovato" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "utente non trovato" - suspended: - other: "utente sospeso" - username_invalid: - other: "utente non valido" - username_duplicate: - other: "utente già in uso" + other: "email" + password: + other: "password" -report: - spam: - name: - other: "spam" - description: - other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" - rude: - name: - other: "scortese o violento" - description: - other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" - duplicate: - name: - other: "duplicato" - description: - other: "Questa domanda è già stata posta e ha già una risposta." - not_answer: - name: - other: "non è una risposta" - description: - other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." - not_need: - name: - other: "non più necessario" - description: - other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." - other: - name: - other: "altro" - description: - other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." + email_or_password_wrong_error: &email_or_password_wrong + other: "Email o password errati" -question: - close: - duplicate: + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "Risposta non trovata" + comment: + edit_without_permission: + other: "Non si hanno di privilegi sufficienti per modificare il commento" + not_found: + other: "Commento non trovato" + email: + duplicate: + other: "email già esistente" + need_to_be_verified: + other: "email deve essere verificata" + verify_url_expired: + other: "l'url di verifica email è scaduto, si prega di reinviare la email" + lang: + not_found: + other: "lingua non trovata" + object: + captcha_verification_failed: + other: "captcha errato" + disallow_follow: + other: "Non sei autorizzato a seguire" + disallow_vote: + other: "non sei autorizzato a votare" + disallow_vote_your_self: + other: "Non puoi votare un tuo post!" + not_found: + other: "oggetto non trovato" + verification_failed: + other: "verifica fallita" + email_or_password_incorrect: + other: "email o password incorretti" + old_password_verification_failed: + other: "la verifica della vecchia password è fallita" + new_password_same_as_previous_setting: + other: "La nuova password è identica alla precedente" + question: + not_found: + other: "domanda non trovata" + rank: + fail_to_meet_the_condition: + other: "Condizioni non valide per il grado" + report: + handle_failed: + other: "Gestione del report fallita" + not_found: + other: "Report non trovato" + tag: + not_found: + other: "Etichetta non trovata" + theme: + not_found: + other: "tema non trovato" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "utente non trovato" + suspended: + other: "utente sospeso" + username_invalid: + other: "utente non valido" + username_duplicate: + other: "utente già in uso" + + report: + spam: name: other: "spam" description: - other: "Questa domanda è già stata chiesta o ha già una risposta." - guideline: + other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" + rude: name: - other: "motivo legato alla community" + other: "scortese o violento" description: - other: "Questa domanda non soddisfa le linee guida della comunità." - multiple: + other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" + duplicate: + name: + other: "duplicato" + description: + other: "Questa domanda è già stata posta e ha già una risposta." + not_answer: name: - other: "richiede maggiori dettagli o chiarezza" + other: "non è una risposta" description: - other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." + not_need: + name: + other: "non più necessario" + description: + other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." other: name: other: "altro" description: - other: "Questo articolo richiede un'altro motivo non listato sopra." + other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." + + question: + close: + duplicate: + name: + other: "spam" + description: + other: "Questa domanda è già stata chiesta o ha già una risposta." + guideline: + name: + other: "motivo legato alla community" + description: + other: "Questa domanda non soddisfa le linee guida della comunità." + multiple: + name: + other: "richiede maggiori dettagli o chiarezza" + description: + other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." + other: + name: + other: "altro" + description: + other: "Questo articolo richiede un'altro motivo non listato sopra." -notification: - action: - update_question: - other: "domanda aggiornata" - answer_the_question: - other: "domanda risposta" - update_answer: - other: "risposta aggiornata" - adopt_answer: - other: "risposta accettata" - comment_question: - other: "domanda commentata" - comment_answer: - other: "risposta commentata" - reply_to_you: - other: "hai ricevuto risposta" - mention_you: - other: "sei stato menzionato" - your_question_is_closed: - other: "la tua domanda è stata chiusa" - your_question_was_deleted: - other: "la tua domanda è stata rimossa" - your_answer_was_deleted: - other: "la tua risposta è stata rimossa" - your_comment_was_deleted: - other: "il tuo commento è stato rimosso" + notification: + action: + update_question: + other: "domanda aggiornata" + answer_the_question: + other: "domanda risposta" + update_answer: + other: "risposta aggiornata" + adopt_answer: + other: "risposta accettata" + comment_question: + other: "domanda commentata" + comment_answer: + other: "risposta commentata" + reply_to_you: + other: "hai ricevuto risposta" + mention_you: + other: "sei stato menzionato" + your_question_is_closed: + other: "la tua domanda è stata chiusa" + your_question_was_deleted: + other: "la tua domanda è stata rimossa" + your_answer_was_deleted: + other: "la tua risposta è stata rimossa" + your_comment_was_deleted: + other: "il tuo commento è stato rimosso" diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 7d76cc061..65e0c5ca4 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1,172 +1,921 @@ -base: - success: - other: "成功" - unknown: - other: "未知错误" - request_format_error: - other: "请求格式错误" - unauthorized_error: - other: "未登录" - database_error: - other: "数据服务异常" +backend: + base: + success: + other: "成功" + unknown: + other: "未知错误" + request_format_error: + other: "请求格式错误" + unauthorized_error: + other: "未登录" + database_error: + other: "数据服务异常" -email: - other: "邮箱" -password: - other: "密码" - -email_or_password_wrong_error: &email_or_password_wrong - other: "邮箱或密码错误" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "答案未找到" - comment: - edit_without_permission: - other: "不允许编辑评论" - not_found: - other: "评论未找到" email: - duplicate: - other: "邮箱已经存在" - need_to_be_verified: - other: "邮箱需要验证" - verify_url_expired: - other: "邮箱验证的网址已过期,请重新发送邮件" - lang: - not_found: - other: "语言未找到" - object: - captcha_verification_failed: - other: "验证码错误" - disallow_follow: - other: "你不能关注" - disallow_vote: - other: "你不能投票" - disallow_vote_your_self: - other: "你不能为自己的帖子投票!" - not_found: - other: "对象未找到" - verification_failed: - other: "验证失败" - email_or_password_incorrect: - other: "邮箱或密码不正确" - old_password_verification_failed: - other: "旧密码验证失败" - new_password_same_as_previous_setting: - other: "新密码与之前的设置相同" - question: - not_found: - other: "问题未找到" - rank: - fail_to_meet_the_condition: - other: "级别不符合条件" - report: - handle_failed: - other: "报告处理失败" - not_found: - other: "报告未找到" - tag: - not_found: - other: "标签未找到" - theme: - not_found: - other: "主题未找到" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "用户未找到" - suspended: - other: "用户已被暂停" - username_invalid: - other: "用户名无效" - username_duplicate: - other: "用户名已被使用" - set_avatar: - other: "头像设置错误" + other: "邮箱" + password: + other: "密码" -report: - spam: - name: - other: "垃圾信息" - description: - other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" - rude: - name: - other: "粗鲁或辱骂的" - description: - other: "有理智的人都会发现此内容不适合进行尊重的讨论。" - duplicate: - name: - other: "重复信息" - description: - other: "此问题以前就有人问过,而且已经有了答案。" - not_answer: - name: - other: "不是答案" - description: - other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" - not_need: - name: - other: "不再需要" - description: - other: "此条评论是过时的,对话性的或与本帖无关。" - other: - name: - other: "其他原因" - description: - other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" + email_or_password_wrong_error: &email_or_password_wrong + other: "邮箱或密码错误" -question: - close: - duplicate: + error: + admin: + email_or_password_wrong: *email_or_password_wrong + answer: + not_found: + other: "答案未找到" + comment: + edit_without_permission: + other: "不允许编辑评论" + not_found: + other: "评论未找到" + email: + duplicate: + other: "邮箱已经存在" + need_to_be_verified: + other: "邮箱需要验证" + verify_url_expired: + other: "邮箱验证的网址已过期,请重新发送邮件" + lang: + not_found: + other: "语言未找到" + object: + captcha_verification_failed: + other: "验证码错误" + disallow_follow: + other: "你不能关注" + disallow_vote: + other: "你不能投票" + disallow_vote_your_self: + other: "你不能为自己的帖子投票!" + not_found: + other: "对象未找到" + verification_failed: + other: "验证失败" + email_or_password_incorrect: + other: "邮箱或密码不正确" + old_password_verification_failed: + other: "旧密码验证失败" + new_password_same_as_previous_setting: + other: "新密码与之前的设置相同" + question: + not_found: + other: "问题未找到" + rank: + fail_to_meet_the_condition: + other: "级别不符合条件" + report: + handle_failed: + other: "报告处理失败" + not_found: + other: "报告未找到" + tag: + not_found: + other: "标签未找到" + theme: + not_found: + other: "主题未找到" + user: + email_or_password_wrong: + other: *email_or_password_wrong + not_found: + other: "用户未找到" + suspended: + other: "用户已被暂停" + username_invalid: + other: "用户名无效" + username_duplicate: + other: "用户名已被使用" + set_avatar: + other: "头像设置错误" + + report: + spam: name: other: "垃圾信息" + description: + other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" + rude: + name: + other: "粗鲁或辱骂的" + description: + other: "有理智的人都会发现此内容不适合进行尊重的讨论。" + duplicate: + name: + other: "重复信息" description: other: "此问题以前就有人问过,而且已经有了答案。" - guideline: + not_answer: name: - other: "社区特定原因" + other: "不是答案" description: - other: "此问题不符合社区准则。" - multiple: + other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" + not_need: name: - other: "需要细节或澄清" + other: "不再需要" description: - other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: "此条评论是过时的,对话性的或与本帖无关。" other: name: other: "其他原因" description: - other: "此帖子需要上述所列以外的其他理由。" + other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" + + question: + close: + duplicate: + name: + other: "垃圾信息" + description: + other: "此问题以前就有人问过,而且已经有了答案。" + guideline: + name: + other: "社区特定原因" + description: + other: "此问题不符合社区准则。" + multiple: + name: + other: "需要细节或澄清" + description: + other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" + other: + name: + other: "其他原因" + description: + other: "此帖子需要上述所列以外的其他理由。" + + notification: + action: + update_question: + other: "更新了问题" + answer_the_question: + other: "回答了问题" + update_answer: + other: "更新了答案" + adopt_answer: + other: "接受了答案" + comment_question: + other: "评论了问题" + comment_answer: + other: "评论了答案" + reply_to_you: + other: "回复了你" + mention_you: + other: "提到了你" + your_question_is_closed: + other: "你的问题已被关闭" + your_question_was_deleted: + other: "你的问题已被删除" + your_answer_was_deleted: + other: "你的答案已被删除" + your_comment_was_deleted: + other: "你的评论已被删除" +# The following fields are used for interface presentation(Front-end) +ui: + how_to_format: + title: 如何设定文本格式 + description: >- +
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 + **粗体**

  • 使用 4 + 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 + `像 _这样_`

  • 使用```创建代码块

    ```
    // + 这是代码
    ```
+ pagination: + prev: 上一页 + next: 下一页 + page_title: + question: 问题 + questions: 问题 + tag: 标签 + tags: 标签 + tag_wiki: 标签 wiki + edit_tag: 编辑标签 + ask_a_question: 提问题 + edit_question: 编辑问题 + edit_answer: 编辑回答 + search: 搜索 + posts_containing: 包含 + settings: 设定 + notifications: 通知 + login: 登录 + sign_up: 注册 + account_recovery: 账号恢复 + account_activation: 账号激活 + confirm_email: 确认电子邮件 + account_suspended: 账号已封禁 + admin: 后台管理 + notifications: + title: 通知 + inbox: 收件箱 + achievement: 成就 + all_read: 全部标记为已读 + show_more: 显示更多 + suspended: + title: 账号已封禁 + until_time: '你的账号被封禁至{{ time }}。' + forever: 你的账号已被永久封禁。 + end: 违反了我们的社区准则。 + editor: + blockquote: + text: 引用 + bold: + text: 粗体 + chart: + text: 图表 + flow_chart: 流程图 + sequence_diagram: 时序图 + class_diagram: 类图 + state_diagram: 状态图 + entity_relationship_diagram: ER 图 + user_defined_diagram: User defined diagram + gantt_chart: 甘特图 + pie_chart: 饼图 + code: + text: 代码块 + add_code: 添加代码块 + form: + fields: + code: + label: 代码块 + msg: + empty: 代码块不能为空 + language: + label: 语言 (可选) + placeholder: 自动识别 + btn_cancel: 取消 + btn_confirm: 添加 + formula: + text: 公式 + options: + inline: 行内公式 + block: 公式块 + heading: + text: 标题 + options: + h1: 标题 1 + h2: 标题 2 + h3: 标题 3 + h4: 标题 4 + h5: 标题 5 + h6: 标题 6 + help: + text: 帮助 + hr: + text: 水平分割线 + image: + text: 图片 + add_image: 添加图片 + tab_image: 上传图片 + form_image: + fields: + file: + label: 图片文件 + btn: 选择图片 + msg: + empty: 请选择图片文件。 + only_image: 只能上传图片文件。 + max_size: 图片文件大小不能超过 4 MB。 + description: + label: 图片描述(可选) + tab_url: 网络图片 + form_url: + fields: + url: + label: 图片地址 + msg: + empty: 图片地址不能为空 + name: + label: 图片描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + uploading: 上传中... + indent: + text: 添加缩进 + outdent: + text: 减少缩进 + italic: + text: 斜体 + link: + text: 超链接 + add_link: 添加超链接 + form: + fields: + url: + label: 链接 + msg: + empty: 链接不能为空。 + name: + label: 链接描述(可选) + btn_cancel: 取消 + btn_confirm: 添加 + ordered_list: + text: 有编号列表 + unordered_list: + text: 无编号列表 + table: + text: 表格 + heading: 表头 + cell: 单元格 + close_modal: + title: 关闭原因是... + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空。 + msg: + empty: 请选择一个原因。 + report_modal: + flag_title: 举报原因是... + close_title: 关闭原因是... + review_question_title: 审查问题 + review_answer_title: 审查回答 + review_comment_title: 审查评论 + btn_cancel: 取消 + btn_submit: 提交 + remark: + empty: 不能为空 + msg: + empty: 请选择一个原因。 + tag_modal: + title: 创建新标签 + form: + fields: + display_name: + label: 显示名称(别名) + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + slug_name: + label: URL 固定链接 + description: '必须由 "a-z", "0-9", "+ # - ." 组成' + msg: + empty: 不能为空 + range: 不能超过 35 个字符 + character: 包含非法字符 + description: + label: 标签描述(可选) + btn_cancel: 取消 + btn_submit: 提交 + tag_info: + created_at: 创建于 + edited_at: 编辑于 + synonyms: + title: 同义词 + text: 以下标签等同于 + empty: 此标签目前没有同义词。 + btn_add: 添加同义词 + btn_edit: 编辑 + btn_save: 保存 + synonyms_text: 以下标签等同于 + delete: + title: 删除标签 + content:

不允许删除有关联问题的标签。

请先从关联的问题中删除此标签的引用。

+ content2: 确定要删除吗? + close: 关闭 + edit_tag: + title: 编辑标签 + default_reason: 编辑标签 + form: + fields: + revision: + label: 编辑历史 + display_name: + label: 名称 + slug_name: + label: URL 固定链接 + info: '必须由 "a-z", "0-9", "+ # - ." 组成' + description: + label: 描述 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + dates: + long_date: MM月DD日 + long_date_with_year: YYYY年MM月DD日 + long_date_with_time: 'YYYY年MM月DD日 HH:mm' + now: 刚刚 + x_seconds_ago: '{{count}} 秒前' + x_minutes_ago: '{{count}} 分钟前' + x_hours_ago: '{{count}} 小时前' + comment: + btn_add_comment: 添加评论 + reply_to: 回复 + btn_reply: 回复 + btn_edit: 编辑 + btn_delete: 删除 + btn_flag: 举报 + btn_save_edits: 保存 + btn_cancel: 取消 + show_more: 显示更多评论 + tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。 + tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 + edit_answer: + title: 编辑回答 + default_reason: 编辑回答 + form: + fields: + revision: + label: 编辑历史 + answer: + label: 回答内容 + edit_summary: + label: 编辑概要 + placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) + btn_save_edits: 保存更改 + btn_cancel: 取消 + tags: + title: 标签 + sort_buttons: + popular: 热门 + name: 名称 + newest: 最新 + button_follow: 关注 + button_following: 已关注 + tag_label: 个问题 + search_placeholder: 通过标签名过滤 + no_description: 此标签无描述。 + more: 更多 + ask: + title: 提交新的问题 + edit_title: 编辑问题 + default_reason: 编辑问题 + similar_questions: 相似的问题 + form: + fields: + revision: + label: 编辑历史 + title: + label: 标题 + placeholder: 请详细描述你的问题 + msg: + empty: 标题不能为空 + range: 标题最多 150 个字符 + body: + label: 内容 + msg: + empty: 内容不能为空 + tags: + label: 标签 + msg: + empty: 必须选择一个标签 + answer: + label: 回答内容 + msg: + empty: 回答内容不能为空 + btn_post_question: 提交问题 + btn_save_edits: 保存更改 + answer_question: 直接发表回答 + post_question&answer: 提交问题和回答 + tag_selector: + add_btn: 添加标签 + create_btn: 创建新标签 + search_tag: 搜索标签 + hint: 选择至少一个与问题相关的标签。 + no_result: 没有匹配的标签 + header: + nav: + question: 问题 + tag: 标签 + user: 用户 + profile: 用户主页 + setting: 账号设置 + logout: 退出登录 + admin: 后台管理 + search: + placeholder: 搜索 + footer: + build_on: >- + Built on <1> Answer - the open-source software that power Q&A + communities
Made with love © 2022 Answer + upload_img: + name: 更改图片 + loading: 加载中... + pic_auth_code: + title: 验证码 + placeholder: 输入图片中的文字 + msg: + empty: 不能为空 + inactive: + first: '马上就好了!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活您的帐户。' + info: 如果没有收到,请检查您的垃圾邮件文件夹。 + another: '我们向您发送了另一封激活电子邮件,地址为 {{mail}}。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。' + btn_name: 重新发送激活邮件 + msg: + empty: 不能为空 + login: + page_title: 欢迎来到 Answer + info_sign: 没有帐户?<1>注册 + info_login: 已经有一个帐户?<1>登录 + forgot_pass: 忘记密码? + name: + label: 昵称 + msg: + empty: 昵称不能为空 + range: 昵称最多 30 个字符 + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password: + label: 密码 + msg: + empty: 密码不能为空 + different: 两次输入密码不一致 + account_forgot: + page_title: 忘记密码 + btn_name: 发送恢复邮件 + send_success: '如无意外,你的邮箱 {{mail}} 将会收到一封重置密码的邮件,请根据指引重置你的密码。' + email: + label: 邮箱 + msg: + empty: 邮箱不能为空 + password_reset: + page_title: 密码重置 + btn_name: 重置我的密码 + reset_success: 你已经成功更改密码,将返回登录页面 + link_invalid: 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? + to_login: 前往登录页面 + password: + label: 密码 + msg: + empty: 密码不能为空 + length: 密码长度在8-32个字符之间 + different: 两次输入密码不一致 + password_confirm: + label: 确认新密码 + settings: + page_title: 设置 + nav: + profile: 我的资料 + notification: 通知 + account: 账号 + interface: 界面 + profile: + btn_name: 保存更改 + display_name: + label: 昵称 + msg: 昵称不能为空 + msg_range: 昵称不能超过 30 个字符 + username: + label: 用户名 + caption: 用户之间可以通过 "@用户名" 进行交互。 + msg: 用户名不能为空 + msg_range: 用户名不能超过 30 个字符 + character: '用户名只能由 "a-z", "0-9", " - . _" 组成' + avatar: + label: 头像 + text: 您可以上传图片作为头像,也可以 <1>重置 为 + bio: + label: 关于我 (可选) + website: + label: 网站 (可选) + placeholder: 'https://example.com' + msg: 格式不正确 + location: + label: 位置 (可选) + placeholder: '城市, 国家' + notification: + email: + label: 邮件通知 + radio: 你的提问有新的回答,评论,和其他 + account: + change_email_btn: 更改邮箱 + change_pass_btn: 更改密码 + change_email_info: 邮件已发送。请根据指引完成验证。 + email: + label: 邮箱 + msg: 邮箱不能为空 + password_title: 密码 + current_pass: + label: 当前密码 + msg: + empty: 当前密码不能为空 + length: 密码长度必须在 8 至 32 之间 + different: 两次输入的密码不匹配 + new_pass: + label: 新密码 + pass_confirm: + label: 确认新密码 + interface: + lang: + label: 界面语言 + text: 设置用户界面语言,在刷新页面后生效。 + toast: + update: 更新成功 + update_password: 更改密码成功。 + flag_success: 感谢您的标记,我们会尽快处理。 + related_question: + title: 相关问题 + btn: 我要提问 + answers: 个回答 + question_detail: + Asked: 提问于 + asked: 提问于 + update: 修改于 + edit: 最后编辑于 + Views: 阅读次数 + Follow: 关注此问题 + Following: 已关注 + answered: 回答于 + closed_in: 关闭于 + show_exist: 查看相关问题。 + answers: + title: 个回答 + score: 评分 + newest: 最新 + btn_accept: 采纳 + btn_accepted: 已被采纳 + write_answer: + title: 你的回答 + btn_name: 提交你的回答 + confirm_title: 继续回答 + continue: 继续 + confirm_info:

您确定要提交一个新的回答吗?

您可以直接编辑和改善您之前的回答的。

+ empty: 回答内容不能为空。 + delete: + title: 删除 + question: >- + 我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? + answer_accepted: >- +

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? + other: 你确定要删除? + tip_question_deleted: 此问题已被删除 + tip_answer_deleted: 此回答已被删除 + btns: + confirm: 确认 + cancel: 取消 + save: 保存 + delete: 删除 + login: 登录 + signup: 注册 + logout: 退出登录 + verify: 验证 + add_question: 我要提问 + search: + title: 搜索结果 + keywords: 关键词 + options: 选项 + follow: 关注 + following: 已关注 + counts: '{{count}} 个结果' + more: 更多 + sort_btns: + relevance: 相关性 + newest: 最新的 + active: 活跃的 + score: 评分 + tips: + title: 高级搜索提示 + tag: '<1>[tag] 在指定标签中搜索' + user: '<1>user:username 根据作者搜索' + answer: '<1>answers:0 搜索未回答的问题' + score: '<1>score:3 评分 3 分或以上' + question: '<1>is:question 只搜索问题' + is_answer: '<1>is:answer 只搜索回答' + empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 + share: + name: 分享 + copy: 复制链接 + via: 分享在... + copied: 已复制 + facebook: 分享到 Facebook + twitter: 分享到 Twitter + cannot_vote_for_self: 不能给自己投票 + modal_confirm: + title: 发生错误... + account_result: + page_title: 欢迎来到 Answer + success: 你的账号已通过验证,即将返回首页。 + link: 返回首页 + invalid: 抱歉,此验证链接已失效。也许是你的账号已经通过验证了? + confirm_new_email: 你的电子邮箱已更新 + confirm_new_email_invalid: 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? + question: + following_tags: 已关注的标签 + edit: 编辑 + save: 保存 + follow_tag_tip: 按照标签整理您的问题列表。 + hot_questions: 热点问题 + all_questions: 全部问题 + x_questions: '{{ count }} 个问题' + x_answers: '{{ count }} 个回答' + questions: 个问题 + answers: 回答 + newest: 最新 + active: 活跃 + frequent: 浏览量 + score: 评分 + unanswered: 未回答 + modified: 修改于 + answered: 回答于 + asked: 提问于 + closed: 已关闭 + follow_a_tag: 关注一个标签 + more: 更多 + personal: + overview: 概览 + answers: 回答 + answer: 回答 + questions: 问题 + question: 问题 + bookmarks: 收藏 + reputation: 声望 + comments: 评论 + votes: 得票 + newest: 最新 + score: 评分 + edit_profile: 编辑我的资料 + visited_x_days: 'Visited {{ count }} days' + viewed: Viewed + joined: 加入于 + last_login: 上次登录 + about_me: 关于我 + about_me_empty: '// Hello, World !' + top_answers: 热门回答 + top_questions: 热门问题 + stats: 状态 + list_empty: 没有找到相关的内容。
试试看其他标签? + accepted: 已采纳 + answered: 回答于 + asked: 提问于 + upvote: 赞同 + downvote: 反对 + mod_short: 管理员 + mod_long: 管理员 + x_reputation: 声望 + x_votes: 得票 + x_answers: 个回答 + x_questions: 个问题 + page_404: + description: 页面不存在 + back_home: 回到主页 + page_50X: + description: 服务器遇到了一个错误,无法完成你的请求。 + back_home: 回到主页 + admin: + admin_header: + title: 后台管理 + nav_menus: + dashboard: 后台管理 + contents: 内容管理 + questions: 问题 + answers: 回答 + users: 用户管理 + flags: 举报管理 + settings: 站点设置 + general: 一般 + interface: 界面 + smtp: SMTP + dashboard: + title: 后台管理 + welcome: 欢迎来到 Answer 后台管理! + version: 版本 + flags: + title: 举报 + pending: 等待处理 + completed: 已完成 + flagged: 被举报内容 + created: 创建于 + action: 操作 + review: 审查 + change_modal: + title: 更改用户状态为... + btn_cancel: 取消 + btn_submit: 提交 + normal_name: 正常 + normal_description: 正常状态的用户可以提问和回答。 + suspended_name: 封禁 + suspended_description: 被封禁的用户将无法登录。 + deleted_name: 删除 + deleted_description: 删除用户的个人信息,认证等等。 + inactive_name: 不活跃 + inactive_description: 不活跃的用户必须重新验证邮箱。 + confirm_title: 删除此用户 + confirm_content: 确定要删除此用户?此操作无法撤销! + confirm_btn: 删除 + msg: + empty: 请选择一个原因 + status_modal: + title: '更改 {{ type }} 状态为...' + normal_name: 正常 + normal_description: 所有用户都可以访问 + closed_name: 关闭 + closed_description: 不能回答,但仍然可以编辑、投票和评论。 + deleted_name: 删除 + deleted_description: 所有获得/损失的声望将会恢复。 + btn_cancel: 取消 + btn_submit: 提交 + btn_next: 下一步 + users: + title: 用户 + name: 名称 + email: 邮箱 + reputation: 声望 + created_at: 创建时间 + delete_at: 删除时间 + suspend_at: 封禁时间 + status: 状态 + action: 操作 + change: 更改 + all: 全部 + inactive: 不活跃 + suspended: 已封禁 + deleted: 已删除 + normal: 正常 + questions: + page_title: 问题 + normal: 正常 + closed: 已关闭 + deleted: 已删除 + post: 标题 + votes: 得票数 + answers: 回答数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + answers: + page_title: 回答 + normal: 正常 + deleted: 已删除 + post: 标题 + votes: 得票数 + created: 创建于 + status: 状态 + action: 操作 + change: 更改 + general: + page_title: 一般 + name: + label: 站点名称 + msg: 不能为空 + text: 站点的名称,作为站点的标题(HTML 的 title 标签)。 + short_description: + label: 简短的站点标语 (可选) + msg: 不能为空 + text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。 + description: + label: 网站描述 (可选) + msg: 不能为空 + text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。 + interface: + page_title: 界面 + logo: + label: Logo (可选) + msg: 不能为空 + text: 可以上传图片,或者<1>重置为站点标题。 + theme: + label: 主题 + msg: 不能为空 + text: 选择一个主题 + language: + label: 界面语言 + msg: 不能为空 + text: 设置用户界面语言,在刷新页面后生效。 + smtp: + page_title: SMTP + from_email: + label: 发件人地址 + msg: 不能为空 + text: 用于发送邮件的地址。 + from_name: + label: 发件人名称 + msg: 不能为空 + text: 发件人的名称 + smtp_host: + label: SMTP 主机 + msg: 不能为空 + text: 邮件服务器 + encryption: + label: 加密 + msg: 不能为空 + text: 对于大多数服务器而言,SSL 是推荐开启的。 + ssl: SSL + none: 无加密 + smtp_port: + label: SMTP 端口 + msg: SMTP 端口必须在 1 ~ 65535 之间。 + text: 邮件服务器的端口号。 + smtp_username: + label: SMTP 用户名 + msg: 不能为空 + smtp_password: + label: SMTP 密码 + msg: 不能为空 + test_email_recipient: + label: 测试邮件收件人 + text: 提供用于接收测试邮件的邮箱地址。 + msg: 地址无效 + smtp_authentication: + label: SMTP 认证 + msg: 不能为空 + 'yes': 是 + 'no': 否 -notification: - action: - update_question: - other: "更新了问题" - answer_the_question: - other: "回答了问题" - update_answer: - other: "更新了答案" - adopt_answer: - other: "接受了答案" - comment_question: - other: "评论了问题" - comment_answer: - other: "评论了答案" - reply_to_you: - other: "回复了你" - mention_you: - other: "提到了你" - your_question_is_closed: - other: "你的问题已被关闭" - your_question_was_deleted: - other: "你的问题已被删除" - your_answer_was_deleted: - other: "你的答案已被删除" - your_comment_was_deleted: - other: "你的评论已被删除" From e81d9cfeddd63aff7e93885355088303a1a9104e Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 14:46:22 +0800 Subject: [PATCH 0212/3337] feat: database init when base-info install --- internal/install/install_controller.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index 1bc0984f3..ab1d9790e 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -126,12 +126,6 @@ func InitEnvironment(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil) return } - - if err := migrations.InitDB(c.Data.Database); err != nil { - log.Error("init database error: ", err.Error()) - handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) - return - } handler.HandleResponse(ctx, nil, nil) } @@ -157,6 +151,12 @@ func InitBaseInfo(ctx *gin.Context) { return } + if err := migrations.InitDB(c.Data.Database); err != nil { + log.Error("init database error: ", err.Error()) + handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) + return + } + err = migrations.UpdateInstallInfo(c.Data.Database, req.Language, req.SiteName, req.SiteURL, req.ContactEmail, req.AdminName, req.AdminPassword, req.AdminEmail) if err != nil { From a5150d06a8383cf15864a362b2c32f8cb1cdce4f Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 14:54:06 +0800 Subject: [PATCH 0213/3337] fix: install process adjustment --- ui/src/pages/Install/index.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index ae8f037b4..b87b13b42 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -116,7 +116,7 @@ const Index: FC = () => { setStep((pre) => pre + 1); }; - const submitDatabaseForm = () => { + const checkInstall = () => { const params = { lang: formData.lang.value, db_type: formData.db_type.value, @@ -126,17 +126,16 @@ const Index: FC = () => { db_name: formData.db_name.value, db_file: formData.db_file.value, }; - dbCheck(params) + installInit(params) .then(() => { handleNext(); }) .catch((err) => { - console.log(err); handleErr(err); }); }; - const checkInstall = () => { + const submitDatabaseForm = () => { const params = { lang: formData.lang.value, db_type: formData.db_type.value, @@ -146,11 +145,13 @@ const Index: FC = () => { db_name: formData.db_name.value, db_file: formData.db_file.value, }; - installInit(params) + dbCheck(params) .then(() => { - handleNext(); + // handleNext(); + checkInstall(); }) .catch((err) => { + console.log(err); handleErr(err); }); }; @@ -183,7 +184,11 @@ const Index: FC = () => { submitDatabaseForm(); } if (step === 3) { - checkInstall(); + if (errorData.msg) { + checkInstall(); + } else { + handleNext(); + } } if (step === 4) { submitSiteConfig(); From 21b7560f2c1ccf88a8944ff733a3dce3028cf8cd Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 9 Nov 2022 14:58:15 +0800 Subject: [PATCH 0214/3337] add dashboard cache --- cmd/answer/wire_gen.go | 2 +- internal/controller/cron_controller.go | 1 + internal/controller/dashboard_controller.go | 2 +- internal/schema/dashboard_schema.go | 5 +++ .../service/dashboard/dashboard_service.go | 34 +++++++++++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 internal/controller/cron_controller.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 49e980eb7..e7ade4d00 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -152,7 +152,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, dataData) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) diff --git a/internal/controller/cron_controller.go b/internal/controller/cron_controller.go new file mode 100644 index 000000000..b0b429f89 --- /dev/null +++ b/internal/controller/cron_controller.go @@ -0,0 +1 @@ +package controller diff --git a/internal/controller/dashboard_controller.go b/internal/controller/dashboard_controller.go index 6ee50f4cf..a525adebd 100644 --- a/internal/controller/dashboard_controller.go +++ b/internal/controller/dashboard_controller.go @@ -29,7 +29,7 @@ func NewDashboardController( // @Router /answer/admin/api/dashboard [get] // @Success 200 {object} handler.RespBody func (ac *DashboardController) DashboardInfo(ctx *gin.Context) { - info, err := ac.dashboardService.Statistical(ctx) + info, err := ac.dashboardService.StatisticalByCache(ctx) handler.HandleResponse(ctx, err, gin.H{ "info": info, }) diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go index 2f5b36220..d08ee2b2a 100644 --- a/internal/schema/dashboard_schema.go +++ b/internal/schema/dashboard_schema.go @@ -4,6 +4,11 @@ import "time" var AppStartTime time.Time +const ( + DashBoardCachekey = "answer@dashboard" + DashBoardCacheTime = 31 * time.Minute +) + type DashboardInfo struct { QuestionCount int64 `json:"question_count"` AnswerCount int64 `json:"answer_count"` diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index aa8765071..f1ce90ad5 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -9,6 +9,7 @@ import ( "time" "github.com/answerdev/answer/internal/base/constant" + "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity_common" @@ -33,6 +34,7 @@ type DashboardService struct { reportRepo report_common.ReportRepo configRepo config.ConfigRepo siteInfoService *siteinfo_common.SiteInfoCommonService + data *data.Data } func NewDashboardService( @@ -44,6 +46,7 @@ func NewDashboardService( reportRepo report_common.ReportRepo, configRepo config.ConfigRepo, siteInfoService *siteinfo_common.SiteInfoCommonService, + data *data.Data, ) *DashboardService { return &DashboardService{ questionRepo: questionRepo, @@ -54,9 +57,40 @@ func NewDashboardService( reportRepo: reportRepo, configRepo: configRepo, siteInfoService: siteInfoService, + data: data, } } +func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.DashboardInfo, error) { + ds.SetCache(ctx) + dashboardInfo := &schema.DashboardInfo{} + infoStr, err := ds.data.Cache.GetString(ctx, schema.DashBoardCachekey) + if err != nil { + return dashboardInfo, err + } + err = json.Unmarshal([]byte(infoStr), dashboardInfo) + if err != nil { + return dashboardInfo, err + } + return dashboardInfo, nil +} + +func (ds *DashboardService) SetCache(ctx context.Context) error { + info, err := ds.Statistical(ctx) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + infoStr, err := json.Marshal(info) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + err = ds.data.Cache.SetString(ctx, schema.DashBoardCachekey, string(infoStr), schema.DashBoardCacheTime) + if err != nil { + return errors.InternalServer(reason.UnknownError).WithError(err).WithStack() + } + return nil +} + // Statistical func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) { dashboardInfo := &schema.DashboardInfo{} From 55060c1a10a01a5ce84392f5ac5832fd0d3578a5 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 14:59:07 +0800 Subject: [PATCH 0215/3337] feat: user change email if email exist return form error --- internal/controller/user_controller.go | 16 +++++++++++----- internal/service/user_service.go | 19 ++++++++++++------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 3d4d2d3ac..92d491fe4 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -495,6 +495,10 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { req.UserID = middleware.GetLoginUserIDFromContext(ctx) // If the user is not logged in, the api cannot be used. // If the user email is not verified, that also can use this api to modify the email. + if len(req.UserID) == 0 { + handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + return + } captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) if !captchaPass { @@ -506,13 +510,15 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) { handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) return } - - if len(req.UserID) == 0 { - handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) + _, _ = 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) + } + handler.HandleResponse(ctx, err, resp) return } - _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP()) - err := uc.userService.UserChangeEmailSendCode(ctx, req) handler.HandleResponse(ctx, err, nil) } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index a4fc1fad5..8673c3d20 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -477,21 +477,26 @@ 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) error { +func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) ( + resp *schema.UserVerifyEmailErrorResponse, err error) { userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) if err != nil { - return err + return nil, err } if !exist { - return errors.BadRequest(reason.UserNotFound) + return nil, errors.BadRequest(reason.UserNotFound) } _, exist, err = us.userRepo.GetByEmail(ctx, req.Email) if err != nil { - return err + return nil, err } if exist { - return errors.BadRequest(reason.EmailDuplicate) + resp = &schema.UserVerifyEmailErrorResponse{ + Key: "e_mail", + Value: reason.EmailDuplicate, + } + return resp, errors.BadRequest(reason.EmailDuplicate) } data := &schema.EmailCodeContent{ @@ -507,12 +512,12 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema. title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL) } if err != nil { - return err + return nil, err } log.Infof("send email confirmation %s", verifyEmailURL) go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString()) - return nil + return nil, nil } // UserChangeEmailVerify user change email verify code From 407e29f63dfc5a3b8e9eea602c29c9b4667ecb29 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 9 Nov 2022 15:00:17 +0800 Subject: [PATCH 0216/3337] fix uploads dir --- .gitignore | 2 +- Dockerfile | 2 +- INSTALL.md | 4 ++-- INSTALL_CN.md | 4 ++-- configs/config.yaml | 2 +- internal/cli/install.go | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 5f3135288..84af0c17e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ /.fleet /.vscode/*.log /cmd/answer/*.sh -/cmd/answer/upfiles/* +/cmd/answer/uploads/* /cmd/logs /configs/config-dev.yaml /go.work* diff --git a/Dockerfile b/Dockerfile index 8b55d58f1..ac06839a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ RUN apk --no-cache add build-base git \ && make clean build \ && cp answer /usr/bin/answer -RUN mkdir -p /data/upfiles && chmod 777 /data/upfiles \ +RUN mkdir -p /data/uploads && chmod 777 /data/uploads \ && mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n # stage3 copy the binary and resource files into fresh container diff --git a/INSTALL.md b/INSTALL.md index 0d3969d21..7bc307b56 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -91,7 +91,7 @@ swaggerui: service_config: secret_key: "answer" #encryption key web_host: "http://127.0.0.1" #Page access using domain name address - upload_path: "./upfiles" #upload directory + upload_path: "./uploads" #upload directory ``` ## Compile the image @@ -100,4 +100,4 @@ If you have modified the source files and want to repackage the image, you can u docker build -t answer:v1.0.0 . ``` ## common problem - 1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`upfiles`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and upfiles directories. + 1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`uploads`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and uploads directories. diff --git a/INSTALL_CN.md b/INSTALL_CN.md index 300851a2e..ec855e949 100644 --- a/INSTALL_CN.md +++ b/INSTALL_CN.md @@ -94,7 +94,7 @@ swaggerui: service_config: secret_key: "answer" #加密key web_host: "http://127.0.0.1" #页面访问使用域名地址 - upload_path: "./upfiles" #上传目录 + upload_path: "./uploads" #上传目录 ``` ## 编译镜像 @@ -103,4 +103,4 @@ service_config: docker build -t answer:v1.0.0 . ``` ## 常见问题 - 1. 项目无法启动,answer 主程序启动依赖配置文件 config.yaml 、国际化翻译目录 /i18n 、上传文件存放目录 /upfiles,需要确保项目启动时加载了配置文件 answer run -c config.yaml 以及在 config.yaml 正确的指定 i18n 和 upfiles 目录的配置项 + 1. 项目无法启动,answer 主程序启动依赖配置文件 config.yaml 、国际化翻译目录 /i18n 、上传文件存放目录 /uploads answer run -c config.yaml 以及在 config.yaml 正确的指定 i18n 和 uploads 目录的配置项 diff --git a/configs/config.yaml b/configs/config.yaml index 6830048f2..8032deeda 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -17,4 +17,4 @@ swaggerui: service_config: secret_key: "answer" web_host: "http://127.0.0.1:9080" - upload_path: "/data/upfiles" + upload_path: "/data/uploads" diff --git a/internal/cli/install.go b/internal/cli/install.go index 8c8ee64f0..28b9966e1 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -17,7 +17,7 @@ const ( var ( ConfigFilePath = "/conf/" - UploadFilePath = "/upfiles/" + UploadFilePath = "/uploads/" I18nPath = "/i18n/" ) From 7f49f8bcb143076d2d6a95f9c70e83a230e5e10a Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 9 Nov 2022 15:23:08 +0800 Subject: [PATCH 0217/3337] fix uploads dir --- internal/cli/install.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/install.go b/internal/cli/install.go index 9273c8e6d..1f759b7b9 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -16,8 +16,8 @@ const ( ) var ( - ConfigFilePath = "/conf/" - UploadFilePath = "/upfiles/" + ConfigFileDir = "/conf/" + UploadFilePath = "/uploads/" I18nPath = "/i18n/" CacheDir = "/cache/" ) From 8637698d3edde6636266fbca43f945b1a66d24f9 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Wed, 9 Nov 2022 16:13:49 +0800 Subject: [PATCH 0218/3337] refactor(utils/common): remove unnecessary method getQueryString --- ui/src/pages/Users/ActiveEmail/index.tsx | 5 +++-- ui/src/pages/Users/Login/index.tsx | 7 ++++--- ui/src/pages/Users/PasswordReset/index.tsx | 7 +++---- ui/src/utils/common.ts | 8 -------- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/ui/src/pages/Users/ActiveEmail/index.tsx b/ui/src/pages/Users/ActiveEmail/index.tsx index ac2223a77..177395d71 100644 --- a/ui/src/pages/Users/ActiveEmail/index.tsx +++ b/ui/src/pages/Users/ActiveEmail/index.tsx @@ -1,16 +1,17 @@ import { FC, memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; import { loggedUserInfoStore } from '@/stores'; -import { getQueryString } from '@/utils'; import { activateAccount } from '@/services'; import { PageTitle } from '@/components'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'page_title' }); + const [searchParams] = useSearchParams(); const updateUser = loggedUserInfoStore((state) => state.update); useEffect(() => { - const code = getQueryString('code'); + const code = searchParams.get('code'); if (code) { activateAccount(encodeURIComponent(code)).then((res) => { diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index 7a499e640..503b9723c 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -1,6 +1,6 @@ import React, { FormEvent, useState, useEffect } from 'react'; import { Container, Form, Button, Col } from 'react-bootstrap'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; import type { @@ -10,7 +10,7 @@ import type { } from '@/common/interface'; import { PageTitle, Unactivate } from '@/components'; import { loggedUserInfoStore } from '@/stores'; -import { getQueryString, guard, floppyNavigation } from '@/utils'; +import { guard, floppyNavigation } from '@/utils'; import { login, checkImgCode } from '@/services'; import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; @@ -20,6 +20,7 @@ import Storage from '@/utils/storage'; const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'login' }); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [refresh, setRefresh] = useState(0); const updateUser = loggedUserInfoStore((state) => state.update); const storeUser = loggedUserInfoStore((state) => state.user); @@ -154,7 +155,7 @@ const Index: React.FC = () => { }, [refresh]); useEffect(() => { - const isInactive = getQueryString('status'); + const isInactive = searchParams.get('status'); if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { setStep(2); diff --git a/ui/src/pages/Users/PasswordReset/index.tsx b/ui/src/pages/Users/PasswordReset/index.tsx index ab00de301..fc95731bb 100644 --- a/ui/src/pages/Users/PasswordReset/index.tsx +++ b/ui/src/pages/Users/PasswordReset/index.tsx @@ -1,17 +1,16 @@ import React, { FormEvent, useState } from 'react'; import { Container, Col, Form, Button } from 'react-bootstrap'; -import { Link } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { loggedUserInfoStore } from '@/stores'; -import { getQueryString } from '@/utils'; import type { FormDataType } from '@/common/interface'; import { replacementPassword } from '@/services'; import { PageTitle } from '@/components'; const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'password_reset' }); - + const [searchParams] = useSearchParams(); const [step, setStep] = useState(1); const clearUser = loggedUserInfoStore((state) => state.clear); const [formData, setFormData] = useState({ @@ -91,7 +90,7 @@ const Index: React.FC = () => { if (checkValidated() === false) { return; } - const code = getQueryString('code'); + const code = searchParams.get('code'); if (!code) { console.error('code is required'); return; diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index 31bb0a40d..16650e0cb 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -1,12 +1,5 @@ import i18next from 'i18next'; -function getQueryString(name: string): string { - const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`); - const r = window.location.search.substr(1).match(reg); - if (r != null) return unescape(r[2]); - return ''; -} - function thousandthDivision(num) { const reg = /\d{1,3}(?=(\d{3})+$)/g; return `${num}`.replace(reg, '$&,'); @@ -87,7 +80,6 @@ function formatUptime(value) { } export { - getQueryString, thousandthDivision, formatCount, scrollTop, From 5bce99208c749c4c3e9ef189e36a141579adc1ac Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 16:24:25 +0800 Subject: [PATCH 0219/3337] feat: cut off answer html description --- internal/service/answer_common/answer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index ef6113510..196d6628f 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -5,6 +5,7 @@ import ( "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/pkg/htmltext" ) type AnswerRepo interface { @@ -75,11 +76,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer info := schema.AdminAnswerInfo{} info.ID = data.ID info.QuestionID = data.QuestionID - info.Description = data.ParsedText info.Adopted = data.Adopted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() info.UserID = data.UserID + info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240) return &info } From b766824a4eaa7ae6dceb1a2799a38b0df7efd5ea Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 16:24:25 +0800 Subject: [PATCH 0220/3337] feat: cut off answer html description --- internal/service/answer_common/answer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index ef6113510..196d6628f 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -5,6 +5,7 @@ import ( "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/pkg/htmltext" ) type AnswerRepo interface { @@ -75,11 +76,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer info := schema.AdminAnswerInfo{} info.ID = data.ID info.QuestionID = data.QuestionID - info.Description = data.ParsedText info.Adopted = data.Adopted info.VoteCount = data.VoteCount info.CreateTime = data.CreatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix() info.UserID = data.UserID + info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240) return &info } From ac62d12ec1220ee9dd18d1d7cc3d2a097adb92c9 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 16:27:23 +0800 Subject: [PATCH 0221/3337] fix: change email route add guard --- ui/src/router/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index c745ba1d9..a723284c5 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -155,7 +155,7 @@ const routes: RouteNode[] = [ path: 'users/change-email', page: 'pages/Users/ChangeEmail', guard: async () => { - return guard.notLogged(); + return guard.notActivated(); }, }, { From 3d95d0391146251d0f2b981082d15aaa9c4c86a7 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 17:20:16 +0800 Subject: [PATCH 0222/3337] feat: add html text cut pkg --- go.mod | 3 +- go.sum | 4 +-- pkg/htmltext/htmltext.go | 61 +++++++++++++++++++++++++++++++++++ pkg/htmltext/htmltext_test.go | 52 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 pkg/htmltext/htmltext.go create mode 100644 pkg/htmltext/htmltext_test.go diff --git a/go.mod b/go.mod index 0da95f1c6..95796a933 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/goccy/go-json v0.9.11 github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 + github.com/grokify/html-strip-tags-go v0.0.1 github.com/jinzhu/copier v0.3.5 github.com/jinzhu/now v1.1.5 github.com/lib/pq v1.10.7 @@ -35,6 +36,7 @@ require ( golang.org/x/crypto v0.1.0 golang.org/x/net v0.1.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/yaml.v3 v3.0.1 xorm.io/builder v0.3.12 xorm.io/core v0.7.3 xorm.io/xorm v1.3.2 @@ -110,6 +112,5 @@ require ( gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 8a6c2d9bb..f12aeb7ac 100644 --- a/go.sum +++ b/go.sum @@ -299,6 +299,8 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -594,8 +596,6 @@ github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1 github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk= github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= -github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc= diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go new file mode 100644 index 000000000..e2acb315a --- /dev/null +++ b/pkg/htmltext/htmltext.go @@ -0,0 +1,61 @@ +package htmltext + +import ( + "regexp" + "strings" + + "github.com/grokify/html-strip-tags-go" +) + +// ClearText clear HTML, get the clear text +func ClearText(html string) (text string) { + if len(html) == 0 { + text = html + return + } + + var ( + re *regexp.Regexp + codeReg = `(?ism)<(pre)>.*<\/pre>` + codeRepl = "{code...}" + linkReg = `(?ism).*?<\/a>` + linkRepl = "[link]" + spaceReg = ` +` + spaceRepl = " " + ) + re = regexp.MustCompile(codeReg) + html = re.ReplaceAllString(html, codeRepl) + + re = regexp.MustCompile(linkReg) + html = re.ReplaceAllString(html, linkRepl) + + text = strings.NewReplacer( + "\n", " ", + "\r", " ", + "\t", " ", + ).Replace(strip.StripTags(html)) + + // replace multiple spaces to one space + re = regexp.MustCompile(spaceReg) + text = strings.TrimSpace(re.ReplaceAllString(text, spaceRepl)) + return +} + +// FetchExcerpt return the excerpt from the HTML string +func FetchExcerpt(html, trimMarker string, limit int) (text string) { + if len(html) == 0 { + text = html + return + } + + text = ClearText(html) + runeText := []rune(text) + if len(runeText) <= limit { + text = string(runeText) + } else { + text = string(runeText[0:limit]) + } + + text += trimMarker + return +} diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go new file mode 100644 index 000000000..568299af6 --- /dev/null +++ b/pkg/htmltext/htmltext_test.go @@ -0,0 +1,52 @@ +package htmltext + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClearText(t *testing.T) { + var ( + expected, + clearedText string + ) + + // test code clear text + expected = "hello{code...}" + clearedText = ClearText("

hello

var a = \"good\"

") + assert.Equal(t, expected, clearedText) + + // test link clear text + expected = "hello[link]" + clearedText = ClearText("

helloexample.com

") + assert.Equal(t, expected, clearedText) + clearedText = ClearText("

helloexample.com

") + assert.Equal(t, expected, clearedText) + + expected = "hello world" + clearedText = ClearText("
hello
\n
world
") + assert.Equal(t, expected, clearedText) +} + +func TestFetchExcerpt(t *testing.T) { + var ( + expected, + text string + ) + + // test english string + expected = "hello..." + text = FetchExcerpt("

hello world

", "...", 5) + assert.Equal(t, expected, text) + + // test mixed string + expected = "hello你好..." + text = FetchExcerpt("

hello你好world

", "...", 7) + assert.Equal(t, expected, text) + + // test mixed string with emoticon + expected = "hello你好😂..." + text = FetchExcerpt("

hello你好😂world

", "...", 8) + assert.Equal(t, expected, text) +} From 8ac92750a41bad3cd5906cc62fdbe9149d29f69b Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 17:22:20 +0800 Subject: [PATCH 0223/3337] feat: add html text cut pkg --- pkg/htmltext/htmltext.go | 3 +-- pkg/htmltext/htmltext_test.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go index e2acb315a..31ae1a223 100644 --- a/pkg/htmltext/htmltext.go +++ b/pkg/htmltext/htmltext.go @@ -1,10 +1,9 @@ package htmltext import ( + "github.com/grokify/html-strip-tags-go" "regexp" "strings" - - "github.com/grokify/html-strip-tags-go" ) // ClearText clear HTML, get the clear text diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go index 568299af6..71aafbb6e 100644 --- a/pkg/htmltext/htmltext_test.go +++ b/pkg/htmltext/htmltext_test.go @@ -1,9 +1,8 @@ package htmltext import ( - "testing" - "github.com/stretchr/testify/assert" + "testing" ) func TestClearText(t *testing.T) { From 498b7f489334d7c75b5f82b44e9f7d0e53ffbc7e Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 9 Nov 2022 17:42:10 +0800 Subject: [PATCH 0224/3337] fix: upload add siteinfo service --- cmd/answer/wire_gen.go | 2 +- internal/service/uploader/upload.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 302a67def..2e002347f 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -108,7 +108,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) - uploaderService := uploader.NewUploaderService(serviceConf) + uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService) userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService) commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go index f4a8e61dc..1d6d24917 100644 --- a/internal/service/uploader/upload.go +++ b/internal/service/uploader/upload.go @@ -33,7 +33,8 @@ type UploaderService struct { } // NewUploaderService new upload service -func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderService { +func NewUploaderService(serviceConfig *service_config.ServiceConfig, + siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService { err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath)) if err != nil { panic(err) @@ -43,7 +44,8 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderSe panic(err) } return &UploaderService{ - serviceConfig: serviceConfig, + serviceConfig: serviceConfig, + siteInfoService: siteInfoService, } } From 596b104e3c593c051cd0a80279eafec6f47415cc Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 9 Nov 2022 18:00:58 +0800 Subject: [PATCH 0225/3337] fix: table add width --- ui/src/pages/Admin/Answers/index.tsx | 15 ++++++++++----- ui/src/pages/Admin/Questions/index.tsx | 17 +++++++++++------ ui/src/pages/Admin/Users/index.tsx | 16 +++++++++------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 3a33fe497..cc584c313 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -110,10 +110,12 @@ const Answers: FC = () => {
- - - - {curFilter !== 'deleted' && } + + + + {curFilter !== 'deleted' && ( + + )} @@ -164,7 +166,10 @@ const Answers: FC = () => { {curFilter !== 'deleted' && ( diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index c2f5e5c67..76a65de96 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -126,12 +126,14 @@ const Questions: FC = () => {
{t('post')}{t('post')} {t('votes')}{t('created')}{t('created')} {t('status')}{t('action')}
{li.vote_count} - {li.answer_count} - + From 27e3e2ec8c9523ba6587df9150dc7f279e268a29 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 11:46:26 +0800 Subject: [PATCH 0098/3337] fix AvatarInfo json.Unmarshal error --- internal/schema/user_schema.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 6ca66f76f..d871d5eaa 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -10,7 +10,6 @@ import ( "github.com/answerdev/answer/pkg/checker" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" ) // UserVerifyEmailReq user verify email request @@ -108,7 +107,6 @@ func FormatAvatarInfo(avatarJson string) string { AvatarInfo := &AvatarInfo{} err := json.Unmarshal([]byte(avatarJson), AvatarInfo) if err != nil { - log.Error("AvatarInfo json.Unmarshal Error", err) return "" } switch AvatarInfo.Type { From 53501a369205a2e328302c12142f3ceaa846003e Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 13:15:52 +0800 Subject: [PATCH 0099/3337] Revert "Fix RouteNode type error" --- ui/src/router/index.tsx | 3 +-- ui/src/router/route-config.ts | 7 ++++++- ui/src/router/types.ts | 14 -------------- 3 files changed, 7 insertions(+), 17 deletions(-) delete mode 100644 ui/src/router/types.ts diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 7f2f0d3e3..99d447233 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -2,10 +2,9 @@ import React, { Suspense, lazy } from 'react'; import { RouteObject, createBrowserRouter } from 'react-router-dom'; import Layout from '@answer/pages/Layout'; -import routeConfig from '@/router/route-config'; +import routeConfig, { RouteNode } from '@/router/route-config'; import RouteRules from '@/router/route-rules'; -import { RouteNode } from '@/router/types'; const routes: RouteObject[] = []; diff --git a/ui/src/router/route-config.ts b/ui/src/router/route-config.ts index 0eb316514..2e46ffbe2 100644 --- a/ui/src/router/route-config.ts +++ b/ui/src/router/route-config.ts @@ -1,5 +1,10 @@ -import { RouteNode } from '@/router/types'; +import { RouteObject } from 'react-router-dom'; +export interface RouteNode extends RouteObject { + page: string; + children?: RouteNode[]; + rules?: string[]; +} const routeConfig: RouteNode[] = [ { path: '/', diff --git a/ui/src/router/types.ts b/ui/src/router/types.ts deleted file mode 100644 index f0d13c875..000000000 --- a/ui/src/router/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom'; - -type CustomRouteObject = { - page: string; - rules?: string[]; -}; - -type IndexRouteNode = IndexRouteObject & CustomRouteObject; - -interface NonIndexRouteNode extends NonIndexRouteObject, CustomRouteObject { - children?: (IndexRouteNode | NonIndexRouteNode)[]; -} - -export type RouteNode = IndexRouteNode | NonIndexRouteNode; From 79cc30355a9fca8945c3127f8a06e81a59a50ac7 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 14:43:45 +0800 Subject: [PATCH 0100/3337] update: gitlab ci only deploy on test --- .gitlab-ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b9175a7e8..c80bde9f6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,9 +38,7 @@ stages: stage: push extends: .docker-build-push only: - - dev - - master - - main + - test variables: DockerNamespace: sf_app DockerImage: answer @@ -52,7 +50,7 @@ stages: stage: deploy-dev extends: .deploy-helm only: - - main + - test variables: LoadBalancerIP: 10.0.10.98 KubernetesCluster: dev From 18e880b9c872a1e649a961b81a553e07d4aa8607 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 15:02:27 +0800 Subject: [PATCH 0101/3337] feat: add time zone config --- docs/docs.go | 30 ++++++++++++------- docs/swagger.json | 30 ++++++++++++------- docs/swagger.yaml | 24 ++++++++++----- .../siteinfo_controller.go | 24 +++++++-------- internal/schema/siteinfo_schema.go | 13 ++++---- 5 files changed, 75 insertions(+), 46 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index f21f508cf..507c0d3bc 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -487,14 +487,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -522,14 +522,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -558,14 +558,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "get site interface", "parameters": [ { "description": "general", @@ -604,14 +604,14 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -5326,7 +5326,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5340,6 +5341,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5347,7 +5352,8 @@ const docTemplate = `{ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5361,6 +5367,10 @@ const docTemplate = `{ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 039f67f5e..caa69fd0a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -475,14 +475,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo general", + "description": "get site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo general", + "summary": "get site general information", "responses": { "200": { "description": "OK", @@ -510,14 +510,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site general information", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site general information", "parameters": [ { "description": "general", @@ -546,14 +546,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "get site interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "get site interface", "parameters": [ { "description": "general", @@ -592,14 +592,14 @@ "ApiKeyAuth": [] } ], - "description": "Get siteinfo interface", + "description": "update site info interface", "produces": [ "application/json" ], "tags": [ "admin" ], - "summary": "Get siteinfo interface", + "summary": "update site info interface", "parameters": [ { "description": "general", @@ -5314,7 +5314,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5328,6 +5329,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, @@ -5335,7 +5340,8 @@ "type": "object", "required": [ "language", - "theme" + "theme", + "time_zone" ], "properties": { "language": { @@ -5349,6 +5355,10 @@ "theme": { "type": "string", "maxLength": 128 + }, + "time_zone": { + "type": "string", + "maxLength": 128 } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c419b60ee..fd74cb63a 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1024,9 +1024,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.SiteInterfaceResp: properties: @@ -1039,9 +1043,13 @@ definitions: theme: maxLength: 128 type: string + time_zone: + maxLength: 128 + type: string required: - language - theme + - time_zone type: object schema.TagItem: properties: @@ -1662,7 +1670,7 @@ paths: - admin /answer/admin/api/siteinfo/general: get: - description: Get siteinfo general + description: get site general information produces: - application/json responses: @@ -1677,11 +1685,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo general + summary: get site general information tags: - admin put: - description: Get siteinfo interface + description: update site general information parameters: - description: general in: body @@ -1698,12 +1706,12 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site general information tags: - admin /answer/admin/api/siteinfo/interface: get: - description: Get siteinfo interface + description: get site interface parameters: - description: general in: body @@ -1725,11 +1733,11 @@ paths: type: object security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: get site interface tags: - admin put: - description: Get siteinfo interface + description: update site info interface parameters: - description: general in: body @@ -1746,7 +1754,7 @@ paths: $ref: '#/definitions/handler.RespBody' security: - ApiKeyAuth: [] - summary: Get siteinfo interface + summary: update site info interface tags: - admin /answer/admin/api/theme/options: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 30bfd1bfd..821b517e1 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -18,9 +18,9 @@ func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoCo } } -// GetGeneral godoc -// @Summary Get siteinfo general -// @Description Get siteinfo general +// GetGeneral get site general information +// @Summary get site general information +// @Description get site general information // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -31,9 +31,9 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// GetInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// GetInterface get site interface +// @Summary get site interface +// @Description get site interface // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -45,9 +45,9 @@ func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { handler.HandleResponse(ctx, err, resp) } -// UpdateGeneral godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// UpdateGeneral update site general information +// @Summary update site general information +// @Description update site general information // @Security ApiKeyAuth // @Tags admin // @Produce json @@ -63,9 +63,9 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) } -// UpdateInterface godoc -// @Summary Get siteinfo interface -// @Description Get siteinfo interface +// UpdateInterface update site interface +// @Summary update site info interface +// @Description update site info interface // @Security ApiKeyAuth // @Tags admin // @Produce json diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index ff93e72d7..446b986db 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -2,16 +2,17 @@ package schema // SiteGeneralReq site general request type SiteGeneralReq struct { - Name string `validate:"required,gt=1,lte=128" comment:"site name" form:"name" json:"name"` - ShortDescription string `validate:"required,gt=3,lte=255" comment:"short site description" form:"short_description" json:"short_description"` - Description string `validate:"required,gt=3,lte=2000" comment:"site description" form:"description" json:"description"` + Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"` + ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"` + Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"` } // SiteInterfaceReq site interface request type SiteInterfaceReq struct { - Logo string `validate:"omitempty,gt=0,lte=256" comment:"logo" form:"logo" json:"logo"` - Theme string `validate:"required,gt=1,lte=128" comment:"theme" form:"theme" json:"theme"` - Language string `validate:"required,gt=1,lte=128" comment:"interface language" form:"language" json:"language"` + 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"` } // SiteGeneralResp site general response From 720754c8bbbf88d4f30adf9f2f4eb03ef1514127 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 15:25:27 +0800 Subject: [PATCH 0102/3337] feat: support to config the default language --- cmd/answer/wire_gen.go | 12 ++++---- i18n/i18n.yaml | 6 ++++ internal/base/translator/provider.go | 41 ++++++++++++++++++++++++++ internal/controller/lang_controller.go | 41 +++++++++++++++++++++----- internal/controller/user_controller.go | 21 +++++++++++++ internal/entity/user_entity.go | 1 + internal/migrations/migrations.go | 12 ++++---- internal/migrations/v1.go | 12 ++++++++ internal/repo/user/user_repo.go | 8 +++++ internal/router/answer_api_router.go | 5 ++-- internal/router/ui.go | 2 ++ internal/schema/lang_schema.go | 18 ----------- internal/schema/user_schema.go | 10 +++++++ internal/service/siteinfo_service.go | 16 ++++------ internal/service/user_common/user.go | 1 + internal/service/user_service.go | 13 ++++++++ 16 files changed, 168 insertions(+), 51 deletions(-) create mode 100644 i18n/i18n.yaml create mode 100644 internal/migrations/v1.go delete mode 100644 internal/schema/lang_schema.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index a965ed8b7..746321b70 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -76,7 +76,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, if err != nil { return nil, nil, err } - langController := controller.NewLangController(i18nTranslator) engine, err := data.NewDB(debug, dbConf) if err != nil { return nil, nil, err @@ -90,17 +89,19 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, cleanup() return nil, nil, err } + siteInfoRepo := site_info.NewSiteInfo(dataData) + configRepo := config.NewConfigRepo(dataData) + emailRepo := export.NewEmailRepo(dataData) + emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) + siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) + langController := controller.NewLangController(i18nTranslator, siteInfoService) authRepo := auth.NewAuthRepo(dataData) authService := auth2.NewAuthService(authRepo) - configRepo := config.NewConfigRepo(dataData) userRepo := user.NewUserRepo(dataData, configRepo) uniqueIDRepo := unique.NewUniqueIDRepo(dataData) activityRepo := activity_common.NewActivityRepo(dataData, uniqueIDRepo, configRepo) userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) - emailRepo := export.NewEmailRepo(dataData) - siteInfoRepo := site_info.NewSiteInfo(dataData) - emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) captchaRepo := captcha.NewCaptchaRepo(dataData) captchaService := action.NewCaptchaService(captchaRepo) @@ -166,7 +167,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, reasonService := reason2.NewReasonService(reasonRepo) reasonController := controller.NewReasonController(reasonService) themeController := controller_backyard.NewThemeController() - siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) siteinfoController := controller.NewSiteinfoController(siteInfoService) notificationRepo := notification.NewNotificationRepo(dataData) diff --git a/i18n/i18n.yaml b/i18n/i18n.yaml new file mode 100644 index 000000000..13a6c5228 --- /dev/null +++ b/i18n/i18n.yaml @@ -0,0 +1,6 @@ +# all support language +language_options: + - label: "简体中文(CN)" + value: "zh_CN" + - label: "English(US)" + value: "en_US" diff --git a/internal/base/translator/provider.go b/internal/base/translator/provider.go index 0c0bcac61..a81576bd5 100644 --- a/internal/base/translator/provider.go +++ b/internal/base/translator/provider.go @@ -1,17 +1,58 @@ package translator import ( + "fmt" + "os" + "path/filepath" + "github.com/google/wire" myTran "github.com/segmentfault/pacman/contrib/i18n" "github.com/segmentfault/pacman/i18n" + "sigs.k8s.io/yaml" ) // ProviderSet is providers. var ProviderSet = wire.NewSet(NewTranslator) var GlobalTrans i18n.Translator +// LangOption language option +type LangOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// LanguageOptions language +var LanguageOptions []*LangOption + // NewTranslator new a translator func NewTranslator(c *I18n) (tr i18n.Translator, err error) { GlobalTrans, err = myTran.NewTranslator(c.BundleDir) + if err != nil { + return nil, err + } + + i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml")) + if err != nil { + return nil, fmt.Errorf("read i18n file failed: %s", err) + } + + s := struct { + LangOption []*LangOption `json:"language_options"` + }{} + err = yaml.Unmarshal(i18nFile, &s) + if err != nil { + return nil, fmt.Errorf("i18n file parsing failed: %s", err) + } + LanguageOptions = s.LangOption return GlobalTrans, err } + +// CheckLanguageIsValid check user input language is valid +func CheckLanguageIsValid(lang string) bool { + for _, option := range LanguageOptions { + if option.Value == lang { + return true + } + } + return false +} diff --git a/internal/controller/lang_controller.go b/internal/controller/lang_controller.go index a689fffc8..384d711c0 100644 --- a/internal/controller/lang_controller.go +++ b/internal/controller/lang_controller.go @@ -4,18 +4,20 @@ import ( "encoding/json" "github.com/answerdev/answer/internal/base/handler" - "github.com/answerdev/answer/internal/schema" + "github.com/answerdev/answer/internal/base/translator" + "github.com/answerdev/answer/internal/service" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" ) type LangController struct { - translator i18n.Translator + translator i18n.Translator + siteInfoService *service.SiteInfoService } // NewLangController new language controller. -func NewLangController(tr i18n.Translator) *LangController { - return &LangController{translator: tr} +func NewLangController(tr i18n.Translator, siteInfoService *service.SiteInfoService) *LangController { + return &LangController{translator: tr, siteInfoService: siteInfoService} } // GetLangMapping get language config mapping @@ -33,15 +35,38 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) { handler.HandleResponse(ctx, nil, resp) } -// GetLangOptions Get language options +// GetAdminLangOptions Get language options // @Summary Get language options // @Description Get language options -// @Security ApiKeyAuth // @Tags Lang // @Produce json // @Success 200 {object} handler.RespBody{} // @Router /answer/api/v1/language/options [get] // @Router /answer/admin/api/language/options [get] -func (u *LangController) GetLangOptions(ctx *gin.Context) { - handler.HandleResponse(ctx, nil, schema.GetLangOptions) +func (u *LangController) GetAdminLangOptions(ctx *gin.Context) { + handler.HandleResponse(ctx, nil, translator.LanguageOptions) +} + +// GetUserLangOptions Get language options +// @Summary Get language options +// @Description Get language options +// @Tags Lang +// @Produce json +// @Success 200 {object} handler.RespBody{} +// @Router /answer/api/v1/language/options [get] +func (u *LangController) GetUserLangOptions(ctx *gin.Context) { + siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } + + options := translator.LanguageOptions + if len(siteInterfaceResp.Language) > 0 { + defaultOption := []*translator.LangOption{ + {Label: "Default", Value: siteInterfaceResp.Language}, + } + options = append(defaultOption, options...) + } + handler.HandleResponse(ctx, nil, options) } diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 495b16599..028f9302f 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -373,6 +373,27 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) } +// UserUpdateInterface update user interface config +// @Summary UserUpdateInterface update user interface config +// @Description UserUpdateInterface update user interface config +// @Tags User +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param Authorization header string true "access-token" +// @Param data body schema.UpdateUserInterfaceRequest true "UpdateInfoRequest" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/user/interface [put] +func (uc *UserController) UserUpdateInterface(ctx *gin.Context) { + req := &schema.UpdateUserInterfaceRequest{} + if handler.BindAndCheck(ctx, req) { + return + } + req.UserId = middleware.GetLoginUserIDFromContext(ctx) + err := uc.userService.UserUpdateInterface(ctx, req) + handler.HandleResponse(ctx, err, nil) +} + // UploadUserAvatar godoc // @Summary UserUpdateInfo // @Description UserUpdateInfo diff --git a/internal/entity/user_entity.go b/internal/entity/user_entity.go index 7ab239a7f..5d6154820 100644 --- a/internal/entity/user_entity.go +++ b/internal/entity/user_entity.go @@ -45,6 +45,7 @@ type User struct { Location string `xorm:"not null default '' VARCHAR(100) location"` IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"` IsAdmin bool `xorm:"not null default false BOOL is_admin"` + Language string `xorm:"not null default '' VARCHAR(100) language"` } // TableName user table name diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index f4f4efd8d..200e4813d 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -5,7 +5,6 @@ import ( "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/entity" - "github.com/segmentfault/pacman/log" "xorm.io/xorm" ) @@ -43,6 +42,7 @@ var noopMigration = func(_ *xorm.Engine) error { return nil } var migrations = []Migration{ // 0->1 NewMigration("this is first version, no operation", noopMigration), + NewMigration("add user language", addUserLanguage), } // GetCurrentDBVersion returns the current db version @@ -86,17 +86,17 @@ func Migrate(dataConf *data.Database) error { expectedVersion := ExpectedVersion() for currentDBVersion < expectedVersion { - log.Infof("[migrate] current db version is %d, try to migrate version %d, latest version is %d", + fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n", currentDBVersion, currentDBVersion+1, expectedVersion) migrationFunc := migrations[currentDBVersion] - log.Infof("[migrate] try to migrate db version %d, description: %s", currentDBVersion+1, migrationFunc.Description()) + fmt.Printf("[migrate] try to migrate db version %d, description: %s\n", currentDBVersion+1, migrationFunc.Description()) if err := migrationFunc.Migrate(engine); err != nil { - log.Errorf("[migrate] migrate to db version %d failed: ", currentDBVersion+1, err.Error()) + fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error()) return err } - log.Infof("[migrate] migrate to db version %d success", currentDBVersion+1) + fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1) if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil { - log.Errorf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error()) + fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error()) return err } currentDBVersion++ diff --git a/internal/migrations/v1.go b/internal/migrations/v1.go new file mode 100644 index 000000000..b9e316d95 --- /dev/null +++ b/internal/migrations/v1.go @@ -0,0 +1,12 @@ +package migrations + +import ( + "xorm.io/xorm" +) + +func addUserLanguage(x *xorm.Engine) error { + type User struct { + Language string `xorm:"not null default '' VARCHAR(100) language"` + } + return x.Sync(new(User)) +} diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index db1679ed2..feaf675d9 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -101,6 +101,14 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err return } +func (ur *userRepo) UpdateLanguage(ctx context.Context, userID, language string) (err error) { + _, err = ur.data.DB.Where("id = ?", userID).Update(&entity.User{Language: language}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // UpdateInfo update user info func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) { _, err = ur.data.DB.Where("id = ?", userInfo.ID). diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 56a972a8e..068486877 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -79,7 +79,7 @@ func NewAnswerAPIRouter( func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // i18n r.GET("/language/config", a.langController.GetLangMapping) - r.GET("/language/options", a.langController.GetLangOptions) + r.GET("/language/options", a.langController.GetUserLangOptions) // comment r.GET("/comment/page", a.commentController.GetCommentWithPage) @@ -177,6 +177,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { // user 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) @@ -213,7 +214,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) { r.GET("/reasons", a.reasonController.Reasons) // language - r.GET("/language/options", a.langController.GetLangOptions) + r.GET("/language/options", a.langController.GetAdminLangOptions) // theme r.GET("/theme/options", a.themeController.GetThemeOptions) diff --git a/internal/router/ui.go b/internal/router/ui.go index 3498265c6..4d4657163 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -7,6 +7,7 @@ import ( "net/http" "os" + "github.com/answerdev/answer/i18n" "github.com/answerdev/answer/ui" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/log" @@ -52,6 +53,7 @@ func (a *UIRouter) Register(r *gin.Engine) { r.LoadHTMLGlob(staticPath + "/*.html") r.Static("/static", staticPath+"/static") + r.StaticFS("/i18n/", http.FS(i18n.I18n)) r.NoRoute(func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }) diff --git a/internal/schema/lang_schema.go b/internal/schema/lang_schema.go deleted file mode 100644 index 98abe8742..000000000 --- a/internal/schema/lang_schema.go +++ /dev/null @@ -1,18 +0,0 @@ -package schema - -// GetLangOption get label option -type GetLangOption struct { - Label string `json:"label"` - Value string `json:"value"` -} - -var GetLangOptions = []*GetLangOption{ - { - Label: "English(US)", - Value: "en_US", - }, - { - Label: "中文(CN)", - Value: "zh_CN", - }, -} diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index d871d5eaa..d94551017 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -62,6 +62,8 @@ type GetUserResp struct { Location string `json:"location"` // ip info IPInfo string `json:"ip_info"` + // language + Language string `json:"language"` // access token AccessToken string `json:"access_token"` // is admin @@ -305,6 +307,14 @@ func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error) return nil, nil } +// UpdateUserInterfaceRequest update user interface request +type UpdateUserInterfaceRequest struct { + // language + Language string `validate:"required,gt=1,lte=100" json:"language"` + // user id + UserId string `json:"-" ` +} + type UserRetrievePassWordRequest struct { Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail CaptchaID string `json:"captcha_id" ` // captcha_id diff --git a/internal/service/siteinfo_service.go b/internal/service/siteinfo_service.go index a4f23fd3d..03e598e4c 100644 --- a/internal/service/siteinfo_service.go +++ b/internal/service/siteinfo_service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "github.com/answerdev/answer/internal/base/reason" + "github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/export" @@ -77,10 +78,9 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) { var ( - siteType = "interface" - themeExist, - langExist bool - content []byte + siteType = "interface" + themeExist bool + content []byte ) // check theme @@ -96,13 +96,7 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site } // check language - for _, lang := range schema.GetLangOptions { - if lang.Value == req.Language { - langExist = true - break - } - } - if !langExist { + if !translator.CheckLanguageIsValid(req.Language) { err = errors.BadRequest(reason.LangNotFound) return } diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 5cc194b62..86f9566d9 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -15,6 +15,7 @@ type UserRepo interface { UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error UpdateEmail(ctx context.Context, userID, email string) error + UpdateLanguage(ctx context.Context, userID, language string) error UpdatePass(ctx context.Context, userID, pass string) error UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 17341095b..fbc227419 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -11,6 +11,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/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity" @@ -283,6 +284,18 @@ func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, er return has, nil } +// UserUpdateInterface update user interface +func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) { + if !translator.CheckLanguageIsValid(req.Language) { + return errors.BadRequest(reason.LangNotFound) + } + err = us.userRepo.UpdateLanguage(ctx, req.UserId, req.Language) + if err != nil { + return + } + return nil +} + // UserRegisterByEmail user register func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) ( resp *schema.GetUserResp, err error, From ca09c60ee1a906f53274ed37870ac4571d5cdff4 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 15:27:52 +0800 Subject: [PATCH 0103/3337] doc: update swagger --- docs/docs.go | 77 +++++++++++++++++++++++++++++++++++++++++------ docs/swagger.json | 77 +++++++++++++++++++++++++++++++++++++++++------ docs/swagger.yaml | 48 ++++++++++++++++++++++++++--- 3 files changed, 178 insertions(+), 24 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index f21f508cf..7a37d7e30 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -121,11 +121,6 @@ const docTemplate = `{ }, "/answer/admin/api/language/options": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Get language options", "produces": [ "application/json" @@ -1432,11 +1427,6 @@ const docTemplate = `{ }, "/answer/api/v1/language/options": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Get language options", "produces": [ "application/json" @@ -3391,6 +3381,52 @@ const docTemplate = `{ } } }, + "/answer/api/v1/user/interface": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserUpdateInterface update user interface config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserUpdateInterface update user interface config", + "parameters": [ + { + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "UpdateInfoRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", @@ -4822,6 +4858,10 @@ const docTemplate = `{ "description": "is admin", "type": "boolean" }, + "language": { + "description": "language", + "type": "string" + }, "last_login_date": { "description": "last login date", "type": "integer" @@ -4918,6 +4958,10 @@ const docTemplate = `{ "description": "is admin", "type": "boolean" }, + "language": { + "description": "language", + "type": "string" + }, "last_login_date": { "description": "last login date", "type": "integer" @@ -5573,6 +5617,19 @@ const docTemplate = `{ } } }, + "schema.UpdateUserInterfaceRequest": { + "type": "object", + "required": [ + "language" + ], + "properties": { + "language": { + "description": "language", + "type": "string", + "maxLength": 100 + } + } + }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 039f67f5e..405c5d2ac 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -109,11 +109,6 @@ }, "/answer/admin/api/language/options": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Get language options", "produces": [ "application/json" @@ -1420,11 +1415,6 @@ }, "/answer/api/v1/language/options": { "get": { - "security": [ - { - "ApiKeyAuth": [] - } - ], "description": "Get language options", "produces": [ "application/json" @@ -3379,6 +3369,52 @@ } } }, + "/answer/api/v1/user/interface": { + "put": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "UserUpdateInterface update user interface config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "UserUpdateInterface update user interface config", + "parameters": [ + { + "type": "string", + "description": "access-token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "UpdateInfoRequest", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.UpdateUserInterfaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/api/v1/user/login/email": { "post": { "description": "UserEmailLogin", @@ -4810,6 +4846,10 @@ "description": "is admin", "type": "boolean" }, + "language": { + "description": "language", + "type": "string" + }, "last_login_date": { "description": "last login date", "type": "integer" @@ -4906,6 +4946,10 @@ "description": "is admin", "type": "boolean" }, + "language": { + "description": "language", + "type": "string" + }, "last_login_date": { "description": "last login date", "type": "integer" @@ -5561,6 +5605,19 @@ } } }, + "schema.UpdateUserInterfaceRequest": { + "type": "object", + "required": [ + "language" + ], + "properties": { + "language": { + "description": "language", + "type": "string", + "maxLength": 100 + } + } + }, "schema.UpdateUserStatusReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c419b60ee..d2a9dab8b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -652,6 +652,9 @@ definitions: is_admin: description: is admin type: boolean + language: + description: language + type: string last_login_date: description: last login date type: integer @@ -723,6 +726,9 @@ definitions: is_admin: description: is admin type: boolean + language: + description: language + type: string last_login_date: description: last login date type: integer @@ -1195,6 +1201,15 @@ definitions: - synonym_tag_list - tag_id type: object + schema.UpdateUserInterfaceRequest: + properties: + language: + description: language + maxLength: 100 + type: string + required: + - language + type: object schema.UpdateUserStatusReq: properties: status: @@ -1444,8 +1459,6 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] summary: Get language options tags: - Lang @@ -2237,8 +2250,6 @@ paths: description: OK schema: $ref: '#/definitions/handler.RespBody' - security: - - ApiKeyAuth: [] summary: Get language options tags: - Lang @@ -3425,6 +3436,35 @@ paths: summary: UserUpdateInfo update user info tags: - User + /answer/api/v1/user/interface: + put: + consumes: + - application/json + description: UserUpdateInterface update user interface config + parameters: + - description: access-token + in: header + name: Authorization + required: true + type: string + - description: UpdateInfoRequest + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.UpdateUserInterfaceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: UserUpdateInterface update user interface config + tags: + - User /answer/api/v1/user/login/email: post: consumes: From 872689b93a87824bb7868174de7ea10592b58fc3 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 15:50:32 +0800 Subject: [PATCH 0104/3337] add dashboard service --- cmd/answer/wire_gen.go | 4 +- internal/controller/answer_controller.go | 27 +++++- internal/repo/activity_common/vote.go | 11 +++ internal/repo/answer/answer_repo.go | 11 +++ internal/repo/comment/comment_repo.go | 9 ++ internal/repo/question/question_repo.go | 11 +++ internal/router/answer_api_router.go | 3 + internal/service/activity_common/vote.go | 1 + internal/service/answer_common/answer.go | 1 + .../service/comment_common/comment_service.go | 1 + .../service/dashboard/dashboard_service.go | 85 +++++++++++++++++++ internal/service/dashboard/dashboard_test.go | 1 + internal/service/provider.go | 2 + internal/service/question_common/question.go | 1 + 14 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 internal/service/dashboard/dashboard_service.go create mode 100644 internal/service/dashboard/dashboard_test.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index a965ed8b7..1a1720ff9 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -44,6 +44,7 @@ import ( auth2 "github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/collection_common" comment2 "github.com/answerdev/answer/internal/service/comment" + "github.com/answerdev/answer/internal/service/dashboard" export2 "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/follow" meta2 "github.com/answerdev/answer/internal/service/meta" @@ -148,7 +149,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionController := controller.NewQuestionController(questionService, rankService) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) - answerController := controller.NewAnswerController(answerService, rankService) + dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo) + answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) searchController := controller.NewSearchController(searchService) diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go index 43ea51a59..842b107f5 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -9,20 +9,30 @@ import ( "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service" + "github.com/answerdev/answer/internal/service/dashboard" "github.com/answerdev/answer/internal/service/rank" + "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) // AnswerController answer controller type AnswerController struct { - answerService *service.AnswerService - rankService *rank.RankService + answerService *service.AnswerService + rankService *rank.RankService + dashboardService *dashboard.DashboardService } // NewAnswerController new controller -func NewAnswerController(answerService *service.AnswerService, rankService *rank.RankService) *AnswerController { - return &AnswerController{answerService: answerService, rankService: rankService} +func NewAnswerController(answerService *service.AnswerService, + rankService *rank.RankService, + dashboardService *dashboard.DashboardService, +) *AnswerController { + return &AnswerController{ + answerService: answerService, + rankService: rankService, + dashboardService: dashboardService, + } } // RemoveAnswer delete answer @@ -236,3 +246,12 @@ func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) { err := ac.answerService.AdminSetAnswerStatus(ctx, req.AnswerID, req.StatusStr) handler.HandleResponse(ctx, err, gin.H{}) } + +// dashboardService +func (ac *AnswerController) Dashboard(ctx *gin.Context) { + err := ac.dashboardService.Statistical(ctx) + spew.Dump(err) + handler.HandleResponse(ctx, err, gin.H{ + "ping": "pong", + }) +} diff --git a/internal/repo/activity_common/vote.go b/internal/repo/activity_common/vote.go index abbea28fd..16f73fc4a 100644 --- a/internal/repo/activity_common/vote.go +++ b/internal/repo/activity_common/vote.go @@ -4,8 +4,10 @@ import ( "context" "github.com/answerdev/answer/internal/base/data" + "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/service/activity_common" + "github.com/segmentfault/pacman/errors" ) // VoteRepo activity repository @@ -39,3 +41,12 @@ func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string) } return "" } + +func (vr *VoteRepo) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) { + list := make([]*entity.Activity, 0) + count, err = vr.data.DB.Where("cancelled =0").In("activity_type", activityTypes).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index 2020fc2ff..ac6c974d5 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -5,6 +5,7 @@ import ( "strings" "time" "unicode" + "xorm.io/builder" "github.com/answerdev/answer/internal/base/constant" @@ -102,6 +103,16 @@ func (ar *answerRepo) GetAnswer(ctx context.Context, id string) ( return } +// GetQuestionCount +func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.Answer, 0) + count, err = ar.data.DB.Where("status = ?", entity.AnswerStatusAvailable).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // GetAnswerList get answer list all func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) { answerList = make([]*entity.Answer, 0) diff --git a/internal/repo/comment/comment_repo.go b/internal/repo/comment/comment_repo.go index a1a7ab901..992e6a2f4 100644 --- a/internal/repo/comment/comment_repo.go +++ b/internal/repo/comment/comment_repo.go @@ -79,6 +79,15 @@ func (cr *commentRepo) GetComment(ctx context.Context, commentID string) ( return } +func (cr *commentRepo) GetCommentCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.Comment, 0) + count, err = cr.data.DB.Where("status = ?", entity.CommentStatusAvailable).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // GetCommentPage get comment page func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) ( commentList []*entity.Comment, total int64, err error, diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 2e26006f3..4d90a09e7 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -5,6 +5,7 @@ import ( "strings" "time" "unicode" + "xorm.io/builder" "github.com/answerdev/answer/internal/base/constant" @@ -162,6 +163,16 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu return } +func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) { + questionList := make([]*entity.Question, 0) + + count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}).FindAndCount(&questionList) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} + // GetQuestionPage get question page func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) { questionList = make([]*entity.Question, 0) diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 56a972a8e..68192ecb7 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -86,6 +86,9 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage) r.GET("/comment", a.commentController.GetComment) + //test + r.GET("/test", a.answerController.Dashboard) + // user r.GET("/user/info", a.userController.GetUserInfoByUserID) r.GET("/user/status", a.userController.GetUserStatus) diff --git a/internal/service/activity_common/vote.go b/internal/service/activity_common/vote.go index fb97885b2..16de6967d 100644 --- a/internal/service/activity_common/vote.go +++ b/internal/service/activity_common/vote.go @@ -7,4 +7,5 @@ import ( // VoteRepo activity repository type VoteRepo interface { GetVoteStatus(ctx context.Context, objectId, userId string) (status string) + GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) } diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index c84a1d15b..ef6113510 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -20,6 +20,7 @@ type AnswerRepo interface { SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error) + GetAnswerCount(ctx context.Context) (count int64, err error) } // AnswerCommon user service diff --git a/internal/service/comment_common/comment_service.go b/internal/service/comment_common/comment_service.go index dc1f9fc9e..19c2b90ab 100644 --- a/internal/service/comment_common/comment_service.go +++ b/internal/service/comment_common/comment_service.go @@ -12,6 +12,7 @@ import ( // CommentCommonRepo comment repository type CommentCommonRepo interface { GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) + GetCommentCount(ctx context.Context) (count int64, err error) } // CommentCommonService user service diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go new file mode 100644 index 000000000..1d0f5400c --- /dev/null +++ b/internal/service/dashboard/dashboard_service.go @@ -0,0 +1,85 @@ +package dashboard + +import ( + "context" + + "github.com/answerdev/answer/internal/service/activity_common" + answercommon "github.com/answerdev/answer/internal/service/answer_common" + "github.com/answerdev/answer/internal/service/comment_common" + "github.com/answerdev/answer/internal/service/config" + questioncommon "github.com/answerdev/answer/internal/service/question_common" + "github.com/answerdev/answer/internal/service/report_common" + usercommon "github.com/answerdev/answer/internal/service/user_common" + "github.com/davecgh/go-spew/spew" +) + +type DashboardService struct { + questionRepo questioncommon.QuestionRepo + answerRepo answercommon.AnswerRepo + commentRepo comment_common.CommentCommonRepo + voteRepo activity_common.VoteRepo + userRepo usercommon.UserRepo + reportRepo report_common.ReportRepo + configRepo config.ConfigRepo +} + +func NewDashboardService( + questionRepo questioncommon.QuestionRepo, + answerRepo answercommon.AnswerRepo, + commentRepo comment_common.CommentCommonRepo, + voteRepo activity_common.VoteRepo, + userRepo usercommon.UserRepo, + reportRepo report_common.ReportRepo, + configRepo config.ConfigRepo, + +) *DashboardService { + return &DashboardService{ + questionRepo: questionRepo, + answerRepo: answerRepo, + commentRepo: commentRepo, + voteRepo: voteRepo, + userRepo: userRepo, + reportRepo: reportRepo, + configRepo: configRepo, + } +} + +// Statistical +func (ds *DashboardService) Statistical(ctx context.Context) error { + questionCount, err := ds.questionRepo.GetQuestionCount(ctx) + if err != nil { + return err + } + answerCount, err := ds.answerRepo.GetAnswerCount(ctx) + if err != nil { + return err + } + commentCount, err := ds.commentRepo.GetCommentCount(ctx) + if err != nil { + return err + } + + typeKeys := []string{ + "question.vote_up", + "question.vote_down", + "answer.vote_up", + "answer.vote_down", + } + var activityTypes []int + + for _, typeKey := range typeKeys { + var t int + t, err = ds.configRepo.GetConfigType(typeKey) + if err != nil { + continue + } + activityTypes = append(activityTypes, t) + } + + voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes) + if err != nil { + return err + } + spew.Dump(questionCount, answerCount, commentCount, activityTypes, voteCount) + return nil +} diff --git a/internal/service/dashboard/dashboard_test.go b/internal/service/dashboard/dashboard_test.go new file mode 100644 index 000000000..cfdd5f819 --- /dev/null +++ b/internal/service/dashboard/dashboard_test.go @@ -0,0 +1 @@ +package dashboard diff --git a/internal/service/provider.go b/internal/service/provider.go index 3901766ef..4562572aa 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -8,6 +8,7 @@ import ( collectioncommon "github.com/answerdev/answer/internal/service/collection_common" "github.com/answerdev/answer/internal/service/comment" "github.com/answerdev/answer/internal/service/comment_common" + "github.com/answerdev/answer/internal/service/dashboard" "github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/follow" "github.com/answerdev/answer/internal/service/meta" @@ -65,4 +66,5 @@ var ProviderSetService = wire.NewSet( notficationcommon.NewNotificationCommon, notification.NewNotificationService, activity.NewAnswerActivityService, + dashboard.NewDashboardService, ) diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index 3cb944ffd..e8b97e187 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -38,6 +38,7 @@ type QuestionRepo interface { UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error) + GetQuestionCount(ctx context.Context) (count int64, err error) } // QuestionCommon user service From 11ebf6e96bb6c91bb4d6d77343194606783353f2 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 16:01:17 +0800 Subject: [PATCH 0105/3337] fix: swagger wrong request parameter --- docs/docs.go | 11 ----------- docs/swagger.json | 11 ----------- docs/swagger.yaml | 7 ------- internal/controller_backyard/siteinfo_controller.go | 1 - 4 files changed, 30 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 507c0d3bc..03a20ec94 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -566,17 +566,6 @@ const docTemplate = `{ "admin" ], "summary": "get site interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], "responses": { "200": { "description": "OK", diff --git a/docs/swagger.json b/docs/swagger.json index caa69fd0a..7062f7eb8 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -554,17 +554,6 @@ "admin" ], "summary": "get site interface", - "parameters": [ - { - "description": "general", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/schema.AddCommentReq" - } - } - ], "responses": { "200": { "description": "OK", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fd74cb63a..3d707b465 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1712,13 +1712,6 @@ paths: /answer/admin/api/siteinfo/interface: get: description: get site interface - parameters: - - description: general - in: body - name: data - required: true - schema: - $ref: '#/definitions/schema.AddCommentReq' produces: - application/json responses: diff --git a/internal/controller_backyard/siteinfo_controller.go b/internal/controller_backyard/siteinfo_controller.go index 821b517e1..339765aae 100644 --- a/internal/controller_backyard/siteinfo_controller.go +++ b/internal/controller_backyard/siteinfo_controller.go @@ -39,7 +39,6 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) { // @Produce json // @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp} // @Router /answer/admin/api/siteinfo/interface [get] -// @Param data body schema.AddCommentReq true "general" func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { resp, err := sc.siteInfoService.GetSiteInterface(ctx) handler.HandleResponse(ctx, err, resp) From 9593963aee12d7a57d62849d0d502f6b29e2a98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=8E=E5=9F=8E?= Date: Wed, 2 Nov 2022 08:01:37 +0000 Subject: [PATCH 0106/3337] ci: docker compose --- docker-compose.yaml | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index dfa1853a7..d7756b9fb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,29 +1,12 @@ -version: "3.9" +version: "3" services: answer: - image: answerdev/answer:latest + image: answerdev/answer ports: - '9080:80' restart: on-failure - depends_on: - db: - condition: service_healthy - links: - - db volumes: - - ./answer-data/data:/data - db: - image: mariadb:10.4.7 - ports: - - '13306:3306' - restart: on-failure - environment: - MYSQL_DATABASE: answer - MYSQL_ROOT_PASSWORD: root - healthcheck: - test: [ "CMD", "mysqladmin" ,"ping", "-uroot", "-proot"] - timeout: 20s - retries: 10 - command: ['mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci', '--skip-character-set-client-handshake'] - volumes: - - ./answer-data/mysql:/var/lib/mysql + - answer-data:/data + +volumes: + answer-data: From bbbc1db10566e869ab4f9df42858ed3db1afe612 Mon Sep 17 00:00:00 2001 From: ppchart Date: Wed, 2 Nov 2022 16:20:53 +0800 Subject: [PATCH 0107/3337] Only allow use pnpm --- ui/package.json | 5 +++-- ui/scripts/preinstall.js | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 ui/scripts/preinstall.js diff --git a/ui/package.json b/ui/package.json index 069456ef6..c75124603 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,7 +15,8 @@ "prepare": "cd .. && husky install", "cz": "cz", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", - "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" + "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"", + "preinstall": "node ./scripts/preinstall.js" }, "config": { "commitizen": { @@ -109,4 +110,4 @@ "pnpm": ">=7" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/ui/scripts/preinstall.js b/ui/scripts/preinstall.js new file mode 100644 index 000000000..076ad6064 --- /dev/null +++ b/ui/scripts/preinstall.js @@ -0,0 +1,5 @@ +// There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed. +if (!/pnpm/.test(process.env.npm_execpath)) { + console.warn(`\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\u001b[39m\n`) + process.exit(1) +} \ No newline at end of file From e84a681ce43398ff76974c0b892b302ceceb7256 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 16:29:10 +0800 Subject: [PATCH 0108/3337] add dashboard info --- cmd/answer/wire_gen.go | 3 +- docs/docs.go | 28 +++++++++++++++ docs/swagger.json | 28 +++++++++++++++ docs/swagger.yaml | 17 +++++++++ internal/controller/answer_controller.go | 10 ------ internal/controller/controller.go | 1 + internal/controller/dashboard_controller.go | 36 +++++++++++++++++++ internal/repo/report/report_repo.go | 9 +++++ internal/repo/user/user_repo.go | 9 +++++ internal/router/answer_api_router.go | 9 +++-- internal/schema/dashboard_schema.go | 15 ++++++++ .../service/dashboard/dashboard_service.go | 36 ++++++++++++++----- .../service/report_common/report_common.go | 2 ++ internal/service/user_common/user.go | 1 + 14 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 internal/controller/dashboard_controller.go create mode 100644 internal/schema/dashboard_schema.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 1a1720ff9..2d9025046 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -175,7 +175,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) notificationController := controller.NewNotificationController(notificationService) - answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController) + 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) swaggerRouter := router.NewSwaggerRouter(swaggerConf) uiRouter := router.NewUIRouter() authUserMiddleware := middleware.NewAuthUserMiddleware(authService) diff --git a/docs/docs.go b/docs/docs.go index 4162d8a03..4f0bfb5ac 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -113,6 +113,34 @@ const docTemplate = `{ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "security": [ diff --git a/docs/swagger.json b/docs/swagger.json index e60de534a..e856d606d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -101,6 +101,34 @@ } } }, + "/answer/admin/api/dashboard": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "DashboardInfo", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "DashboardInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "security": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 810cfef8c..adc19f984 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1430,6 +1430,23 @@ paths: summary: AdminSetAnswerStatus tags: - admin + /answer/admin/api/dashboard: + get: + consumes: + - application/json + description: DashboardInfo + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: DashboardInfo + tags: + - admin /answer/admin/api/language/options: get: description: Get language options diff --git a/internal/controller/answer_controller.go b/internal/controller/answer_controller.go index 842b107f5..a6443a91b 100644 --- a/internal/controller/answer_controller.go +++ b/internal/controller/answer_controller.go @@ -11,7 +11,6 @@ import ( "github.com/answerdev/answer/internal/service" "github.com/answerdev/answer/internal/service/dashboard" "github.com/answerdev/answer/internal/service/rank" - "github.com/davecgh/go-spew/spew" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/errors" ) @@ -246,12 +245,3 @@ func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) { err := ac.answerService.AdminSetAnswerStatus(ctx, req.AnswerID, req.StatusStr) handler.HandleResponse(ctx, err, gin.H{}) } - -// dashboardService -func (ac *AnswerController) Dashboard(ctx *gin.Context) { - err := ac.dashboardService.Statistical(ctx) - spew.Dump(err) - handler.HandleResponse(ctx, err, gin.H{ - "ping": "pong", - }) -} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 657760437..4a76acf79 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -20,4 +20,5 @@ var ProviderSetController = wire.NewSet( NewReasonController, NewNotificationController, NewSiteinfoController, + NewDashboardController, ) diff --git a/internal/controller/dashboard_controller.go b/internal/controller/dashboard_controller.go new file mode 100644 index 000000000..6ee50f4cf --- /dev/null +++ b/internal/controller/dashboard_controller.go @@ -0,0 +1,36 @@ +package controller + +import ( + "github.com/answerdev/answer/internal/base/handler" + "github.com/answerdev/answer/internal/service/dashboard" + "github.com/gin-gonic/gin" +) + +type DashboardController struct { + dashboardService *dashboard.DashboardService +} + +// NewDashboardController new controller +func NewDashboardController( + dashboardService *dashboard.DashboardService, +) *DashboardController { + return &DashboardController{ + dashboardService: dashboardService, + } +} + +// DashboardInfo godoc +// @Summary DashboardInfo +// @Description DashboardInfo +// @Tags admin +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Router /answer/admin/api/dashboard [get] +// @Success 200 {object} handler.RespBody +func (ac *DashboardController) DashboardInfo(ctx *gin.Context) { + info, err := ac.dashboardService.Statistical(ctx) + handler.HandleResponse(ctx, err, gin.H{ + "info": info, + }) +} diff --git a/internal/repo/report/report_repo.go b/internal/repo/report/report_repo.go index 1ebf820ee..08747246e 100644 --- a/internal/repo/report/report_repo.go +++ b/internal/repo/report/report_repo.go @@ -94,3 +94,12 @@ func (ar *reportRepo) UpdateByID( } return } + +func (vr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.Report, 0) + count, err = vr.data.DB.Where("status =?", entity.ReportStatusPending).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/user/user_repo.go b/internal/repo/user/user_repo.go index db1679ed2..f6ba0b6a9 100644 --- a/internal/repo/user/user_repo.go +++ b/internal/repo/user/user_repo.go @@ -149,3 +149,12 @@ func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *ent } return } + +func (vr *userRepo) GetUserCount(ctx context.Context) (count int64, err error) { + list := make([]*entity.User, 0) + count, err = vr.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list) + if err != nil { + return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 68192ecb7..dbe34405b 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -27,6 +27,7 @@ type AnswerAPIRouter struct { siteInfoController *controller_backyard.SiteInfoController siteinfoController *controller.SiteinfoController notificationController *controller.NotificationController + dashboardController *controller.DashboardController } func NewAnswerAPIRouter( @@ -50,6 +51,7 @@ func NewAnswerAPIRouter( siteInfoController *controller_backyard.SiteInfoController, siteinfoController *controller.SiteinfoController, notificationController *controller.NotificationController, + dashboardController *controller.DashboardController, ) *AnswerAPIRouter { return &AnswerAPIRouter{ @@ -73,6 +75,7 @@ func NewAnswerAPIRouter( siteInfoController: siteInfoController, notificationController: notificationController, siteinfoController: siteinfoController, + dashboardController: dashboardController, } } @@ -86,9 +89,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage) r.GET("/comment", a.commentController.GetComment) - //test - r.GET("/test", a.answerController.Dashboard) - // user r.GET("/user/info", a.userController.GetUserInfoByUserID) r.GET("/user/status", a.userController.GetUserStatus) @@ -228,4 +228,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) { r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface) r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig) r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig) + + //dashboard + r.GET("/dashboard", a.dashboardController.DashboardInfo) } diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go new file mode 100644 index 000000000..f14c9e9ba --- /dev/null +++ b/internal/schema/dashboard_schema.go @@ -0,0 +1,15 @@ +package schema + +type DashboardInfo struct { + QuestionCount int64 `json:"question_count"` + AnswerCount int64 `json:"answer_count"` + CommentCount int64 `json:"comment_count"` + VoteCount int64 `json:"vote_count"` + UserCount int64 `json:"user_count"` + ReportCount int64 `json:"report_count"` + UploadingFiles string `json:"uploading_files"` //Allowed or Not allowed + SMTP string `json:"smtp"` //Enabled or Disabled + TimeZone string `json:"time_zone"` + OccupyingStorageSpace string `json:"occupying_storage_space"` + AppStartTime string `json:"app_start_time"` +} diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index 1d0f5400c..aaeabcfce 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -3,6 +3,7 @@ package dashboard import ( "context" + "github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/service/activity_common" answercommon "github.com/answerdev/answer/internal/service/answer_common" "github.com/answerdev/answer/internal/service/comment_common" @@ -10,7 +11,6 @@ import ( questioncommon "github.com/answerdev/answer/internal/service/question_common" "github.com/answerdev/answer/internal/service/report_common" usercommon "github.com/answerdev/answer/internal/service/user_common" - "github.com/davecgh/go-spew/spew" ) type DashboardService struct { @@ -45,18 +45,19 @@ func NewDashboardService( } // Statistical -func (ds *DashboardService) Statistical(ctx context.Context) error { +func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) { + dashboardInfo := &schema.DashboardInfo{} questionCount, err := ds.questionRepo.GetQuestionCount(ctx) if err != nil { - return err + return dashboardInfo, err } answerCount, err := ds.answerRepo.GetAnswerCount(ctx) if err != nil { - return err + return dashboardInfo, err } commentCount, err := ds.commentRepo.GetCommentCount(ctx) if err != nil { - return err + return dashboardInfo, err } typeKeys := []string{ @@ -78,8 +79,27 @@ func (ds *DashboardService) Statistical(ctx context.Context) error { voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes) if err != nil { - return err + return dashboardInfo, err } - spew.Dump(questionCount, answerCount, commentCount, activityTypes, voteCount) - return nil + userCount, err := ds.userRepo.GetUserCount(ctx) + if err != nil { + return dashboardInfo, err + } + + reportCount, err := ds.reportRepo.GetReportCount(ctx) + if err != nil { + return dashboardInfo, err + } + dashboardInfo.QuestionCount = questionCount + dashboardInfo.AnswerCount = answerCount + dashboardInfo.CommentCount = commentCount + dashboardInfo.VoteCount = voteCount + dashboardInfo.UserCount = userCount + dashboardInfo.ReportCount = reportCount + + dashboardInfo.UploadingFiles = "Allowed" + dashboardInfo.SMTP = "Enabled" + dashboardInfo.OccupyingStorageSpace = "1MB" + dashboardInfo.AppStartTime = "102" + return dashboardInfo, nil } diff --git a/internal/service/report_common/report_common.go b/internal/service/report_common/report_common.go index 5b5f3827b..1b8c59cc6 100644 --- a/internal/service/report_common/report_common.go +++ b/internal/service/report_common/report_common.go @@ -2,6 +2,7 @@ package report_common import ( "context" + "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/schema" ) @@ -12,4 +13,5 @@ type ReportRepo interface { GetReportListPage(ctx context.Context, query schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error) GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error) UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error) + GetReportCount(ctx context.Context) (count int64, err error) } diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 5cc194b62..ec995e0e8 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -21,6 +21,7 @@ type UserRepo interface { BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) + GetUserCount(ctx context.Context) (count int64, err error) } // UserCommon user service From 412edc1ba40cf9ae73bf39cd983702d57fef5129 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 2 Nov 2022 16:35:41 +0800 Subject: [PATCH 0109/3337] fix: move i18n dir --- internal/router/ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/router/ui.go b/internal/router/ui.go index 4d4657163..6589d5b12 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -41,6 +41,7 @@ func (r *_resource) Open(name string) (fs.File, error) { // Register a new static resource which generated by ui directory func (a *UIRouter) Register(r *gin.Engine) { staticPath := os.Getenv("ANSWER_STATIC_PATH") + r.StaticFS("/i18n/", http.FS(i18n.I18n)) // if ANSWER_STATIC_PATH is set and not empty, ignore embed resource if staticPath != "" { @@ -53,7 +54,6 @@ func (a *UIRouter) Register(r *gin.Engine) { r.LoadHTMLGlob(staticPath + "/*.html") r.Static("/static", staticPath+"/static") - r.StaticFS("/i18n/", http.FS(i18n.I18n)) r.NoRoute(func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }) From 50fa034095d8fc356c77a8bc6ee4721c7ad9a47a Mon Sep 17 00:00:00 2001 From: robin Date: Wed, 2 Nov 2022 16:38:41 +0800 Subject: [PATCH 0110/3337] feat(admin): Add time zone in admin background --- ui/src/common/constants.ts | 226 +++++++++++++++++++++++++ ui/src/pages/Admin/Interface/index.tsx | 34 +++- 2 files changed, 258 insertions(+), 2 deletions(-) diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index ba74f143f..b700ad542 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -56,3 +56,229 @@ export const ADMIN_NAV_MENUS = [ child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], }, ]; +// timezones +export const TIMEZONES = [ + { + label: 'UTC-12', + value: 'UTC-12', + }, + { + label: 'UTC-11:30', + value: 'UTC-11.5', + }, + { + label: 'UTC-11', + value: 'UTC-11', + }, + { + label: 'UTC-10:30', + value: 'UTC-10.5', + }, + { + label: 'UTC-10', + value: 'UTC-10', + }, + { + label: 'UTC-9:30', + value: 'UTC-9.5', + }, + { + label: 'UTC-9', + value: 'UTC-9', + }, + { + label: 'UTC-8:30', + value: 'UTC-8.5', + }, + { + label: 'UTC-8', + value: 'UTC-8', + }, + { + label: 'UTC-7:30', + value: 'UTC-7.5', + }, + { + label: 'UTC-7', + value: 'UTC-7', + }, + { + label: 'UTC-6:30', + value: 'UTC-6.5', + }, + { + label: 'UTC-6', + value: 'UTC-6', + }, + { + label: 'UTC-5:30', + value: 'UTC-5.5', + }, + { + label: 'UTC-5', + value: 'UTC-5', + }, + { + label: 'UTC-4:30', + value: 'UTC-4.5', + }, + { + label: 'UTC-4', + value: 'UTC-4', + }, + { + label: 'UTC-3:30', + value: 'UTC-3.5', + }, + { + label: 'UTC-3', + value: 'UTC-3', + }, + { + label: 'UTC-2:30', + value: 'UTC-2.5', + }, + { + label: 'UTC-2', + value: 'UTC-2', + }, + { + label: 'UTC-1:30', + value: 'UTC-1.5', + }, + { + label: 'UTC-1', + value: 'UTC-1', + }, + { + label: 'UTC-0:30', + value: 'UTC-0.5', + }, + { + label: 'UTC+0', + value: 'UTC+0', + }, + { + label: 'UTC+0:30', + value: 'UTC+0.5', + }, + { + label: 'UTC+1', + value: 'UTC+1', + }, + { + label: 'UTC+1:30', + value: 'UTC+1.5', + }, + { + label: 'UTC+2', + value: 'UTC+2', + }, + { + label: 'UTC+2:30', + value: 'UTC+2.5', + }, + { + label: 'UTC+3', + value: 'UTC+3', + }, + { + label: 'UTC+3:30', + + value: 'UTC+3.5', + }, + { + label: 'UTC+4', + value: 'UTC+4', + }, + { + label: 'UTC+4:30', + value: 'UTC+4.5', + }, + { + label: 'UTC+5', + value: 'UTC+5', + }, + { + label: 'UTC+5:30', + value: 'UTC+5.5', + }, + { + label: 'UTC+5:45', + value: 'UTC+5.75', + }, + { + label: 'UTC+6', + value: 'UTC+6', + }, + { + label: 'UTC+6:30', + + value: 'UTC+6.5', + }, + { + label: 'UTC+7', + value: 'UTC+7', + }, + { + label: 'UTC+7:30', + value: 'UTC+7.5', + }, + { + label: 'UTC+8', + value: 'UTC+8', + }, + { + label: 'UTC+8:30', + value: 'UTC+8.5', + }, + { + label: 'UTC+8:45', + value: 'UTC+8.75', + }, + { + label: 'UTC+9', + value: 'UTC+9', + }, + { + label: 'UTC+9:30', + value: 'UTC+9.5', + }, + { + label: 'UTC+10', + value: 'UTC+10', + }, + { + label: 'UTC+10:30', + value: 'UTC+10.5', + }, + { + label: 'UTC+11', + value: 'UTC+11', + }, + { + label: 'UTC+11:30', + value: 'UTC+11.5', + }, + { + label: 'UTC+12', + value: 'UTC+12', + }, + { + label: 'UTC+12:45', + value: 'UTC+12.75', + }, + { + label: 'UTC+13', + value: 'UTC+13', + }, + { + label: 'UTC+13:45', + value: 'UTC+13.75', + }, + { + label: 'UTC+14', + value: 'UTC+14', + }, +]; +export const DEFAULT_TIMEZONE = 'UTC+0'; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index e5d68f326..deebcf9f3 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -10,6 +10,7 @@ import { } from '@answer/common/interface'; import { interfaceStore } from '@answer/stores'; import { UploadImg } from '@answer/components'; +import { TIMEZONES, DEFAULT_TIMEZONE } from '@answer/common/constants'; import { languages, @@ -28,6 +29,7 @@ const Interface: FC = () => { const Toast = useToast(); const [langs, setLangs] = useState(); const { data: setting } = useInterfaceSetting(); + const [formData, setFormData] = useState({ logo: { value: setting?.logo || '', @@ -44,6 +46,11 @@ const Interface: FC = () => { isInvalid: false, errorMsg: '', }, + time_zone: { + value: setting?.time_zone || DEFAULT_TIMEZONE, + isInvalid: false, + errorMsg: '', + }, }); const getLangs = async () => { const res: LangsType[] = await languages(); @@ -107,6 +114,7 @@ const Interface: FC = () => { logo: formData.logo.value, theme: formData.theme.value, language: formData.language.value, + time_zone: formData.time_zone.value, }; updateInterfaceSetting(reqParams) @@ -159,12 +167,14 @@ const Interface: FC = () => { Object.keys(setting).forEach((k) => { formMeta[k] = { ...formData[k], value: setting[k] }; }); - setFormData(formMeta); + setFormData({ ...formData, ...formMeta }); } }, [setting]); useEffect(() => { getLangs(); }, []); + + console.log('formData', formData); return ( <>

{t('page_title')}

@@ -250,7 +260,27 @@ const Interface: FC = () => { {formData.language.errorMsg} - + + {t('time_zone.label')} + { + onChange('time_zone', evt.target.value); + }}> + {TIMEZONES?.map((item) => { + return ( + + ); + })} + + {t('time_zone.text')} + + {formData.time_zone.errorMsg} + + From b78f3061cb294cac31e700af123ab9c57909e62d Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 2 Nov 2022 17:09:16 +0800 Subject: [PATCH 0111/3337] add install web api --- cmd/answer/command.go | 3 +++ internal/base/server/install.go | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 internal/base/server/install.go diff --git a/cmd/answer/command.go b/cmd/answer/command.go index d0efb0aba..276554434 100644 --- a/cmd/answer/command.go +++ b/cmd/answer/command.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/answerdev/answer/internal/base/server" "github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/migrations" "github.com/spf13/cobra" @@ -59,6 +60,8 @@ To run answer, use: Short: "init answer application", Long: `init answer application`, Run: func(_ *cobra.Command, _ []string) { + installwebapi := server.NewInstallHTTPServer() + installwebapi.Run(":8088") cli.InstallAllInitialEnvironment(dataDirPath) c, err := readConfig() if err != nil { diff --git a/internal/base/server/install.go b/internal/base/server/install.go new file mode 100644 index 000000000..d4b44d903 --- /dev/null +++ b/internal/base/server/install.go @@ -0,0 +1,26 @@ +package server + +import ( + "embed" + "net/http" + + "github.com/answerdev/answer/ui" + "github.com/gin-gonic/gin" +) + +type _resource struct { + fs embed.FS +} + +// NewHTTPServer new http server. +func NewInstallHTTPServer() *gin.Engine { + r := gin.New() + gin.SetMode(gin.DebugMode) + + r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK??") }) + + // gin.SetMode(gin.ReleaseMode) + r.StaticFS("/static", http.FS(ui.Build)) + + return r +} From 02de299c8692d28abc3b9a9f01180cb0c824156f Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Wed, 2 Nov 2022 17:14:47 +0800 Subject: [PATCH 0112/3337] refactor(router-guard): refactor router guard --- ui/.eslintrc.js | 2 +- ui/config-overrides.js | 7 - ui/src/components/AccordionNav/index.tsx | 2 +- ui/src/components/Actions/index.tsx | 9 +- ui/src/components/BaseUserCard/index.tsx | 3 +- .../Comment/components/ActionBar/index.tsx | 2 +- .../Comment/components/Form/index.tsx | 4 +- .../Comment/components/Reply/index.tsx | 4 +- ui/src/components/Comment/index.tsx | 15 +- ui/src/components/Editor/ToolBars/image.tsx | 3 +- ui/src/components/FollowingTags/index.tsx | 5 +- .../Header/components/NavItems/index.tsx | 2 +- ui/src/components/Header/index.tsx | 11 +- ui/src/components/HotQuestions/index.tsx | 3 +- ui/src/components/Mentions/index.tsx | 2 +- ui/src/components/Modal/PicAuthCodeModal.tsx | 9 +- ui/src/components/Operate/index.tsx | 7 +- ui/src/components/QuestionList/index.tsx | 5 +- ui/src/components/Share/index.tsx | 2 +- ui/src/components/TagSelector/index.tsx | 5 +- ui/src/components/Unactivate/index.tsx | 11 +- ui/src/components/UserCard/index.tsx | 3 +- ui/src/hooks/useChangeModal/index.tsx | 3 +- ui/src/hooks/usePageUsers/index.tsx | 2 +- ui/src/hooks/useReportModal/index.tsx | 5 +- ui/src/i18n/init.ts | 4 +- ui/src/index.tsx | 6 +- ui/src/pages/Admin/Answers/index.tsx | 9 +- ui/src/pages/Admin/Flags/index.tsx | 7 +- ui/src/pages/Admin/General/index.tsx | 7 +- ui/src/pages/Admin/Interface/index.tsx | 9 +- ui/src/pages/Admin/Questions/index.tsx | 9 +- ui/src/pages/Admin/Smtp/index.tsx | 5 +- ui/src/pages/Admin/Users/index.tsx | 7 +- ui/src/pages/Admin/index.tsx | 4 +- ui/src/pages/Layout/index.tsx | 5 +- .../Ask/components/SearchQuestion/index.tsx | 2 +- ui/src/pages/Questions/Ask/index.tsx | 9 +- .../Detail/components/Answer/index.tsx | 7 +- .../Detail/components/AnswerHead/index.tsx | 2 +- .../Detail/components/Question/index.tsx | 5 +- .../components/RelatedQuestions/index.tsx | 3 +- .../Detail/components/WriteAnswer/index.tsx | 5 +- ui/src/pages/Questions/Detail/index.tsx | 13 +- ui/src/pages/Questions/EditAnswer/index.tsx | 5 +- ui/src/pages/Questions/index.tsx | 3 +- ui/src/pages/Search/components/Head/index.tsx | 2 +- .../Search/components/SearchHead/index.tsx | 2 +- .../Search/components/SearchItem/index.tsx | 4 +- ui/src/pages/Search/index.tsx | 5 +- ui/src/pages/Tags/Detail/index.tsx | 5 +- ui/src/pages/Tags/Edit/index.tsx | 7 +- ui/src/pages/Tags/Info/index.tsx | 9 +- ui/src/pages/Tags/index.tsx | 5 +- .../AccountForgot/components/sendEmail.tsx | 3 +- ui/src/pages/Users/AccountForgot/index.tsx | 6 +- ui/src/pages/Users/ActiveEmail/index.tsx | 5 +- .../ChangeEmail/components/sendEmail.tsx | 5 +- ui/src/pages/Users/ChangeEmail/index.tsx | 4 +- ui/src/pages/Users/Login/index.tsx | 20 +- .../components/Achievements/index.tsx | 2 +- .../Notifications/components/Inbox/index.tsx | 2 +- ui/src/pages/Users/Notifications/index.tsx | 9 +- ui/src/pages/Users/PasswordReset/index.tsx | 9 +- .../Personal/components/Answers/index.tsx | 2 +- .../Personal/components/Comments/index.tsx | 2 +- .../Personal/components/DefaultList/index.tsx | 2 +- .../Personal/components/ListHead/index.tsx | 2 +- .../Personal/components/Reputation/index.tsx | 2 +- .../Personal/components/TopList/index.tsx | 2 +- .../Personal/components/UserInfo/index.tsx | 4 +- .../Users/Personal/components/Votes/index.tsx | 2 +- ui/src/pages/Users/Personal/index.tsx | 15 +- .../Register/components/SignUpForm/index.tsx | 3 +- ui/src/pages/Users/Register/index.tsx | 5 +- .../Account/components/ModifyEmail/index.tsx | 5 +- .../Account/components/ModifyPass/index.tsx | 5 +- .../pages/Users/Settings/Interface/index.tsx | 5 +- .../Users/Settings/Notification/index.tsx | 5 +- ui/src/pages/Users/Settings/Profile/index.tsx | 9 +- ui/src/pages/Users/Settings/index.tsx | 7 +- ui/src/pages/Users/Suspended/index.tsx | 3 +- ui/src/router/guarder.ts | 42 ---- ui/src/router/index.tsx | 15 +- ui/src/router/routes.ts | 76 +++++-- ui/src/services/admin/answer.ts | 4 +- ui/src/services/admin/flag.ts | 4 +- ui/src/services/admin/question.ts | 4 +- ui/src/services/admin/settings.ts | 4 +- ui/src/services/client/activity.ts | 4 +- ui/src/services/client/notification.ts | 7 +- ui/src/services/client/personal.ts | 4 +- ui/src/services/client/question.ts | 4 +- ui/src/services/client/search.ts | 4 +- ui/src/services/client/tag.ts | 7 +- ui/src/services/common.ts | 4 +- ui/src/stores/userInfo.ts | 6 +- ui/src/utils/floppyNavigation.ts | 3 +- ui/src/utils/guard.ts | 182 ++++++++++++++++ ui/src/utils/guards.ts | 196 ------------------ ui/src/utils/index.ts | 6 +- ui/src/utils/request.ts | 11 +- ui/tsconfig.json | 13 +- 103 files changed, 471 insertions(+), 565 deletions(-) delete mode 100644 ui/src/router/guarder.ts create mode 100644 ui/src/utils/guard.ts delete mode 100644 ui/src/utils/guards.ts diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 877a00391..98edc4083 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -64,7 +64,7 @@ module.exports = { position: 'before', }, { - pattern: '@answer/**', + pattern: '@/**', group: 'internal', }, { diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 06464361b..ac52fa636 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -8,13 +8,6 @@ module.exports = { config.resolve.alias = { ...config.resolve.alias, '@': path.resolve(__dirname, 'src'), - '@answer/pages': path.resolve(__dirname, 'src/pages'), - '@answer/components': path.resolve(__dirname, 'src/components'), - '@answer/stores': path.resolve(__dirname, 'src/stores'), - '@answer/hooks': path.resolve(__dirname, 'src/hooks'), - '@answer/utils': path.resolve(__dirname, 'src/utils'), - '@answer/common': path.resolve(__dirname, 'src/common'), - '@answer/api': path.resolve(__dirname, 'src/services/api'), }; return config; diff --git a/ui/src/components/AccordionNav/index.tsx b/ui/src/components/AccordionNav/index.tsx index ccc14d446..b3403bc81 100644 --- a/ui/src/components/AccordionNav/index.tsx +++ b/ui/src/components/AccordionNav/index.tsx @@ -5,7 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom'; import { useAccordionButton } from 'react-bootstrap/AccordionButton'; -import { Icon } from '@answer/components'; +import { Icon } from '@/components'; function MenuNode({ menu, callback, activeKey, isLeaf = false }) { const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' }); diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx index 25402ab2d..d9395723d 100644 --- a/ui/src/components/Actions/index.tsx +++ b/ui/src/components/Actions/index.tsx @@ -4,11 +4,10 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { Icon } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import { useToast } from '@answer/hooks'; - -import { tryNormalLogged } from '@/utils/guards'; +import { Icon } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { useToast } from '@/hooks'; +import { tryNormalLogged } from '@/utils/guard'; import { bookmark, postVote } from '@/services'; interface Props { diff --git a/ui/src/components/BaseUserCard/index.tsx b/ui/src/components/BaseUserCard/index.tsx index fa524bda9..d170d3f8d 100644 --- a/ui/src/components/BaseUserCard/index.tsx +++ b/ui/src/components/BaseUserCard/index.tsx @@ -1,8 +1,7 @@ import { memo, FC } from 'react'; import { Link } from 'react-router-dom'; -import { Avatar } from '@answer/components'; - +import { Avatar } from '@/components'; import { formatCount } from '@/utils'; interface Props { diff --git a/ui/src/components/Comment/components/ActionBar/index.tsx b/ui/src/components/Comment/components/ActionBar/index.tsx index a801a6bb6..a13bff1a1 100644 --- a/ui/src/components/Comment/components/ActionBar/index.tsx +++ b/ui/src/components/Comment/components/ActionBar/index.tsx @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; -import { Icon, FormatTime } from '@answer/components'; +import { Icon, FormatTime } from '@/components'; const ActionBar = ({ nickName, diff --git a/ui/src/components/Comment/components/Form/index.tsx b/ui/src/components/Comment/components/Form/index.tsx index 4971c4ef7..70c62fdd7 100644 --- a/ui/src/components/Comment/components/Form/index.tsx +++ b/ui/src/components/Comment/components/Form/index.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { TextArea, Mentions } from '@answer/components'; -import { usePageUsers } from '@answer/hooks'; +import { TextArea, Mentions } from '@/components'; +import { usePageUsers } from '@/hooks'; const Form = ({ className = '', diff --git a/ui/src/components/Comment/components/Reply/index.tsx b/ui/src/components/Comment/components/Reply/index.tsx index dcbf53bb5..16b04822d 100644 --- a/ui/src/components/Comment/components/Reply/index.tsx +++ b/ui/src/components/Comment/components/Reply/index.tsx @@ -2,8 +2,8 @@ import { useState, memo } from 'react'; import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { TextArea, Mentions } from '@answer/components'; -import { usePageUsers } from '@answer/hooks'; +import { TextArea, Mentions } from '@/components'; +import { usePageUsers } from '@/hooks'; const Form = ({ userName, onSendReply, onCancel, mode }) => { const [value, setValue] = useState(''); diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index a6dcdc98c..1f57e7375 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -7,14 +7,11 @@ import classNames from 'classnames'; import { unionBy } from 'lodash'; import { marked } from 'marked'; -import * as Types from '@answer/common/interface'; -import { Modal } from '@answer/components'; -import { usePageUsers, useReportModal } from '@answer/hooks'; -import { matchedUsers, parseUserInfo } from '@answer/utils'; - -import { Form, ActionBar, Reply } from './components'; - -import { tryNormalLogged } from '@/utils/guards'; +import * as Types from '@/common/interface'; +import { Modal } from '@/components'; +import { usePageUsers, useReportModal } from '@/hooks'; +import { matchedUsers, parseUserInfo } from '@/utils'; +import { tryNormalLogged } from '@/utils/guard'; import { useQueryComments, addComment, @@ -23,6 +20,8 @@ import { postVote, } from '@/services'; +import { Form, ActionBar, Reply } from './components'; + import './index.scss'; const Comment = ({ objectId, mode }) => { diff --git a/ui/src/components/Editor/ToolBars/image.tsx b/ui/src/components/Editor/ToolBars/image.tsx index ead20eee9..bfa4c5307 100644 --- a/ui/src/components/Editor/ToolBars/image.tsx +++ b/ui/src/components/Editor/ToolBars/image.tsx @@ -2,10 +2,9 @@ import { FC, useEffect, useState, memo } from 'react'; import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Modal as AnswerModal } from '@answer/components'; +import { Modal as AnswerModal } from '@/components'; import ToolItem from '../toolItem'; import { IEditorContext } from '../types'; - import { uploadImage } from '@/services'; const Image: FC = ({ editor }) => { diff --git a/ui/src/components/FollowingTags/index.tsx b/ui/src/components/FollowingTags/index.tsx index 55e9bc762..06fec6ecb 100644 --- a/ui/src/components/FollowingTags/index.tsx +++ b/ui/src/components/FollowingTags/index.tsx @@ -3,9 +3,8 @@ import { Card, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { NavLink } from 'react-router-dom'; -import { TagSelector, Tag } from '@answer/components'; - -import { tryNormalLogged } from '@/utils/guards'; +import { TagSelector, Tag } from '@/components'; +import { tryNormalLogged } from '@/utils/guard'; import { useFollowingTags, followTags } from '@/services'; const Index: FC = () => { diff --git a/ui/src/components/Header/components/NavItems/index.tsx b/ui/src/components/Header/components/NavItems/index.tsx index 67112758e..8a4872d9f 100644 --- a/ui/src/components/Header/components/NavItems/index.tsx +++ b/ui/src/components/Header/components/NavItems/index.tsx @@ -3,7 +3,7 @@ import { Nav, Dropdown } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link, NavLink } from 'react-router-dom'; -import { Avatar, Icon } from '@answer/components'; +import { Avatar, Icon } from '@/components'; interface Props { redDot; diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index b4cd4e164..748eb3183 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -17,17 +17,12 @@ import { useLocation, } from 'react-router-dom'; -import { - loggedUserInfoStore, - siteInfoStore, - interfaceStore, -} from '@answer/stores'; - -import NavItems from './components/NavItems'; - +import { loggedUserInfoStore, siteInfoStore, interfaceStore } from '@/stores'; import { logout, useQueryNotificationStatus } from '@/services'; import { RouteAlias } from '@/router/alias'; +import NavItems from './components/NavItems'; + import './index.scss'; const Header: FC = () => { diff --git a/ui/src/components/HotQuestions/index.tsx b/ui/src/components/HotQuestions/index.tsx index 920bdce22..da0305d05 100644 --- a/ui/src/components/HotQuestions/index.tsx +++ b/ui/src/components/HotQuestions/index.tsx @@ -3,8 +3,7 @@ import { Card, ListGroup, ListGroupItem } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; - +import { Icon } from '@/components'; import { useHotQuestions } from '@/services'; const HotQuestions: FC = () => { diff --git a/ui/src/components/Mentions/index.tsx b/ui/src/components/Mentions/index.tsx index 9b29c86d1..23cfa083f 100644 --- a/ui/src/components/Mentions/index.tsx +++ b/ui/src/components/Mentions/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState, FC } from 'react'; import { Dropdown } from 'react-bootstrap'; -import * as Types from '@answer/common/interface'; +import * as Types from '@/common/interface'; interface IProps { children: React.ReactNode; diff --git a/ui/src/components/Modal/PicAuthCodeModal.tsx b/ui/src/components/Modal/PicAuthCodeModal.tsx index 3a081a0cf..ec194a64f 100644 --- a/ui/src/components/Modal/PicAuthCodeModal.tsx +++ b/ui/src/components/Modal/PicAuthCodeModal.tsx @@ -2,13 +2,8 @@ import React from 'react'; import { Modal, Form, Button, InputGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; -import type { - FormValue, - FormDataType, - ImgCodeRes, -} from '@answer/common/interface'; - +import { Icon } from '@/components'; +import type { FormValue, FormDataType, ImgCodeRes } from '@/common/interface'; import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index 201774c6c..cdacc7516 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -3,12 +3,11 @@ import { Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Modal } from '@answer/components'; -import { useReportModal, useToast } from '@answer/hooks'; +import { Modal } from '@/components'; +import { useReportModal, useToast } from '@/hooks'; import Share from '../Share'; - import { deleteQuestion, deleteAnswer } from '@/services'; -import { tryNormalLogged } from '@/utils/guards'; +import { tryNormalLogged } from '@/utils/guard'; interface IProps { type: 'answer' | 'question'; diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 85b72ce17..76a6ccd91 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -3,7 +3,7 @@ import { Row, Col, ListGroup } from 'react-bootstrap'; import { NavLink, useParams, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; +import type * as Type from '@/common/interface'; import { Icon, Tag, @@ -12,8 +12,7 @@ import { Empty, BaseUserCard, QueryGroup, -} from '@answer/components'; - +} from '@/components'; import { useQuestionList } from '@/services'; const QuestionOrderKeys: Type.QuestionOrderBy[] = [ diff --git a/ui/src/components/Share/index.tsx b/ui/src/components/Share/index.tsx index 74b3de34c..652dcef12 100644 --- a/ui/src/components/Share/index.tsx +++ b/ui/src/components/Share/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { FacebookShareButton, TwitterShareButton } from 'next-share'; import copy from 'copy-to-clipboard'; -import { loggedUserInfoStore } from '@answer/stores'; +import { loggedUserInfoStore } from '@/stores'; interface IProps { type: 'answer' | 'question'; diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index cedb85a3c..8747f65ab 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'; import { marked } from 'marked'; import classNames from 'classnames'; -import { useTagModal } from '@answer/hooks'; -import type * as Type from '@answer/common/interface'; - +import { useTagModal } from '@/hooks'; +import type * as Type from '@/common/interface'; import { queryTags } from '@/services'; import './index.scss'; diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index 6b7a26bfb..420891cc4 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -3,14 +3,9 @@ import { Button, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { PicAuthCodeModal } from '@answer/components/Modal'; -import type { - ImgCodeRes, - ImgCodeReq, - FormDataType, -} from '@answer/common/interface'; -import { loggedUserInfoStore } from '@answer/stores'; - +import { PicAuthCodeModal } from '@/components/Modal'; +import type { ImgCodeRes, ImgCodeReq, FormDataType } from '@/common/interface'; +import { loggedUserInfoStore } from '@/stores'; import { resendEmail, checkImgCode } from '@/services'; import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/components/UserCard/index.tsx b/ui/src/components/UserCard/index.tsx index 0110c82a0..274412317 100644 --- a/ui/src/components/UserCard/index.tsx +++ b/ui/src/components/UserCard/index.tsx @@ -3,8 +3,7 @@ import { Link } from 'react-router-dom'; import classnames from 'classnames'; -import { Avatar, FormatTime } from '@answer/components'; - +import { Avatar, FormatTime } from '@/components'; import { formatCount } from '@/utils'; interface Props { diff --git a/ui/src/hooks/useChangeModal/index.tsx b/ui/src/hooks/useChangeModal/index.tsx index f484331cb..529f7a3a1 100644 --- a/ui/src/hooks/useChangeModal/index.tsx +++ b/ui/src/hooks/useChangeModal/index.tsx @@ -4,8 +4,7 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { Modal as AnswerModal } from '@answer/components'; - +import { Modal as AnswerModal } from '@/components'; import { changeUserStatus } from '@/services'; const div = document.createElement('div'); diff --git a/ui/src/hooks/usePageUsers/index.tsx b/ui/src/hooks/usePageUsers/index.tsx index 722036190..2c8286957 100644 --- a/ui/src/hooks/usePageUsers/index.tsx +++ b/ui/src/hooks/usePageUsers/index.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { uniqBy } from 'lodash'; -import * as Types from '@answer/common/interface'; +import * as Types from '@/common/interface'; let globalUsers: Types.PageUser[] = []; const usePageUsers = () => { diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx index 2f7c8f184..28b85eb5d 100644 --- a/ui/src/hooks/useReportModal/index.tsx +++ b/ui/src/hooks/useReportModal/index.tsx @@ -4,9 +4,8 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { useToast } from '@answer/hooks'; -import type * as Type from '@answer/common/interface'; - +import { useToast } from '@/hooks'; +import type * as Type from '@/common/interface'; import { reportList, postReport, closeQuestion, putReport } from '@/services'; interface Params { diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index deecc0e23..e8b41e314 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -3,11 +3,11 @@ import { initReactI18next } from 'react-i18next'; import i18next from 'i18next'; import Backend from 'i18next-http-backend'; +import { DEFAULT_LANG } from '@/common/constants'; + import en from './locales/en.json'; import zh from './locales/zh_CN.json'; -import { DEFAULT_LANG } from '@/common/constants'; - i18next // load translation using http .use(Backend) diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 1943b61a3..67e5ee50a 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -2,9 +2,9 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import { Guard } from '@/utils'; -import { pullLoggedUser } from '@/utils/guards'; +import App from './App'; import './i18n/init'; import './index.scss'; @@ -17,7 +17,7 @@ async function bootstrapApp() { /** * NOTICE: must pre init logged user info for router */ - await pullLoggedUser(); + await Guard.pullLoggedUser(); root.render( diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 2a25f53cb..2c14c6d7c 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -11,11 +11,10 @@ import { BaseUserCard, Empty, QueryGroup, -} from '@answer/components'; -import { ADMIN_LIST_STATUS } from '@answer/common/constants'; -import { useEditStatusModal } from '@answer/hooks'; -import * as Type from '@answer/common/interface'; - +} from '@/components'; +import { ADMIN_LIST_STATUS } from '@/common/constants'; +import { useEditStatusModal } from '@/hooks'; +import * as Type from '@/common/interface'; import { useAnswerSearch, changeAnswerStatus } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Flags/index.tsx b/ui/src/pages/Admin/Flags/index.tsx index a7842429f..353bd382f 100644 --- a/ui/src/pages/Admin/Flags/index.tsx +++ b/ui/src/pages/Admin/Flags/index.tsx @@ -9,10 +9,9 @@ import { Empty, Pagination, QueryGroup, -} from '@answer/components'; -import { useReportModal } from '@answer/hooks'; -import * as Type from '@answer/common/interface'; - +} from '@/components'; +import { useReportModal } from '@/hooks'; +import * as Type from '@/common/interface'; import { useFlagSearch } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 3154caecf..14b959788 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -2,10 +2,9 @@ import React, { FC, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; -import { siteInfoStore } from '@answer/stores'; - +import type * as Type from '@/common/interface'; +import { useToast } from '@/hooks'; +import { siteInfoStore } from '@/stores'; import { useGeneralSetting, updateGeneralSetting } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index e5d68f326..3f179c57e 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -2,15 +2,14 @@ import React, { FC, FormEvent, useEffect, useState } from 'react'; import { Form, Button, Image, Stack } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import { useToast } from '@answer/hooks'; +import { useToast } from '@/hooks'; import { LangsType, FormDataType, AdminSettingsInterface, -} from '@answer/common/interface'; -import { interfaceStore } from '@answer/stores'; -import { UploadImg } from '@answer/components'; - +} from '@/common/interface'; +import { interfaceStore } from '@/stores'; +import { UploadImg } from '@/components'; import { languages, uploadAvatar, diff --git a/ui/src/pages/Admin/Questions/index.tsx b/ui/src/pages/Admin/Questions/index.tsx index 1a1885129..e27a715c7 100644 --- a/ui/src/pages/Admin/Questions/index.tsx +++ b/ui/src/pages/Admin/Questions/index.tsx @@ -11,11 +11,10 @@ import { BaseUserCard, Empty, QueryGroup, -} from '@answer/components'; -import { ADMIN_LIST_STATUS } from '@answer/common/constants'; -import { useEditStatusModal, useReportModal } from '@answer/hooks'; -import * as Type from '@answer/common/interface'; - +} from '@/components'; +import { ADMIN_LIST_STATUS } from '@/common/constants'; +import { useEditStatusModal, useReportModal } from '@/hooks'; +import * as Type from '@/common/interface'; import { useQuestionSearch, changeQuestionStatus, diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx index 17abd481d..33de30ab3 100644 --- a/ui/src/pages/Admin/Smtp/index.tsx +++ b/ui/src/pages/Admin/Smtp/index.tsx @@ -2,9 +2,8 @@ import React, { FC, useEffect, useState } from 'react'; import { Form, Button, Stack } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; - +import type * as Type from '@/common/interface'; +import { useToast } from '@/hooks'; import { useSmtpSetting, updateSmtpSetting } from '@/services'; import pattern from '@/common/pattern'; diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index de434084e..a14f6b742 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -9,10 +9,9 @@ import { BaseUserCard, Empty, QueryGroup, -} from '@answer/components'; -import * as Type from '@answer/common/interface'; -import { useChangeModal } from '@answer/hooks'; - +} from '@/components'; +import * as Type from '@/common/interface'; +import { useChangeModal } from '@/hooks'; import { useQueryUsers } from '@/services'; import '../index.scss'; diff --git a/ui/src/pages/Admin/index.tsx b/ui/src/pages/Admin/index.tsx index 8d7b452ec..27ef85650 100644 --- a/ui/src/pages/Admin/index.tsx +++ b/ui/src/pages/Admin/index.tsx @@ -3,8 +3,8 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { AccordionNav, PageTitle } from '@answer/components'; -import { ADMIN_NAV_MENUS } from '@answer/common/constants'; +import { AccordionNav, PageTitle } from '@/components'; +import { ADMIN_NAV_MENUS } from '@/common/constants'; import './index.scss'; diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 7affe2f03..363c9b011 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -5,9 +5,8 @@ import { Helmet, HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { siteInfoStore, interfaceStore, toastStore } from '@answer/stores'; -import { Header, AdminHeader, Footer, Toast } from '@answer/components'; - +import { siteInfoStore, interfaceStore, toastStore } from '@/stores'; +import { Header, AdminHeader, Footer, Toast } from '@/components'; import { useSiteSettings } from '@/services'; import Storage from '@/utils/storage'; import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; diff --git a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx index 163889636..99a8026ad 100644 --- a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx +++ b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import { Accordion, ListGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; +import { Icon } from '@/components'; import './index.scss'; diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index f233150e2..fe4d33199 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -6,11 +6,8 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import classNames from 'classnames'; -import { Editor, EditorRef, TagSelector, PageTitle } from '@answer/components'; -import type * as Type from '@answer/common/interface'; - -import SearchQuestion from './components/SearchQuestion'; - +import { Editor, EditorRef, TagSelector, PageTitle } from '@/components'; +import type * as Type from '@/common/interface'; import { saveQuestion, questionDetail, @@ -20,6 +17,8 @@ import { useQueryQuestionByTitle, } from '@/services'; +import SearchQuestion from './components/SearchQuestion'; + interface FormDataItem { title: Type.FormValue; tags: Type.FormValue; diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index e97238cff..2fe87d1cc 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -10,10 +10,9 @@ import { Comment, FormatTime, htmlRender, -} from '@answer/components'; -import { scrollTop } from '@answer/utils'; -import { AnswerItem } from '@answer/common/interface'; - +} from '@/components'; +import { scrollTop } from '@/utils'; +import { AnswerItem } from '@/common/interface'; import { acceptanceAnswer } from '@/services'; interface Props { diff --git a/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx b/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx index f2c7efe91..32abc17b6 100644 --- a/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx +++ b/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx @@ -1,7 +1,7 @@ import { memo, FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@answer/components'; +import { QueryGroup } from '@/components'; interface Props { count: number; diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index aa3a38ec0..1b46a869d 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -11,9 +11,8 @@ import { Comment, FormatTime, htmlRender, -} from '@answer/components'; -import { formatCount } from '@answer/utils'; - +} from '@/components'; +import { formatCount } from '@/utils'; import { following } from '@/services'; interface Props { diff --git a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx index 8493408ce..3dd91c7f6 100644 --- a/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/RelatedQuestions/index.tsx @@ -3,8 +3,7 @@ import { Card, ListGroup } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; - +import { Icon } from '@/components'; import { useSimilarQuestion } from '@/services'; import { loggedUserInfoStore } from '@/stores'; diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx index 9659b2862..8d1ef1be9 100644 --- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx @@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'; import { marked } from 'marked'; import classNames from 'classnames'; -import { Editor, Modal } from '@answer/components'; -import { FormDataType } from '@answer/common/interface'; - +import { Editor, Modal } from '@/components'; +import { FormDataType } from '@/common/interface'; import { postAnswer } from '@/services'; interface Props { diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 2f469415b..cc19a7193 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -2,15 +2,16 @@ import { useEffect, useState } from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; -import { Pagination, PageTitle } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import { scrollTop } from '@answer/utils'; -import { usePageUsers } from '@answer/hooks'; +import { Pagination, PageTitle } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { scrollTop } from '@/utils'; +import { usePageUsers } from '@/hooks'; import type { ListResult, QuestionDetailRes, AnswerItem, -} from '@answer/common/interface'; +} from '@/common/interface'; +import { questionDetail, getAnswers } from '@/services'; import { Question, @@ -21,8 +22,6 @@ import { Alert, } from './components'; -import { questionDetail, getAnswers } from '@/services'; - import './index.scss'; const Index = () => { diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index c87733f4d..0fab103cc 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -6,9 +6,8 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import classNames from 'classnames'; -import { Editor, EditorRef, Icon, PageTitle } from '@answer/components'; -import type * as Type from '@answer/common/interface'; - +import { Editor, EditorRef, Icon, PageTitle } from '@/components'; +import type * as Type from '@/common/interface'; import { useQueryAnswerInfo, modifyAnswer, diff --git a/ui/src/pages/Questions/index.tsx b/ui/src/pages/Questions/index.tsx index 9e7ed3186..ea10ec153 100644 --- a/ui/src/pages/Questions/index.tsx +++ b/ui/src/pages/Questions/index.tsx @@ -3,8 +3,7 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useMatch } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { PageTitle, FollowingTags } from '@answer/components'; - +import { PageTitle, FollowingTags } from '@/components'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; import { siteInfoStore } from '@/stores'; diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index 1558bc35d..095796d21 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -4,7 +4,7 @@ import { Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { following } from '@/services'; -import { tryNormalLogged } from '@/utils/guards'; +import { tryNormalLogged } from '@/utils/guard'; interface Props { data; diff --git a/ui/src/pages/Search/components/SearchHead/index.tsx b/ui/src/pages/Search/components/SearchHead/index.tsx index 2d7549a4e..fb185bc1a 100644 --- a/ui/src/pages/Search/components/SearchHead/index.tsx +++ b/ui/src/pages/Search/components/SearchHead/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@answer/components'; +import { QueryGroup } from '@/components'; const sortBtns = ['relevance', 'newest', 'active', 'score']; diff --git a/ui/src/pages/Search/components/SearchItem/index.tsx b/ui/src/pages/Search/components/SearchItem/index.tsx index ec7032777..09cecd32e 100644 --- a/ui/src/pages/Search/components/SearchItem/index.tsx +++ b/ui/src/pages/Search/components/SearchItem/index.tsx @@ -2,8 +2,8 @@ import { memo, FC } from 'react'; import { ListGroupItem, Badge } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon, Tag, FormatTime, BaseUserCard } from '@answer/components'; -import type { SearchResItem } from '@answer/common/interface'; +import { Icon, Tag, FormatTime, BaseUserCard } from '@/components'; +import type { SearchResItem } from '@/common/interface'; interface Props { data: SearchResItem; diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index 914565ae4..f22dc3cee 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -3,12 +3,11 @@ import { Container, Row, Col, ListGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; -import { Pagination, PageTitle } from '@answer/components'; +import { Pagination, PageTitle } from '@/components'; +import { useSearch } from '@/services'; import { Head, SearchHead, SearchItem, Tips, Empty } from './components'; -import { useSearch } from '@/services'; - const Index = () => { const { t } = useTranslation('translation'); const [searchParams] = useSearchParams(); diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index e37ab0abf..452ef37d6 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -3,9 +3,8 @@ import { Container, Row, Col, Button } from 'react-bootstrap'; import { useParams, Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import * as Type from '@answer/common/interface'; -import { PageTitle, FollowingTags } from '@answer/components'; - +import * as Type from '@/common/interface'; +import { PageTitle, FollowingTags } from '@/components'; import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; diff --git a/ui/src/pages/Tags/Edit/index.tsx b/ui/src/pages/Tags/Edit/index.tsx index 58e7d59c9..890bd32e4 100644 --- a/ui/src/pages/Tags/Edit/index.tsx +++ b/ui/src/pages/Tags/Edit/index.tsx @@ -6,10 +6,9 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import classNames from 'classnames'; -import { Editor, EditorRef, PageTitle } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import type * as Type from '@answer/common/interface'; - +import { Editor, EditorRef, PageTitle } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import type * as Type from '@/common/interface'; import { useTagInfo, modifyTag, useQueryRevisions } from '@/services'; interface FormDataItem { diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index d8c4ad756..3b792adb8 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -5,14 +5,7 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { - Tag, - TagSelector, - FormatTime, - Modal, - PageTitle, -} from '@answer/components'; - +import { Tag, TagSelector, FormatTime, Modal, PageTitle } from '@/components'; import { useTagInfo, useQuerySynonymsTags, diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index 80256d943..a01e3e56f 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -3,9 +3,8 @@ import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap'; import { useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Tag, Pagination, PageTitle, QueryGroup } from '@answer/components'; -import { formatCount } from '@answer/utils'; - +import { Tag, Pagination, PageTitle, QueryGroup } from '@/components'; +import { formatCount } from '@/utils'; import { useQueryTags, following } from '@/services'; const sortBtns = ['popular', 'name', 'newest']; diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index e95b13545..f919e5606 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -6,8 +6,7 @@ import type { ImgCodeRes, PasswordResetReq, FormDataType, -} from '@answer/common/interface'; - +} from '@/common/interface'; import { resetPassword, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; diff --git a/ui/src/pages/Users/AccountForgot/index.tsx b/ui/src/pages/Users/AccountForgot/index.tsx index 5065e9b61..e7a77ea5e 100644 --- a/ui/src/pages/Users/AccountForgot/index.tsx +++ b/ui/src/pages/Users/AccountForgot/index.tsx @@ -2,11 +2,11 @@ import React, { useState, useEffect } from 'react'; import { Container, Col } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; -import SendEmail from './components/sendEmail'; - -import { tryNormalLogged } from '@/utils/guards'; +import { tryNormalLogged } from '@/utils/guard'; import { PageTitle } from '@/components'; +import SendEmail from './components/sendEmail'; + const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'account_forgot' }); const [step, setStep] = useState(1); diff --git a/ui/src/pages/Users/ActiveEmail/index.tsx b/ui/src/pages/Users/ActiveEmail/index.tsx index f50fbc3e0..ac2223a77 100644 --- a/ui/src/pages/Users/ActiveEmail/index.tsx +++ b/ui/src/pages/Users/ActiveEmail/index.tsx @@ -1,9 +1,8 @@ import { FC, memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { loggedUserInfoStore } from '@answer/stores'; -import { getQueryString } from '@answer/utils'; - +import { loggedUserInfoStore } from '@/stores'; +import { getQueryString } from '@/utils'; import { activateAccount } from '@/services'; import { PageTitle } from '@/components'; diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx index 4d419091a..f87a6adfe 100644 --- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx +++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx @@ -7,9 +7,8 @@ import type { ImgCodeRes, PasswordResetReq, FormDataType, -} from '@answer/common/interface'; -import { loggedUserInfoStore } from '@answer/stores'; - +} from '@/common/interface'; +import { loggedUserInfoStore } from '@/stores'; import { changeEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; diff --git a/ui/src/pages/Users/ChangeEmail/index.tsx b/ui/src/pages/Users/ChangeEmail/index.tsx index cbb743a5d..cabc5e5fa 100644 --- a/ui/src/pages/Users/ChangeEmail/index.tsx +++ b/ui/src/pages/Users/ChangeEmail/index.tsx @@ -2,10 +2,10 @@ import { FC, memo } from 'react'; import { Container, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import SendEmail from './components/sendEmail'; - import { PageTitle } from '@/components'; +import SendEmail from './components/sendEmail'; + const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'change_email' }); diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index 6d9af5c2d..423a66543 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -7,13 +7,11 @@ import type { LoginReqParams, ImgCodeRes, FormDataType, -} from '@answer/common/interface'; -import { PageTitle, Unactivate } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import { getQueryString } from '@answer/utils'; - +} from '@/common/interface'; +import { PageTitle, Unactivate } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { getQueryString, Guard, floppyNavigation } from '@/utils'; import { login, checkImgCode } from '@/services'; -import { deriveUserStat, tryNormalLogged } from '@/utils/guards'; import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; import { PicAuthCodeModal } from '@/components/Modal'; @@ -106,8 +104,8 @@ const Index: React.FC = () => { login(params) .then((res) => { updateUser(res); - const userStat = deriveUserStat(); - if (!userStat.isActivated) { + const userStat = Guard.deriveLoginState(); + if (userStat.isNotActivated) { // inactive setStep(2); setRefresh((pre) => pre + 1); @@ -115,7 +113,9 @@ const Index: React.FC = () => { const path = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home; Storage.remove(REDIRECT_PATH_STORAGE_KEY); - navigate(path, { replace: true }); + floppyNavigation.navigate(path, () => { + navigate(path, { replace: true }); + }); } setModalState(false); @@ -159,7 +159,7 @@ const Index: React.FC = () => { if ((storeUser.id && storeUser.mail_status === 2) || isInactive) { setStep(2); } else { - tryNormalLogged(); + Guard.tryNormalLogged(); } }, []); diff --git a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx index 12cfe93b4..54bd4cdb3 100644 --- a/ui/src/pages/Users/Notifications/components/Achievements/index.tsx +++ b/ui/src/pages/Users/Notifications/components/Achievements/index.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; -import { Empty } from '@answer/components'; +import { Empty } from '@/components'; import './index.scss'; diff --git a/ui/src/pages/Users/Notifications/components/Inbox/index.tsx b/ui/src/pages/Users/Notifications/components/Inbox/index.tsx index 27366b81e..68739b0ca 100644 --- a/ui/src/pages/Users/Notifications/components/Inbox/index.tsx +++ b/ui/src/pages/Users/Notifications/components/Inbox/index.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { isEmpty } from 'lodash'; -import { FormatTime, Empty } from '@answer/components'; +import { FormatTime, Empty } from '@/components'; const Inbox = ({ data, handleReadNotification }) => { if (!data) { diff --git a/ui/src/pages/Users/Notifications/index.tsx b/ui/src/pages/Users/Notifications/index.tsx index b24367b74..1ee7158ca 100644 --- a/ui/src/pages/Users/Notifications/index.tsx +++ b/ui/src/pages/Users/Notifications/index.tsx @@ -3,11 +3,7 @@ import { Container, Row, Col, ButtonGroup, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useParams, useNavigate } from 'react-router-dom'; -import { PageTitle } from '@answer/components'; - -import Inbox from './components/Inbox'; -import Achievements from './components/Achievements'; - +import { PageTitle } from '@/components'; import { useQueryNotifications, clearUnreadNotification, @@ -15,6 +11,9 @@ import { readNotification, } from '@/services'; +import Inbox from './components/Inbox'; +import Achievements from './components/Achievements'; + const PAGE_SIZE = 10; const Notifications = () => { diff --git a/ui/src/pages/Users/PasswordReset/index.tsx b/ui/src/pages/Users/PasswordReset/index.tsx index af97001f7..b97bd7077 100644 --- a/ui/src/pages/Users/PasswordReset/index.tsx +++ b/ui/src/pages/Users/PasswordReset/index.tsx @@ -3,12 +3,11 @@ import { Container, Col, Form, Button } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { loggedUserInfoStore } from '@answer/stores'; -import { getQueryString } from '@answer/utils'; -import type { FormDataType } from '@answer/common/interface'; - +import { loggedUserInfoStore } from '@/stores'; +import { getQueryString } from '@/utils'; +import type { FormDataType } from '@/common/interface'; import { replacementPassword } from '@/services'; -import { tryNormalLogged } from '@/utils/guards'; +import { tryNormalLogged } from '@/utils/guard'; import { PageTitle } from '@/components'; const Index: React.FC = () => { diff --git a/ui/src/pages/Users/Personal/components/Answers/index.tsx b/ui/src/pages/Users/Personal/components/Answers/index.tsx index 12ab0059e..a7d8c48ae 100644 --- a/ui/src/pages/Users/Personal/components/Answers/index.tsx +++ b/ui/src/pages/Users/Personal/components/Answers/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon, FormatTime, Tag } from '@answer/components'; +import { Icon, FormatTime, Tag } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/Comments/index.tsx b/ui/src/pages/Users/Personal/components/Comments/index.tsx index 483ce361d..f53e468ee 100644 --- a/ui/src/pages/Users/Personal/components/Comments/index.tsx +++ b/ui/src/pages/Users/Personal/components/Comments/index.tsx @@ -1,7 +1,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; -import { FormatTime } from '@answer/components'; +import { FormatTime } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx index 7eb675259..980a2df30 100644 --- a/ui/src/pages/Users/Personal/components/DefaultList/index.tsx +++ b/ui/src/pages/Users/Personal/components/DefaultList/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon, FormatTime, Tag, BaseUserCard } from '@answer/components'; +import { Icon, FormatTime, Tag, BaseUserCard } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/ListHead/index.tsx b/ui/src/pages/Users/Personal/components/ListHead/index.tsx index e1ce494fc..de5507547 100644 --- a/ui/src/pages/Users/Personal/components/ListHead/index.tsx +++ b/ui/src/pages/Users/Personal/components/ListHead/index.tsx @@ -1,7 +1,7 @@ import { FC, memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { QueryGroup } from '@answer/components'; +import { QueryGroup } from '@/components'; const sortBtns = ['newest', 'score']; diff --git a/ui/src/pages/Users/Personal/components/Reputation/index.tsx b/ui/src/pages/Users/Personal/components/Reputation/index.tsx index fa1cb3db9..6458e9213 100644 --- a/ui/src/pages/Users/Personal/components/Reputation/index.tsx +++ b/ui/src/pages/Users/Personal/components/Reputation/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { FormatTime } from '@answer/components'; +import { FormatTime } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/components/TopList/index.tsx b/ui/src/pages/Users/Personal/components/TopList/index.tsx index cdd7ba2ee..861793052 100644 --- a/ui/src/pages/Users/Personal/components/TopList/index.tsx +++ b/ui/src/pages/Users/Personal/components/TopList/index.tsx @@ -2,7 +2,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { Icon } from '@answer/components'; +import { Icon } from '@/components'; interface Props { data: any[]; diff --git a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx index 97caca3e1..880b810f5 100644 --- a/ui/src/pages/Users/Personal/components/UserInfo/index.tsx +++ b/ui/src/pages/Users/Personal/components/UserInfo/index.tsx @@ -3,8 +3,8 @@ import { Badge, OverlayTrigger, Tooltip } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { Avatar, Icon } from '@answer/components'; -import type { UserInfoRes } from '@answer/common/interface'; +import { Avatar, Icon } from '@/components'; +import type { UserInfoRes } from '@/common/interface'; interface Props { data: UserInfoRes; diff --git a/ui/src/pages/Users/Personal/components/Votes/index.tsx b/ui/src/pages/Users/Personal/components/Votes/index.tsx index 308af8778..fa5023db7 100644 --- a/ui/src/pages/Users/Personal/components/Votes/index.tsx +++ b/ui/src/pages/Users/Personal/components/Votes/index.tsx @@ -1,7 +1,7 @@ import { FC, memo } from 'react'; import { ListGroup, ListGroupItem } from 'react-bootstrap'; -import { FormatTime } from '@answer/components'; +import { FormatTime } from '@/components'; interface Props { visible: boolean; diff --git a/ui/src/pages/Users/Personal/index.tsx b/ui/src/pages/Users/Personal/index.tsx index fcab49ff6..4720db93f 100644 --- a/ui/src/pages/Users/Personal/index.tsx +++ b/ui/src/pages/Users/Personal/index.tsx @@ -3,8 +3,13 @@ import { Container, Row, Col, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useParams, useSearchParams } from 'react-router-dom'; -import { Pagination, FormatTime, PageTitle, Empty } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; +import { Pagination, FormatTime, PageTitle, Empty } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { + usePersonalInfoByName, + usePersonalTop, + usePersonalListByTabName, +} from '@/services'; import { UserInfo, @@ -19,12 +24,6 @@ import { Votes, } from './components'; -import { - usePersonalInfoByName, - usePersonalTop, - usePersonalListByTabName, -} from '@/services'; - const Personal: FC = () => { const { tabName = 'overview', username = '' } = useParams(); const [searchParams] = useSearchParams(); diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx index 4f072fe7b..0d92c5fa3 100644 --- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx +++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx @@ -3,8 +3,7 @@ import { Form, Button, Col } from 'react-bootstrap'; import { Link } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import type { FormDataType } from '@answer/common/interface'; - +import type { FormDataType } from '@/common/interface'; import { register } from '@/services'; import userStore from '@/stores/userInfo'; diff --git a/ui/src/pages/Users/Register/index.tsx b/ui/src/pages/Users/Register/index.tsx index 5f8370a04..e4d94b2db 100644 --- a/ui/src/pages/Users/Register/index.tsx +++ b/ui/src/pages/Users/Register/index.tsx @@ -2,12 +2,11 @@ import React, { useState, useEffect } from 'react'; import { Container } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { PageTitle, Unactivate } from '@answer/components'; +import { PageTitle, Unactivate } from '@/components'; +import { tryNormalLogged } from '@/utils/guard'; import SignUpForm from './components/SignUpForm'; -import { tryNormalLogged } from '@/utils/guards'; - const Index: React.FC = () => { const [showForm, setShowForm] = useState(true); const { t } = useTranslation('translation', { keyPrefix: 'login' }); diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index 4ed6b0ff9..8bfa6dfa3 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -2,9 +2,8 @@ import React, { FC, FormEvent, useEffect, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type * as Type from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; - +import type * as Type from '@/common/interface'; +import { useToast } from '@/hooks'; import { getLoggedUserInfo, changeEmail } from '@/services'; const reg = /(?<=.{2}).+(?=@)/gi; diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx index 2c7ed9ef7..56d8f17b2 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx @@ -2,9 +2,8 @@ import React, { FC, FormEvent, useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { useToast } from '@answer/hooks'; -import type { FormDataType } from '@answer/common/interface'; - +import { useToast } from '@/hooks'; +import type { FormDataType } from '@/common/interface'; import { modifyPassword } from '@/services'; const Index: FC = () => { diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx index f38f92e87..3a3941b9c 100644 --- a/ui/src/pages/Users/Settings/Interface/index.tsx +++ b/ui/src/pages/Users/Settings/Interface/index.tsx @@ -6,9 +6,8 @@ import dayjs from 'dayjs'; import en from 'dayjs/locale/en'; import zh from 'dayjs/locale/zh-cn'; -import type { LangsType, FormDataType } from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; - +import type { LangsType, FormDataType } from '@/common/interface'; +import { useToast } from '@/hooks'; import { languages } from '@/services'; import { DEFAULT_LANG, CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; diff --git a/ui/src/pages/Users/Settings/Notification/index.tsx b/ui/src/pages/Users/Settings/Notification/index.tsx index 6d0cab613..e73101457 100644 --- a/ui/src/pages/Users/Settings/Notification/index.tsx +++ b/ui/src/pages/Users/Settings/Notification/index.tsx @@ -2,9 +2,8 @@ import React, { useState, FormEvent, useEffect } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import type { FormDataType } from '@answer/common/interface'; -import { useToast } from '@answer/hooks'; - +import type { FormDataType } from '@/common/interface'; +import { useToast } from '@/hooks'; import { setNotice, getLoggedUserInfo } from '@/services'; const Index = () => { diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 670bec91f..a4442b08a 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -5,11 +5,10 @@ import { Trans, useTranslation } from 'react-i18next'; import { marked } from 'marked'; import MD5 from 'md5'; -import type { FormDataType } from '@answer/common/interface'; -import { UploadImg, Avatar } from '@answer/components'; -import { loggedUserInfoStore } from '@answer/stores'; -import { useToast } from '@answer/hooks'; - +import type { FormDataType } from '@/common/interface'; +import { UploadImg, Avatar } from '@/components'; +import { loggedUserInfoStore } from '@/stores'; +import { useToast } from '@/hooks'; import { modifyUserInfo, uploadAvatar, getLoggedUserInfo } from '@/services'; const Index: React.FC = () => { diff --git a/ui/src/pages/Users/Settings/index.tsx b/ui/src/pages/Users/Settings/index.tsx index 671707feb..dc5913238 100644 --- a/ui/src/pages/Users/Settings/index.tsx +++ b/ui/src/pages/Users/Settings/index.tsx @@ -3,13 +3,12 @@ import { Container, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import type { FormDataType } from '@answer/common/interface'; - -import Nav from './components/Nav'; - +import type { FormDataType } from '@/common/interface'; import { getLoggedUserInfo } from '@/services'; import { PageTitle } from '@/components'; +import Nav from './components/Nav'; + const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'settings.profile', diff --git a/ui/src/pages/Users/Suspended/index.tsx b/ui/src/pages/Users/Suspended/index.tsx index 4c381d449..403595a92 100644 --- a/ui/src/pages/Users/Suspended/index.tsx +++ b/ui/src/pages/Users/Suspended/index.tsx @@ -1,7 +1,6 @@ import { useTranslation } from 'react-i18next'; -import { loggedUserInfoStore } from '@answer/stores'; - +import { loggedUserInfoStore } from '@/stores'; import { PageTitle } from '@/components'; const Suspended = () => { diff --git a/ui/src/router/guarder.ts b/ui/src/router/guarder.ts deleted file mode 100644 index e3b815982..000000000 --- a/ui/src/router/guarder.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - pullLoggedUser, - isLoggedAndNormal, - isAdminLogged, - isNotLogged, - isNotLoggedOrNormal, - isLoggedAndInactive, - isLoggedAndSuspended, - isNotLoggedOrInactive, - isNotLoggedOrNotSuspend, -} from '@/utils/guards'; - -const RouteGuarder = { - base: async () => { - return isNotLoggedOrNotSuspend(); - }, - loggedAndNormal: async () => { - await pullLoggedUser(true); - return isLoggedAndNormal(); - }, - loggedAndInactive: async () => { - return isLoggedAndInactive(); - }, - loggedAndSuspended: async () => { - return isLoggedAndSuspended(); - }, - adminLogged: async () => { - await pullLoggedUser(true); - return isAdminLogged(); - }, - notLogged: async () => { - return isNotLogged(); - }, - notLoggedOrNormal: async () => { - return isNotLoggedOrNormal(); - }, - notLoggedOrInactive: async () => { - return isNotLoggedOrInactive(); - }, -}; - -export default RouteGuarder; diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 84eb37a65..e5aa2797d 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -28,18 +28,19 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteObject[]) => { } root.push(rn); if (rn.guard) { - const { guard } = rn; - const loaderRef = rn.loader; + const refLoader = rn.loader; + const refGuard = rn.guard; rn.loader = async (args) => { - const gr = await guard(args); + const gr = await refGuard(); if (gr?.redirect && floppyNavigation.differentCurrent(gr.redirect)) { return redirect(gr.redirect); } - let ret; - if (typeof loaderRef === 'function') { - ret = await loaderRef(args); + + let lr; + if (typeof refLoader === 'function') { + lr = await refLoader(args); } - return ret; + return lr; }; } const children = Array.isArray(rn.children) ? rn.children : null; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 5b49fb1ae..126dbac0e 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -1,18 +1,28 @@ import { RouteObject } from 'react-router-dom'; -import RouteGuarder from '@/router/guarder'; +import { Guard } from '@/utils'; +import type { TGuardResult } from '@/utils/guard'; export interface RouteNode extends RouteObject { page: string; children?: RouteNode[]; - guard?: Function; + /** + * a method to auto guard route before route enter + * if the `ok` field in guard returned `TGuardResult` is true, + * it means the guard passed then enter the route. + * if guard returned the `TGuardResult` has `redirect` field, + * then auto redirect route to the `redirect` target. + */ + guard?: () => Promise; } const routes: RouteNode[] = [ { path: '/', page: 'pages/Layout', - guard: RouteGuarder.base, + guard: async () => { + return Guard.notForbidden(); + }, children: [ // question and answer { @@ -35,12 +45,16 @@ const routes: RouteNode[] = [ { path: 'questions/ask', page: 'pages/Questions/Ask', - guard: RouteGuarder.loggedAndNormal, + guard: async () => { + return Guard.activated(); + }, }, { path: 'posts/:qid/edit', page: 'pages/Questions/Ask', - guard: RouteGuarder.loggedAndNormal, + guard: async () => { + return Guard.activated(); + }, }, { path: 'posts/:qid/:aid/edit', @@ -66,6 +80,9 @@ const routes: RouteNode[] = [ { path: 'tags/:tagId/edit', page: 'pages/Tags/Edit', + guard: async () => { + return Guard.activated(); + }, }, // users { @@ -79,6 +96,9 @@ const routes: RouteNode[] = [ { path: 'users/settings', page: 'pages/Users/Settings', + guard: async () => { + return Guard.logged(); + }, children: [ { index: true, @@ -109,55 +129,85 @@ const routes: RouteNode[] = [ { path: 'users/login', page: 'pages/Users/Login', - guard: RouteGuarder.notLoggedOrInactive, + guard: async () => { + const notLogged = Guard.notLogged(); + if (notLogged.ok) { + return notLogged; + } + return Guard.notActivated(); + }, }, { path: 'users/register', page: 'pages/Users/Register', - guard: RouteGuarder.notLogged, + guard: async () => { + return Guard.notLogged(); + }, }, { path: 'users/account-recovery', page: 'pages/Users/AccountForgot', - guard: RouteGuarder.loggedAndNormal, + guard: async () => { + return Guard.activated(); + }, }, { path: 'users/change-email', page: 'pages/Users/ChangeEmail', + // TODO: guard this (change email when user not activated) ? }, { path: 'users/password-reset', page: 'pages/Users/PasswordReset', - guard: RouteGuarder.loggedAndNormal, + guard: async () => { + return Guard.activated(); + }, }, - // TODO: guard '/account-activation/*', '/users/confirm-new-email' { path: 'users/account-activation', page: 'pages/Users/ActiveEmail', - guard: RouteGuarder.loggedAndInactive, + guard: async () => { + const notActivated = Guard.notActivated(); + if (notActivated.ok) { + return notActivated; + } + return Guard.notLogged(); + }, }, { path: 'users/account-activation/success', page: 'pages/Users/ActivationResult', + guard: async () => { + return Guard.activated(); + }, }, { path: '/users/account-activation/failed', page: 'pages/Users/ActivationResult', + guard: async () => { + return Guard.notActivated(); + }, }, { path: '/users/confirm-new-email', page: 'pages/Users/ConfirmNewEmail', + // TODO: guard this }, { path: '/users/account-suspended', page: 'pages/Users/Suspended', - guard: RouteGuarder.loggedAndSuspended, + guard: async () => { + return Guard.forbidden(); + }, }, // for admin { path: 'admin', page: 'pages/Admin', - guard: RouteGuarder.adminLogged, + guard: async () => { + await Guard.pullLoggedUser(true); + return Guard.admin(); + }, children: [ { index: true, diff --git a/ui/src/services/admin/answer.ts b/ui/src/services/admin/answer.ts index 6fd0fbb66..c3340502e 100644 --- a/ui/src/services/admin/answer.ts +++ b/ui/src/services/admin/answer.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useAnswerSearch = (params: Type.AdminContentsReq) => { const apiUrl = `/answer/admin/api/answer/page?${qs.stringify(params)}`; diff --git a/ui/src/services/admin/flag.ts b/ui/src/services/admin/flag.ts index 710cb447c..64ea59f7e 100644 --- a/ui/src/services/admin/flag.ts +++ b/ui/src/services/admin/flag.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const putReport = (params) => { return request.instance.put('/answer/admin/api/report', params); diff --git a/ui/src/services/admin/question.ts b/ui/src/services/admin/question.ts index a6308bf60..9e8d726aa 100644 --- a/ui/src/services/admin/question.ts +++ b/ui/src/services/admin/question.ts @@ -1,8 +1,8 @@ import qs from 'qs'; import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const changeUserStatus = (params) => { return request.put('/answer/admin/api/user/status', params); diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index e1f486e70..274e24eb0 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -1,7 +1,7 @@ import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useGeneralSetting = () => { const apiUrl = `/answer/admin/api/siteinfo/general`; diff --git a/ui/src/services/client/activity.ts b/ui/src/services/client/activity.ts index 7b7b539c2..6f1a4fdb5 100644 --- a/ui/src/services/client/activity.ts +++ b/ui/src/services/client/activity.ts @@ -1,7 +1,7 @@ import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useFollow = (params?: Type.FollowParams) => { const apiUrl = '/answer/api/v1/follow'; diff --git a/ui/src/services/client/notification.ts b/ui/src/services/client/notification.ts index dd9d880e9..a849db0ac 100644 --- a/ui/src/services/client/notification.ts +++ b/ui/src/services/client/notification.ts @@ -1,10 +1,9 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; - -import { tryNormalLogged } from '@/utils/guards'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; +import { tryNormalLogged } from '@/utils/guard'; export const useQueryNotifications = (params) => { const apiUrl = `/answer/api/v1/notification/page?${qs.stringify(params, { diff --git a/ui/src/services/client/personal.ts b/ui/src/services/client/personal.ts index b84e260f5..6b61aaba5 100644 --- a/ui/src/services/client/personal.ts +++ b/ui/src/services/client/personal.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const usePersonalInfoByName = (username: string) => { const apiUrl = '/answer/api/v1/personal/user/info'; diff --git a/ui/src/services/client/question.ts b/ui/src/services/client/question.ts index c35fe5441..c418f26ca 100644 --- a/ui/src/services/client/question.ts +++ b/ui/src/services/client/question.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useQuestionList = (params: Type.QueryQuestionsReq) => { const apiUrl = `/answer/api/v1/question/page?${qs.stringify(params)}`; diff --git a/ui/src/services/client/search.ts b/ui/src/services/client/search.ts index f5fe86fe4..8d3802940 100644 --- a/ui/src/services/client/search.ts +++ b/ui/src/services/client/search.ts @@ -1,8 +1,8 @@ import useSWR from 'swr'; import qs from 'qs'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const useSearch = (params?: Type.SearchParams) => { const apiUrl = '/answer/api/v1/search'; diff --git a/ui/src/services/client/tag.ts b/ui/src/services/client/tag.ts index 634b44729..42b9f1ac1 100644 --- a/ui/src/services/client/tag.ts +++ b/ui/src/services/client/tag.ts @@ -1,9 +1,8 @@ import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; - -import { tryNormalLogged } from '@/utils/guards'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; +import { tryNormalLogged } from '@/utils/guard'; export const deleteTag = (id) => { return request.delete('/answer/api/v1/tag', { diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 6f2cf83aa..3e9e78721 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -1,8 +1,8 @@ import qs from 'qs'; import useSWR from 'swr'; -import request from '@answer/utils/request'; -import type * as Type from '@answer/common/interface'; +import request from '@/utils/request'; +import type * as Type from '@/common/interface'; export const uploadImage = (file) => { const form = new FormData(); diff --git a/ui/src/stores/userInfo.ts b/ui/src/stores/userInfo.ts index 9aa540b55..017c3149c 100644 --- a/ui/src/stores/userInfo.ts +++ b/ui/src/stores/userInfo.ts @@ -1,8 +1,7 @@ import create from 'zustand'; -import type { UserInfoRes } from '@answer/common/interface'; -import Storage from '@answer/utils/storage'; - +import type { UserInfoRes } from '@/common/interface'; +import Storage from '@/utils/storage'; import { LOGGED_USER_STORAGE_KEY, LOGGED_TOKEN_STORAGE_KEY, @@ -15,6 +14,7 @@ interface UserInfoStore { } const initUser: UserInfoRes = { + access_token: '', username: '', avatar: '', rank: 0, diff --git a/ui/src/utils/floppyNavigation.ts b/ui/src/utils/floppyNavigation.ts index 7edbbaa0f..68e6f8a58 100644 --- a/ui/src/utils/floppyNavigation.ts +++ b/ui/src/utils/floppyNavigation.ts @@ -25,7 +25,8 @@ const navigate = (pathname: string, callback: Function) => { const navigateToLogin = () => { const { pathname } = window.location; if (pathname !== RouteAlias.login && pathname !== RouteAlias.register) { - const redirectUrl = window.location.href; + const loc = window.location; + const redirectUrl = loc.href.replace(loc.origin, ''); Storage.set(REDIRECT_PATH_STORAGE_KEY, redirectUrl); } navigate(RouteAlias.login, () => { diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts new file mode 100644 index 000000000..bc73ab9a3 --- /dev/null +++ b/ui/src/utils/guard.ts @@ -0,0 +1,182 @@ +import { getLoggedUserInfo } from '@/services'; +import { loggedUserInfoStore } from '@/stores'; +import { RouteAlias } from '@/router/alias'; +import Storage from '@/utils/storage'; +import { LOGGED_USER_STORAGE_KEY } from '@/common/constants'; + +import { floppyNavigation } from './floppyNavigation'; + +type TLoginState = { + isLogged: boolean; + isNotActivated: boolean; + isActivated: boolean; + isForbidden: boolean; + isNormal: boolean; + isAdmin: boolean; +}; + +export type TGuardResult = { + ok: boolean; + redirect?: string; +}; + +export const deriveLoginState = (): TLoginState => { + const ls: TLoginState = { + isLogged: false, + isNotActivated: false, + isActivated: false, + isForbidden: false, + isNormal: false, + isAdmin: false, + }; + const { user } = loggedUserInfoStore.getState(); + if (user.access_token) { + ls.isLogged = true; + } + if (ls.isLogged && user.mail_status === 1) { + ls.isActivated = true; + } + if (ls.isLogged && user.mail_status === 2) { + ls.isNotActivated = true; + } + if (ls.isLogged && user.status === 'forbidden') { + ls.isForbidden = true; + } + if (ls.isActivated && !ls.isForbidden) { + ls.isNormal = true; + } + if (ls.isNormal && user.is_admin === true) { + ls.isAdmin = true; + } + + return ls; +}; + +let pullLock = false; +let dedupeTimestamp = 0; +export const pullLoggedUser = async (forceRePull = false) => { + // only pull once if not force re-pull + if (pullLock && !forceRePull) { + return; + } + // dedupe pull requests in this time span in 10 seconds + if (Date.now() - dedupeTimestamp < 1000 * 10) { + return; + } + dedupeTimestamp = Date.now(); + const loggedUserInfo = await getLoggedUserInfo().catch((ex) => { + dedupeTimestamp = 0; + if (!deriveLoginState().isLogged) { + // load fallback userInfo from local storage + const storageLoggedUserInfo = Storage.get(LOGGED_USER_STORAGE_KEY); + if (storageLoggedUserInfo) { + loggedUserInfoStore.getState().update(storageLoggedUserInfo); + } + } + console.error(ex); + }); + if (loggedUserInfo) { + pullLock = true; + loggedUserInfoStore.getState().update(loggedUserInfo); + } +}; + +export const logged = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + if (!us.isLogged) { + gr.ok = false; + gr.redirect = RouteAlias.login; + } + return gr; +}; + +export const notLogged = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + if (us.isLogged) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +export const notActivated = () => { + const gr = logged(); + const us = deriveLoginState(); + if (us.isActivated) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +export const activated = () => { + const gr = logged(); + const us = deriveLoginState(); + if (us.isNotActivated) { + gr.ok = false; + gr.redirect = RouteAlias.activation; + } + return gr; +}; + +export const forbidden = () => { + const gr = logged(); + const us = deriveLoginState(); + if (gr.ok && !us.isForbidden) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +export const notForbidden = () => { + const gr: TGuardResult = { ok: true }; + const us = deriveLoginState(); + if (us.isForbidden) { + gr.ok = false; + gr.redirect = RouteAlias.suspended; + } + return gr; +}; + +export const admin = () => { + const gr = logged(); + const us = deriveLoginState(); + if (gr.ok && !us.isAdmin) { + gr.ok = false; + gr.redirect = RouteAlias.home; + } + return gr; +}; + +/** + * try user was logged and all state ok + * @param autoLogin + */ +export const tryNormalLogged = (autoLogin: boolean = false) => { + const us = deriveLoginState(); + + if (us.isNormal) { + return true; + } + // must assert logged state first and return + if (!us.isLogged) { + if (autoLogin) { + floppyNavigation.navigateToLogin(); + } + return false; + } + if (us.isNotActivated) { + floppyNavigation.navigate(RouteAlias.activation, () => { + window.location.href = RouteAlias.activation; + }); + } else if (us.isForbidden) { + floppyNavigation.navigate(RouteAlias.suspended, () => { + window.location.replace(RouteAlias.suspended); + }); + } + + return false; +}; diff --git a/ui/src/utils/guards.ts b/ui/src/utils/guards.ts deleted file mode 100644 index 47f79605f..000000000 --- a/ui/src/utils/guards.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { getLoggedUserInfo } from '@/services'; -import { loggedUserInfoStore } from '@/stores'; -import { RouteAlias } from '@/router/alias'; -import Storage from '@/utils/storage'; -import { LOGGED_USER_STORAGE_KEY } from '@/common/constants'; -import { floppyNavigation } from '@/utils/floppyNavigation'; - -type UserStat = { - isLogged: boolean; - isActivated: boolean; - isSuspended: boolean; - isNormal: boolean; - isAdmin: boolean; -}; -export const deriveUserStat = (): UserStat => { - const stat: UserStat = { - isLogged: false, - isActivated: false, - isSuspended: false, - isNormal: false, - isAdmin: false, - }; - const { user } = loggedUserInfoStore.getState(); - if (user.id && user.username) { - stat.isLogged = true; - } - if (stat.isLogged && user.mail_status === 1) { - stat.isActivated = true; - } - if (stat.isLogged && user.status === 'forbidden') { - stat.isSuspended = true; - } - if (stat.isLogged && stat.isActivated && !stat.isSuspended) { - stat.isNormal = true; - } - if (stat.isNormal && user.is_admin === true) { - stat.isAdmin = true; - } - - return stat; -}; - -type GuardResult = { - ok: boolean; - redirect?: string; -}; -let pullLock = false; -let dedupeTimestamp = 0; -export const pullLoggedUser = async (forceRePull = false) => { - // only pull once if not force re-pull - if (pullLock && !forceRePull) { - return; - } - // dedupe pull requests in this time span in 10 seconds - if (Date.now() - dedupeTimestamp < 1000 * 10) { - return; - } - dedupeTimestamp = Date.now(); - const loggedUserInfo = await getLoggedUserInfo().catch((ex) => { - dedupeTimestamp = 0; - if (!deriveUserStat().isLogged) { - // load fallback userInfo from local storage - const storageLoggedUserInfo = Storage.get(LOGGED_USER_STORAGE_KEY); - if (storageLoggedUserInfo) { - loggedUserInfoStore.getState().update(storageLoggedUserInfo); - } - } - console.error(ex); - }); - if (loggedUserInfo) { - pullLock = true; - loggedUserInfoStore.getState().update(loggedUserInfo); - } -}; - -export const isLogged = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (!userStat.isLogged) { - ret.ok = false; - ret.redirect = RouteAlias.login; - } - return ret; -}; - -export const isNotLogged = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (userStat.isLogged) { - ret.ok = false; - ret.redirect = RouteAlias.home; - } - return ret; -}; - -export const isLoggedAndInactive = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (userStat.isActivated) { - ret.ok = false; - ret.redirect = RouteAlias.home; - } - return ret; -}; - -export const isLoggedAndSuspended = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (!userStat.isSuspended) { - ret.ok = false; - ret.redirect = RouteAlias.home; - } - return ret; -}; - -export const isLoggedAndNormal = () => { - const ret: GuardResult = { ok: false, redirect: undefined }; - const userStat = deriveUserStat(); - if (userStat.isNormal) { - ret.ok = true; - } else if (!userStat.isActivated) { - ret.redirect = RouteAlias.activation; - } else if (userStat.isSuspended) { - ret.redirect = RouteAlias.suspended; - } else if (!userStat.isLogged) { - ret.redirect = RouteAlias.login; - } - return ret; -}; - -export const isNotLoggedOrNormal = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - const gr = isLoggedAndNormal(); - if (!gr.ok && userStat.isLogged) { - ret.ok = false; - ret.redirect = gr.redirect; - } - return ret; -}; - -export const isNotLoggedOrNotSuspend = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - const gr = isLoggedAndNormal(); - if (!gr.ok && userStat.isSuspended) { - ret.ok = false; - ret.redirect = gr.redirect; - } - return ret; -}; - -export const isNotLoggedOrInactive = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (userStat.isActivated) { - ret.ok = false; - ret.redirect = RouteAlias.home; - } else if (userStat.isSuspended) { - ret.ok = false; - ret.redirect = RouteAlias.suspended; - } - return ret; -}; - -export const isAdminLogged = () => { - const ret: GuardResult = { ok: true, redirect: undefined }; - const userStat = deriveUserStat(); - if (!userStat.isAdmin) { - ret.redirect = RouteAlias.home; - ret.ok = false; - } - return ret; -}; - -/** - * try user was logged and all state ok - * @param autoLogin - */ -export const tryNormalLogged = (autoLogin: boolean = false) => { - const gr = isLoggedAndNormal(); - if (gr.ok) { - return true; - } - - if (gr.redirect === RouteAlias.login && autoLogin) { - floppyNavigation.navigateToLogin(); - } else if (gr.redirect) { - floppyNavigation.navigate(gr.redirect, () => { - // @ts-ignore - window.location.replace(gr.redirect); - }); - } - - return false; -}; diff --git a/ui/src/utils/index.ts b/ui/src/utils/index.ts index a1eaf02c6..69f70696c 100644 --- a/ui/src/utils/index.ts +++ b/ui/src/utils/index.ts @@ -1,6 +1,6 @@ -export * from './common'; -export * as guards from './guards'; - export { default as request } from './request'; export { default as Storage } from './storage'; export { floppyNavigation } from './floppyNavigation'; + +export * as Guard from './guard'; +export * from './common'; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index d80cadf5d..4e878cb34 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -1,12 +1,8 @@ import axios, { AxiosResponse } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; -import { Modal } from '@answer/components'; -import { loggedUserInfoStore, toastStore } from '@answer/stores'; - -import Storage from './storage'; -import { floppyNavigation } from './floppyNavigation'; - +import { Modal } from '@/components'; +import { loggedUserInfoStore, toastStore } from '@/stores'; import { LOGGED_TOKEN_STORAGE_KEY, CURRENT_LANG_STORAGE_KEY, @@ -14,6 +10,9 @@ import { } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; +import Storage from './storage'; +import { floppyNavigation } from './floppyNavigation'; + const API = { development: '', production: '', diff --git a/ui/tsconfig.json b/ui/tsconfig.json index d7c5decfa..f270b720b 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -20,18 +20,7 @@ "jsx": "react-jsx", "baseUrl": "./", "paths": { - "@/*": ["src/*"], - "@answer/pages/*": ["src/pages/*"], - "@answer/components": ["src/components/index.ts"], - "@answer/components/*": ["src/components/*"], - "@answer/stores": ["src/stores"], - "@answer/stores/*": ["src/stores/*"], - "@answer/services/*": ["src/services/*"], - "@answer/hooks": ["src/hooks"], - "@answer/common": ["src/common"], - "@answer/common/*": ["src/common/*"], - "@answer/utils": ["src/utils"], - "@answer/utils/*": ["src/utils/*"] + "@/*": ["src/*"] } }, "include": ["src", "node_modules/@testing-library/jest-dom"] From f196972fff1e87f678447b09229d4e44e659a7ae Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Wed, 2 Nov 2022 17:23:41 +0800 Subject: [PATCH 0113/3337] ci(admin/interface): fix interface merge issue --- ui/src/pages/Admin/Interface/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index ec8c770e5..395b9616b 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -8,11 +8,9 @@ import { FormDataType, AdminSettingsInterface, } from '@/common/interface'; - import { interfaceStore } from '@/stores'; import { UploadImg } from '@/components'; import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; - import { languages, uploadAvatar, From bf4e9824f59a1006d7c029163c9d5be84725a911 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 2 Nov 2022 17:26:55 +0800 Subject: [PATCH 0114/3337] fix: handle install page forms --- ui/src/common/interface.ts | 2 +- ui/src/components/Header/index.scss | 13 + ui/src/components/Header/index.tsx | 4 +- ui/src/i18n/locales/en.json | 31 ++- ui/src/index.scss | 1 + .../Install/components/FirstStep/index.tsx | 58 ++++- .../Install/components/FourthStep/index.tsx | 172 +++++++++++- .../Install/components/SecondStep/index.tsx | 245 ++++++++++++++++-- .../Install/components/ThirdStep/index.tsx | 5 +- ui/src/pages/Install/index.tsx | 135 +++++++++- ui/src/pages/Maintenance/index.tsx | 22 +- ui/src/pages/Upgrade/index.tsx | 64 ++--- .../Personal/components/DefaultList/index.tsx | 2 +- .../Personal/components/NavBar/index.tsx | 5 +- .../Personal/components/UserInfo/index.tsx | 6 +- .../Users/Personal/components/Votes/index.tsx | 2 +- ui/src/pages/Users/Personal/index.tsx | 6 +- 17 files changed, 651 insertions(+), 122 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 7516a2d7e..732964d1e 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -264,7 +264,7 @@ export interface AdminSettingsInterface { logo: string; language: string; theme: string; - time_zone: string; + time_zone?: string; } export interface AdminSettingsSmtp { diff --git a/ui/src/components/Header/index.scss b/ui/src/components/Header/index.scss index 348632a37..f3f67b789 100644 --- a/ui/src/components/Header/index.scss +++ b/ui/src/components/Header/index.scss @@ -50,6 +50,10 @@ @media (max-width: 992.9px) { #header { + .logo { + max-width: 93px; + max-height: auto; + } .nav-grow { flex-grow: 1!important; } @@ -65,3 +69,12 @@ } +@media (max-width: 576px) { + #header { + .logo { + max-width: 93px; + max-height: auto; + } + } +} + diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 748eb3183..4de50cee7 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -71,8 +71,8 @@ const Header: FC = () => { id="navBarToggle" /> -
- +
+ {interfaceInfo.logo ? ( void; + nextCallback: () => void; visible: boolean; } -const Index: FC = ({ visible }) => { +const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); + const [langs, setLangs] = useState(); + + const getLangs = async () => { + const res: LangsType[] = await languages(); + setLangs(res); + }; + + const handleSubmit = () => { + nextCallback(); + }; + + useEffect(() => { + getLangs(); + }, []); + if (!visible) return null; return ( -
- - {t('choose_lang.label')} - - + + + {t('lang.label')} + { + changeCallback({ + lang: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }}> + {langs?.map((item) => { + return ( + + ); + })}
- +
); diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index 25fb47ef1..ccd44be2d 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -1,50 +1,204 @@ -import { FC } from 'react'; +import { FC, FormEvent } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import type { FormDataType } from '@/common/interface'; import Progress from '../Progress'; interface Props { + data: FormDataType; + changeCallback: (value: FormDataType) => void; + nextCallback: () => void; visible: boolean; } -const Index: FC = ({ visible }) => { +const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); + const checkValidated = (): boolean => { + let bol = true; + const { + site_name, + contact_email, + admin_name, + admin_password, + admin_email, + } = data; + + if (!site_name.value) { + bol = false; + data.site_name = { + value: '', + isInvalid: true, + errorMsg: t('site_name.msg'), + }; + } + + if (!contact_email.value) { + bol = false; + data.contact_email = { + value: '', + isInvalid: true, + errorMsg: t('contact_email.msg'), + }; + } + + if (!admin_name.value) { + bol = false; + data.admin_name = { + value: '', + isInvalid: true, + errorMsg: t('admin_name.msg'), + }; + } + + if (!admin_password.value) { + bol = false; + data.admin_password = { + value: '', + isInvalid: true, + errorMsg: t('admin_password.msg'), + }; + } + + if (!admin_email.value) { + bol = false; + data.admin_email = { + value: '', + isInvalid: true, + errorMsg: t('admin_email.msg'), + }; + } + + changeCallback({ + ...data, + }); + return bol; + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!checkValidated()) { + return; + } + nextCallback(); + }; + if (!visible) return null; return ( -
+
{t('site_information')}
{t('site_name.label')} - + { + changeCallback({ + site_name: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.site_name.errorMsg} + {t('contact_email.label')} - + { + changeCallback({ + contact_email: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> {t('contact_email.text')} + + {data.contact_email.errorMsg} +
{t('admin_account')}
{t('admin_name.label')} - + { + changeCallback({ + admin_name: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.admin_name.errorMsg} + {t('admin_password.label')} - + { + changeCallback({ + admin_password: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> {t('admin_password.text')} + + {data.admin_password.errorMsg} + {t('admin_email.label')} - + { + changeCallback({ + admin_email: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> {t('admin_email.text')} + + {data.admin_email.errorMsg} +
- +
); diff --git a/ui/src/pages/Install/components/SecondStep/index.tsx b/ui/src/pages/Install/components/SecondStep/index.tsx index 7b21ab738..6c97ec61c 100644 --- a/ui/src/pages/Install/components/SecondStep/index.tsx +++ b/ui/src/pages/Install/components/SecondStep/index.tsx @@ -1,54 +1,243 @@ -import { FC } from 'react'; +import { FC, FormEvent } from 'react'; import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import Progress from '../Progress'; +import type { FormDataType } from '@/common/interface'; interface Props { + data: FormDataType; + changeCallback: (value: FormDataType) => void; + nextCallback: () => void; visible: boolean; } -const Index: FC = ({ visible }) => { +const sqlData = [ + { + value: 'mysql', + label: 'MariaDB/MySQL', + }, + { + value: 'sqlite3', + label: 'SQLite', + }, + { + value: 'postgres', + label: 'PostgreSQL', + }, +]; + +const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); + const checkValidated = (): boolean => { + let bol = true; + const { db_type, db_username, db_password, db_host, db_name, db_file } = + data; + + if (db_type.value !== 'sqllite3') { + if (!db_username.value) { + bol = false; + data.db_username = { + value: '', + isInvalid: true, + errorMsg: t('db_username.msg'), + }; + } + + if (!db_password.value) { + bol = false; + data.db_password = { + value: '', + isInvalid: true, + errorMsg: t('db_password.msg'), + }; + } + + if (!db_host.value) { + bol = false; + data.db_host = { + value: '', + isInvalid: true, + errorMsg: t('db_host.msg'), + }; + } + + if (!db_name.value) { + bol = false; + data.db_name = { + value: '', + isInvalid: true, + errorMsg: t('db_name.msg'), + }; + } + } else if (!db_file.value) { + bol = false; + data.db_file = { + value: '', + isInvalid: true, + errorMsg: t('db_file.msg'), + }; + } + changeCallback({ + ...data, + }); + return bol; + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!checkValidated()) { + return; + } + nextCallback(); + }; + if (!visible) return null; return ( -
+ - {t('database_engine.label')} - - + {t('db_type.label')} + { + changeCallback({ + db_type: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }}> + {sqlData.map((item) => { + return ( + + ); + })} + {data.db_type.value !== 'sqlite3' ? ( + <> + + {t('db_username.label')} + { + changeCallback({ + db_username: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_username.errorMsg} + + - - {t('username.label')} - - - - - {t('password.label')} - - + + {t('db_password.label')} + { + changeCallback({ + db_password: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> - - {t('database_host.label')} - - + + {data.db_password.errorMsg} + + - - {t('database_name.label')} - - + + {t('db_host.label')} + { + changeCallback({ + db_host: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_host.errorMsg} + + - - {t('table_prefix.label')} - - + + {t('db_name.label')} + { + changeCallback({ + db_name: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_name.errorMsg} + + + + ) : ( + + {t('db_file.label')} + { + changeCallback({ + db_file: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + /> + + {data.db_file.errorMsg} + + + )}
- +
); diff --git a/ui/src/pages/Install/components/ThirdStep/index.tsx b/ui/src/pages/Install/components/ThirdStep/index.tsx index 4d3f702f6..3b5ca08c9 100644 --- a/ui/src/pages/Install/components/ThirdStep/index.tsx +++ b/ui/src/pages/Install/components/ThirdStep/index.tsx @@ -6,9 +6,10 @@ import Progress from '../Progress'; interface Props { visible: boolean; + nextCallback: () => void; } -const Index: FC = ({ visible }) => { +const Index: FC = ({ visible, nextCallback }) => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); if (!visible) return null; @@ -30,7 +31,7 @@ const Index: FC = ({ visible }) => {
{t('config_yaml.info')}
- +
); diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 8de7cb116..e8c40d5a5 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -1,7 +1,11 @@ -import { FC, useState } from 'react'; +import { FC, useState, useEffect } from 'react'; import { Container, Row, Col, Card, Alert } from 'react-bootstrap'; import { useTranslation, Trans } from 'react-i18next'; +import type { FormDataType } from '@/common/interface'; +import { Storage } from '@/utils'; +import { PageTitle } from '@/components'; + import { FirstStep, SecondStep, @@ -10,14 +14,109 @@ import { Fifth, } from './components'; -import { PageTitle } from '@/components'; - const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'install' }); - const [step] = useState(7); + const [step, setStep] = useState(1); + const [showError] = useState(false); + + const [formData, setFormData] = useState({ + lang: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_type: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_username: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_password: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_host: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + db_file: { + value: '', + isInvalid: false, + errorMsg: '', + }, + + site_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + contact_email: { + value: '', + isInvalid: false, + errorMsg: '', + }, + admin_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + admin_password: { + value: '', + isInvalid: false, + errorMsg: '', + }, + admin_email: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }); + + const handleChange = (params: FormDataType) => { + console.log(params); + setFormData({ ...formData, ...params }); + }; + + const handleStep = () => { + setStep((pre) => pre + 1); + }; + + // const handleSubmit = () => { + // const params = { + // lang: formData.lang.value, + // db_type: formData.db_type.value, + // db_username: formData.db_username.value, + // db_password: formData.db_password.value, + // db_host: formData.db_host.value, + // db_name: formData.db_name.value, + // db_file: formData.db_file.value, + // site_name: formData.site_name.value, + // contact_email: formData.contact_email.value, + // admin_name: formData.admin_name.value, + // admin_password: formData.admin_password.value, + // admin_email: formData.admin_email.value, + // }; + + // console.log(params); + // }; + + useEffect(() => { + console.log('step===', Storage.get('INSTALL_STEP')); + }, []); return ( -
+
@@ -25,14 +124,30 @@ const Index: FC = () => {

{t('title')}

- show error msg - + {showError && show error msg } + + - + - + - + {step === 6 && ( diff --git a/ui/src/pages/Maintenance/index.tsx b/ui/src/pages/Maintenance/index.tsx index 3a2c2d862..560108bf0 100644 --- a/ui/src/pages/Maintenance/index.tsx +++ b/ui/src/pages/Maintenance/index.tsx @@ -8,15 +8,19 @@ const Index = () => { keyPrefix: 'page_maintenance', }); return ( - - -
- (=‘_‘=) -
-
{t('description')}
-
+
+ + +
+ (=‘_‘=) +
+
{t('description')}
+
+
); }; diff --git a/ui/src/pages/Upgrade/index.tsx b/ui/src/pages/Upgrade/index.tsx index aee2a37a9..c65c90981 100644 --- a/ui/src/pages/Upgrade/index.tsx +++ b/ui/src/pages/Upgrade/index.tsx @@ -14,38 +14,40 @@ const Index = () => { setStep(2); }; return ( - - - -
{t('post')}{t('votes')}{t('created')}{t('status')}{t('action')}{t('votes')}{t('created')}{t('status')}{t('action')}
-
- - - + + + - - {curFilter !== 'deleted' && } + + {curFilter !== 'deleted' && ( + + )} @@ -177,7 +179,10 @@ const Questions: FC = () => { {curFilter !== 'deleted' && ( diff --git a/ui/src/pages/Admin/Users/index.tsx b/ui/src/pages/Admin/Users/index.tsx index ae8942b4b..58b849d17 100644 --- a/ui/src/pages/Admin/Users/index.tsx +++ b/ui/src/pages/Admin/Users/index.tsx @@ -86,20 +86,22 @@ const Users: FC = () => {
{t('post')}{t('votes')}{t('answers')}{t('post')}{t('votes')}{t('answers')} {t('created')}{t('status')}{t('action')}{t('status')}{t('action')}
-
- - + + - {(curFilter === 'deleted' || curFilter === 'suspended') && ( - )} - - {curFilter !== 'deleted' ? : null} + + {curFilter !== 'deleted' ? ( + + ) : null} @@ -136,7 +138,7 @@ const Users: FC = () => { - + pathname.includes(v)) ? 6 : 10}> diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 500c2ef1d..f62a747d6 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -364,7 +364,11 @@ const Index: React.FC = () => { className="me-3 rounded" />
- +
diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index a723284c5..110c1d4d3 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -251,6 +251,10 @@ const routes: RouteNode[] = [ path: 'smtp', page: 'pages/Admin/Smtp', }, + { + path: 'branding', + page: 'pages/Admin/Branding', + }, ], }, { From e30eaecee49f2c8950ef7c57c28836f3c29e3bfe Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 10 Nov 2022 13:02:19 +0800 Subject: [PATCH 0239/3337] fix: remove time zone check --- internal/schema/siteinfo_schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 6184a3c31..85a643ed0 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -27,7 +27,7 @@ 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,timezone" form:"time_zone" json:"time_zone"` + TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"` } // SiteGeneralResp site general response From 51e1c483f6ef59ce75d19cb44e07a203ac6c12b3 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 10 Nov 2022 13:02:19 +0800 Subject: [PATCH 0240/3337] fix: remove time zone check --- internal/schema/siteinfo_schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 70be00ba3..446b986db 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -12,7 +12,7 @@ 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,timezone" form:"time_zone" json:"time_zone"` + TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"` } // SiteGeneralResp site general response From 8e10de39a00ae8f43a59d779494195c32b6e194b Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 10 Nov 2022 14:00:31 +0800 Subject: [PATCH 0241/3337] fix: delete code --- i18n/en_US.yaml | 2 -- ui/src/i18n/locales/en_US.yaml | 2 -- 2 files changed, 4 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index fcd7d18f4..9ff1ea455 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1112,8 +1112,6 @@ ui: 'no': 'No' branding: page_title: Branding - "yes": "Yes" - "no": "No" form: empty: cannot be empty invalid: is invalid diff --git a/ui/src/i18n/locales/en_US.yaml b/ui/src/i18n/locales/en_US.yaml index fcd7d18f4..9ff1ea455 100644 --- a/ui/src/i18n/locales/en_US.yaml +++ b/ui/src/i18n/locales/en_US.yaml @@ -1112,8 +1112,6 @@ ui: 'no': 'No' branding: page_title: Branding - "yes": "Yes" - "no": "No" form: empty: cannot be empty invalid: is invalid From c468d2ab2391c684381238949a5a422bc2b100aa Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 10 Nov 2022 14:13:29 +0800 Subject: [PATCH 0242/3337] feat: separate db connection and table check --- cmd/answer/command.go | 4 ++-- internal/cli/install_check.go | 21 ++++++++++++++++----- internal/install/install_controller.go | 20 ++++++++++++++++++-- internal/install/install_req.go | 5 +++-- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/cmd/answer/command.go b/cmd/answer/command.go index b5d83f9fb..5d026f771 100644 --- a/cmd/answer/command.go +++ b/cmd/answer/command.go @@ -73,7 +73,7 @@ To run answer, use: } fmt.Println("config file read successfully, try to connect database...") - if cli.CheckDB(c.Data.Database, true) { + if cli.CheckDBTableExist(c.Data.Database) { fmt.Println("connect to database successfully and table already exists, do nothing.") return } @@ -152,7 +152,7 @@ To run answer, use: return } - if cli.CheckDB(c.Data.Database, false) { + if cli.CheckDBConnection(c.Data.Database) { fmt.Println("db connection successfully [✔]") } else { fmt.Println("db connection failed [x]") diff --git a/internal/cli/install_check.go b/internal/cli/install_check.go index 4526ccd80..6cb25987a 100644 --- a/internal/cli/install_check.go +++ b/internal/cli/install_check.go @@ -16,9 +16,8 @@ func CheckUploadDir() bool { return dir.CheckDirExist(UploadFilePath) } -// CheckDB check database whether the connection is normal -// if mustInstalled is true, will check table if already exists -func CheckDB(dataConf *data.Database, mustInstalled bool) bool { +// CheckDBConnection check database whether the connection is normal +func CheckDBConnection(dataConf *data.Database) bool { db, err := data.NewDB(false, dataConf) if err != nil { fmt.Printf("connection database failed: %s\n", err) @@ -28,8 +27,20 @@ func CheckDB(dataConf *data.Database, mustInstalled bool) bool { fmt.Printf("connection ping database failed: %s\n", err) return false } - if !mustInstalled { - return true + + return true +} + +// CheckDBTableExist check database whether the table is already exists +func CheckDBTableExist(dataConf *data.Database) bool { + db, err := data.NewDB(false, dataConf) + if err != nil { + fmt.Printf("connection database failed: %s\n", err) + return false + } + if err = db.Ping(); err != nil { + fmt.Printf("connection ping database failed: %s\n", err) + return false } exist, err := db.IsTableExist(&entity.Version{}) diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index 25552ccff..b4449203a 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -52,7 +52,10 @@ func CheckConfigFile(ctx *gin.Context) { handler.HandleResponse(ctx, err, nil) return } - resp.DbTableExist = cli.CheckDB(allConfig.Data.Database, true) + resp.DBConnectionSuccess = cli.CheckDBConnection(allConfig.Data.Database) + if resp.DBConnectionSuccess { + resp.DbTableExist = cli.CheckDBTableExist(allConfig.Data.Database) + } handler.HandleResponse(ctx, nil, resp) } @@ -76,7 +79,7 @@ func CheckDatabase(ctx *gin.Context) { Driver: req.DbType, Connection: req.GetConnection(), } - resp.ConnectionSuccess = cli.CheckDB(dataConf, false) + resp.ConnectionSuccess = cli.CheckDBConnection(dataConf) if !resp.ConnectionSuccess { handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) return @@ -99,6 +102,13 @@ func InitEnvironment(ctx *gin.Context) { return } + // check config file if exist + if cli.CheckConfigFile(confPath) { + log.Debugf("config file already exists") + handler.HandleResponse(ctx, nil, nil) + return + } + if err := cli.InstallConfigFile(confPath); err != nil { handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{ Success: false, @@ -152,6 +162,12 @@ func InitBaseInfo(ctx *gin.Context) { return } + if cli.CheckDBTableExist(c.Data.Database) { + log.Warnf("database is already initialized") + handler.HandleResponse(ctx, nil, nil) + return + } + if err := migrations.InitDB(c.Data.Database); err != nil { log.Error("init database error: ", err.Error()) handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert) diff --git a/internal/install/install_req.go b/internal/install/install_req.go index a3add9ca8..534a0fa60 100644 --- a/internal/install/install_req.go +++ b/internal/install/install_req.go @@ -10,8 +10,9 @@ import ( // CheckConfigFileResp check config file if exist or not response type CheckConfigFileResp struct { - ConfigFileExist bool `json:"config_file_exist"` - DbTableExist bool `json:"db_table_exist"` + ConfigFileExist bool `json:"config_file_exist"` + DBConnectionSuccess bool `json:"db_connection_success"` + DbTableExist bool `json:"db_table_exist"` } // CheckDatabaseReq check database From 0ec5ecad468aeac45c5f7c71c8fe665384060352 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 10 Nov 2022 14:57:05 +0800 Subject: [PATCH 0243/3337] fix: install add db connection failed state --- i18n/en_US.yaml | 4 ++++ ui/src/pages/Install/index.tsx | 29 ++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 9892f9456..cba58e527 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -893,6 +893,10 @@ ui: installed_description: >- You appear to have already installed. To reinstall please clear your old database tables first. + db_failed: Database connection failed + db_failed_description: >- + This either means that the database information in your <1>config.yaml file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down. + page_404: description: 'Unfortunately, this page doesn''t exist.' back_home: Back to homepage diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index c78e0adfb..0cfbb7ee4 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -29,7 +29,10 @@ const Index: FC = () => { const [errorData, setErrorData] = useState<{ [propName: string]: any }>({ msg: '', }); - const [tableExist, setTableExist] = useState(false); + const [checkData, setCheckData] = useState({ + db_table_exist: false, + db_connection_success: false, + }); const [formData, setFormData] = useState({ lang: { @@ -200,17 +203,24 @@ const Index: FC = () => { const handleInstallNow = (e) => { e.preventDefault(); - if (tableExist) { - setStep(7); + if (checkData.db_connection_success) { + if (checkData.db_table_exist) { + setStep(8); + } else { + setStep(4); + } } else { - setStep(4); + setStep(7); } }; const configYmlCheck = () => { checkConfigFileExists() .then((res) => { - setTableExist(res?.db_table_exist); + setCheckData({ + db_table_exist: res.data.db_table_exist, + db_connection_success: res.data.db_connection_success, + }); if (res && res.config_file_exist) { setStep(6); } @@ -283,6 +293,15 @@ const Index: FC = () => { )} {step === 7 && ( +
+
{t('db_failed')}
+

+ }} /> +

+
+ )} + + {step === 8 && (
{t('installed')}

{t('installed_description')}

From d51f589efef431ec49070eb9eba821bc3763fa9d Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 10 Nov 2022 15:28:51 +0800 Subject: [PATCH 0244/3337] fix: install process adjustment --- ui/config-overrides.js | 4 ++-- ui/src/pages/Install/index.tsx | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 0a5deeffd..eb88e5664 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -35,12 +35,12 @@ module.exports = { const config = configFunction(proxy, allowedHost); config.proxy = { "/answer": { - target: "http://10.0.10.98:2060", + target: "http://10.0.20.88:8080/", changeOrigin: true, secure: false }, "/installation": { - target: "http://10.0.10.98:2060", + target: "http://10.0.20.88:8080/", changeOrigin: true, secure: false } diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 0cfbb7ee4..d4170ca24 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -203,14 +203,10 @@ const Index: FC = () => { const handleInstallNow = (e) => { e.preventDefault(); - if (checkData.db_connection_success) { - if (checkData.db_table_exist) { - setStep(8); - } else { - setStep(4); - } + if (checkData.db_table_exist) { + setStep(8); } else { - setStep(7); + setStep(4); } }; @@ -218,11 +214,15 @@ const Index: FC = () => { checkConfigFileExists() .then((res) => { setCheckData({ - db_table_exist: res.data.db_table_exist, - db_connection_success: res.data.db_connection_success, + db_table_exist: res.db_table_exist, + db_connection_success: res.db_connection_success, }); if (res && res.config_file_exist) { - setStep(6); + if (res.db_connection_success) { + setStep(6) + } else { + setStep(7); + } } }) .finally(() => { From 3a8742f6feae79a874d0288539700a6a34e0c6fe Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 10 Nov 2022 15:30:48 +0800 Subject: [PATCH 0245/3337] fix: log format --- internal/install/install_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/install/install_controller.go b/internal/install/install_controller.go index b4449203a..93ab5c080 100644 --- a/internal/install/install_controller.go +++ b/internal/install/install_controller.go @@ -104,7 +104,7 @@ func InitEnvironment(ctx *gin.Context) { // check config file if exist if cli.CheckConfigFile(confPath) { - log.Debugf("config file already exists") + log.Debug("config file already exists") handler.HandleResponse(ctx, nil, nil) return } From f86386940610f87b4366ef9279a403a93fe595f5 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Nov 2022 16:01:44 +0800 Subject: [PATCH 0246/3337] refactor(ui): update BrandUpload component --- ui/src/components/BrandUpload/index.tsx | 35 ++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/ui/src/components/BrandUpload/index.tsx b/ui/src/components/BrandUpload/index.tsx index 477efea0e..c830ac2a9 100644 --- a/ui/src/components/BrandUpload/index.tsx +++ b/ui/src/components/BrandUpload/index.tsx @@ -2,33 +2,38 @@ import { FC } from 'react'; import { ButtonGroup, Button } from 'react-bootstrap'; import { Icon, UploadImg } from '@/components'; +import { uploadAvatar } from '@/services'; interface Props { - type: string; - imgPath: string; - uploadCallback: (data: FormData) => Promise; - deleteCallback: (type: string) => void; + type: 'logo' | 'avatar'; + value: string; + onChange: (value: string) => void; } -const Index: FC = ({ - type, - imgPath, - uploadCallback, - deleteCallback, -}) => { +const Index: FC = ({ type = 'logo', value, onChange }) => { + const onUpload = (file: any) => { + return new Promise((resolve) => { + uploadAvatar(file).then((res) => { + onChange(res); + resolve(true); + }); + }); + }; + + const onRemove = () => { + onChange(''); + }; return (
- +
- + - From 860f39664ea81139e9d8da8321a14c0303d4647c Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Nov 2022 16:02:19 +0800 Subject: [PATCH 0247/3337] feat(ui): add TimeZonePicker component --- ui/src/components/TimeZonePicker/index.tsx | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 ui/src/components/TimeZonePicker/index.tsx diff --git a/ui/src/components/TimeZonePicker/index.tsx b/ui/src/components/TimeZonePicker/index.tsx new file mode 100644 index 000000000..bd0d0f5c3 --- /dev/null +++ b/ui/src/components/TimeZonePicker/index.tsx @@ -0,0 +1,25 @@ +import { Form } from 'react-bootstrap'; + +import { TIMEZONES } from '@/common/constants'; + +const TimeZonePicker = (props) => { + return ( + + {TIMEZONES?.map((item) => { + return ( + + {item.options.map((option) => { + return ( + + ); + })} + + ); + })} + + ); +}; + +export default TimeZonePicker; From 297186d1eaf2bcb3c2e42489cfcc174630fc13bb Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Nov 2022 16:06:56 +0800 Subject: [PATCH 0248/3337] feat(ui): add timezone,upload type --- ui/src/components/SchemaForm/index.tsx | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/ui/src/components/SchemaForm/index.tsx b/ui/src/components/SchemaForm/index.tsx index 4c7a01d0c..becf5fbf5 100644 --- a/ui/src/components/SchemaForm/index.tsx +++ b/ui/src/components/SchemaForm/index.tsx @@ -2,6 +2,8 @@ import { FC } from 'react'; import { Form, Button, Stack } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import BrandUpload from '../BrandUpload'; +import TimeZonePicker from '../TimeZonePicker'; import type * as Type from '@/common/interface'; export interface JSONSchema { @@ -27,6 +29,8 @@ export interface UISchema { | 'checkbox' | 'radio' | 'select' + | 'upload' + | 'timezone' | 'switch'; 'ui:options'?: { rows?: number; @@ -49,6 +53,8 @@ export interface UISchema { empty?: string; invalid?: string; validator?: (value) => boolean; + textRender?: () => React.ReactElement; + imageType?: 'avatar' | 'logo'; }; }; } @@ -61,6 +67,14 @@ interface IProps { onSubmit: (e: React.FormEvent) => void; } +/** + * json schema form + * @param schema json schema + * @param uiSchema ui schema + * @param formData form data + * @param onChange change event + * @param onSubmit submit event + */ const SchemaForm: FC = ({ schema, uiSchema = {}, @@ -81,6 +95,7 @@ const SchemaForm: FC = ({ onChange(data); } }; + const requiredValidator = () => { const required = schema.required || []; const errors: string[] = []; @@ -91,6 +106,7 @@ const SchemaForm: FC = ({ }); return errors; }; + const syncValidator = () => { const errors: string[] = []; keys.forEach((key) => { @@ -104,6 +120,7 @@ const SchemaForm: FC = ({ }); return errors; }; + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const errors = requiredValidator(); @@ -149,6 +166,14 @@ const SchemaForm: FC = ({ } onSubmit(e); }; + + const handleUploadChange = (name: string, value: string) => { + const data = { ...formData, [name]: { ...formData[name], value } }; + if (onChange instanceof Function) { + onChange(data); + } + }; + return (
{keys.map((key) => { @@ -222,6 +247,32 @@ const SchemaForm: FC = ({ ); } + if (widget === 'timezone') { + return ( + + {title} + + {description} + + ); + } + + if (widget === 'upload') { + return ( + + {title} + handleUploadChange(key, value)} + /> + {description} + + ); + } const as = widget === 'textarea' ? 'textarea' : 'input'; return ( From 46e4bf84a079f36dd3756fe59f82d3cf35800882 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Nov 2022 16:07:56 +0800 Subject: [PATCH 0249/3337] refactor(ui): Handling details --- ui/src/pages/Admin/Branding/index.tsx | 16 +--------------- ui/src/pages/Admin/General/index.tsx | 3 +-- ui/src/pages/Admin/Smtp/index.tsx | 3 +-- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/ui/src/pages/Admin/Branding/index.tsx b/ui/src/pages/Admin/Branding/index.tsx index 621f83055..5586958c0 100644 --- a/ui/src/pages/Admin/Branding/index.tsx +++ b/ui/src/pages/Admin/Branding/index.tsx @@ -12,24 +12,10 @@ const Index: FC = () => { 'https://image-static.segmentfault.com/405/057/4050570037-636c7b0609a49', ); - const imgUpload = (file: any) => { - return new Promise((resolve) => { - console.log(file); - resolve(true); - }); - }; return (

{t('page_title')}

- { - console.log('delete'); - setImg(''); - }} - /> +
); }; diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 41ec1a14b..3906f0fe0 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -18,8 +18,7 @@ const General: FC = () => { const { data: setting } = useGeneralSetting(); const schema: JSONSchema = { - title: t('title'), - description: t('description'), + title: t('page_title'), required: ['name', 'site_url', 'contact_email'], properties: { name: { diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx index 0c3892bd6..ebfb4004e 100644 --- a/ui/src/pages/Admin/Smtp/index.tsx +++ b/ui/src/pages/Admin/Smtp/index.tsx @@ -15,8 +15,7 @@ const Smtp: FC = () => { const Toast = useToast(); const { data: setting } = useSmtpSetting(); const schema: JSONSchema = { - title: t('title'), - description: t('description'), + title: t('page_title'), properties: { from_email: { type: 'string', From 2f887039c0f9bb18d1e3fa4559d6da652ded3c0d Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Nov 2022 16:12:09 +0800 Subject: [PATCH 0250/3337] refactor(ui): Unified use of SchemaForm --- ui/src/pages/Admin/Interface/index.tsx | 147 +++++++++++++++++++------ 1 file changed, 112 insertions(+), 35 deletions(-) diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index f1b93525f..90f39baa0 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -1,5 +1,6 @@ -import { FC, FormEvent, useEffect, useState } from 'react'; -import { Form, Button, Image, Stack } from 'react-bootstrap'; +/* eslint-disable react/no-unstable-nested-components */ +import React, { FC, FormEvent, useEffect, useState } from 'react'; +import { Button } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; import { useToast } from '@/hooks'; @@ -9,10 +10,9 @@ import { AdminSettingsInterface, } from '@/common/interface'; import { interfaceStore } from '@/stores'; -import { UploadImg } from '@/components'; -import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; +import { JSONSchema, SchemaForm, UISchema } from '@/components'; +import { DEFAULT_TIMEZONE } from '@/common/constants'; import { - uploadAvatar, updateInterfaceSetting, useInterfaceSetting, useThemeOptions, @@ -33,6 +33,36 @@ const Interface: FC = () => { const [langs, setLangs] = useState(); const { data: setting } = useInterfaceSetting(); + const schema: JSONSchema = { + title: t('page_title'), + properties: { + logo: { + type: 'string', + title: t('logo.label'), + description: t('logo.text'), + }, + theme: { + type: 'string', + title: t('theme.label'), + description: t('theme.text'), + enum: themes?.map((theme) => theme.value) || [], + enumNames: themes?.map((theme) => theme.label) || [], + }, + language: { + type: 'string', + title: t('language.label'), + description: t('language.text'), + enum: langs?.map((lang) => lang.value), + enumNames: langs?.map((lang) => lang.label), + }, + time_zone: { + type: 'string', + title: t('time_zone.label'), + description: t('time_zone.text'), + }, + }, + }; + const [formData, setFormData] = useState({ logo: { value: setting?.logo || storeInterface.logo, @@ -55,6 +85,54 @@ const Interface: FC = () => { errorMsg: '', }, }); + + const onChange = (fieldName, fieldValue) => { + if (!formData[fieldName]) { + return; + } + const fieldData: FormDataType = { + [fieldName]: { + value: fieldValue, + isInvalid: false, + errorMsg: '', + }, + }; + setFormData({ ...formData, ...fieldData }); + }; + const uiSchema: UISchema = { + logo: { + 'ui:widget': 'upload', + 'ui:options': { + textRender: () => { + return ( + + You can upload your image or + + to the site title text. + + ); + }, + }, + }, + theme: { + 'ui:widget': 'select', + }, + language: { + 'ui:widget': 'select', + }, + time_zone: { + 'ui:widget': 'timezone', + }, + }; const getLangs = async () => { const res: LangsType[] = await loadLanguageOptions(true); setLangs(res); @@ -127,34 +205,22 @@ const Interface: FC = () => { setFormData({ ...formData }); }); }; - const imgUpload = (file: any) => { - return new Promise((resolve) => { - uploadAvatar(file).then((res) => { - setFormData({ - ...formData, - logo: { - value: res, - isInvalid: false, - errorMsg: '', - }, - }); - resolve(true); - }); - }); - }; - const onChange = (fieldName, fieldValue) => { - if (!formData[fieldName]) { - return; - } - const fieldData: FormDataType = { - [fieldName]: { - value: fieldValue, - isInvalid: false, - errorMsg: '', - }, - }; - setFormData({ ...formData, ...fieldData }); - }; + // const imgUpload = (file: any) => { + // return new Promise((resolve) => { + // uploadAvatar(file).then((res) => { + // setFormData({ + // ...formData, + // logo: { + // value: res, + // isInvalid: false, + // errorMsg: '', + // }, + // }); + // resolve(true); + // }); + // }); + // }; + useEffect(() => { if (setting) { const formMeta = {}; @@ -167,10 +233,21 @@ const Interface: FC = () => { useEffect(() => { getLangs(); }, []); + + const handleOnChange = (data) => { + setFormData(data); + }; return ( <>

{t('page_title')}

- + + {/* {t('logo.label')} @@ -282,7 +359,7 @@ const Interface: FC = () => { - + */} ); }; From 424f97f53909cef4c32e17b0bb432f2899dc422c Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 10 Nov 2022 16:37:03 +0800 Subject: [PATCH 0251/3337] refactor(env): migrate env config by .env file --- ui/.env | 1 - ui/.env.development | 3 +- ui/.env.production | 5 +-- ui/.env.test | 0 ui/.gitignore | 5 +-- ui/.gitlab-ci.yml | 93 ----------------------------------------- ui/config-overrides.js | 5 +-- ui/package.json | 6 +-- ui/src/utils/request.ts | 10 +---- 9 files changed, 9 insertions(+), 119 deletions(-) delete mode 100644 ui/.env delete mode 100644 ui/.env.test delete mode 100644 ui/.gitlab-ci.yml diff --git a/ui/.env b/ui/.env deleted file mode 100644 index 6893217c2..000000000 --- a/ui/.env +++ /dev/null @@ -1 +0,0 @@ -PUBLIC_URL= diff --git a/ui/.env.development b/ui/.env.development index 8648d82a1..c78ae289c 100644 --- a/ui/.env.development +++ b/ui/.env.development @@ -1 +1,2 @@ -REACT_APP_API_URL=http://10.0.10.98:2060 +PUBLIC_URL +REACT_APP_API_URL = http://127.0.0.1 diff --git a/ui/.env.production b/ui/.env.production index 940bd4a8f..e04b4d355 100644 --- a/ui/.env.production +++ b/ui/.env.production @@ -1,3 +1,2 @@ -REACT_APP_API_URL=/ -REACT_APP_PUBLIC_PATH=/ -REACT_APP_VERSION= +PUBLIC_URL = / +REACT_APP_API_URL = / diff --git a/ui/.env.test b/ui/.env.test deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/.gitignore b/ui/.gitignore index c4796f76e..fe3fdef06 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -13,10 +13,7 @@ # misc .DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local +.env*.local npm-debug.log* yarn-debug.log* diff --git a/ui/.gitlab-ci.yml b/ui/.gitlab-ci.yml deleted file mode 100644 index df03dd154..000000000 --- a/ui/.gitlab-ci.yml +++ /dev/null @@ -1,93 +0,0 @@ -include: - - project: 'segmentfault/devops/templates' - file: - - .deploy-cdn.yml - - .deploy-helm.yml - -variables: - FF_USE_FASTZIP: 'true' - PROJECT_NAME: 'answer_static' - -stages: - - install - - publish - - deploy - -# 静态资源构建 -install: - image: dockerhub.qingcloud.com/sf_base/node-build:14 - stage: install - allow_failure: false - - cache: - - key: - files: - - pnpm-lock.yml - paths: - - node_modules/ - policy: pull-push - script: - - pnpm install - - if [ "$CI_COMMIT_BRANCH" = "dev" ]; then - sed -i "s//$PROJECT_NAME/g" .env.development; - sed -i "s//$CI_COMMIT_SHORT_SHA/g" .env.development; - pnpm run build:dev; - elif [ "$CI_COMMIT_BRANCH" = "main" ]; then - sed -i "s//$PROJECT_NAME/g" .env.test; - sed -i "s//$CI_COMMIT_SHORT_SHA/g" .env.test; - pnpm run build:test; - elif [ "$CI_COMMIT_BRANCH" = "release" ]; then - sed -i "s//$PROJECT_NAME/g" .env.production; - sed -i "s//$CI_COMMIT_SHORT_SHA/g" .env.production; - pnpm run build:prod; - fi - artifacts: - paths: - - build/ - -publish:cdn:dev: - extends: .deploy-cdn - stage: publish - only: - - dev - variables: - AssetsPath: ./build - Project: $PROJECT_NAME - Version: $CI_COMMIT_SHORT_SHA - Destination: dev - -publish:cdn:test: - extends: .deploy-cdn - stage: publish - only: - - main - variables: - AssetsPath: ./build - Project: $PROJECT_NAME - Version: $CI_COMMIT_SHORT_SHA - Destination: test - -publish:cdn:prod: - extends: .deploy-cdn - stage: publish - only: - - release - variables: - AssetsPath: ./build - Project: $PROJECT_NAME - Version: $CI_COMMIT_SHORT_SHA - Destination: prod - -deploy:dev: - extends: .deploy-helm - stage: deploy - only: - - dev - needs: - - publish:cdn:dev - variables: - KubernetesCluster: dev - KubernetesNamespace: 'sf-test' - DockerTag: $CI_COMMIT_SHORT_SHA - ChartName: answer-web - InstallPolicy: replace diff --git a/ui/config-overrides.js b/ui/config-overrides.js index e8d2dceb8..f1bec311e 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -4,7 +4,6 @@ const i18nLocaleTool = require('./scripts/i18n-locale-tool'); module.exports = { webpack: function (config, env) { if (env === 'production') { - config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH; i18nLocaleTool.resolvePresetLocales(); } @@ -33,12 +32,12 @@ module.exports = { const config = configFunction(proxy, allowedHost); config.proxy = { '/answer': { - target: 'http://10.0.10.98:2060', + target: process.env.REACT_APP_API_URL, changeOrigin: true, secure: false, }, '/installation': { - target: 'http://10.0.10.98:2060', + target: process.env.REACT_APP_API_URL, changeOrigin: true, secure: false, }, diff --git a/ui/package.json b/ui/package.json index fd2a34709..97759d932 100644 --- a/ui/package.json +++ b/ui/package.json @@ -5,11 +5,7 @@ "homepage": "/", "scripts": { "start": "react-app-rewired start", - "build:dev": "env-cmd -f .env.development react-app-rewired build", - "build:test": "env-cmd -f .env.test react-app-rewired build", - "build:prod": "env-cmd -f .env.production react-app-rewired build", - "build": "env-cmd -f .env react-app-rewired build", - "test": "react-app-rewired test", + "build": "react-app-rewired build", "lint": "eslint . --cache --fix --ext .ts,.tsx", "prepare": "cd .. && husky install", "cz": "cz", diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index 7859b90d6..c58af99c7 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -10,16 +10,8 @@ import { getCurrentLang } from '@/utils/localize'; import Storage from './storage'; import { floppyNavigation } from './floppyNavigation'; -const API = { - development: '', - production: '', - test: '', -}; - -const baseApiUrl = process.env.REACT_APP_API_URL || API[process.env.NODE_ENV]; - const baseConfig = { - baseUrl: baseApiUrl, + baseUrl: process.env.REACT_APP_API_URL || '', timeout: 10000, withCredentials: true, }; From f022c97ab6ddccdd775dbc2b9848c1ec98257b66 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 10 Nov 2022 16:38:22 +0800 Subject: [PATCH 0252/3337] fix: abstract escape character handling --- ui/config-overrides.js | 4 ++-- ui/src/pages/Admin/Answers/index.tsx | 11 +++++------ ui/src/pages/Admin/Flags/index.tsx | 3 ++- ui/src/pages/Search/components/Head/index.tsx | 3 ++- .../Search/components/SearchItem/index.tsx | 3 ++- ui/src/pages/Tags/Detail/index.tsx | 3 ++- ui/src/utils/common.ts | 17 +++++++++++++++++ 7 files changed, 32 insertions(+), 12 deletions(-) diff --git a/ui/config-overrides.js b/ui/config-overrides.js index eb88e5664..0a5deeffd 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -35,12 +35,12 @@ module.exports = { const config = configFunction(proxy, allowedHost); config.proxy = { "/answer": { - target: "http://10.0.20.88:8080/", + target: "http://10.0.10.98:2060", changeOrigin: true, secure: false }, "/installation": { - target: "http://10.0.20.88:8080/", + target: "http://10.0.10.98:2060", changeOrigin: true, secure: false } diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index cc584c313..3221f60f3 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -16,6 +16,7 @@ import { ADMIN_LIST_STATUS } from '@/common/constants'; import { useEditStatusModal } from '@/hooks'; import * as Type from '@/common/interface'; import { useAnswerSearch, changeAnswerStatus } from '@/services'; +import { escapeRemove } from '@/utils'; import '../index.scss'; @@ -140,12 +141,10 @@ const Answers: FC = () => { )}
+ className="text-truncate-2 fs-14" + style={{ maxWidth: '30rem' }}> + {escapeRemove(li.description)} +
diff --git a/ui/src/pages/Admin/Flags/index.tsx b/ui/src/pages/Admin/Flags/index.tsx index 353bd382f..ff85ea1e9 100644 --- a/ui/src/pages/Admin/Flags/index.tsx +++ b/ui/src/pages/Admin/Flags/index.tsx @@ -13,6 +13,7 @@ import { import { useReportModal } from '@/hooks'; import * as Type from '@/common/interface'; import { useFlagSearch } from '@/services'; +import { escapeRemove } from '@/utils'; import '../index.scss'; @@ -107,7 +108,7 @@ const Flags: FC = () => { {li.title} - {li.excerpt} + {escapeRemove(li.excerpt)} diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index 095796d21..f8d07e7ae 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { following } from '@/services'; import { tryNormalLogged } from '@/utils/guard'; +import { escapeRemove } from '@/utils'; interface Props { data; @@ -51,7 +52,7 @@ const Index: FC = ({ data }) => { <> {data.excerpt && (

- {data.excerpt} + {escapeRemove(data.excerpt)} [{t('more')}]

)} diff --git a/ui/src/pages/Search/components/SearchItem/index.tsx b/ui/src/pages/Search/components/SearchItem/index.tsx index 09cecd32e..ca4df6e65 100644 --- a/ui/src/pages/Search/components/SearchItem/index.tsx +++ b/ui/src/pages/Search/components/SearchItem/index.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { Icon, Tag, FormatTime, BaseUserCard } from '@/components'; import type { SearchResItem } from '@/common/interface'; +import { escapeRemove } from '@/utils'; interface Props { data: SearchResItem; @@ -61,7 +62,7 @@ const Index: FC = ({ data }) => { {data.object?.excerpt && (

- {data.object.excerpt} + {escapeRemove(data.object.excerpt)}

)} diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index 452ef37d6..26252e1e4 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -8,6 +8,7 @@ import { PageTitle, FollowingTags } from '@/components'; import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; +import { escapeRemove } from '@/utils'; const Questions: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'tags' }); @@ -69,7 +70,7 @@ const Questions: FC = () => {

- {tagInfo.excerpt || t('no_description')} + {escapeRemove(tagInfo.excerpt) || t('no_description')} [{t('more')}]

diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index 31bb0a40d..2f846382f 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -86,6 +86,22 @@ function formatUptime(value) { return `< 1 ${t('dates.hour')}`; } +function escapeRemove(str) { + if (!str || typeof str !== 'string') return str; + const arrEntities = { + lt: '<', + gt: '>', + nbsp: ' ', + amp: '&', + quot: '"', + '#39': "'", + }; + + return str.replace(/&(lt|gt|nbsp|amp|quot|#39);/gi, function (all, t) { + return arrEntities[t]; + }); +} + export { getQueryString, thousandthDivision, @@ -94,4 +110,5 @@ export { matchedUsers, parseUserInfo, formatUptime, + escapeRemove, }; From cbe0bdb17a31ada0ad7209c7729159794e194964 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 10 Nov 2022 16:59:28 +0800 Subject: [PATCH 0253/3337] fix: install page add form error handle --- .../pages/Install/components/FourthStep/index.tsx | 2 +- ui/src/pages/Install/index.tsx | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ui/src/pages/Install/components/FourthStep/index.tsx b/ui/src/pages/Install/components/FourthStep/index.tsx index 3807ac690..412b19121 100644 --- a/ui/src/pages/Install/components/FourthStep/index.tsx +++ b/ui/src/pages/Install/components/FourthStep/index.tsx @@ -101,7 +101,7 @@ const Index: FC = ({ visible, data, changeCallback, nextCallback }) => { if (admin_email.value && !admin_email.value.match(mailReg)) { bol = false; data.admin_email = { - value: '', + value: admin_email.value, isInvalid: true, errorMsg: t('admin_email.msg.incorrect'), }; diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index d4170ca24..8e9e22a3a 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -76,7 +76,7 @@ const Index: FC = () => { errorMsg: '', }, site_url: { - value: '', + value: window.location.origin, isInvalid: false, errorMsg: '', }, @@ -104,6 +104,9 @@ const Index: FC = () => { const handleChange = (params: FormDataType) => { // console.log(params); + setErrorData({ + msg: '', + }); setFormData({ ...formData, ...params }); }; @@ -150,11 +153,9 @@ const Index: FC = () => { }; dbCheck(params) .then(() => { - // handleNext(); checkInstall(); }) .catch((err) => { - console.log(err); handleErr(err); }); }; @@ -174,7 +175,13 @@ const Index: FC = () => { handleNext(); }) .catch((err) => { - handleErr(err); + if (err.isError && err.key) { + formData[err.key].isInvalid = true; + formData[err.key].errorMsg = err.value; + setFormData({ ...formData }); + } else { + handleErr(err); + } }); }; From 6ce4fc8cb6cc11a0dda6eca9d1fa072ca6fe0f65 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 10 Nov 2022 17:02:52 +0800 Subject: [PATCH 0254/3337] fix: style adjustment --- ui/src/pages/Search/components/Head/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index f8d07e7ae..45906e05c 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -41,7 +41,7 @@ const Index: FC = ({ data }) => {
{options?.length && ( <> - {t('options')} + {t('options')} {options?.map((item) => { return {item} ; })} From 3fb4167870e046ca96216580380f6a5b1213e430 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Nov 2022 17:55:20 +0800 Subject: [PATCH 0255/3337] docs(ui): Add SchemaForm doc --- ui/src/components/SchemaForm/README.md | 73 ++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 ui/src/components/SchemaForm/README.md diff --git a/ui/src/components/SchemaForm/README.md b/ui/src/components/SchemaForm/README.md new file mode 100644 index 000000000..df5a3ceba --- /dev/null +++ b/ui/src/components/SchemaForm/README.md @@ -0,0 +1,73 @@ +# SchemaForm User Guide + +## Introduction + +SchemaForm is a component that can be used to render a form based on a JSON schema. + +## Usage + +### Basic Usage + +```jsx +import React from 'react'; +import { SchemaForm, initFormData, JSONSchema, UISchema } from '@/components'; + +const schema: JSONSchema = { + type: 'object', + properties: { + name: { + type: 'string', + title: 'Name', + }, + age: { + type: 'number', + title: 'Age', + }, + sex:{ + type: 'boolean', + title: 'sex', + enum: [1, 2] + enumNames: ['male', 'female'], + } + }, +}; + +const uiSchema: UISchema = { + name: { + 'ui:widget': 'input', + }, + age: { + 'ui:widget': 'input', + 'ui:options': { + type: 'number' + } + }, + sex: { + 'ui:widget': 'radio', + } +}; + +// form component + +const Form = () => { + const [formData, setFormData] = useState(initFormData(schema)); + return ( + + ); +}; +``` + +## Props + +| Property | Description | Type | Default | +| -------- | ---------------------------------------- | ---------------------------- | ------- | +| schema | JSON schema | [JSONSchema]() | - | +| uiSchema | UI schema | [UISchema]() | - | +| formData | Form data | [FormData]() | - | +| onChange | Callback function when form data changes | (data: [FormData]()) => void | - | +| onSubmit | Callback function when form is submitted | (data: [FormData]()) => void | - | From b0adf81b8020be74e4bb32f86dcdb05bfa330f80 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 10 Nov 2022 18:06:18 +0800 Subject: [PATCH 0256/3337] feat(homepage): add a loading spinner for app --- ui/public/index.html | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/ui/public/index.html b/ui/public/index.html index efa315fc5..97b4bcb29 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -24,7 +24,34 @@ -
+
+ +
+
+
+
diff --git a/ui/src/assets/images/favicon.ico b/ui/src/assets/images/favicon.ico deleted file mode 100644 index 0013c7e1b42ec9209e21551fbe205602fbfca0ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16958 zcmeHO>5~*S6d#Hz{15!99}#av1Vmf`6$AxE1Vy1JP*J>4E;+2FKrBlw5Lpfp6%P;! z2X?w$LYUoUAgGkv7z*d4&Pl})CTyu4pJd3ni8+iacK zUsaWj{lC)IwZF}Fxy@$l$^v_2t7ZYu6{>2BjQ$f*Hx?GLP|HGyg$td40a$>kn;v$S z*M)@|7Sb$StORVp*hTCI{AOVf3zrH3tJ()FjXt^5s0L$zfyR8ln8&={dj)2$>-5SP zK2HY&6y5y|`Ij!DrUkE1)BL&A^5$z4cyBp{zt}>t-+rZZz;DPyFEicJLb1cYPUXj@14}L7%nZ&6RhHavK57S%FX)=_H!B;(- zGO?IURf^UAWhzU`RWHR$wh-1Iv5#}pezx9OArZgxo&l6@ZI!MrgFm?TBWZpz1sf-h z)y3}`HJmcZWFDJLB2Hx$Kl(ShA00_`J+4;kxW|poDkI|euU?sz*R*h+T91#jq|2$6k-^`P)1F$n zz=U&P-#nQho1NJKnN&*UnqoGa{w!e)=Rq7X&wcri94eU0Liz{hzDCJfA73L!2UD*)pluC<-qT}yI?@Z5pN{t`qB{)-QFTx(vJ@P)yK2Yby&DT)fE!QcbVZvisqKWt&Y&_Ku z8>EIh)MI@q2ItU$YQA&u9a(wqF%K$dNLdZE!w={K-^>@YWD#W+)46tnh+iycHQj{0 zL!x~1U8WlY?=Ca#gHq$Sv%N)-odZ+~MRt5a_FMGNLQ9F?(dR~re!II;psRAkemO{v z+k0w`g;L>n4(LPizy4I)th7Y@$RF%X`lh@-a6SA0B^`D(XxWHnb02GL;^grTgAAP? zW;R`D-NzK(zLipL?K7s>p`QuokY;$Sb z;J!K2HEnJ9eyrd#E2}l%;aBobdp@rn=tk`S;9Dt{;Dd4vyF0H9QQn5}j}QrGAiPcA ztf_|eL>@+I_;+#%XQi-pjq1TZDRv(OUz<@IJWp#a%7j8x&+HanrjXb*VyNc1uz$rn zQhZw$zmeN@(!X(zhp{9hw|mS8O}=qbemz~+4$$!siu`qqs z#Lnb7ADKAS*r3TXym_M~KD;gXPqYYjJ})B^x4iY578}!NE@qR<@aJWs%-5qwVdm43 z9S2xHBRhBKu_EYr-lj~9{XroFJ4=Q?FB9Ff)36!LCo>;f|H`EVn+Rv6&O2`-&vTOqGRqL|M`1kSj1f8*dIsUA3XJB!gI_K z**1;jf^Dle-yj?Ca*F?bg!Fu+kO_LBR4OD@gkDv*(a^lpF2~m-r;GZb(>=rW2{*s z;cmN_ZlOmzLy5d6A&c%rJ}jtq1mt)jK?m?+~=^1&A{9G}f=Y_3y+n zY5cGQDIV`|%(()OVRyN)U&Y=GXQKqXDbGCzV8=ILB^^{c zT~ld~fm!(;mc=!dCR{tnqkR8(?i@H1?a}o ztc{FO2NGZ76kR7`MLO2@;EJLfte{5# diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 8f5a9d5d1..822673d3d 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -24,6 +24,8 @@ export interface ReportParams { export interface TagBase { display_name: string; slug_name: string; + recommend: boolean; + reserved: boolean; } export interface Tag extends TagBase { @@ -303,8 +305,9 @@ export interface AdminSettingsLegal { } export interface AdminSettingsWrite { - recommend_tags: string; + recommend_tags: string[]; required_tag: string; + reserved_tags: string[]; } /** diff --git a/ui/src/components/SchemaForm/index.tsx b/ui/src/components/SchemaForm/index.tsx index dfbcc167f..833e07b6b 100644 --- a/ui/src/components/SchemaForm/index.tsx +++ b/ui/src/components/SchemaForm/index.tsx @@ -90,7 +90,21 @@ const SchemaForm: FC = ({ const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - const data = { ...formData, [name]: { ...formData[name], value } }; + const data = { + ...formData, + [name]: { ...formData[name], value, isInvalid: false }, + }; + if (onChange instanceof Function) { + onChange(data); + } + }; + + const handleSwitchChange = (e: React.ChangeEvent) => { + const { name, checked } = e.target; + const data = { + ...formData, + [name]: { ...formData[name], value: checked, isInvalid: false }, + }; if (onChange instanceof Function) { onChange(data); } @@ -155,6 +169,7 @@ const SchemaForm: FC = ({ const errors = requiredValidator(); if (errors.length > 0) { formData = errors.reduce((acc, cur) => { + console.log('schema.properties[cur]', cur); acc[cur] = { ...formData[cur], isInvalid: true, @@ -262,17 +277,21 @@ const SchemaForm: FC = ({ } if (widget === 'switch') { + console.log(formData[key]?.value, 'switch====='); return ( {title} {formData[key]?.errorMsg} diff --git a/ui/src/components/Tag/index.tsx b/ui/src/components/Tag/index.tsx index 729c75b3c..ac7910ff4 100644 --- a/ui/src/components/Tag/index.tsx +++ b/ui/src/components/Tag/index.tsx @@ -14,7 +14,14 @@ const Index: FC = ({ className = '', href, data }) => { href = href || `/tags/${data.main_tag_slug_name || data.slug_name}`.toLowerCase(); return ( - + {data.slug_name} ); diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index 8747f65ab..ab028dac4 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ import { FC, useState, useEffect } from 'react'; import { Dropdown, FormControl, Button, Form } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; @@ -95,17 +96,16 @@ const TagSelector: FC = ({ } }, [value]); - useEffect(() => { - if (!tag) { - setTags(null); - return; - } - - queryTags(tag).then((res) => { + const fetchTags = (str) => { + queryTags(str).then((res) => { const tagArray: Type.Tag[] = filterTags(res || []); setTags(tagArray); }); - }, [tag]); + }; + + useEffect(() => { + fetchTags(tag); + }, [visibleMenu]); const handleClick = (val: Type.Tag) => { const findIndex = initialValue.findIndex( @@ -143,7 +143,9 @@ const TagSelector: FC = ({ }; const handleSearch = async (e: React.ChangeEvent) => { - setTag(e.currentTarget.value.replace(';', '')); + const searchStr = e.currentTarget.value.replace(';', ''); + setTag(searchStr); + fetchTags(searchStr); }; const handleSelect = (eventKey) => { @@ -186,7 +188,9 @@ const TagSelector: FC = ({ 'm-1 text-nowrap d-flex align-items-center', index === repeatIndex && 'warning', )} - variant="outline-secondary" + variant={`outline-${ + item.reserved ? 'danger' : item.recommend ? 'dark' : 'secondary' + }`} size="sm"> {item.slug_name} handleRemove(item)}> @@ -220,6 +224,14 @@ const TagSelector: FC = ({ )} + {tags && tags.filter((v) => v.recommend)?.length > 0 && ( + + Required tag (at least one) + + )} {tags?.map((item, index) => { return ( diff --git a/ui/src/index.scss b/ui/src/index.scss index 69fcb80cd..7cfa95b8a 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -69,20 +69,32 @@ a { padding: 1px 0.5rem 2px; color: $blue-700; height: 24px; + border: 1px solid rgba($blue-100, 0.5); &:hover { background: rgba($blue-100, 1); } } .badge-tag-required { - background: rgba($gray-400, 0.5); + background: rgba($gray-200, 0.5); color: $gray-700; + border: 1px solid $gray-400; &:hover { color: $gray-700; background: rgba($gray-400, 1); } } +.badge-tag-reserved { + background: rgba($orange-100, 0.5); + color: $orange-700; + border: 1px solid $orange-400; + &:hover { + color: $orange-700; + background: rgba($orange-400, 1); + } +} + .divide-line { border-bottom: 1px solid rgba(33, 37, 41, 0.25); } diff --git a/ui/src/pages/Admin/Write/index.tsx b/ui/src/pages/Admin/Write/index.tsx index 3ee344691..12cf4e980 100644 --- a/ui/src/pages/Admin/Write/index.tsx +++ b/ui/src/pages/Admin/Write/index.tsx @@ -3,9 +3,11 @@ import { useTranslation } from 'react-i18next'; import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components'; import type * as Type from '@/common/interface'; -// import { useToast } from '@/hooks'; -// import { siteInfoStore } from '@/stores'; -import { useGeneralSetting } from '@/services'; +import { useToast } from '@/hooks'; +import { + getRequireAndReservedTag, + postRequireAndReservedTag, +} from '@/services'; import '../index.scss'; @@ -13,13 +15,11 @@ const Legal: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.write', }); - // const Toast = useToast(); + const Toast = useToast(); // const updateSiteInfo = siteInfoStore((state) => state.update); - const { data: setting } = useGeneralSetting(); const schema: JSONSchema = { title: t('page_title'), - required: ['terms_of_service', 'privacy_policy'], properties: { recommend_tags: { type: 'string', @@ -62,38 +62,40 @@ const Legal: FC = () => { evt.stopPropagation(); const reqParams: Type.AdminSettingsWrite = { - recommend_tags: formData.recommend_tags.value, + recommend_tags: formData.recommend_tags.value.trim().split('\n'), required_tag: formData.required_tag.value, + reserved_tags: formData.reserved_tags.value.trim().split('\n'), }; console.log(reqParams); - // updateGeneralSetting(reqParams) - // .then(() => { - // Toast.onShow({ - // msg: t('update', { keyPrefix: 'toast' }), - // variant: 'success', - // }); - // updateSiteInfo(reqParams); - // }) - // .catch((err) => { - // if (err.isError && err.key) { - // formData[err.key].isInvalid = true; - // formData[err.key].errorMsg = err.value; - // } - // setFormData({ ...formData }); - // }); + postRequireAndReservedTag(reqParams) + .then(() => { + Toast.onShow({ + msg: t('update', { keyPrefix: 'toast' }), + variant: 'success', + }); + }) + .catch((err) => { + if (err.isError && err.key) { + formData[err.key].isInvalid = true; + formData[err.key].errorMsg = err.value; + } + setFormData({ ...formData }); + }); }; - useEffect(() => { - if (!setting) { - return; - } - const formMeta = {}; - Object.keys(setting).forEach((k) => { - formMeta[k] = { ...formData[k], value: setting[k] }; + const initData = () => { + getRequireAndReservedTag().then((res) => { + formData.recommend_tags.value = res.recommend_tags.join('\n'); + formData.required_tag.value = res.required_tag; + formData.reserved_tags.value = res.reserved_tags.join('\n'); + setFormData({ ...formData }); }); - setFormData({ ...formData, ...formMeta }); - }, [setting]); + }; + + useEffect(() => { + initData(); + }, []); const handleOnChange = (data) => { setFormData(data); diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 7539c2fa2..94478af9b 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -4,12 +4,13 @@ import { Helmet, HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { siteInfoStore, toastStore } from '@/stores'; +import { siteInfoStore, toastStore, brandingStore } from '@/stores'; import { Header, Footer, Toast } from '@/components'; const Layout: FC = () => { const { msg: toastMsg, variant, clear: toastClear } = toastStore(); const { siteInfo } = siteInfoStore.getState(); + const { favicon } = brandingStore((state) => state.branding); const closeToast = () => { toastClear(); }; @@ -17,6 +18,7 @@ const Layout: FC = () => { return ( + {siteInfo && } { slug_name: tagName || '', main_tag_slug_name: '', display_name: '', + recommend: false, + reserved: false, }} /> diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index b901a365c..f0fe7399f 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -96,3 +96,11 @@ export const getBrandSetting = () => { export const brandSetting = (params: Type.AdmingSettingBranding) => { return request.put('/answer/admin/api/siteinfo/branding', params); }; + +export const getRequireAndReservedTag = () => { + return request.get('/answer/admin/api/siteinfo/write'); +}; + +export const postRequireAndReservedTag = (params) => { + return request.put('/answer/admin/api/siteinfo/write', params); +}; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index e553b8036..7d880bbfa 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -49,7 +49,7 @@ class Request { }, (error) => { const { status, data: respData, msg: respMsg } = error.response || {}; - const { data, msg = '' } = respData; + const { data = {}, msg = '' } = respData || {}; if (status === 400) { // show error message if (data instanceof Object && data.err_type) { From acc6ce1b3df2632e4469f56e45d9451ce08ebf22 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 16 Nov 2022 12:07:05 +0800 Subject: [PATCH 0339/3337] fix: required tags and reserved tags --- i18n/en_US.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 7fff9ebe8..171a63ca0 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1131,6 +1131,20 @@ ui: "no": "No" branding: page_title: Branding + logo: + label: Logo + msg: Logo cannot be empty. + text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown. + mobile_logo: + label: Mobile Logo (optional) + text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the “logo” setting will be used. + square_icon: + label: Square Icon + msg: Square icon cannot be empty. + text: Image used as the base for metadata icons. Should ideally be larger than 512x512. + favicon: + label: Favicon (optional) + text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, “square icon” will be used. legal: page_title: Legal terms_of_service: From 59daaa5c5db7c9cf556e7e3551e63b47cd6c2359 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 16 Nov 2022 14:39:58 +0800 Subject: [PATCH 0340/3337] fix: remove site write tag val --- internal/schema/siteinfo_schema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 64325343d..af93de624 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -40,8 +40,8 @@ type SiteBrandingReq struct { // SiteWriteReq site write request type SiteWriteReq struct { RequiredTag bool `validate:"required" form:"required_tag" json:"required_tag"` - RecommendTags []string `validate:"omitempty,dive,gt=0,lte=65536" form:"recommend_tags" json:"recommend_tags"` - ReservedTags []string `validate:"omitempty,dive,gt=0,lte=65536" form:"reserved_tags" json:"reserved_tags"` + RecommendTags []string `validate:"omitempty" form:"recommend_tags" json:"recommend_tags"` + ReservedTags []string `validate:"omitempty" form:"reserved_tags" json:"reserved_tags"` UserID string `json:"-"` } From a7d486bf554cc231935bf9fdf6e2968e1dbe6dae Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 16 Nov 2022 14:48:08 +0800 Subject: [PATCH 0341/3337] fix: tags color --- ui/src/index.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/index.scss b/ui/src/index.scss index 7cfa95b8a..f94104166 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -81,7 +81,7 @@ a { border: 1px solid $gray-400; &:hover { color: $gray-700; - background: rgba($gray-400, 1); + background: rgba($gray-200, 1); } } @@ -91,7 +91,7 @@ a { border: 1px solid $orange-400; &:hover { color: $orange-700; - background: rgba($orange-400, 1); + background: rgba($orange-100, 1); } } From 5dd8fa4e7fdeb9ee2734862879b0ed55f82345c1 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Wed, 16 Nov 2022 15:02:11 +0800 Subject: [PATCH 0342/3337] fix: remove site write required tag val --- internal/schema/siteinfo_schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index af93de624..2bcd5c436 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -39,7 +39,7 @@ type SiteBrandingReq struct { // SiteWriteReq site write request type SiteWriteReq struct { - RequiredTag bool `validate:"required" form:"required_tag" json:"required_tag"` + 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:"-"` From 0863eec2a0381e70946aa1300a6c62afab59b427 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 16 Nov 2022 15:28:23 +0800 Subject: [PATCH 0343/3337] update tag show --- docs/docs.go | 27 +++++++++++++++++++++++ docs/swagger.json | 27 +++++++++++++++++++++++ docs/swagger.yaml | 18 +++++++++++++++ internal/repo/tag/tag_repo.go | 2 +- internal/schema/tag_schema.go | 8 +++++++ internal/service/revision_service.go | 2 ++ internal/service/search/tag.go | 2 ++ internal/service/tag/tag_service.go | 8 +++++++ internal/service/tag_common/tag_common.go | 9 ++++++++ 9 files changed, 102 insertions(+), 1 deletion(-) diff --git a/docs/docs.go b/docs/docs.go index aee0a2677..9c9785078 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4776,6 +4776,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" @@ -5054,6 +5060,12 @@ const docTemplate = `{ "description": "question amount", "type": "integer" }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, "slug_name": { "description": "slug_name", "type": "string" @@ -5114,6 +5126,12 @@ const docTemplate = `{ "description": "question amount", "type": "integer" }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, "slug_name": { "description": "slug name", "type": "string" @@ -5139,6 +5157,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" @@ -5953,6 +5977,9 @@ const docTemplate = `{ "recommend": { "type": "boolean" }, + "reserved": { + "type": "boolean" + }, "slug_name": { "type": "string" } diff --git a/docs/swagger.json b/docs/swagger.json index 4c3c2c3ae..78aa5450b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4764,6 +4764,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" @@ -5042,6 +5048,12 @@ "description": "question amount", "type": "integer" }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, "slug_name": { "description": "slug_name", "type": "string" @@ -5102,6 +5114,12 @@ "description": "question amount", "type": "integer" }, + "recommend": { + "type": "boolean" + }, + "reserved": { + "type": "boolean" + }, "slug_name": { "description": "slug name", "type": "string" @@ -5127,6 +5145,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" @@ -5941,6 +5965,9 @@ "recommend": { "type": "boolean" }, + "reserved": { + "type": "boolean" + }, "slug_name": { "type": "string" } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a4fca4e2f..932b05060 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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 @@ -570,6 +574,10 @@ definitions: question_count: description: question amount type: integer + recommend: + type: boolean + reserved: + type: boolean slug_name: description: slug_name type: string @@ -615,6 +623,10 @@ definitions: question_count: description: question amount type: integer + recommend: + type: boolean + reserved: + type: boolean slug_name: description: slug name type: string @@ -634,6 +646,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 @@ -1223,6 +1239,8 @@ definitions: type: string recommend: type: boolean + reserved: + type: boolean slug_name: type: string type: object diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 95f4dde1c..76ab6cbfa 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -128,7 +128,7 @@ func (tr *tagRepo) GetReservedTagList(ctx context.Context) (tagList []*entity.Ta // 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 := 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 { diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go index f22b89b33..0ab27033f 100644 --- a/internal/schema/tag_schema.go +++ b/internal/schema/tag_schema.go @@ -61,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() { @@ -96,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() { @@ -190,6 +194,8 @@ type GetTagSynonymsResp 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"` } // UpdateTagSynonymReq update tag request @@ -218,6 +224,8 @@ 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 { diff --git a/internal/service/revision_service.go b/internal/service/revision_service.go index bb5443a31..fe9da6479 100644 --- a/internal/service/revision_service.go +++ b/internal/service/revision_service.go @@ -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 diff --git a/internal/service/search/tag.go b/internal/service/search/tag.go index 73bd3a719..6fa59936d 100644 --- a/internal/service/search/tag.go +++ b/internal/service/search/tag.go @@ -82,6 +82,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() diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index aa012be35..641717c23 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -157,6 +157,8 @@ 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.GetExcerpt() @@ -183,6 +185,8 @@ 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)) @@ -239,6 +243,8 @@ func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSyno SlugName: t.SlugName, DisplayName: t.DisplayName, MainTagSlugName: mainTagSlugName, + Recommend: t.Recommend, + Reserved: t.Reserved, }) } return @@ -364,6 +370,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 diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index fb28d744f..0a55b857c 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "sort" "strings" "github.com/answerdev/answer/internal/base/reason" @@ -221,6 +222,14 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s 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 } From dc1a672a334f26f1da5ed2e850b5487513847b41 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 16 Nov 2022 15:31:24 +0800 Subject: [PATCH 0344/3337] swagger --- docs/docs.go | 38 +++++++++++++++++++++++++++++++------- docs/swagger.json | 38 +++++++++++++++++++++++++++++++------- docs/swagger.yaml | 26 +++++++++++++++++++++----- 3 files changed, 83 insertions(+), 19 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 4403f136f..ff29782af 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3028,6 +3028,19 @@ const docTemplate = `{ "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", @@ -3040,7 +3053,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.SiteGeneralResp" + "$ref": "#/definitions/schema.GetSiteLegalInfoResp" } } } @@ -5094,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": { @@ -6000,9 +6030,6 @@ const docTemplate = `{ }, "schema.SiteWriteReq": { "type": "object", - "required": [ - "required_tag" - ], "properties": { "recommend_tags": { "type": "array", @@ -6023,9 +6050,6 @@ const docTemplate = `{ }, "schema.SiteWriteResp": { "type": "object", - "required": [ - "required_tag" - ], "properties": { "recommend_tags": { "type": "array", diff --git a/docs/swagger.json b/docs/swagger.json index 02db376c8..d3eb7b903 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3016,6 +3016,19 @@ "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", @@ -3028,7 +3041,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/schema.SiteGeneralResp" + "$ref": "#/definitions/schema.GetSiteLegalInfoResp" } } } @@ -5082,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": { @@ -5988,9 +6018,6 @@ }, "schema.SiteWriteReq": { "type": "object", - "required": [ - "required_tag" - ], "properties": { "recommend_tags": { "type": "array", @@ -6011,9 +6038,6 @@ }, "schema.SiteWriteResp": { "type": "object", - "required": [ - "required_tag" - ], "properties": { "recommend_tags": { "type": "array", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a01dfe2bd..36f8ef692 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -548,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: @@ -1216,8 +1227,6 @@ definitions: items: type: string type: array - required: - - required_tag type: object schema.SiteWriteResp: properties: @@ -1231,8 +1240,6 @@ definitions: items: type: string type: array - required: - - required_tag type: object schema.TagItem: properties: @@ -3408,6 +3415,15 @@ paths: /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: @@ -3418,7 +3434,7 @@ paths: - $ref: '#/definitions/handler.RespBody' - properties: data: - $ref: '#/definitions/schema.SiteGeneralResp' + $ref: '#/definitions/schema.GetSiteLegalInfoResp' type: object summary: get site legal info tags: From 19075b869e9dfb1a4141dbb59e5f421e79cbcd92 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 16 Nov 2022 15:35:43 +0800 Subject: [PATCH 0345/3337] fix ExistRecommend --- internal/service/tag_common/tag_common.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index 0a55b857c..952589877 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -143,6 +143,13 @@ func (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []st } 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) From 82130b70b15288d8acd3c03fba6e058bd7c45b3c Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Wed, 16 Nov 2022 15:35:59 +0800 Subject: [PATCH 0346/3337] swagger --- docs/docs.go | 6 ------ docs/swagger.json | 6 ------ docs/swagger.yaml | 4 ---- 3 files changed, 16 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 9c9785078..cf9dd7878 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5897,9 +5897,6 @@ const docTemplate = `{ }, "schema.SiteWriteReq": { "type": "object", - "required": [ - "required_tag" - ], "properties": { "recommend_tags": { "type": "array", @@ -5920,9 +5917,6 @@ const docTemplate = `{ }, "schema.SiteWriteResp": { "type": "object", - "required": [ - "required_tag" - ], "properties": { "recommend_tags": { "type": "array", diff --git a/docs/swagger.json b/docs/swagger.json index 78aa5450b..a0e0547e0 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5885,9 +5885,6 @@ }, "schema.SiteWriteReq": { "type": "object", - "required": [ - "required_tag" - ], "properties": { "recommend_tags": { "type": "array", @@ -5908,9 +5905,6 @@ }, "schema.SiteWriteResp": { "type": "object", - "required": [ - "required_tag" - ], "properties": { "recommend_tags": { "type": "array", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 932b05060..1fe5445aa 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1194,8 +1194,6 @@ definitions: items: type: string type: array - required: - - required_tag type: object schema.SiteWriteResp: properties: @@ -1209,8 +1207,6 @@ definitions: items: type: string type: array - required: - - required_tag type: object schema.TagItem: properties: From 13dccb4e2942feb68fa51589bd89dc61d5759a44 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 16 Nov 2022 15:42:28 +0800 Subject: [PATCH 0347/3337] fix: mobile logo --- ui/src/components/Header/index.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index d4af750cd..f56ad023d 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -74,11 +74,19 @@ const Header: FC = () => {
{brandingInfo.logo ? ( - + <> + + + + ) : ( {siteInfo.name || 'Answer'} )} From 19a0ba04470155041e0bf19675f1b504e7a71b67 Mon Sep 17 00:00:00 2001 From: shuai Date: Wed, 16 Nov 2022 15:49:41 +0800 Subject: [PATCH 0348/3337] fix: mobile logo style asjustment --- ui/src/components/Header/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index f56ad023d..8aaee1e59 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -76,7 +76,7 @@ const Header: FC = () => { {brandingInfo.logo ? ( <> From f6095fd4769efd30a9b6f5dc2b469083d0821eae Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 17 Nov 2022 10:09:18 +0800 Subject: [PATCH 0349/3337] feat: add recommend and reserved tag field migration --- internal/migrations/migrations.go | 1 + internal/migrations/v2.go | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 internal/migrations/v2.go diff --git a/internal/migrations/migrations.go b/internal/migrations/migrations.go index 200e4813d..da34800cb 100644 --- a/internal/migrations/migrations.go +++ b/internal/migrations/migrations.go @@ -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 diff --git a/internal/migrations/v2.go b/internal/migrations/v2.go new file mode 100644 index 000000000..b006daf86 --- /dev/null +++ b/internal/migrations/v2.go @@ -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)) +} From e8ab1c42028fd402ab19834c4320f580ec3d9197 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 17 Nov 2022 12:10:20 +0800 Subject: [PATCH 0350/3337] fix: user verified email will update email status. so user status cache should be updated. --- internal/service/auth/auth.go | 4 ++++ internal/service/user_service.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/internal/service/auth/auth.go b/internal/service/auth/auth.go index 95604a147..b7ded1c82 100644 --- a/internal/service/auth/auth.go +++ b/internal/service/auth/auth.go @@ -58,6 +58,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 { diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 8673c3d20..0bf9ac830 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -413,6 +413,10 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri 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}) From e733d37b1fe14a49bb2de41b69419b14885f5801 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 17 Nov 2022 14:04:57 +0800 Subject: [PATCH 0351/3337] fix: cancel active email page guard --- ui/src/pages/Users/ActiveEmail/index.tsx | 7 +++++-- ui/src/router/routes.ts | 14 +++++++------- ui/src/utils/request.ts | 1 + 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/ui/src/pages/Users/ActiveEmail/index.tsx b/ui/src/pages/Users/ActiveEmail/index.tsx index 177395d71..77a5afdfd 100644 --- a/ui/src/pages/Users/ActiveEmail/index.tsx +++ b/ui/src/pages/Users/ActiveEmail/index.tsx @@ -1,6 +1,6 @@ import { FC, memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useSearchParams } from 'react-router-dom'; +import { useSearchParams, useNavigate } from 'react-router-dom'; import { loggedUserInfoStore } from '@/stores'; import { activateAccount } from '@/services'; @@ -10,6 +10,7 @@ const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'page_title' }); const [searchParams] = useSearchParams(); const updateUser = loggedUserInfoStore((state) => state.update); + const navigate = useNavigate(); useEffect(() => { const code = searchParams.get('code'); @@ -17,9 +18,11 @@ const Index: FC = () => { activateAccount(encodeURIComponent(code)).then((res) => { updateUser(res); setTimeout(() => { - window.location.replace('/users/account-activation/success'); + navigate('/users/account-activation/success', { replace: true }); }, 0); }); + } else { + navigate('/', { replace: true }); } }, []); return ; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 7336b995f..be7e61aca 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -167,13 +167,13 @@ const routes: RouteNode[] = [ { path: 'users/account-activation', page: 'pages/Users/ActiveEmail', - guard: async () => { - const notActivated = guard.notActivated(); - if (notActivated.ok) { - return notActivated; - } - return guard.notLogged(); - }, + // guard: async () => { + // const notActivated = guard.notActivated(); + // if (notActivated.ok) { + // return notActivated; + // } + // return guard.notLogged(); + // }, }, { path: 'users/account-activation/success', diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index 7d880bbfa..aac4c1491 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -103,6 +103,7 @@ class Request { return Promise.reject(false); } if (status === 403) { + debugger; // Permission interception if (data?.type === 'url_expired') { // url expired From 1b242a182c21f3de07bb37892115afb9c2336b3b Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 17 Nov 2022 14:14:02 +0800 Subject: [PATCH 0352/3337] feat: add question_id to search result --- docs/docs.go | 3 +++ docs/swagger.json | 3 +++ docs/swagger.yaml | 2 ++ internal/repo/search_common/search_repo.go | 1 + internal/schema/search_schema.go | 1 + 5 files changed, 10 insertions(+) diff --git a/docs/docs.go b/docs/docs.go index 8bba4a206..1d83ff74a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5714,6 +5714,9 @@ const docTemplate = `{ "id": { "type": "string" }, + "question_id": { + "type": "string" + }, "status": { "description": "Status", "type": "string" diff --git a/docs/swagger.json b/docs/swagger.json index 4439bf32f..02eef704e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5702,6 +5702,9 @@ "id": { "type": "string" }, + "question_id": { + "type": "string" + }, "status": { "description": "Status", "type": "string" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ddc1f8a3e..f55c3f476 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1025,6 +1025,8 @@ definitions: type: string id: type: string + question_id: + type: string status: description: Status type: string diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go index 1157b6bd1..cc0556775 100644 --- a/internal/repo/search_common/search_repo.go +++ b/internal/repo/search_common/search_repo.go @@ -450,6 +450,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(), diff --git a/internal/schema/search_schema.go b/internal/schema/search_schema.go index 143a93c16..5f40046ea 100644 --- a/internal/schema/search_schema.go +++ b/internal/schema/search_schema.go @@ -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"` From aeb60ffa383aa47b43aa9260d1fc104f25042abc Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 17 Nov 2022 14:42:23 +0800 Subject: [PATCH 0353/3337] fix(search): complete href for SearchItem answer --- ui/src/common/interface.ts | 1 + ui/src/pages/Search/components/SearchItem/index.tsx | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 38fb8deb3..2ab3afcde 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -334,6 +334,7 @@ export interface SearchResItem { object_type: string; object: { id: string; + question_id?: string; title: string; excerpt: string; created_at: number; diff --git a/ui/src/pages/Search/components/SearchItem/index.tsx b/ui/src/pages/Search/components/SearchItem/index.tsx index 8b9d8e6e2..ad08aa5e9 100644 --- a/ui/src/pages/Search/components/SearchItem/index.tsx +++ b/ui/src/pages/Search/components/SearchItem/index.tsx @@ -14,6 +14,10 @@ const Index: FC = ({ data }) => { if (!data?.object_type) { return null; } + let itemUrl = `/questions/${data.object.id}`; + if (data.object_type === 'answer') { + itemUrl = `/questions/${data.object.question_id}/${data.object.id}`; + } return (
@@ -23,9 +27,7 @@ const Index: FC = ({ data }) => { style={{ marginTop: '2px' }}> {data.object_type === 'question' ? 'Q' : 'A'} - + {data.object.title} {data.object.status === 'closed' ? ` [${t('closed', { keyPrefix: 'question' })}]` From 750d442b596002c20ac6959a7e27c651111562e8 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 17 Nov 2022 14:45:59 +0800 Subject: [PATCH 0354/3337] chore: remove legacy console.log --- ui/src/utils/localize.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts index 37b0919be..5071ee9d8 100644 --- a/ui/src/utils/localize.ts +++ b/ui/src/utils/localize.ts @@ -85,7 +85,6 @@ export const getCurrentLang = () => { export const setupAppLanguage = async () => { const lang = getCurrentLang(); - console.log(lang); if (!i18next.getDataByLanguage(lang)) { await addI18nResource(lang); } From a5ff36f358a7a1fbb3dc873d3d90e57b640b035e Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 17 Nov 2022 14:53:58 +0800 Subject: [PATCH 0355/3337] fix: remove debugger --- ui/src/utils/request.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index aac4c1491..7d880bbfa 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -103,7 +103,6 @@ class Request { return Promise.reject(false); } if (status === 403) { - debugger; // Permission interception if (data?.type === 'url_expired') { // url expired From bcd94c7e017b25fd72d319fdc67deb650877f287 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 17 Nov 2022 16:40:59 +0800 Subject: [PATCH 0356/3337] fix: branding logos in header --- ui/src/components/Header/index.tsx | 4 ++-- ui/src/pages/Layout/index.tsx | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ui/src/components/Header/index.tsx b/ui/src/components/Header/index.tsx index 8aaee1e59..f6537d749 100644 --- a/ui/src/components/Header/index.tsx +++ b/ui/src/components/Header/index.tsx @@ -82,8 +82,8 @@ const Header: FC = () => { /> diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 94478af9b..d0f5a0751 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -10,7 +10,8 @@ import { Header, Footer, Toast } from '@/components'; const Layout: FC = () => { const { msg: toastMsg, variant, clear: toastClear } = toastStore(); const { siteInfo } = siteInfoStore.getState(); - const { favicon } = brandingStore((state) => state.branding); + const { favicon, square_icon } = brandingStore((state) => state.branding); + const closeToast = () => { toastClear(); }; @@ -18,7 +19,14 @@ const Layout: FC = () => { return ( - + + + + {siteInfo && } Date: Thu, 17 Nov 2022 17:12:27 +0800 Subject: [PATCH 0357/3337] update tag recommend show --- cmd/answer/wire_gen.go | 2 +- internal/repo/tag/tag_repo.go | 66 ++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index dbd2a8488..f035cbe4e 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -115,7 +115,7 @@ 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) + tagRepo := tag.NewTagRepo(dataData, uniqueIDRepo, siteInfoCommonService) objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagRepo) voteRepo := activity_common.NewVoteRepo(dataData, activityRepo) commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo) diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 76ab6cbfa..02ae9c39c 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -7,27 +7,42 @@ import ( "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" + "github.com/answerdev/answer/internal/service/siteinfo_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" "xorm.io/builder" ) // tagRepo tag repository type tagRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo + data *data.Data + uniqueIDRepo unique.UniqueIDRepo + siteInfoService *siteinfo_common.SiteInfoCommonService } // NewTagRepo new repository func NewTagRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, + siteInfoService *siteinfo_common.SiteInfoCommonService, + ) tagcommon.TagRepo { return &tagRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, + data: data, + uniqueIDRepo: uniqueIDRepo, + siteInfoService: siteInfoService, + } +} + +func (tr *tagRepo) tagRecommendStatus(ctx context.Context) bool { + tagconfig, err := tr.siteInfoService.GetSiteWrite(ctx) + if err != nil { + log.Error("siteInfoService.GetSiteWrite error", err) + return false } + return tagconfig.RequiredTag } // AddTagList add tag @@ -55,6 +70,11 @@ func (tr *tagRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList [ if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } return } @@ -67,6 +87,11 @@ func (tr *tagRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagIn if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + + if !tr.tagRecommendStatus(ctx) { + tagInfo.Recommend = false + } + return } @@ -92,6 +117,11 @@ func (tr *tagRepo) GetTagListByName(ctx context.Context, name string, limit int, if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } return } @@ -107,6 +137,11 @@ func (tr *tagRepo) GetRecommendTagList(ctx context.Context) (tagList []*entity.T if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } return } @@ -122,6 +157,11 @@ func (tr *tagRepo) GetReservedTagList(ctx context.Context) (tagList []*entity.Ta if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } return } @@ -134,6 +174,11 @@ func (tr *tagRepo) GetTagListByNames(ctx context.Context, names []string) (tagLi if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } return } @@ -208,6 +253,9 @@ func (tr *tagRepo) GetTagByID(ctx context.Context, tagID string) ( if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !tr.tagRecommendStatus(ctx) { + tag.Recommend = false + } return } @@ -219,6 +267,11 @@ func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []* if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } return } @@ -249,5 +302,10 @@ func (tr *tagRepo) GetTagPage(ctx context.Context, page, pageSize int, tag *enti if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } return } From 0519b558b7a259b96867a3348ee18adef4145ba1 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 17 Nov 2022 17:46:02 +0800 Subject: [PATCH 0358/3337] update tag error --- internal/service/tag_common/tag_common.go | 54 +++++++++++++++-------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index 952589877..6cd682b4d 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -74,18 +74,36 @@ func (ts *TagCommonService) GetSiteWriteRecommendTag(ctx context.Context) (tags return tags, nil } -func (ts *TagCommonService) SetSiteWriteRecommendTag(ctx context.Context, tags []string, required bool, userID string) (msg string, err error) { - err = ts.UpdateTag(ctx, tags, userID) +func (ts *TagCommonService) SetSiteWriteTag(ctx context.Context, recommendtags, reservedtags []string, required bool, userID string) (msg string, err error) { + recommenderr := ts.CheckTag(ctx, recommendtags, userID) + reservederr := ts.CheckTag(ctx, reservedtags, userID) + if recommenderr != nil || reservederr != nil { + return "", nil + } + err = ts.SetTagsAttribute(ctx, recommendtags, "recommend") if err != nil { - return err.Error(), err + return "", err } - err = ts.SetTagsAttribute(ctx, tags, "recommend", true) + err = ts.SetTagsAttribute(ctx, reservedtags, "reserved") if err != nil { return "", err } return "", nil } +// func (ts *TagCommonService) SetSiteWriteRecommendTag(ctx context.Context, tags []string, userID string) (msg string, err error) { +// err = ts.UpdateTag(ctx, tags, userID) +// if err != nil { +// return err.Error(), err +// } +// err = ts.SetTagsAttribute(ctx, tags, "recommend") +// if err != nil { +// return "", err +// } + +// return "", nil +// } + func (ts *TagCommonService) GetSiteWriteReservedTag(ctx context.Context) (tags []string, err error) { tags = make([]string, 0) list, err := ts.tagRepo.GetReservedTagList(ctx) @@ -95,20 +113,20 @@ func (ts *TagCommonService) GetSiteWriteReservedTag(ctx context.Context) (tags [ return tags, nil } -func (ts *TagCommonService) SetSiteWriteReservedTag(ctx context.Context, tags []string, userID string) (msg string, err error) { - err = ts.UpdateTag(ctx, tags, userID) - if err != nil { - return err.Error(), err - } - err = ts.SetTagsAttribute(ctx, tags, "reserved", true) - if err != nil { - return "", err - } - return "", nil -} +// func (ts *TagCommonService) SetSiteWriteReservedTag(ctx context.Context, tags []string, userID string) (msg string, err error) { +// err = ts.UpdateTag(ctx, tags, userID) +// if err != nil { +// return err.Error(), err +// } +// err = ts.SetTagsAttribute(ctx, tags, "reserved") +// if err != nil { +// return "", err +// } +// return "", nil +// } // SetTagsAttribute -func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, attribute string, value bool) (err error) { +func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, attribute string) (err error) { var tagslist []string switch attribute { case "recommend": @@ -122,7 +140,7 @@ func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, if err != nil { return err } - err = ts.tagRepo.UpdateTagsAttribute(ctx, tags, attribute, value) + err = ts.tagRepo.UpdateTagsAttribute(ctx, tags, attribute, true) if err != nil { return err } @@ -240,7 +258,7 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s return objectIDTagMap, nil } -func (ts *TagCommonService) UpdateTag(ctx context.Context, tags []string, userID string) (err error) { +func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID string) (err error) { if len(tags) == 0 { return nil } From c53d5d602a1db131877c33642573c2e8f45e64a8 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 17 Nov 2022 17:53:01 +0800 Subject: [PATCH 0359/3337] fix(Tag): #1175 --- ui/src/components/Tag/index.tsx | 6 ++++-- ui/src/pages/Tags/Detail/index.tsx | 8 ++++++-- ui/src/pages/Tags/Info/index.tsx | 1 - ui/src/services/client/tag.ts | 1 + 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ui/src/components/Tag/index.tsx b/ui/src/components/Tag/index.tsx index ac7910ff4..54022e23c 100644 --- a/ui/src/components/Tag/index.tsx +++ b/ui/src/components/Tag/index.tsx @@ -11,8 +11,10 @@ interface IProps { } const Index: FC = ({ className = '', href, data }) => { - href = - href || `/tags/${data.main_tag_slug_name || data.slug_name}`.toLowerCase(); + href ||= `/tags/${encodeURIComponent( + data.main_tag_slug_name || data.slug_name, + )}`.toLowerCase(); + return ( { const { t } = useTranslation('translation', { keyPrefix: 'tags' }); const navigate = useNavigate(); const routeParams = useParams(); - const curTagName = routeParams.tagName; + const curTagName = routeParams.tagName || ''; const [tagInfo, setTagInfo] = useState({}); const [tagFollow, setTagFollow] = useState(); const { data: tagResp } = useTagInfo({ name: curTagName }); @@ -71,7 +71,11 @@ const Questions: FC = () => {

{escapeRemove(tagInfo.excerpt) || t('no_description')} - [{t('more')}] + + [{t('more')}] +

diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 557580389..4f1ef3f74 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -154,7 +154,6 @@ const TagIntroduction = () => { {t('synonyms.text')}{' '} { if (id) { apiUrl = `/answer/api/v1/tag/?id=${id}`; } else if (name) { + name = encodeURIComponent(name); apiUrl = `/answer/api/v1/tag/?name=${name}`; } const { data, error } = useSWR(apiUrl, request.instance.get); From 83ad1a185a19c767ac8d548baeae8c33506cb70c Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 17 Nov 2022 17:57:13 +0800 Subject: [PATCH 0360/3337] feat: change the original single form error return to multiple form error return --- internal/base/reason/reason.go | 2 +- internal/base/validator/validator.go | 33 ++++++----- internal/controller/user_controller.go | 4 +- internal/schema/tag_schema.go | 6 +- internal/schema/user_schema.go | 57 +++++++++++-------- internal/service/siteinfo/siteinfo_service.go | 22 +------ internal/service/tag_common/tag_common.go | 37 ++++++++---- 7 files changed, 87 insertions(+), 74 deletions(-) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 25ff1d2f4..2f65c0e93 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -14,7 +14,7 @@ const ( ) const ( - EmailOrPasswordWrong = "error.user.email_or_password_wrong" + EmailOrPasswordWrong = "error.object.email_or_password_incorrect" CommentNotFound = "error.comment.not_found" QuestionNotFound = "error.question.not_found" AnswerNotFound = "error.answer.not_found" diff --git a/internal/base/validator/validator.go b/internal/base/validator/validator.go index b089ccf05..554d079c3 100644 --- a/internal/base/validator/validator.go +++ b/internal/base/validator/validator.go @@ -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) { diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 92d491fe4..9006df22c 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -120,10 +120,10 @@ func (uc *UserController) UserEmailLogin(ctx *gin.Context) { _, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) resp := schema.UserVerifyEmailErrorResponse{ Key: "e_mail", - Value: "error.object.email_or_password_incorrect", + Value: reason.EmailOrPasswordWrong, } resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) - handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) + handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), resp) return } uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeLogin, ctx.ClientIP()) diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go index 0ab27033f..26f0656e5 100644 --- a/internal/schema/tag_schema.go +++ b/internal/schema/tag_schema.go @@ -25,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) } @@ -155,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 } diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index d94551017..38d4d2c6a 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -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 } diff --git a/internal/service/siteinfo/siteinfo_service.go b/internal/service/siteinfo/siteinfo_service.go index 3980a6e20..68f5a450f 100644 --- a/internal/service/siteinfo/siteinfo_service.go +++ b/internal/service/siteinfo/siteinfo_service.go @@ -7,7 +7,6 @@ import ( "github.com/answerdev/answer/internal/base/constant" "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/export" @@ -166,26 +165,9 @@ func (s *SiteInfoService) SaveSiteBranding(ctx context.Context, req *schema.Site // SaveSiteWrite save site configuration about write func (s *SiteInfoService) SaveSiteWrite(ctx context.Context, req *schema.SiteWriteReq) (resp interface{}, err error) { - errData, err := s.tagCommonService.SetSiteWriteRecommendTag(ctx, req.RecommendTags, req.RequiredTag, req.UserID) + errData, err := s.tagCommonService.SetSiteWriteTag(ctx, req.RecommendTags, req.ReservedTags, req.UserID) if err != nil { - if len(errData) > 0 { - resp = &validator.ErrorField{ - Key: "recommend_tags", - Value: errData, - } - } - return resp, err - } - - errData, err = s.tagCommonService.SetSiteWriteReservedTag(ctx, req.ReservedTags, req.UserID) - if err != nil { - if len(errData) > 0 { - resp = &validator.ErrorField{ - Key: "reserved_tags", - Value: errData, - } - } - return resp, err + return errData, err } content, _ := json.Marshal(req) diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index 6cd682b4d..0f3ee25fe 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -8,6 +8,7 @@ import ( "strings" "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" @@ -74,21 +75,37 @@ func (ts *TagCommonService) GetSiteWriteRecommendTag(ctx context.Context) (tags return tags, nil } -func (ts *TagCommonService) SetSiteWriteTag(ctx context.Context, recommendtags, reservedtags []string, required bool, userID string) (msg string, err error) { - recommenderr := ts.CheckTag(ctx, recommendtags, userID) - reservederr := ts.CheckTag(ctx, reservedtags, userID) - if recommenderr != nil || reservederr != nil { - return "", 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") + + err = ts.SetTagsAttribute(ctx, recommendTags, "recommend") if err != nil { - return "", err + return nil, err } - err = ts.SetTagsAttribute(ctx, reservedtags, "reserved") + err = ts.SetTagsAttribute(ctx, reservedTags, "reserved") if err != nil { - return "", err + return nil, err } - return "", nil + return nil, nil } // func (ts *TagCommonService) SetSiteWriteRecommendTag(ctx context.Context, tags []string, userID string) (msg string, err error) { From 158727825c0703fb2ab7cd887383bbf3e5e6bf37 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 17 Nov 2022 18:40:07 +0800 Subject: [PATCH 0361/3337] fix: add form error function --- ui/src/components/Unactivate/index.tsx | 8 +++---- ui/src/index.scss | 13 +++++------ ui/src/pages/Admin/Branding/index.tsx | 9 ++++---- ui/src/pages/Admin/General/index.tsx | 8 +++---- ui/src/pages/Admin/Interface/index.tsx | 8 +++---- ui/src/pages/Admin/Legal/index.tsx | 10 ++++----- ui/src/pages/Admin/Smtp/index.tsx | 8 +++---- ui/src/pages/Admin/Write/index.tsx | 8 +++---- ui/src/pages/Install/index.tsx | 9 ++++---- ui/src/pages/Questions/Ask/index.tsx | 22 +++++++++---------- .../AccountForgot/components/sendEmail.tsx | 18 ++++++++++----- .../ChangeEmail/components/sendEmail.tsx | 18 ++++++++++----- ui/src/pages/Users/Login/index.tsx | 20 ++++++++++++----- ui/src/pages/Users/PasswordReset/index.tsx | 8 +++---- .../Register/components/SignUpForm/index.tsx | 8 +++---- .../Account/components/ModifyEmail/index.tsx | 8 +++---- .../Account/components/ModifyPass/index.tsx | 8 +++---- ui/src/pages/Users/Settings/Profile/index.tsx | 8 +++---- ui/src/utils/common.ts | 14 ++++++++++++ ui/src/utils/request.ts | 8 ++----- 20 files changed, 125 insertions(+), 96 deletions(-) diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index 420891cc4..ebfbc7776 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -9,6 +9,7 @@ import { loggedUserInfoStore } from '@/stores'; import { resendEmail, checkImgCode } from '@/services'; import { CAPTCHA_CODE_STORAGE_KEY } from '@/common/constants'; import Storage from '@/utils/storage'; +import { handleFormError } from '@/utils'; interface IProps { visible: boolean; @@ -58,11 +59,10 @@ const Index: React.FC = ({ visible = false }) => { setModalState(false); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err.list, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }) .finally(() => { getImgCode(); diff --git a/ui/src/index.scss b/ui/src/index.scss index f94104166..e502ab5b0 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -66,29 +66,28 @@ a { display: inline-block; font-size: 14px; background: rgba($blue-100, 0.5); - padding: 1px 0.5rem 2px; + padding: 1px 7px; color: $blue-700; - height: 24px; - border: 1px solid rgba($blue-100, 0.5); + border: 1px solid transparent; &:hover { - background: rgba($blue-100, 1); + background: $blue-100; } } .badge-tag-required { - background: rgba($gray-200, 0.5); + background: $gray-200; color: $gray-700; border: 1px solid $gray-400; &:hover { color: $gray-700; - background: rgba($gray-200, 1); + background: $gray-300; } } .badge-tag-reserved { background: rgba($orange-100, 0.5); color: $orange-700; - border: 1px solid $orange-400; + border: 1px solid $orange-100; &:hover { color: $orange-700; background: rgba($orange-100, 1); diff --git a/ui/src/pages/Admin/Branding/index.tsx b/ui/src/pages/Admin/Branding/index.tsx index b517e7b54..7d7c89ede 100644 --- a/ui/src/pages/Admin/Branding/index.tsx +++ b/ui/src/pages/Admin/Branding/index.tsx @@ -6,6 +6,7 @@ import { FormDataType } from '@/common/interface'; import { brandSetting, getBrandSetting } from '@/services'; import { brandingStore } from '@/stores'; import { useToast } from '@/hooks'; +import { handleFormError } from '@/utils'; const uploadType = 'branding'; const Index: FC = () => { @@ -113,11 +114,9 @@ const Index: FC = () => { }); }) .catch((err) => { - console.log(err); - if (err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; - setFormData({ ...formData }); + if (err.isError) { + const data = handleFormError(err.list, formData); + setFormData({ ...data }); } }); }; diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index d33883b66..fd41842ab 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -7,6 +7,7 @@ import { useToast } from '@/hooks'; import { siteInfoStore } from '@/stores'; import { useGeneralSetting, updateGeneralSetting } from '@/services'; import Pattern from '@/common/pattern'; +import { handleFormError } from '@/utils'; import '../index.scss'; @@ -106,11 +107,10 @@ const General: FC = () => { updateSiteInfo(reqParams); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 9d307cbb8..6226c2734 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -20,6 +20,7 @@ import { loadLanguageOptions, setupAppTimeZone, } from '@/utils/localize'; +import { handleFormError } from '@/utils'; const Interface: FC = () => { const { t } = useTranslation('translation', { @@ -162,11 +163,10 @@ const Interface: FC = () => { }); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; // const imgUpload = (file: any) => { diff --git a/ui/src/pages/Admin/Legal/index.tsx b/ui/src/pages/Admin/Legal/index.tsx index 76a0bc7f5..4e5480dd1 100644 --- a/ui/src/pages/Admin/Legal/index.tsx +++ b/ui/src/pages/Admin/Legal/index.tsx @@ -1,4 +1,4 @@ -import React, { FC, useEffect, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { marked } from 'marked'; @@ -7,6 +7,7 @@ import type * as Type from '@/common/interface'; import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components'; import { useToast } from '@/hooks'; import { getLegalSetting, putLegalSetting } from '@/services'; +import { handleFormError } from '@/utils'; import '../index.scss'; const Legal: FC = () => { @@ -68,11 +69,10 @@ const Legal: FC = () => { }); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx index 7b02aed82..2af421a62 100644 --- a/ui/src/pages/Admin/Smtp/index.tsx +++ b/ui/src/pages/Admin/Smtp/index.tsx @@ -7,6 +7,7 @@ import { useSmtpSetting, updateSmtpSetting } from '@/services'; import pattern from '@/common/pattern'; import { SchemaForm, JSONSchema, UISchema } from '@/components'; import { initFormData } from '../../../components/SchemaForm/index'; +import { handleFormError } from '@/utils'; const Smtp: FC = () => { const { t } = useTranslation('translation', { @@ -128,11 +129,10 @@ const Smtp: FC = () => { }); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; diff --git a/ui/src/pages/Admin/Write/index.tsx b/ui/src/pages/Admin/Write/index.tsx index 12cf4e980..36c8cd475 100644 --- a/ui/src/pages/Admin/Write/index.tsx +++ b/ui/src/pages/Admin/Write/index.tsx @@ -8,6 +8,7 @@ import { getRequireAndReservedTag, postRequireAndReservedTag, } from '@/services'; +import { handleFormError } from '@/utils'; import '../index.scss'; @@ -76,11 +77,10 @@ const Legal: FC = () => { }); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 52dca0ec4..27bf41c8f 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -11,7 +11,7 @@ import { installBaseInfo, checkConfigFileExists, } from '@/services'; -import { Storage } from '@/utils'; +import { Storage, handleFormError } from '@/utils'; import { CURRENT_LANG_STORAGE_KEY } from '@/common/constants'; import { @@ -175,10 +175,9 @@ const Index: FC = () => { handleNext(); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; - setFormData({ ...formData }); + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } else { handleErr(err); } diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index f8f1ea1e5..7b5053f37 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -16,6 +16,7 @@ import { postAnswer, useQueryQuestionByTitle, } from '@/services'; +import { handleFormError } from '@/utils'; import SearchQuestion from './components/SearchQuestion'; @@ -209,19 +210,17 @@ const Ask = () => { navigate(`/questions/${qid}`); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); } else { const res = await saveQuestion(params).catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); const id = res?.id; @@ -236,11 +235,10 @@ const Ask = () => { navigate(`/questions/${id}`); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); } else { navigate(`/questions/${id}`); diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index f919e5606..7d6f11be4 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -9,6 +9,7 @@ import type { } from '@/common/interface'; import { resetPassword, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; +import { handleFormError } from '@/utils'; interface IProps { visible: boolean; @@ -83,14 +84,21 @@ const Index: FC = ({ visible = false, callback }) => { setModalState(false); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; - if (err.key.indexOf('captcha') < 0) { + // if (err.isError && err.key) { + // formData[err.key].isInvalid = true; + // formData[err.key].errorMsg = err.value; + // if (err.key.indexOf('captcha') < 0) { + // setModalState(false); + // } + // } + + if (err.isError) { + const data = handleFormError(err, formData); + if (err.list.filter((v) => v.error_field.indexOf('captcha') < 0)) { setModalState(false); } + setFormData({ ...data }); } - setFormData({ ...formData }); }) .finally(() => { getImgCode(); diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx index f87a6adfe..037b98c1a 100644 --- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx +++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx @@ -11,6 +11,7 @@ import type { import { loggedUserInfoStore } from '@/stores'; import { changeEmail, checkImgCode } from '@/services'; import { PicAuthCodeModal } from '@/components/Modal'; +import { handleFormError } from '@/utils'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'change_email' }); @@ -84,14 +85,21 @@ const Index: FC = () => { setModalState(false); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; - if (err.key.indexOf('captcha') < 0) { + // if (err.isError && err.key) { + // formData[err.key].isInvalid = true; + // formData[err.key].errorMsg = err.value; + // if (err.key.indexOf('captcha') < 0) { + // setModalState(false); + // } + // } + // setFormData({ ...formData }); + if (err.isError) { + const data = handleFormError(err, formData); + if (err.list.filter((v) => v.error_field.indexOf('captcha') < 0)) { setModalState(false); } + setFormData({ ...data }); } - setFormData({ ...formData }); }) .finally(() => { getImgCode(); diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index 503b9723c..b278b4365 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -10,7 +10,7 @@ import type { } from '@/common/interface'; import { PageTitle, Unactivate } from '@/components'; import { loggedUserInfoStore } from '@/stores'; -import { guard, floppyNavigation } from '@/utils'; +import { guard, floppyNavigation, handleFormError } from '@/utils'; import { login, checkImgCode } from '@/services'; import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; @@ -122,14 +122,22 @@ const Index: React.FC = () => { setModalState(false); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; - if (err.key.indexOf('captcha') < 0) { + // if (err.isError && err.key) { + // formData[err.key].isInvalid = true; + // formData[err.key].errorMsg = err.value; + // if (err.key.indexOf('captcha') < 0) { + // setModalState(false); + // } + // } + if (err.isError) { + console.log('err===', err); + const data = handleFormError(err, formData); + console.log('err===', data); + if (err.list.filter((v) => v.error_field.indexOf('captcha') < 0)) { setModalState(false); } + setFormData({ ...data }); } - setFormData({ ...formData }); setRefresh((pre) => pre + 1); }); }; diff --git a/ui/src/pages/Users/PasswordReset/index.tsx b/ui/src/pages/Users/PasswordReset/index.tsx index fc95731bb..7b262801e 100644 --- a/ui/src/pages/Users/PasswordReset/index.tsx +++ b/ui/src/pages/Users/PasswordReset/index.tsx @@ -7,6 +7,7 @@ import { loggedUserInfoStore } from '@/stores'; import type { FormDataType } from '@/common/interface'; import { replacementPassword } from '@/services'; import { PageTitle } from '@/components'; +import { handleFormError } from '@/utils'; const Index: React.FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'password_reset' }); @@ -105,11 +106,10 @@ const Index: React.FC = () => { setStep(2); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; diff --git a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx index 61ccbb2eb..c69ec60ef 100644 --- a/ui/src/pages/Users/Register/components/SignUpForm/index.tsx +++ b/ui/src/pages/Users/Register/components/SignUpForm/index.tsx @@ -6,6 +6,7 @@ import { Trans, useTranslation } from 'react-i18next'; import type { FormDataType } from '@/common/interface'; import { register, useLegalTos, useLegalPrivacy } from '@/services'; import userStore from '@/stores/userInfo'; +import { handleFormError } from '@/utils'; interface Props { callback: () => void; @@ -119,11 +120,10 @@ const Index: React.FC = ({ callback }) => { callback(); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index 8bfa6dfa3..56ba7cb0a 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import type * as Type from '@/common/interface'; import { useToast } from '@/hooks'; import { getLoggedUserInfo, changeEmail } from '@/services'; +import { handleFormError } from '@/utils'; const reg = /(?<=.{2}).+(?=@)/gi; @@ -74,11 +75,10 @@ const Index: FC = () => { }); }) .catch((err) => { - if (err.isError && err.key) { - formData.e_mail.isInvalid = true; - formData.e_mail.errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx index 56d8f17b2..e0ae09912 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyPass/index.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'; import { useToast } from '@/hooks'; import type { FormDataType } from '@/common/interface'; import { modifyPassword } from '@/services'; +import { handleFormError } from '@/utils'; const Index: FC = () => { const { t } = useTranslation('translation', { @@ -118,11 +119,10 @@ const Index: FC = () => { handleFormState(); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 0b874997a..03d5639fc 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -10,6 +10,7 @@ import { UploadImg, Avatar } from '@/components'; import { loggedUserInfoStore } from '@/stores'; import { useToast } from '@/hooks'; import { modifyUserInfo, getLoggedUserInfo } from '@/services'; +import { handleFormError } from '@/utils'; const Index: React.FC = () => { const { t } = useTranslation('translation', { @@ -174,11 +175,10 @@ const Index: React.FC = () => { }); }) .catch((err) => { - if (err.isError && err.key) { - formData[err.key].isInvalid = true; - formData[err.key].errorMsg = err.value; + if (err.isError) { + const data = handleFormError(err, formData); + setFormData({ ...data }); } - setFormData({ ...formData }); }); }; diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index a6e356821..41622307f 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -150,6 +150,19 @@ function labelStyle(color, hover) { }; } +function handleFormError( + error: { list: Array<{ error_field: string; error_msg: string }> }, + data: any, +) { + if (error.list?.length > 0) { + error.list.forEach((item) => { + data[item.error_field].isInvalid = true; + data[item.error_field].errorMsg = item.error_msg; + }); + } + return data; +} + export { thousandthDivision, formatCount, @@ -161,4 +174,5 @@ export { mixColor, colorRgb, labelStyle, + handleFormError, }; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index 7d880bbfa..410992f21 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -78,13 +78,9 @@ class Request { return Promise.reject(false); } - if ( - data instanceof Object && - Object.keys(data).length > 0 && - data.key - ) { + if (data instanceof Array && data.length > 0) { // handle form error - return Promise.reject({ ...data, isError: true }); + return Promise.reject({ isError: true, list: data }); } if (!data || Object.keys(data).length <= 0) { From c3101a95bd45b388bccb7bb1f02bfb1848800fbe Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 17 Nov 2022 18:50:53 +0800 Subject: [PATCH 0362/3337] fix(question): #1249 --- ui/src/pages/Questions/Detail/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index 503469384..cc19a7193 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import { useParams, useSearchParams, useNavigate } from 'react-router-dom'; -import { Pagination, PageTitle, Labels } from '@/components'; +import { Pagination, PageTitle } from '@/components'; import { loggedUserInfoStore } from '@/stores'; import { scrollTop } from '@/utils'; import { usePageUsers } from '@/hooks'; @@ -167,7 +167,6 @@ const Index = () => { )}
- From 4a7c176094c52bf5ab795fe993fab1a04f7973e2 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 17 Nov 2022 18:57:01 +0800 Subject: [PATCH 0363/3337] fix: style adjustment --- ui/src/components/TagSelector/index.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/src/components/TagSelector/index.scss b/ui/src/components/TagSelector/index.scss index 7789074e4..dc016ba66 100644 --- a/ui/src/components/TagSelector/index.scss +++ b/ui/src/components/TagSelector/index.scss @@ -13,6 +13,11 @@ display: none; } } + + .dropdown-item.active { + color: #212529; + background-color: #e9ecef; + } @-webkit-keyframes tag-input-warning { 0% { background-color: #ffc107; From 808487009640a658ed71515ffce6ab7fd068546b Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 17 Nov 2022 18:59:09 +0800 Subject: [PATCH 0364/3337] feat: change the original single form error return to multiple form error return --- internal/base/reason/reason.go | 64 ++++++++++---------- internal/controller/user_controller.go | 81 ++++++++++++-------------- internal/schema/user_schema.go | 5 -- internal/service/user_service.go | 9 +-- 4 files changed, 75 insertions(+), 84 deletions(-) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 2f65c0e93..085e06f7c 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -14,35 +14,37 @@ const ( ) const ( - EmailOrPasswordWrong = "error.object.email_or_password_incorrect" - 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" - 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" - RecommendTagNotExist = "error.tag.recommend_tag_not_found" + EmailOrPasswordWrong = "error.object.email_or_password_incorrect" + 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" + 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" + RecommendTagNotExist = "error.tag.recommend_tag_not_found" ) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 9006df22c..4e2143917 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -9,6 +9,7 @@ import ( "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" @@ -106,24 +107,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: reason.EmailOrPasswordWrong, - } - resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value) - handler.HandleResponse(ctx, errors.BadRequest(reason.EmailOrPasswordWrong), 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 +145,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 +275,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 +311,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) @@ -502,19 +496,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 diff --git a/internal/schema/user_schema.go b/internal/schema/user_schema.go index 38d4d2c6a..fdae241cf 100644 --- a/internal/schema/user_schema.go +++ b/internal/schema/user_schema.go @@ -417,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"` -} diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 288161c51..cf7aa5fb0 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -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" @@ -481,7 +482,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 @@ -495,9 +496,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) } From a5e9225d18cf4d1ac230236ae104a0e761b3aedf Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 17 Nov 2022 19:01:42 +0800 Subject: [PATCH 0365/3337] fix(agreements): #1219 --- ui/src/pages/Legal/Privacy/index.tsx | 12 ++++++++++++ ui/src/pages/Legal/Tos/index.tsx | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ui/src/pages/Legal/Privacy/index.tsx b/ui/src/pages/Legal/Privacy/index.tsx index 90e0610fb..2fe2fdb9e 100644 --- a/ui/src/pages/Legal/Privacy/index.tsx +++ b/ui/src/pages/Legal/Privacy/index.tsx @@ -4,6 +4,18 @@ import { useLegalPrivacy } from '@/services'; const Index: FC = () => { const { data: privacy } = useLegalPrivacy(); + const contentText = privacy?.privacy_policy_original_text; + let matchUrl: URL | undefined; + try { + if (contentText) { + matchUrl = new URL(contentText); + } + // eslint-disable-next-line no-empty + } catch (ex) {} + if (matchUrl) { + window.location.replace(matchUrl.toString()); + return null; + } return (
{ const { data: tos } = useLegalTos(); + const contentText = tos?.terms_of_service_original_text; + let matchUrl: URL | undefined; + try { + if (contentText) { + matchUrl = new URL(contentText); + } + // eslint-disable-next-line no-empty + } catch (ex) {} + if (matchUrl) { + window.location.replace(matchUrl.toString()); + return null; + } return (
Date: Thu, 17 Nov 2022 19:09:56 +0800 Subject: [PATCH 0366/3337] check tags --- internal/service/question_service.go | 27 ++++++++++++++++ internal/service/tag_common/tag_common.go | 39 +++++++++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 85bf7673c..437d7936d 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -3,6 +3,7 @@ package service import ( "encoding/json" "fmt" + "strings" "time" "github.com/answerdev/answer/internal/base/constant" @@ -225,6 +226,28 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest if dbinfo.UserID != req.UserID { return } + + //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 @@ -276,6 +299,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) diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index 6cd682b4d..c9ee60eb7 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -188,7 +188,11 @@ func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.T // 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) +} + +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 { @@ -197,11 +201,17 @@ 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.tagRepo.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, @@ -331,6 +341,29 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID 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 { From 974e4c75be2d161a22867aa399b2cbe58bb0fa0e Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 17 Nov 2022 19:20:26 +0800 Subject: [PATCH 0367/3337] fix(agreements): #1218 --- ui/src/pages/Legal/Privacy/index.tsx | 20 ++++++++++++++------ ui/src/pages/Legal/Tos/index.tsx | 19 +++++++++++++------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/ui/src/pages/Legal/Privacy/index.tsx b/ui/src/pages/Legal/Privacy/index.tsx index 2fe2fdb9e..d7acf07e0 100644 --- a/ui/src/pages/Legal/Privacy/index.tsx +++ b/ui/src/pages/Legal/Privacy/index.tsx @@ -1,8 +1,11 @@ import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; import { useLegalPrivacy } from '@/services'; +import { PageTitle } from '@/components'; const Index: FC = () => { + const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); const { data: privacy } = useLegalPrivacy(); const contentText = privacy?.privacy_policy_original_text; let matchUrl: URL | undefined; @@ -16,13 +19,18 @@ const Index: FC = () => { window.location.replace(matchUrl.toString()); return null; } + return ( -
+ <> + +

{t('privacy')}

+
+ ); }; diff --git a/ui/src/pages/Legal/Tos/index.tsx b/ui/src/pages/Legal/Tos/index.tsx index 4039bfb1e..93182c748 100644 --- a/ui/src/pages/Legal/Tos/index.tsx +++ b/ui/src/pages/Legal/Tos/index.tsx @@ -1,8 +1,11 @@ import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; import { useLegalTos } from '@/services'; +import { PageTitle } from '@/components'; const Index: FC = () => { + const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); const { data: tos } = useLegalTos(); const contentText = tos?.terms_of_service_original_text; let matchUrl: URL | undefined; @@ -17,12 +20,16 @@ const Index: FC = () => { return null; } return ( -
+ <> + +

{t('tos')}

+
+ ); }; From 6742c5d707c369708bf57928fd7da3a2dc8619d0 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Thu, 17 Nov 2022 19:23:02 +0800 Subject: [PATCH 0368/3337] update synonym error --- docs/docs.go | 11 ++++++-- docs/swagger.json | 11 ++++++-- docs/swagger.yaml | 6 +++++ internal/service/tag_common/tag_common.go | 31 ++++++----------------- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index cf9dd7878..fe221fb73 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -6257,6 +6257,10 @@ const docTemplate = `{ }, "schema.UserEmailLogin": { "type": "object", + "required": [ + "e_mail", + "pass" + ], "properties": { "captcha_code": { "description": "captcha_code", @@ -6268,11 +6272,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 } } }, diff --git a/docs/swagger.json b/docs/swagger.json index a0e0547e0..8ac3e0a14 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -6245,6 +6245,10 @@ }, "schema.UserEmailLogin": { "type": "object", + "required": [ + "e_mail", + "pass" + ], "properties": { "captcha_code": { "description": "captcha_code", @@ -6256,11 +6260,14 @@ }, "e_mail": { "description": "e_mail", - "type": "string" + "type": "string", + "maxLength": 500 }, "pass": { "description": "password", - "type": "string" + "type": "string", + "maxLength": 32, + "minLength": 8 } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1fe5445aa..d1f66631e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1449,10 +1449,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: diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index 1f8762022..b37e2a839 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -291,7 +291,6 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID } thisTagNameList := make([]string, 0) - thisTagIDList := make([]string, 0) for _, t := range tags { t = strings.ToLower(t) thisTagNameList = append(thisTagNameList, t) @@ -304,13 +303,17 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID } tagInDbMapping := make(map[string]*entity.Tag) + checktags := make([]string, 0) + for _, tag := range tagListInDb { if tag.MainTagID != 0 { - err = errors.BadRequest(reason.TagNotContainSynonym).WithMsg(fmt.Sprintf("tag name:%s", tag.SlugName)) - return err + checktags = append(checktags, fmt.Sprintf("\"%s\"", tag.SlugName)) } tagInDbMapping[tag.SlugName] = tag - thisTagIDList = append(thisTagIDList, tag.ID) + } + 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) @@ -334,25 +337,7 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID err = errors.BadRequest(reason.TagNotFound).WithMsg(fmt.Sprintf("tag [%s] does not exist", strings.Join(addTagMsgList, ","))) return err - // todo if need add - // err = ts.tagRepo.AddTagList(ctx, addTagList) - // if err != nil { - // return err - // } - // for _, tag := range addTagList { - // thisTagIDList = append(thisTagIDList, tag.ID) - // revisionDTO := &schema.AddRevisionDTO{ - // UserID: userID, - // ObjectID: tag.ID, - // Title: tag.SlugName, - // } - // tagInfoJson, _ := json.Marshal(tag) - // revisionDTO.Content = string(tagInfoJson) - // err = ts.revisionService.AddRevision(ctx, revisionDTO, true) - // if err != nil { - // return err - // } - // } + } return nil From 776a0130131b3cf3e0598954eecc05e484ea56a6 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 18 Nov 2022 10:09:39 +0800 Subject: [PATCH 0369/3337] fix(comment): #992 --- ui/src/components/Comment/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index 1f57e7375..d76f94d67 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -239,7 +239,7 @@ const Comment = ({ objectId, mode }) => { onCancel={() => handleCancel(item.comment_id)} /> ) : ( -
+
{item.reply_user_display_name && ( @{item.reply_user_display_name} From 34f4c072ddb19c97f19e95faf20d207c7ce1eccc Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 18 Nov 2022 10:36:46 +0800 Subject: [PATCH 0370/3337] fix: only write or edit content show required tag text --- i18n/en_US.yaml | 1 + ui/src/components/TagSelector/index.tsx | 20 ++++++++++++-------- ui/src/pages/Questions/Ask/index.tsx | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 50734d4c1..faa0cf7aa 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -507,6 +507,7 @@ ui: search_tag: Search tag hint: "Describe what your question is about, at least one tag is required." no_result: No tags matched + tag_required_text: Required tag (at least one) header: nav: question: Questions diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index ab028dac4..3597a6a7a 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -19,6 +19,7 @@ interface IProps { onBlur?: () => void; hiddenDescription?: boolean; hiddenCreateBtn?: boolean; + showRequiredTagText?: boolean; alwaysShowAddBtn?: boolean; } @@ -32,6 +33,7 @@ const TagSelector: FC = ({ hiddenDescription = false, hiddenCreateBtn = false, alwaysShowAddBtn = false, + showRequiredTagText = false, }) => { const [initialValue, setInitialValue] = useState([...value]); const [currentIndex, setCurrentIndex] = useState(0); @@ -224,14 +226,16 @@ const TagSelector: FC = ({ )} - {tags && tags.filter((v) => v.recommend)?.length > 0 && ( - - Required tag (at least one) - - )} + {showRequiredTagText && + tags && + tags.filter((v) => v.recommend)?.length > 0 && ( + + {t('tag_required_text')} + + )} {tags?.map((item, index) => { return ( diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index 7b5053f37..79614a493 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -348,6 +348,7 @@ const Ask = () => { {formData.tags.errorMsg} From b47b19bb5e88208637b319c68d779507cdcfea9f Mon Sep 17 00:00:00 2001 From: Fen Date: Fri, 18 Nov 2022 10:38:38 +0800 Subject: [PATCH 0371/3337] Update README.md add discord --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 09662decf..88fb2360d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ To learn more about the project, visit [answer.dev](https://answer.dev). [![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)](https://discord.gg/Jm7Y4cbUej) ## Screenshots From 4d63d72a0ba07f6d52e8b698d6e94b651d6d1679 Mon Sep 17 00:00:00 2001 From: Fen Date: Fri, 18 Nov 2022 10:59:45 +0800 Subject: [PATCH 0372/3337] Update README.md fix: typo --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 88fb2360d..a4daf1d59 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ 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)](https://discord.gg/Jm7Y4cbUej) From 62b0521b9fd116bcc7b094b6ec36d6d66387cec3 Mon Sep 17 00:00:00 2001 From: Fen Date: Fri, 18 Nov 2022 12:22:47 +0800 Subject: [PATCH 0373/3337] update: discord badge --- README.md | 2 +- README_CN.md | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a4daf1d59..b22a63d95 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ To learn more about the project, visit [answer.dev](https://answer.dev). [![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)](https://discord.gg/Jm7Y4cbUej) +[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord)](https://discord.gg/Jm7Y4cbUej) ## Screenshots diff --git a/README_CN.md b/README_CN.md index 8ec43f68c..6a01e0e04 100644 --- a/README_CN.md +++ b/README_CN.md @@ -8,10 +8,11 @@ 了解更多关于该项目的内容,请访问 [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)](https://discord.gg/Jm7Y4cbUej) ## 截图 From 7c725f535d76b69cd7b284ec2e268edbe3028880 Mon Sep 17 00:00:00 2001 From: Fen Date: Fri, 18 Nov 2022 13:48:15 +0800 Subject: [PATCH 0374/3337] update: badge color --- README.md | 2 +- README_CN.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b22a63d95..e6ea7a9cf 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ To learn more about the project, visit [answer.dev](https://answer.dev). [![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)](https://discord.gg/Jm7Y4cbUej) +[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/Jm7Y4cbUej) ## Screenshots diff --git a/README_CN.md b/README_CN.md index 6a01e0e04..781e7b8ce 100644 --- a/README_CN.md +++ b/README_CN.md @@ -4,7 +4,7 @@ # Answer - 构建问答社区 -一款极简的、问答形式的知识社区开源软件,用来快速构建产品你的产品问答支持社区、用户问答社区、粉丝社区等。 +一款问答形式的知识社区开源软件,用来快速构建产品你的产品技术社区、客户支持社区、用户社区等。 了解更多关于该项目的内容,请访问 [answer.dev](https://answer.dev). @@ -12,7 +12,7 @@ [![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)](https://discord.gg/Jm7Y4cbUej) +[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/Jm7Y4cbUej) ## 截图 From 0ce9b24a909669cd67119f7da2a540254a237585 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 18 Nov 2022 15:21:18 +0800 Subject: [PATCH 0375/3337] fix: handle form error --- ui/src/components/Unactivate/index.tsx | 2 +- ui/src/pages/Admin/Branding/index.tsx | 2 +- ui/src/pages/Users/Login/index.tsx | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index ebfbc7776..e45eea5d9 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -60,7 +60,7 @@ const Index: React.FC = ({ visible = false }) => { }) .catch((err) => { if (err.isError) { - const data = handleFormError(err.list, formData); + const data = handleFormError(err, formData); setFormData({ ...data }); } }) diff --git a/ui/src/pages/Admin/Branding/index.tsx b/ui/src/pages/Admin/Branding/index.tsx index 7d7c89ede..1b3765254 100644 --- a/ui/src/pages/Admin/Branding/index.tsx +++ b/ui/src/pages/Admin/Branding/index.tsx @@ -115,7 +115,7 @@ const Index: FC = () => { }) .catch((err) => { if (err.isError) { - const data = handleFormError(err.list, formData); + const data = handleFormError(err, formData); setFormData({ ...data }); } }); diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index b278b4365..2f5b9180a 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -130,9 +130,7 @@ const Index: React.FC = () => { // } // } if (err.isError) { - console.log('err===', err); const data = handleFormError(err, formData); - console.log('err===', data); if (err.list.filter((v) => v.error_field.indexOf('captcha') < 0)) { setModalState(false); } From 1a90a2c0eac57c52722994ed97642d65b3e82961 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 18 Nov 2022 16:05:20 +0800 Subject: [PATCH 0376/3337] refactor(layout): remove min-height calc form page-wrap, prevent unexpected browser's scroll restoration --- ui/src/index.scss | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/src/index.scss b/ui/src/index.scss index e502ab5b0..6703e0365 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -98,9 +98,15 @@ a { border-bottom: 1px solid rgba(33, 37, 41, 0.25); } -.page-wrap { - min-height: calc(100vh - 138px); +#root { + min-height: 100vh; + display: flex; + flex-direction: column; +} +#root > footer { + margin-top: auto !important; } + .page-wrap2 { background-color: #f5f5f5; min-height: 100vh; From 6e580d1a5a440c7009b707d471b95a16f14c3bf2 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 18 Nov 2022 16:19:32 +0800 Subject: [PATCH 0377/3337] fix: add bg-f5 class --- ui/src/index.scss | 3 +-- ui/src/pages/Install/index.tsx | 2 +- ui/src/pages/Maintenance/index.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/src/index.scss b/ui/src/index.scss index 6703e0365..a767e0a00 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -107,9 +107,8 @@ a { margin-top: auto !important; } -.page-wrap2 { +.bg-f5 { background-color: #f5f5f5; - min-height: 100vh; } .btn-no-border, diff --git a/ui/src/pages/Install/index.tsx b/ui/src/pages/Install/index.tsx index 27bf41c8f..73341c490 100644 --- a/ui/src/pages/Install/index.tsx +++ b/ui/src/pages/Install/index.tsx @@ -245,7 +245,7 @@ const Index: FC = () => { } return ( -
+
diff --git a/ui/src/pages/Maintenance/index.tsx b/ui/src/pages/Maintenance/index.tsx index 560108bf0..d216e6e15 100644 --- a/ui/src/pages/Maintenance/index.tsx +++ b/ui/src/pages/Maintenance/index.tsx @@ -8,7 +8,7 @@ const Index = () => { keyPrefix: 'page_maintenance', }); return ( -
+
From 77b53e583dc18b1656b08f4030b834ae42085f8d Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 18 Nov 2022 16:20:19 +0800 Subject: [PATCH 0378/3337] fix: upgrade bootstrap-icons --- ui/package.json | 6 +++--- ui/pnpm-lock.yaml | 43 ++++++------------------------------------- 2 files changed, 9 insertions(+), 40 deletions(-) diff --git a/ui/package.json b/ui/package.json index 51baaa063..4dd402630 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,7 @@ "dependencies": { "axios": "^0.27.2", "bootstrap": "^5.2.0", - "bootstrap-icons": "^1.9.1", + "bootstrap-icons": "1.10.2", "classnames": "^2.3.1", "codemirror": "5.65.0", "copy-to-clipboard": "^3.3.2", @@ -39,9 +39,8 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@fullhuman/postcss-purgecss": "^4.1.3", - "purgecss-webpack-plugin": "^4.1.3", - "@testing-library/jest-dom": "^4.2.4", "@testing-library/dom": "^8.17.1", + "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", @@ -71,6 +70,7 @@ "lint-staged": "^13.0.3", "postcss": "^8.0.0", "prettier": "^2.7.1", + "purgecss-webpack-plugin": "^4.1.3", "react-app-rewired": "^2.2.1", "react-scripts": "5.0.1", "sass": "^1.54.4", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index c0bad1845..78e838351 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -19,7 +19,7 @@ specifiers: '@typescript-eslint/parser': ^5.33.0 axios: ^0.27.2 bootstrap: ^5.2.0 - bootstrap-icons: ^1.9.1 + bootstrap-icons: 1.10.2 classnames: ^2.3.1 codemirror: 5.65.0 copy-to-clipboard: ^3.3.2 @@ -67,7 +67,7 @@ specifiers: dependencies: axios: 0.27.2 bootstrap: 5.2.1_@popperjs+core@2.11.6 - bootstrap-icons: 1.9.1 + bootstrap-icons: 1.10.2 classnames: 2.3.2 codemirror: 5.65.0 copy-to-clipboard: 3.3.2 @@ -1480,7 +1480,7 @@ packages: cosmiconfig-typescript-loader: 4.1.0_3owiowz3ujipd4k6pbqn3n7oui lodash: 4.17.21 resolve-from: 5.0.0 - ts-node: 10.9.1_ck2axrxkiif44rdbzjywaqjysa + ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra typescript: 4.8.3 transitivePeerDependencies: - '@swc/core' @@ -3415,8 +3415,8 @@ packages: /boolbase/1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - /bootstrap-icons/1.9.1: - resolution: {integrity: sha512-d4ZkO30MIkAhQ2nNRJqKXJVEQorALGbLWTuRxyCTJF96lRIV6imcgMehWGJUiJMJhglN0o2tqLIeDnMdiQEE9g==} + /bootstrap-icons/1.10.2: + resolution: {integrity: sha512-PTPYadRn1AMGr+QTSxe4ZCc+Wzv9DGZxbi3lNse/dajqV31n2/wl/7NX78ZpkvFgRNmH4ogdIQPQmxAfhEV6nA==} dev: false /bootstrap/5.2.1_@popperjs+core@2.11.6: @@ -3821,7 +3821,7 @@ packages: dependencies: '@types/node': 14.18.29 cosmiconfig: 7.0.1 - ts-node: 10.9.1_ck2axrxkiif44rdbzjywaqjysa + ts-node: 10.9.1_ao52im6kiihokc7tdj7weudhra typescript: 4.8.3 dev: true @@ -10186,37 +10186,6 @@ 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: From 0c1a77d52cca78ea33fc9cc47af3a6419cd49ad6 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 18 Nov 2022 16:40:09 +0800 Subject: [PATCH 0379/3337] update answer order by --- internal/repo/answer/answer_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index a21c2aa9b..8791e1b1a 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -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) From 4075739fec2cb65a9dab76525ab535e07ca827e2 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 18 Nov 2022 16:52:05 +0800 Subject: [PATCH 0380/3337] fix: repo test set notification userID to correct --- internal/repo/repo_test/notification_repo_test.go | 2 +- internal/repo/repo_test/repo_main_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/repo/repo_test/notification_repo_test.go b/internal/repo/repo_test/notification_repo_test.go index e07255a50..778b1a333 100644 --- a/internal/repo/repo_test/notification_repo_test.go +++ b/internal/repo/repo_test/notification_repo_test.go @@ -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) diff --git a/internal/repo/repo_test/repo_main_test.go b/internal/repo/repo_test/repo_main_test.go index e67223ff4..8fcd8cec6 100644 --- a/internal/repo/repo_test/repo_main_test.go +++ b/internal/repo/repo_test/repo_main_test.go @@ -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 { From fd4469f3db83d92c13824b1b4d460d28ebff5225 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 18 Nov 2022 18:23:27 +0800 Subject: [PATCH 0381/3337] refactor: separate tag common repo and tag repo --- cmd/answer/wire_gen.go | 16 +- internal/controller/tag_controller.go | 8 +- internal/repo/provider.go | 2 + internal/repo/tag/tag_repo.go | 238 +----------------- internal/repo/tag_common/tag_common_repo.go | 261 ++++++++++++++++++++ internal/schema/tag_schema.go | 2 - internal/service/follow/follow_service.go | 4 +- internal/service/object_info/object_info.go | 4 +- internal/service/search/tag.go | 4 +- internal/service/search_service.go | 2 +- internal/service/tag/tag_service.go | 54 ++-- internal/service/tag_common/tag_common.go | 83 +++---- 12 files changed, 343 insertions(+), 335 deletions(-) create mode 100644 internal/repo/tag_common/tag_common_repo.go diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index f035cbe4e..aa07fc7d2 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -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" @@ -62,7 +63,7 @@ import ( "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 +116,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, siteInfoCommonService) - objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagRepo) + tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo, siteInfoCommonService) + 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 +128,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, siteInfoCommonService) revisionRepo := revision.NewRevisionRepo(dataData, uniqueIDRepo) revisionService := revision_common.NewRevisionService(revisionRepo, userRepo) followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) - tagService := tag2.NewTagService(tagRepo, revisionService, followRepo, siteInfoCommonService) + tagService := tag2.NewTagService(tagRepo, tagCommonRepo, revisionService, followRepo, siteInfoCommonService) tagController := controller.NewTagController(tagService, 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, siteInfoCommonService) + tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, revisionService, siteInfoCommonService) collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) answerCommon := answercommon.NewAnswerCommon(answerRepo) metaRepo := meta.NewMetaRepo(dataData) @@ -155,7 +157,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) - searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) + searchService := service.NewSearchService(searchRepo, tagCommonRepo, userCommon, followRepo) searchController := controller.NewSearchController(searchService) serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService) revisionController := controller.NewRevisionController(serviceRevisionService) diff --git a/internal/controller/tag_controller.go b/internal/controller/tag_controller.go index 034bf6e4a..33791cb1a 100644 --- a/internal/controller/tag_controller.go +++ b/internal/controller/tag_controller.go @@ -7,14 +7,16 @@ 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 @@ -38,7 +40,7 @@ func (tc *TagController) SearchTagLike(ctx *gin.Context) { } userinfo := middleware.GetUserInfoFromContext(ctx) req.IsAdmin = userinfo.IsAdmin - resp, err := tc.tagService.SearchTagLike(ctx, req) + resp, err := tc.tagCommonService.SearchTagLike(ctx, req) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 54d43c100..dca7a595b 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -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, diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 02ae9c39c..36591d456 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -4,14 +4,12 @@ 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" "github.com/answerdev/answer/internal/service/siteinfo_common" - 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" - "github.com/segmentfault/pacman/log" "xorm.io/builder" ) @@ -27,8 +25,7 @@ func NewTagRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, siteInfoService *siteinfo_common.SiteInfoCommonService, - -) tagcommon.TagRepo { +) tag.TagRepo { return &tagRepo{ data: data, uniqueIDRepo: uniqueIDRepo, @@ -36,152 +33,6 @@ func NewTagRepo( } } -func (tr *tagRepo) tagRecommendStatus(ctx context.Context) bool { - tagconfig, err := tr.siteInfoService.GetSiteWrite(ctx) - if err != nil { - log.Error("siteInfoService.GetSiteWrite error", err) - return false - } - return tagconfig.RequiredTag -} - -// 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.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - 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() - } - - if !tr.tagRecommendStatus(ctx) { - tagInfo.Recommend = false - } - - return -} - -// GetTagListByName get tag list all like name -func (tr *tagRepo) 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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - return -} - -func (tr *tagRepo) 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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - return -} - -func (tr *tagRepo) 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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - 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).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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - return -} - // RemoveTag delete tag func (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) { session := tr.data.DB.Where(builder.Eq{"id": tagID}) @@ -201,16 +52,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, @@ -224,41 +65,6 @@ func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []strin return } -func (tr *tagRepo) 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 -} - -// 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() - } - if !tr.tagRecommendStatus(ctx) { - tag.Recommend = false - } - 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) @@ -267,45 +73,5 @@ func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []* if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - 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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } diff --git a/internal/repo/tag_common/tag_common_repo.go b/internal/repo/tag_common/tag_common_repo.go new file mode 100644 index 000000000..4bdaaf89d --- /dev/null +++ b/internal/repo/tag_common/tag_common_repo.go @@ -0,0 +1,261 @@ +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" + "github.com/answerdev/answer/internal/service/siteinfo_common" + tagcommon "github.com/answerdev/answer/internal/service/tag_common" + "github.com/answerdev/answer/internal/service/unique" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "xorm.io/builder" +) + +// tagCommonRepo tag repository +type tagCommonRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo + siteInfoService *siteinfo_common.SiteInfoCommonService +} + +// NewTagCommonRepo new repository +func NewTagCommonRepo( + data *data.Data, + uniqueIDRepo unique.UniqueIDRepo, + siteInfoService *siteinfo_common.SiteInfoCommonService, +) tagcommon.TagCommonRepo { + return &tagCommonRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + siteInfoService: siteInfoService, + } +} + +func (tr *tagCommonRepo) tagRecommendStatus(ctx context.Context) bool { + tagconfig, err := tr.siteInfoService.GetSiteWrite(ctx) + if err != nil { + log.Error("siteInfoService.GetSiteWrite error", err) + return false + } + return tagconfig.RequiredTag +} + +// 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + + if !tr.tagRecommendStatus(ctx) { + tagInfo.Recommend = false + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + tag.Recommend = false + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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 +} diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go index 26f0656e5..8e1b6da0f 100644 --- a/internal/schema/tag_schema.go +++ b/internal/schema/tag_schema.go @@ -194,8 +194,6 @@ type GetTagSynonymsResp 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"` } // UpdateTagSynonymReq update tag request diff --git a/internal/service/follow/follow_service.go b/internal/service/follow/follow_service.go index e383d119e..67e8221b7 100644 --- a/internal/service/follow/follow_service.go +++ b/internal/service/follow/follow_service.go @@ -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, diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go index d0749612a..aea2208d3 100644 --- a/internal/service/object_info/object_info.go +++ b/internal/service/object_info/object_info.go @@ -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, diff --git a/internal/service/search/tag.go b/internal/service/search/tag.go index 6fa59936d..2335faa51 100644 --- a/internal/service/search/tag.go +++ b/internal/service/search/tag.go @@ -14,7 +14,7 @@ import ( type TagSearch struct { repo search_common.SearchRepo - tagRepo tagcommon.TagRepo + tagRepo tagcommon.TagCommonRepo followCommon activity_common.FollowRepo page int size int @@ -25,7 +25,7 @@ type TagSearch struct { order string } -func NewTagSearch(repo search_common.SearchRepo, tagRepo tagcommon.TagRepo, followCommon activity_common.FollowRepo) *TagSearch { +func NewTagSearch(repo search_common.SearchRepo, tagRepo tagcommon.TagCommonRepo, followCommon activity_common.FollowRepo) *TagSearch { return &TagSearch{ repo: repo, tagRepo: tagRepo, diff --git a/internal/service/search_service.go b/internal/service/search_service.go index 098fac586..0839d516a 100644 --- a/internal/service/search_service.go +++ b/internal/service/search_service.go @@ -34,7 +34,7 @@ type SearchService struct { func NewSearchService( searchRepo search_common.SearchRepo, - tagRepo tagcommon.TagRepo, + tagRepo tagcommon.TagCommonRepo, userCommon *usercommon.UserCommon, followCommon activity_common.FollowRepo, ) *SearchService { diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index 641717c23..dc5729d5d 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -21,9 +21,17 @@ import ( "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 + tagRepo TagRepo + tagCommonRepo tagcommon.TagCommonRepo revisionService *revision_common.RevisionService followCommon activity_common.FollowRepo siteInfoService *siteinfo_common.SiteInfoCommonService @@ -31,34 +39,20 @@ type TagService struct { // NewTagService new tag service func NewTagService( - tagRepo tagcommon.TagRepo, + tagRepo TagRepo, + tagCommonRepo tagcommon.TagCommonRepo, revisionService *revision_common.RevisionService, followCommon activity_common.FollowRepo, siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService { return &TagService{ tagRepo: tagRepo, + tagCommonRepo: tagCommonRepo, revisionService: revisionService, followCommon: followCommon, siteInfoService: siteInfoService, } } -// SearchTagLike get tag list all -func (ts *TagService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []schema.SearchTagLikeResp, err error) { - tags, err := ts.tagRepo.GetTagListByName(ctx, req.Tag, 5, req.IsAdmin) - if err != nil { - return - } - 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 -} - // RemoveTag delete tag func (ts *TagService) RemoveTag(ctx context.Context, tagID string) (err error) { // TODO permission @@ -80,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.tagCommonRepo.GetTagByID(ctx, req.TagID) if err != nil { return err } @@ -125,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.tagCommonRepo.GetTagByID(ctx, req.ID) } else { - tagInfo, exist, err = ts.tagRepo.GetTagBySlugName(ctx, req.Name) + tagInfo, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, req.Name) } if err != nil { return nil, err @@ -139,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.tagCommonRepo.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID)) if err != nil { return nil, err } @@ -176,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.tagCommonRepo.GetTagListByIDs(ctx, objIDs) if err != nil { return nil, err } @@ -189,7 +183,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( Reserved: t.Reserved, } if t.MainTagID > 0 { - mainTag, exist, err := ts.tagRepo.GetTagByID(ctx, converter.IntToString(t.MainTagID)) + mainTag, exist, err := ts.tagCommonRepo.GetTagByID(ctx, converter.IntToString(t.MainTagID)) if err != nil { return nil, err } @@ -205,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.tagCommonRepo.GetTagByID(ctx, req.TagID) if err != nil { return } @@ -243,8 +237,6 @@ func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSyno SlugName: t.SlugName, DisplayName: t.DisplayName, MainTagSlugName: mainTagSlugName, - Recommend: t.Recommend, - Reserved: t.Reserved, }) } return @@ -256,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.tagCommonRepo.GetTagByID(ctx, req.TagID) if err != nil { return err } @@ -268,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.tagCommonRepo.GetTagListByNames(ctx, addSynonymTagList) if err != nil { return err } @@ -293,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.tagCommonRepo.AddTagList(ctx, needAddTagList) if err != nil { return err } @@ -351,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.tagCommonRepo.GetTagPage(ctx, page, pageSize, tag, req.QueryCond) if err != nil { return } diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index b37e2a839..b14b9476c 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -1,4 +1,4 @@ -package tagcommon +package tag_common import ( "context" @@ -17,22 +17,18 @@ import ( "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, 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 { @@ -48,27 +44,43 @@ 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, } } +// 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 + } + 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.tagRepo.GetRecommendTagList(ctx) + list, err := ts.tagCommonRepo.GetRecommendTagList(ctx) for _, item := range list { tags = append(tags, item.SlugName) } @@ -108,40 +120,15 @@ func (ts *TagCommonService) SetSiteWriteTag(ctx context.Context, recommendTags, return nil, nil } -// func (ts *TagCommonService) SetSiteWriteRecommendTag(ctx context.Context, tags []string, userID string) (msg string, err error) { -// err = ts.UpdateTag(ctx, tags, userID) -// if err != nil { -// return err.Error(), err -// } -// err = ts.SetTagsAttribute(ctx, tags, "recommend") -// if err != nil { -// return "", err -// } - -// return "", nil -// } - func (ts *TagCommonService) GetSiteWriteReservedTag(ctx context.Context) (tags []string, err error) { tags = make([]string, 0) - list, err := ts.tagRepo.GetReservedTagList(ctx) + list, err := ts.tagCommonRepo.GetReservedTagList(ctx) for _, item := range list { tags = append(tags, item.SlugName) } return tags, nil } -// func (ts *TagCommonService) SetSiteWriteReservedTag(ctx context.Context, tags []string, userID string) (msg string, err error) { -// err = ts.UpdateTag(ctx, tags, userID) -// if err != nil { -// return err.Error(), err -// } -// err = ts.SetTagsAttribute(ctx, tags, "reserved") -// if err != nil { -// return "", err -// } -// return "", nil -// } - // SetTagsAttribute func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, attribute string) (err error) { var tagslist []string @@ -153,11 +140,11 @@ func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, default: return } - err = ts.tagRepo.UpdateTagsAttribute(ctx, tagslist, attribute, false) + err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tagslist, attribute, false) if err != nil { return err } - err = ts.tagRepo.UpdateTagsAttribute(ctx, tags, attribute, true) + err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tags, attribute, true) if err != nil { return err } @@ -167,14 +154,14 @@ func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, // 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) + return ts.tagCommonRepo.GetTagBySlugName(ctx, tagName) } 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) + return ts.tagCommonRepo.GetTagListByNames(ctx, tagNames) } func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.TagItem) (bool, error) { @@ -201,8 +188,6 @@ func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.T return false, nil } -// - // GetObjectTag get object tag func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (objTags []*schema.TagResp, err error) { tagsInfoList, err := ts.GetObjectEntityTag(ctx, objectId) @@ -218,7 +203,7 @@ func (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId str for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } - objTags, err = ts.tagRepo.GetTagListByIDs(ctx, tagIDList) + objTags, err = ts.tagCommonRepo.GetTagListByIDs(ctx, tagIDList) if err != nil { return nil, err } @@ -253,7 +238,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.tagCommonRepo.GetTagListByIDs(ctx, tagIDList) if err != nil { return objectIDTagMap, err } @@ -297,7 +282,7 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID } // find tags name - tagListInDb, err := ts.tagRepo.GetTagListByNames(ctx, thisTagNameList) + tagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisTagNameList) if err != nil { return err } @@ -380,7 +365,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 } @@ -407,7 +392,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 } @@ -441,7 +426,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 } From d01e7f5529789dfc9092a2dc71244be7b562007f Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 18 Nov 2022 18:42:06 +0800 Subject: [PATCH 0382/3337] fix: tag selector component style adjustment --- ui/src/components/TagSelector/index.tsx | 7 +------ ui/src/index.scss | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index 3597a6a7a..c1646fa30 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -229,12 +229,7 @@ const TagSelector: FC = ({ {showRequiredTagText && tags && tags.filter((v) => v.recommend)?.length > 0 && ( - - {t('tag_required_text')} - +
{t('tag_required_text')}
)} {tags?.map((item, index) => { diff --git a/ui/src/index.scss b/ui/src/index.scss index a767e0a00..105b5b7c1 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -66,7 +66,7 @@ a { display: inline-block; font-size: 14px; background: rgba($blue-100, 0.5); - padding: 1px 7px; + padding: 0px 7px 1px; color: $blue-700; border: 1px solid transparent; &:hover { From b5068db44363d65a38160d5c635d771f3dae5b39 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 18 Nov 2022 18:46:13 +0800 Subject: [PATCH 0383/3337] fix: write page styles adjustment --- ui/src/pages/Admin/Write/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/Admin/Write/index.tsx b/ui/src/pages/Admin/Write/index.tsx index 36c8cd475..c24b41b54 100644 --- a/ui/src/pages/Admin/Write/index.tsx +++ b/ui/src/pages/Admin/Write/index.tsx @@ -43,7 +43,7 @@ const Legal: FC = () => { recommend_tags: { 'ui:widget': 'textarea', 'ui:options': { - rows: 5, + rows: 10, }, }, required_tag: { @@ -52,7 +52,7 @@ const Legal: FC = () => { reserved_tags: { 'ui:widget': 'textarea', 'ui:options': { - rows: 5, + rows: 10, }, }, }; From 696ff4a94d73655add42451493725959caf352c4 Mon Sep 17 00:00:00 2001 From: shuai Date: Fri, 18 Nov 2022 19:08:18 +0800 Subject: [PATCH 0384/3337] fix: lang --- i18n/en_US.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index faa0cf7aa..f7a7af2ec 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1163,8 +1163,8 @@ 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" + 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." From 432f8c330957dc7e3ca5d0446fa32196e1f74544 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 18 Nov 2022 19:56:10 +0800 Subject: [PATCH 0385/3337] refactor: tag repo remove site info service --- cmd/answer/wire_gen.go | 12 +-- internal/repo/tag/tag_repo.go | 12 +-- internal/repo/tag_common/tag_common_repo.go | 59 +------------ internal/service/question_service.go | 5 +- internal/service/search/tag.go | 33 +++---- internal/service/search_service.go | 6 +- internal/service/tag/tag_service.go | 46 +++++----- internal/service/tag_common/tag_common.go | 97 +++++++++++++++++---- 8 files changed, 139 insertions(+), 131 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index aa07fc7d2..d8ea06b3b 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -116,7 +116,7 @@ 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) - tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo, siteInfoCommonService) + 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) @@ -128,19 +128,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, siteInfoCommonService) + 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, tagCommonRepo, revisionService, followRepo, siteInfoCommonService) + tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService) tagController := controller.NewTagController(tagService, rankService) followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo) followController := controller.NewFollowController(followService) collectionRepo := collection.NewCollectionRepo(dataData, uniqueIDRepo) collectionGroupRepo := collection.NewCollectionGroupRepo(dataData) - tagRelRepo := tag.NewTagRelRepo(dataData) - tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, revisionService, siteInfoCommonService) collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) answerCommon := answercommon.NewAnswerCommon(answerRepo) metaRepo := meta.NewMetaRepo(dataData) @@ -157,7 +157,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData) answerController := controller.NewAnswerController(answerService, rankService, dashboardService) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) - searchService := service.NewSearchService(searchRepo, tagCommonRepo, userCommon, followRepo) + searchService := service.NewSearchService(searchRepo, tagCommonService, userCommon, followRepo) searchController := controller.NewSearchController(searchService) serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService) revisionController := controller.NewRevisionController(serviceRevisionService) diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 36591d456..4a9222e32 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -6,7 +6,6 @@ import ( "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/internal/service/tag" "github.com/answerdev/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" @@ -15,21 +14,18 @@ import ( // tagRepo tag repository type tagRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo - siteInfoService *siteinfo_common.SiteInfoCommonService + data *data.Data + uniqueIDRepo unique.UniqueIDRepo } // NewTagRepo new repository func NewTagRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, - siteInfoService *siteinfo_common.SiteInfoCommonService, ) tag.TagRepo { return &tagRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - siteInfoService: siteInfoService, + data: data, + uniqueIDRepo: uniqueIDRepo, } } diff --git a/internal/repo/tag_common/tag_common_repo.go b/internal/repo/tag_common/tag_common_repo.go index 4bdaaf89d..50f8517f2 100644 --- a/internal/repo/tag_common/tag_common_repo.go +++ b/internal/repo/tag_common/tag_common_repo.go @@ -7,43 +7,29 @@ import ( "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/siteinfo_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" "xorm.io/builder" ) // tagCommonRepo tag repository type tagCommonRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo - siteInfoService *siteinfo_common.SiteInfoCommonService + data *data.Data + uniqueIDRepo unique.UniqueIDRepo } // NewTagCommonRepo new repository func NewTagCommonRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, - siteInfoService *siteinfo_common.SiteInfoCommonService, ) tagcommon.TagCommonRepo { return &tagCommonRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - siteInfoService: siteInfoService, + data: data, + uniqueIDRepo: uniqueIDRepo, } } -func (tr *tagCommonRepo) tagRecommendStatus(ctx context.Context) bool { - tagconfig, err := tr.siteInfoService.GetSiteWrite(ctx) - if err != nil { - log.Error("siteInfoService.GetSiteWrite error", err) - return false - } - return tagconfig.RequiredTag -} - // GetTagListByIDs get tag list all func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) { tagList = make([]*entity.Tag, 0) @@ -53,11 +39,6 @@ func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tag if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -70,10 +51,6 @@ func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - - if !tr.tagRecommendStatus(ctx) { - tagInfo.Recommend = false - } return } @@ -99,11 +76,6 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limi if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -119,11 +91,6 @@ func (tr *tagCommonRepo) GetRecommendTagList(ctx context.Context) (tagList []*en if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -139,11 +106,6 @@ func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*ent if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -156,11 +118,6 @@ func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -175,9 +132,6 @@ func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string) ( if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - tag.Recommend = false - } return } @@ -208,11 +162,6 @@ func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 437d7936d..6f36fbe78 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -539,14 +539,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 != "" { diff --git a/internal/service/search/tag.go b/internal/service/search/tag.go index 2335faa51..806880f05 100644 --- a/internal/service/search/tag.go +++ b/internal/service/search/tag.go @@ -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.TagCommonRepo - 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.TagCommonRepo, 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 } diff --git a/internal/service/search_service.go b/internal/service/search_service.go index 0839d516a..582984add 100644 --- a/internal/service/search_service.go +++ b/internal/service/search_service.go @@ -7,7 +7,7 @@ import ( "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" + "github.com/answerdev/answer/internal/service/tag_common" usercommon "github.com/answerdev/answer/internal/service/user_common" ) @@ -34,13 +34,13 @@ type SearchService struct { func NewSearchService( searchRepo search_common.SearchRepo, - tagRepo tagcommon.TagCommonRepo, + tagCommonService *tag_common.TagCommonService, userCommon *usercommon.UserCommon, followCommon activity_common.FollowRepo, ) *SearchService { return &SearchService{ searchRepo: searchRepo, - tagSearch: search.NewTagSearch(searchRepo, tagRepo, followCommon), + tagSearch: search.NewTagSearch(searchRepo, tagCommonService, followCommon), withinSearch: search.NewWithinSearch(searchRepo), authorSearch: search.NewAuthorSearch(searchRepo, userCommon), scoreSearch: search.NewScoreSearch(searchRepo), diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index dc5729d5d..dcf020294 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -6,6 +6,7 @@ import ( "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" @@ -14,7 +15,6 @@ 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" @@ -30,26 +30,26 @@ type TagRepo interface { // TagService user service type TagService struct { - tagRepo TagRepo - tagCommonRepo tagcommon.TagCommonRepo - revisionService *revision_common.RevisionService - followCommon activity_common.FollowRepo - siteInfoService *siteinfo_common.SiteInfoCommonService + 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 TagRepo, - tagCommonRepo tagcommon.TagCommonRepo, + tagCommonService *tag_common.TagCommonService, revisionService *revision_common.RevisionService, followCommon activity_common.FollowRepo, siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService { return &TagService{ - tagRepo: tagRepo, - tagCommonRepo: tagCommonRepo, - revisionService: revisionService, - followCommon: followCommon, - siteInfoService: siteInfoService, + tagRepo: tagRepo, + tagCommonService: tagCommonService, + revisionService: revisionService, + followCommon: followCommon, + siteInfoService: siteInfoService, } } @@ -74,7 +74,7 @@ func (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) ( return err } - tagInfo, exist, err := ts.tagCommonRepo.GetTagByID(ctx, req.TagID) + tagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return err } @@ -119,9 +119,9 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq) exist bool ) if len(req.ID) > 0 { - tagInfo, exist, err = ts.tagCommonRepo.GetTagByID(ctx, req.ID) + tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, req.ID) } else { - tagInfo, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, req.Name) + tagInfo, exist, err = ts.tagCommonService.GetTagBySlugName(ctx, req.Name) } if err != nil { return nil, err @@ -133,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.tagCommonRepo.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID)) + tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID)) if err != nil { return nil, err } @@ -170,7 +170,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( if err != nil { return nil, err } - tagList, err := ts.tagCommonRepo.GetTagListByIDs(ctx, objIDs) + tagList, err := ts.tagCommonService.GetTagListByIDs(ctx, objIDs) if err != nil { return nil, err } @@ -183,7 +183,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( Reserved: t.Reserved, } if t.MainTagID > 0 { - mainTag, exist, err := ts.tagCommonRepo.GetTagByID(ctx, converter.IntToString(t.MainTagID)) + mainTag, exist, err := ts.tagCommonService.GetTagByID(ctx, converter.IntToString(t.MainTagID)) if err != nil { return nil, err } @@ -199,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.tagCommonRepo.GetTagByID(ctx, req.TagID) + tag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return } @@ -248,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.tagCommonRepo.GetTagByID(ctx, req.TagID) + mainTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return err } @@ -260,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.tagCommonRepo.GetTagListByNames(ctx, addSynonymTagList) + tagListInDB, err := ts.tagCommonService.GetTagListByNames(ctx, addSynonymTagList) if err != nil { return err } @@ -285,7 +285,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa } if len(needAddTagList) > 0 { - err = ts.tagCommonRepo.AddTagList(ctx, needAddTagList) + err = ts.tagCommonService.AddTagList(ctx, needAddTagList) if err != nil { return err } @@ -343,7 +343,7 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith page := req.Page pageSize := req.PageSize - tags, total, err := ts.tagCommonRepo.GetTagPage(ctx, page, pageSize, tag, req.QueryCond) + tags, total, err := ts.tagCommonService.GetTagPage(ctx, page, pageSize, tag, req.QueryCond) if err != nil { return } diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index b14b9476c..dda1c9238 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -68,6 +68,7 @@ func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.Searc if err != nil { return } + ts.tagsFormatRecommendAndReserved(ctx, tags) for _, tag := range tags { item := schema.SearchTagLikeResp{} item.SlugName = tag.SlugName @@ -151,17 +152,16 @@ func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, return nil } -// GetTagListByName -func (ts *TagCommonService) GetTagListByName(ctx context.Context, tagName string) (tagInfo *entity.Tag, exist bool, err error) { - tagName = strings.ToLower(tagName) - return ts.tagCommonRepo.GetTagBySlugName(ctx, tagName) -} - func (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []string) ([]*entity.Tag, error) { for k, tagname := range tagNames { tagNames[k] = strings.ToLower(tagname) } - return ts.tagCommonRepo.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) { @@ -194,6 +194,52 @@ func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) ( 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) @@ -203,11 +249,10 @@ func (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId str for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } - objTags, err = ts.tagCommonRepo.GetTagListByIDs(ctx, tagIDList) + objTags, err = ts.GetTagListByIDs(ctx, tagIDList) if err != nil { return nil, err } - return objTags, nil } @@ -225,6 +270,30 @@ func (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) ( return objTags, nil } +func (ts *TagCommonService) tagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) { + 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) { + 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) @@ -238,7 +307,7 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } - tagsInfoList, err := ts.tagCommonRepo.GetTagListByIDs(ctx, tagIDList) + tagsInfoList, err := ts.GetTagListByIDs(ctx, tagIDList) if err != nil { return objectIDTagMap, err } @@ -275,14 +344,8 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID return nil } - thisTagNameList := make([]string, 0) - for _, t := range tags { - t = strings.ToLower(t) - thisTagNameList = append(thisTagNameList, t) - } - // find tags name - tagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisTagNameList) + tagListInDb, err := ts.GetTagListByNames(ctx, tags) if err != nil { return err } From 7868adcd7ebcd4e3b970a604fde78ac2d3f21c18 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 18 Nov 2022 20:11:19 +0800 Subject: [PATCH 0386/3337] feat: add tag common repo unit test --- Dockerfile | 2 +- Makefile | 2 +- internal/repo/repo_test/tag_repo_test.go | 48 +++++++++++++----------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index ac06839a8..e0f1eb3e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ COPY . ${BUILD_DIR} WORKDIR ${BUILD_DIR} COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build RUN apk --no-cache add build-base git \ - && make clean build \ + && make clean test build \ && cp answer /usr/bin/answer RUN mkdir -p /data/uploads && chmod 777 /data/uploads \ diff --git a/Makefile b/Makefile index f98526b7e..1ecff1c9c 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ generate: go mod tidy test: - @$(GO) test ./... + @$(GO) test ./internal/repo/repo_test # clean all build result clean: diff --git a/internal/repo/repo_test/tag_repo_test.go b/internal/repo/repo_test/tag_repo_test.go index 9c2e012a6..d659651cd 100644 --- a/internal/repo/repo_test/tag_repo_test.go +++ b/internal/repo/repo_test/tag_repo_test.go @@ -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)) From d7de1f2468e34c0d06b185f25ddb176c38514928 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Sat, 19 Nov 2022 13:39:37 +0800 Subject: [PATCH 0387/3337] chore: update agreements, fix some issue dashbaord --- i18n/zh_CN.yaml | 2 +- ui/package.json | 1 + ui/pnpm-lock.yaml | 65 +++++++++++++++---- .../components/HealthStatus/index.tsx | 11 +++- ui/src/pages/Legal/Privacy/index.tsx | 2 +- ui/src/pages/Legal/Tos/index.tsx | 2 +- ui/src/pages/Legal/index.scss | 4 -- ui/src/pages/Legal/index.tsx | 31 +++++---- ui/src/pages/Users/Settings/Profile/index.tsx | 1 - 9 files changed, 85 insertions(+), 34 deletions(-) delete mode 100644 ui/src/pages/Legal/index.scss diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 24ed4dc49..1493e29e1 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -571,7 +571,7 @@ ui: character: '用户名只能由 "a-z", "0-9", " - . _" 组成' avatar: label: 头像 - text: 您可以上传图片作为头像,也可以 <1>重置 为 + text: 您可以上传图片作为头像。 bio: label: 关于我 (可选) website: diff --git a/ui/package.json b/ui/package.json index 4dd402630..246cd3d37 100644 --- a/ui/package.json +++ b/ui/package.json @@ -33,6 +33,7 @@ "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" }, diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 78e838351..2b7ee3b51 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -59,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 @@ -86,6 +87,7 @@ 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 @@ -1480,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' @@ -2667,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: @@ -2748,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: @@ -3472,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: @@ -3821,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 @@ -3905,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: @@ -5272,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: @@ -5736,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 @@ -6908,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 @@ -7615,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 @@ -8219,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: @@ -9044,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 @@ -9491,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==} @@ -10186,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: diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx index 2193df176..2d6c57004 100644 --- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -3,6 +3,8 @@ import { Card, Row, Col, Badge } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; +import { gt, gte } from 'semver'; + import type * as Type from '@/common/interface'; interface IProps { @@ -12,7 +14,12 @@ interface IProps { const HealthStatus: FC = ({ data }) => { const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); const { version, remote_version } = data.version_info || {}; - const isLatest = version === remote_version; + let isLatest = false; + let hasNewerVersion = false; + if (version && remote_version) { + isLatest = gte(version, remote_version); + hasNewerVersion = gt(remote_version, version); + } return ( @@ -32,7 +39,7 @@ const HealthStatus: FC = ({ data }) => { {t('latest')} )} - {!isLatest && remote_version && ( + {!isLatest && hasNewerVersion && ( { return ( <> -

{t('privacy')}

+

{t('privacy')}

{ return ( <> -

{t('tos')}

+

{t('tos')}

{ + const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' }); return ( - - -
- + + + + - + diff --git a/ui/src/pages/Users/Settings/Profile/index.tsx b/ui/src/pages/Users/Settings/Profile/index.tsx index 03d5639fc..aa3dd84b2 100644 --- a/ui/src/pages/Users/Settings/Profile/index.tsx +++ b/ui/src/pages/Users/Settings/Profile/index.tsx @@ -196,7 +196,6 @@ const Index: React.FC = () => { if (res.e_mail) { const str = res.e_mail.toLowerCase().trim(); const hash = MD5(str); - console.log(str, hash, mailHash); setMailHash(hash); } }); From a0b0adde8d9f21addc09056297bff4f3bc1de9c8 Mon Sep 17 00:00:00 2001 From: aichy126 <16996097+aichy126@users.noreply.github.com> Date: Fri, 18 Nov 2022 16:40:09 +0800 Subject: [PATCH 0388/3337] update answer order by --- internal/repo/answer/answer_repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index a21c2aa9b..8791e1b1a 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -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) From d833cadae25de5b911043b5b5694805e396b0b6a Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 18 Nov 2022 16:52:05 +0800 Subject: [PATCH 0389/3337] fix: repo test set notification userID to correct --- internal/repo/repo_test/notification_repo_test.go | 2 +- internal/repo/repo_test/repo_main_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/repo/repo_test/notification_repo_test.go b/internal/repo/repo_test/notification_repo_test.go index e07255a50..778b1a333 100644 --- a/internal/repo/repo_test/notification_repo_test.go +++ b/internal/repo/repo_test/notification_repo_test.go @@ -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) diff --git a/internal/repo/repo_test/repo_main_test.go b/internal/repo/repo_test/repo_main_test.go index e67223ff4..8fcd8cec6 100644 --- a/internal/repo/repo_test/repo_main_test.go +++ b/internal/repo/repo_test/repo_main_test.go @@ -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 { From 0467165264c3813d6cddf6efd4b64bd79dcb421c Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 18 Nov 2022 18:23:27 +0800 Subject: [PATCH 0390/3337] refactor: separate tag common repo and tag repo --- internal/controller/tag_controller.go | 8 +- internal/repo/provider.go | 2 + internal/repo/tag/tag_repo.go | 238 +----------------- internal/repo/tag_common/tag_common_repo.go | 261 ++++++++++++++++++++ internal/schema/tag_schema.go | 2 - internal/service/follow/follow_service.go | 4 +- internal/service/object_info/object_info.go | 4 +- internal/service/search/tag.go | 4 +- internal/service/tag/tag_service.go | 54 ++-- internal/service/tag_common/tag_common.go | 83 +++---- 10 files changed, 333 insertions(+), 327 deletions(-) create mode 100644 internal/repo/tag_common/tag_common_repo.go diff --git a/internal/controller/tag_controller.go b/internal/controller/tag_controller.go index 034bf6e4a..33791cb1a 100644 --- a/internal/controller/tag_controller.go +++ b/internal/controller/tag_controller.go @@ -7,14 +7,16 @@ 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 @@ -38,7 +40,7 @@ func (tc *TagController) SearchTagLike(ctx *gin.Context) { } userinfo := middleware.GetUserInfoFromContext(ctx) req.IsAdmin = userinfo.IsAdmin - resp, err := tc.tagService.SearchTagLike(ctx, req) + resp, err := tc.tagCommonService.SearchTagLike(ctx, req) handler.HandleResponse(ctx, err, resp) } diff --git a/internal/repo/provider.go b/internal/repo/provider.go index 54d43c100..dca7a595b 100644 --- a/internal/repo/provider.go +++ b/internal/repo/provider.go @@ -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, diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 02ae9c39c..36591d456 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -4,14 +4,12 @@ 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" "github.com/answerdev/answer/internal/service/siteinfo_common" - 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" - "github.com/segmentfault/pacman/log" "xorm.io/builder" ) @@ -27,8 +25,7 @@ func NewTagRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, siteInfoService *siteinfo_common.SiteInfoCommonService, - -) tagcommon.TagRepo { +) tag.TagRepo { return &tagRepo{ data: data, uniqueIDRepo: uniqueIDRepo, @@ -36,152 +33,6 @@ func NewTagRepo( } } -func (tr *tagRepo) tagRecommendStatus(ctx context.Context) bool { - tagconfig, err := tr.siteInfoService.GetSiteWrite(ctx) - if err != nil { - log.Error("siteInfoService.GetSiteWrite error", err) - return false - } - return tagconfig.RequiredTag -} - -// 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.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList) - if err != nil { - err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - 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() - } - - if !tr.tagRecommendStatus(ctx) { - tagInfo.Recommend = false - } - - return -} - -// GetTagListByName get tag list all like name -func (tr *tagRepo) 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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - return -} - -func (tr *tagRepo) 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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - return -} - -func (tr *tagRepo) 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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - 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).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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - return -} - // RemoveTag delete tag func (tr *tagRepo) RemoveTag(ctx context.Context, tagID string) (err error) { session := tr.data.DB.Where(builder.Eq{"id": tagID}) @@ -201,16 +52,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, @@ -224,41 +65,6 @@ func (tr *tagRepo) UpdateTagSynonym(ctx context.Context, tagSlugNameList []strin return } -func (tr *tagRepo) 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 -} - -// 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() - } - if !tr.tagRecommendStatus(ctx) { - tag.Recommend = false - } - 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) @@ -267,45 +73,5 @@ func (tr *tagRepo) GetTagList(ctx context.Context, tag *entity.Tag) (tagList []* if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } - 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() - } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } diff --git a/internal/repo/tag_common/tag_common_repo.go b/internal/repo/tag_common/tag_common_repo.go new file mode 100644 index 000000000..4bdaaf89d --- /dev/null +++ b/internal/repo/tag_common/tag_common_repo.go @@ -0,0 +1,261 @@ +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" + "github.com/answerdev/answer/internal/service/siteinfo_common" + tagcommon "github.com/answerdev/answer/internal/service/tag_common" + "github.com/answerdev/answer/internal/service/unique" + "github.com/segmentfault/pacman/errors" + "github.com/segmentfault/pacman/log" + "xorm.io/builder" +) + +// tagCommonRepo tag repository +type tagCommonRepo struct { + data *data.Data + uniqueIDRepo unique.UniqueIDRepo + siteInfoService *siteinfo_common.SiteInfoCommonService +} + +// NewTagCommonRepo new repository +func NewTagCommonRepo( + data *data.Data, + uniqueIDRepo unique.UniqueIDRepo, + siteInfoService *siteinfo_common.SiteInfoCommonService, +) tagcommon.TagCommonRepo { + return &tagCommonRepo{ + data: data, + uniqueIDRepo: uniqueIDRepo, + siteInfoService: siteInfoService, + } +} + +func (tr *tagCommonRepo) tagRecommendStatus(ctx context.Context) bool { + tagconfig, err := tr.siteInfoService.GetSiteWrite(ctx) + if err != nil { + log.Error("siteInfoService.GetSiteWrite error", err) + return false + } + return tagconfig.RequiredTag +} + +// 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + + if !tr.tagRecommendStatus(ctx) { + tagInfo.Recommend = false + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + tag.Recommend = false + } + 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() + } + if !tr.tagRecommendStatus(ctx) { + for _, tag := range tagList { + tag.Recommend = false + } + } + 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 +} diff --git a/internal/schema/tag_schema.go b/internal/schema/tag_schema.go index 26f0656e5..8e1b6da0f 100644 --- a/internal/schema/tag_schema.go +++ b/internal/schema/tag_schema.go @@ -194,8 +194,6 @@ type GetTagSynonymsResp 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"` } // UpdateTagSynonymReq update tag request diff --git a/internal/service/follow/follow_service.go b/internal/service/follow/follow_service.go index e383d119e..67e8221b7 100644 --- a/internal/service/follow/follow_service.go +++ b/internal/service/follow/follow_service.go @@ -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, diff --git a/internal/service/object_info/object_info.go b/internal/service/object_info/object_info.go index d0749612a..aea2208d3 100644 --- a/internal/service/object_info/object_info.go +++ b/internal/service/object_info/object_info.go @@ -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, diff --git a/internal/service/search/tag.go b/internal/service/search/tag.go index 6fa59936d..2335faa51 100644 --- a/internal/service/search/tag.go +++ b/internal/service/search/tag.go @@ -14,7 +14,7 @@ import ( type TagSearch struct { repo search_common.SearchRepo - tagRepo tagcommon.TagRepo + tagRepo tagcommon.TagCommonRepo followCommon activity_common.FollowRepo page int size int @@ -25,7 +25,7 @@ type TagSearch struct { order string } -func NewTagSearch(repo search_common.SearchRepo, tagRepo tagcommon.TagRepo, followCommon activity_common.FollowRepo) *TagSearch { +func NewTagSearch(repo search_common.SearchRepo, tagRepo tagcommon.TagCommonRepo, followCommon activity_common.FollowRepo) *TagSearch { return &TagSearch{ repo: repo, tagRepo: tagRepo, diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index 641717c23..dc5729d5d 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -21,9 +21,17 @@ import ( "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 + tagRepo TagRepo + tagCommonRepo tagcommon.TagCommonRepo revisionService *revision_common.RevisionService followCommon activity_common.FollowRepo siteInfoService *siteinfo_common.SiteInfoCommonService @@ -31,34 +39,20 @@ type TagService struct { // NewTagService new tag service func NewTagService( - tagRepo tagcommon.TagRepo, + tagRepo TagRepo, + tagCommonRepo tagcommon.TagCommonRepo, revisionService *revision_common.RevisionService, followCommon activity_common.FollowRepo, siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService { return &TagService{ tagRepo: tagRepo, + tagCommonRepo: tagCommonRepo, revisionService: revisionService, followCommon: followCommon, siteInfoService: siteInfoService, } } -// SearchTagLike get tag list all -func (ts *TagService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []schema.SearchTagLikeResp, err error) { - tags, err := ts.tagRepo.GetTagListByName(ctx, req.Tag, 5, req.IsAdmin) - if err != nil { - return - } - 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 -} - // RemoveTag delete tag func (ts *TagService) RemoveTag(ctx context.Context, tagID string) (err error) { // TODO permission @@ -80,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.tagCommonRepo.GetTagByID(ctx, req.TagID) if err != nil { return err } @@ -125,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.tagCommonRepo.GetTagByID(ctx, req.ID) } else { - tagInfo, exist, err = ts.tagRepo.GetTagBySlugName(ctx, req.Name) + tagInfo, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, req.Name) } if err != nil { return nil, err @@ -139,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.tagCommonRepo.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID)) if err != nil { return nil, err } @@ -176,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.tagCommonRepo.GetTagListByIDs(ctx, objIDs) if err != nil { return nil, err } @@ -189,7 +183,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( Reserved: t.Reserved, } if t.MainTagID > 0 { - mainTag, exist, err := ts.tagRepo.GetTagByID(ctx, converter.IntToString(t.MainTagID)) + mainTag, exist, err := ts.tagCommonRepo.GetTagByID(ctx, converter.IntToString(t.MainTagID)) if err != nil { return nil, err } @@ -205,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.tagCommonRepo.GetTagByID(ctx, req.TagID) if err != nil { return } @@ -243,8 +237,6 @@ func (ts *TagService) GetTagSynonyms(ctx context.Context, req *schema.GetTagSyno SlugName: t.SlugName, DisplayName: t.DisplayName, MainTagSlugName: mainTagSlugName, - Recommend: t.Recommend, - Reserved: t.Reserved, }) } return @@ -256,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.tagCommonRepo.GetTagByID(ctx, req.TagID) if err != nil { return err } @@ -268,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.tagCommonRepo.GetTagListByNames(ctx, addSynonymTagList) if err != nil { return err } @@ -293,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.tagCommonRepo.AddTagList(ctx, needAddTagList) if err != nil { return err } @@ -351,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.tagCommonRepo.GetTagPage(ctx, page, pageSize, tag, req.QueryCond) if err != nil { return } diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index b37e2a839..b14b9476c 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -1,4 +1,4 @@ -package tagcommon +package tag_common import ( "context" @@ -17,22 +17,18 @@ import ( "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, 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 { @@ -48,27 +44,43 @@ 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, } } +// 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 + } + 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.tagRepo.GetRecommendTagList(ctx) + list, err := ts.tagCommonRepo.GetRecommendTagList(ctx) for _, item := range list { tags = append(tags, item.SlugName) } @@ -108,40 +120,15 @@ func (ts *TagCommonService) SetSiteWriteTag(ctx context.Context, recommendTags, return nil, nil } -// func (ts *TagCommonService) SetSiteWriteRecommendTag(ctx context.Context, tags []string, userID string) (msg string, err error) { -// err = ts.UpdateTag(ctx, tags, userID) -// if err != nil { -// return err.Error(), err -// } -// err = ts.SetTagsAttribute(ctx, tags, "recommend") -// if err != nil { -// return "", err -// } - -// return "", nil -// } - func (ts *TagCommonService) GetSiteWriteReservedTag(ctx context.Context) (tags []string, err error) { tags = make([]string, 0) - list, err := ts.tagRepo.GetReservedTagList(ctx) + list, err := ts.tagCommonRepo.GetReservedTagList(ctx) for _, item := range list { tags = append(tags, item.SlugName) } return tags, nil } -// func (ts *TagCommonService) SetSiteWriteReservedTag(ctx context.Context, tags []string, userID string) (msg string, err error) { -// err = ts.UpdateTag(ctx, tags, userID) -// if err != nil { -// return err.Error(), err -// } -// err = ts.SetTagsAttribute(ctx, tags, "reserved") -// if err != nil { -// return "", err -// } -// return "", nil -// } - // SetTagsAttribute func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, attribute string) (err error) { var tagslist []string @@ -153,11 +140,11 @@ func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, default: return } - err = ts.tagRepo.UpdateTagsAttribute(ctx, tagslist, attribute, false) + err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tagslist, attribute, false) if err != nil { return err } - err = ts.tagRepo.UpdateTagsAttribute(ctx, tags, attribute, true) + err = ts.tagCommonRepo.UpdateTagsAttribute(ctx, tags, attribute, true) if err != nil { return err } @@ -167,14 +154,14 @@ func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, // 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) + return ts.tagCommonRepo.GetTagBySlugName(ctx, tagName) } 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) + return ts.tagCommonRepo.GetTagListByNames(ctx, tagNames) } func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.TagItem) (bool, error) { @@ -201,8 +188,6 @@ func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.T return false, nil } -// - // GetObjectTag get object tag func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) (objTags []*schema.TagResp, err error) { tagsInfoList, err := ts.GetObjectEntityTag(ctx, objectId) @@ -218,7 +203,7 @@ func (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId str for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } - objTags, err = ts.tagRepo.GetTagListByIDs(ctx, tagIDList) + objTags, err = ts.tagCommonRepo.GetTagListByIDs(ctx, tagIDList) if err != nil { return nil, err } @@ -253,7 +238,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.tagCommonRepo.GetTagListByIDs(ctx, tagIDList) if err != nil { return objectIDTagMap, err } @@ -297,7 +282,7 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID } // find tags name - tagListInDb, err := ts.tagRepo.GetTagListByNames(ctx, thisTagNameList) + tagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisTagNameList) if err != nil { return err } @@ -380,7 +365,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 } @@ -407,7 +392,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 } @@ -441,7 +426,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 } From dc1f83f819c9d4d31d730ac6532802e0748c58f6 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 18 Nov 2022 19:56:10 +0800 Subject: [PATCH 0391/3337] refactor: tag repo remove site info service --- internal/repo/tag/tag_repo.go | 12 +-- internal/repo/tag_common/tag_common_repo.go | 59 +------------ internal/service/question_service.go | 5 +- internal/service/search/tag.go | 33 +++---- internal/service/tag/tag_service.go | 46 +++++----- internal/service/tag_common/tag_common.go | 97 +++++++++++++++++---- 6 files changed, 130 insertions(+), 122 deletions(-) diff --git a/internal/repo/tag/tag_repo.go b/internal/repo/tag/tag_repo.go index 36591d456..4a9222e32 100644 --- a/internal/repo/tag/tag_repo.go +++ b/internal/repo/tag/tag_repo.go @@ -6,7 +6,6 @@ import ( "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/siteinfo_common" "github.com/answerdev/answer/internal/service/tag" "github.com/answerdev/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" @@ -15,21 +14,18 @@ import ( // tagRepo tag repository type tagRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo - siteInfoService *siteinfo_common.SiteInfoCommonService + data *data.Data + uniqueIDRepo unique.UniqueIDRepo } // NewTagRepo new repository func NewTagRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, - siteInfoService *siteinfo_common.SiteInfoCommonService, ) tag.TagRepo { return &tagRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - siteInfoService: siteInfoService, + data: data, + uniqueIDRepo: uniqueIDRepo, } } diff --git a/internal/repo/tag_common/tag_common_repo.go b/internal/repo/tag_common/tag_common_repo.go index 4bdaaf89d..50f8517f2 100644 --- a/internal/repo/tag_common/tag_common_repo.go +++ b/internal/repo/tag_common/tag_common_repo.go @@ -7,43 +7,29 @@ import ( "github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/entity" - "github.com/answerdev/answer/internal/service/siteinfo_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/unique" "github.com/segmentfault/pacman/errors" - "github.com/segmentfault/pacman/log" "xorm.io/builder" ) // tagCommonRepo tag repository type tagCommonRepo struct { - data *data.Data - uniqueIDRepo unique.UniqueIDRepo - siteInfoService *siteinfo_common.SiteInfoCommonService + data *data.Data + uniqueIDRepo unique.UniqueIDRepo } // NewTagCommonRepo new repository func NewTagCommonRepo( data *data.Data, uniqueIDRepo unique.UniqueIDRepo, - siteInfoService *siteinfo_common.SiteInfoCommonService, ) tagcommon.TagCommonRepo { return &tagCommonRepo{ - data: data, - uniqueIDRepo: uniqueIDRepo, - siteInfoService: siteInfoService, + data: data, + uniqueIDRepo: uniqueIDRepo, } } -func (tr *tagCommonRepo) tagRecommendStatus(ctx context.Context) bool { - tagconfig, err := tr.siteInfoService.GetSiteWrite(ctx) - if err != nil { - log.Error("siteInfoService.GetSiteWrite error", err) - return false - } - return tagconfig.RequiredTag -} - // GetTagListByIDs get tag list all func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error) { tagList = make([]*entity.Tag, 0) @@ -53,11 +39,6 @@ func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tag if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -70,10 +51,6 @@ func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - - if !tr.tagRecommendStatus(ctx) { - tagInfo.Recommend = false - } return } @@ -99,11 +76,6 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limi if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -119,11 +91,6 @@ func (tr *tagCommonRepo) GetRecommendTagList(ctx context.Context) (tagList []*en if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -139,11 +106,6 @@ func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*ent if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -156,11 +118,6 @@ func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } @@ -175,9 +132,6 @@ func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string) ( if err != nil { return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - tag.Recommend = false - } return } @@ -208,11 +162,6 @@ func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() } - if !tr.tagRecommendStatus(ctx) { - for _, tag := range tagList { - tag.Recommend = false - } - } return } diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 437d7936d..6f36fbe78 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -539,14 +539,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 != "" { diff --git a/internal/service/search/tag.go b/internal/service/search/tag.go index 2335faa51..806880f05 100644 --- a/internal/service/search/tag.go +++ b/internal/service/search/tag.go @@ -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.TagCommonRepo - 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.TagCommonRepo, 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 } diff --git a/internal/service/tag/tag_service.go b/internal/service/tag/tag_service.go index dc5729d5d..dcf020294 100644 --- a/internal/service/tag/tag_service.go +++ b/internal/service/tag/tag_service.go @@ -6,6 +6,7 @@ import ( "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" @@ -14,7 +15,6 @@ 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" @@ -30,26 +30,26 @@ type TagRepo interface { // TagService user service type TagService struct { - tagRepo TagRepo - tagCommonRepo tagcommon.TagCommonRepo - revisionService *revision_common.RevisionService - followCommon activity_common.FollowRepo - siteInfoService *siteinfo_common.SiteInfoCommonService + 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 TagRepo, - tagCommonRepo tagcommon.TagCommonRepo, + tagCommonService *tag_common.TagCommonService, revisionService *revision_common.RevisionService, followCommon activity_common.FollowRepo, siteInfoService *siteinfo_common.SiteInfoCommonService) *TagService { return &TagService{ - tagRepo: tagRepo, - tagCommonRepo: tagCommonRepo, - revisionService: revisionService, - followCommon: followCommon, - siteInfoService: siteInfoService, + tagRepo: tagRepo, + tagCommonService: tagCommonService, + revisionService: revisionService, + followCommon: followCommon, + siteInfoService: siteInfoService, } } @@ -74,7 +74,7 @@ func (ts *TagService) UpdateTag(ctx context.Context, req *schema.UpdateTagReq) ( return err } - tagInfo, exist, err := ts.tagCommonRepo.GetTagByID(ctx, req.TagID) + tagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return err } @@ -119,9 +119,9 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq) exist bool ) if len(req.ID) > 0 { - tagInfo, exist, err = ts.tagCommonRepo.GetTagByID(ctx, req.ID) + tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, req.ID) } else { - tagInfo, exist, err = ts.tagCommonRepo.GetTagBySlugName(ctx, req.Name) + tagInfo, exist, err = ts.tagCommonService.GetTagBySlugName(ctx, req.Name) } if err != nil { return nil, err @@ -133,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.tagCommonRepo.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID)) + tagInfo, exist, err = ts.tagCommonService.GetTagByID(ctx, converter.IntToString(tagInfo.MainTagID)) if err != nil { return nil, err } @@ -170,7 +170,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( if err != nil { return nil, err } - tagList, err := ts.tagCommonRepo.GetTagListByIDs(ctx, objIDs) + tagList, err := ts.tagCommonService.GetTagListByIDs(ctx, objIDs) if err != nil { return nil, err } @@ -183,7 +183,7 @@ func (ts *TagService) GetFollowingTags(ctx context.Context, userID string) ( Reserved: t.Reserved, } if t.MainTagID > 0 { - mainTag, exist, err := ts.tagCommonRepo.GetTagByID(ctx, converter.IntToString(t.MainTagID)) + mainTag, exist, err := ts.tagCommonService.GetTagByID(ctx, converter.IntToString(t.MainTagID)) if err != nil { return nil, err } @@ -199,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.tagCommonRepo.GetTagByID(ctx, req.TagID) + tag, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return } @@ -248,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.tagCommonRepo.GetTagByID(ctx, req.TagID) + mainTagInfo, exist, err := ts.tagCommonService.GetTagByID(ctx, req.TagID) if err != nil { return err } @@ -260,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.tagCommonRepo.GetTagListByNames(ctx, addSynonymTagList) + tagListInDB, err := ts.tagCommonService.GetTagListByNames(ctx, addSynonymTagList) if err != nil { return err } @@ -285,7 +285,7 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa } if len(needAddTagList) > 0 { - err = ts.tagCommonRepo.AddTagList(ctx, needAddTagList) + err = ts.tagCommonService.AddTagList(ctx, needAddTagList) if err != nil { return err } @@ -343,7 +343,7 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith page := req.Page pageSize := req.PageSize - tags, total, err := ts.tagCommonRepo.GetTagPage(ctx, page, pageSize, tag, req.QueryCond) + tags, total, err := ts.tagCommonService.GetTagPage(ctx, page, pageSize, tag, req.QueryCond) if err != nil { return } diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index b14b9476c..dda1c9238 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -68,6 +68,7 @@ func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.Searc if err != nil { return } + ts.tagsFormatRecommendAndReserved(ctx, tags) for _, tag := range tags { item := schema.SearchTagLikeResp{} item.SlugName = tag.SlugName @@ -151,17 +152,16 @@ func (ts *TagCommonService) SetTagsAttribute(ctx context.Context, tags []string, return nil } -// GetTagListByName -func (ts *TagCommonService) GetTagListByName(ctx context.Context, tagName string) (tagInfo *entity.Tag, exist bool, err error) { - tagName = strings.ToLower(tagName) - return ts.tagCommonRepo.GetTagBySlugName(ctx, tagName) -} - func (ts *TagCommonService) GetTagListByNames(ctx context.Context, tagNames []string) ([]*entity.Tag, error) { for k, tagname := range tagNames { tagNames[k] = strings.ToLower(tagname) } - return ts.tagCommonRepo.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) { @@ -194,6 +194,52 @@ func (ts *TagCommonService) GetObjectTag(ctx context.Context, objectId string) ( 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) @@ -203,11 +249,10 @@ func (ts *TagCommonService) GetObjectEntityTag(ctx context.Context, objectId str for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } - objTags, err = ts.tagCommonRepo.GetTagListByIDs(ctx, tagIDList) + objTags, err = ts.GetTagListByIDs(ctx, tagIDList) if err != nil { return nil, err } - return objTags, nil } @@ -225,6 +270,30 @@ func (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) ( return objTags, nil } +func (ts *TagCommonService) tagsFormatRecommendAndReserved(ctx context.Context, tagList []*entity.Tag) { + 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) { + 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) @@ -238,7 +307,7 @@ func (ts *TagCommonService) BatchGetObjectTag(ctx context.Context, objectIds []s for _, tag := range tagList { tagIDList = append(tagIDList, tag.TagID) } - tagsInfoList, err := ts.tagCommonRepo.GetTagListByIDs(ctx, tagIDList) + tagsInfoList, err := ts.GetTagListByIDs(ctx, tagIDList) if err != nil { return objectIDTagMap, err } @@ -275,14 +344,8 @@ func (ts *TagCommonService) CheckTag(ctx context.Context, tags []string, userID return nil } - thisTagNameList := make([]string, 0) - for _, t := range tags { - t = strings.ToLower(t) - thisTagNameList = append(thisTagNameList, t) - } - // find tags name - tagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisTagNameList) + tagListInDb, err := ts.GetTagListByNames(ctx, tags) if err != nil { return err } From 3a2c8700c9a93a36d0f55acbcd8eebafaa8c04a6 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Fri, 18 Nov 2022 20:11:19 +0800 Subject: [PATCH 0392/3337] feat: add tag common repo unit test --- Dockerfile | 2 +- Makefile | 2 +- internal/repo/repo_test/tag_repo_test.go | 48 +++++++++++++----------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index c1bd53ba4..2cbe7a6df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ COPY . ${BUILD_DIR} WORKDIR ${BUILD_DIR} COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build RUN apk --no-cache add build-base git \ - && make clean build \ + && make clean test build \ && cp answer /usr/bin/answer RUN mkdir -p /data/uploads && chmod 777 /data/uploads \ diff --git a/Makefile b/Makefile index f98526b7e..1ecff1c9c 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ generate: go mod tidy test: - @$(GO) test ./... + @$(GO) test ./internal/repo/repo_test # clean all build result clean: diff --git a/internal/repo/repo_test/tag_repo_test.go b/internal/repo/repo_test/tag_repo_test.go index 9c2e012a6..d659651cd 100644 --- a/internal/repo/repo_test/tag_repo_test.go +++ b/internal/repo/repo_test/tag_repo_test.go @@ -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)) From 697a98951f2b94b0be21ddb12d72303de080086a Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Sat, 19 Nov 2022 14:03:16 +0800 Subject: [PATCH 0393/3337] doc: regenerate swagger doc --- cmd/answer/wire_gen.go | 18 ++++++++++-------- docs/docs.go | 6 ------ docs/swagger.json | 6 ------ docs/swagger.yaml | 4 ---- .../service/search_parser/search_parser.go | 19 ++++++++++--------- 5 files changed, 20 insertions(+), 33 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 3ca98751a..bf3c483c5 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -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" @@ -63,7 +64,7 @@ import ( "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" @@ -116,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, siteInfoCommonService) - 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) @@ -128,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, siteInfoCommonService) + tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService) tagController := controller.NewTagController(tagService, 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, siteInfoCommonService) collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo) answerCommon := answercommon.NewAnswerCommon(answerRepo) metaRepo := meta.NewMetaRepo(dataData) @@ -155,7 +157,7 @@ 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(tagRepo, userCommon) + searchParser := search_parser.NewSearchParser(tagCommonService, userCommon) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchService := service.NewSearchService(searchParser, searchRepo) searchController := controller.NewSearchController(searchService) diff --git a/docs/docs.go b/docs/docs.go index 316bb89ee..18be2dd5d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5256,12 +5256,6 @@ 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" diff --git a/docs/swagger.json b/docs/swagger.json index 25d181d73..e8d96d622 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -5244,12 +5244,6 @@ "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" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d0a9d553d..0cdd69bf6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -657,10 +657,6 @@ 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 diff --git a/internal/service/search_parser/search_parser.go b/internal/service/search_parser/search_parser.go index bf8ee04ec..3fbbba563 100644 --- a/internal/service/search_parser/search_parser.go +++ b/internal/service/search_parser/search_parser.go @@ -2,23 +2,24 @@ package search_parser import ( "context" + "regexp" + "strings" + "github.com/answerdev/answer/internal/schema" - tagcommon "github.com/answerdev/answer/internal/service/tag_common" + "github.com/answerdev/answer/internal/service/tag_common" usercommon "github.com/answerdev/answer/internal/service/user_common" "github.com/answerdev/answer/pkg/converter" - "regexp" - "strings" ) type SearchParser struct { - tagRepo tagcommon.TagRepo - userCommon *usercommon.UserCommon + tagCommonService *tag_common.TagCommonService + userCommon *usercommon.UserCommon } -func NewSearchParser(tagRepo tagcommon.TagRepo, userCommon *usercommon.UserCommon) *SearchParser { +func NewSearchParser(tagCommonService *tag_common.TagCommonService, userCommon *usercommon.UserCommon) *SearchParser { return &SearchParser{ - tagRepo: tagRepo, - userCommon: userCommon, + tagCommonService: tagCommonService, + userCommon: userCommon, } } @@ -161,7 +162,7 @@ func (sp *SearchParser) parseTags(query *string) (tags []string) { } tags = make([]string, len(res)) for i, item := range res { - tag, exists, err := sp.tagRepo.GetTagBySlugName(context.TODO(), item[1]) + tag, exists, err := sp.tagCommonService.GetTagBySlugName(context.TODO(), item[1]) if err != nil || !exists { continue } From 747323b49c8c6890da7689b8ecbef313232fd67b Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Sat, 19 Nov 2022 17:04:46 +0800 Subject: [PATCH 0394/3337] fix: add tag common service to controller --- cmd/answer/wire_gen.go | 2 +- internal/controller/tag_controller.go | 8 ++++++-- internal/service/tag_common/tag_common.go | 6 ++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index d8ea06b3b..8c88552fe 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -135,7 +135,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, tagCommonService := tag_common2.NewTagCommonService(tagCommonRepo, tagRelRepo, revisionService, siteInfoCommonService) followRepo := activity_common.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService) - tagController := controller.NewTagController(tagService, rankService) + tagController := controller.NewTagController(tagService, tagCommonService, rankService) followFollowRepo := activity.NewFollowRepo(dataData, uniqueIDRepo, activityRepo) followService := follow.NewFollowService(followFollowRepo, followRepo, tagCommonRepo) followController := controller.NewFollowController(followService) diff --git a/internal/controller/tag_controller.go b/internal/controller/tag_controller.go index 33791cb1a..673a746cb 100644 --- a/internal/controller/tag_controller.go +++ b/internal/controller/tag_controller.go @@ -20,8 +20,12 @@ type TagController struct { } // 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 diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go index dda1c9238..ec67720a5 100644 --- a/internal/service/tag_common/tag_common.go +++ b/internal/service/tag_common/tag_common.go @@ -271,6 +271,9 @@ func (ts *TagCommonService) TagFormat(ctx context.Context, tags []*entity.Tag) ( } 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) @@ -284,6 +287,9 @@ func (ts *TagCommonService) tagsFormatRecommendAndReserved(ctx context.Context, } 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) From ba02cf4095e597869c983da59d2c7488ff911352 Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Sat, 19 Nov 2022 17:07:20 +0800 Subject: [PATCH 0395/3337] feat: regenerate wire --- cmd/answer/wire_gen.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/answer/wire_gen.go b/cmd/answer/wire_gen.go index 8c88552fe..acee8475f 100644 --- a/cmd/answer/wire_gen.go +++ b/cmd/answer/wire_gen.go @@ -59,6 +59,7 @@ 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" @@ -156,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, tagCommonService, userCommon, followRepo) + searchService := service.NewSearchService(searchParser, searchRepo) searchController := controller.NewSearchController(searchService) serviceRevisionService := service.NewRevisionService(revisionRepo, userCommon, questionCommon, answerService) revisionController := controller.NewRevisionController(serviceRevisionService) @@ -181,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) From b97f0fe9d3102f600e51813a977dc27b121e4a27 Mon Sep 17 00:00:00 2001 From: shuai Date: Mon, 21 Nov 2022 10:12:27 +0800 Subject: [PATCH 0396/3337] feat: add diff function --- ui/package.json | 1 + ui/pnpm-lock.yaml | 7 +++++++ ui/src/index.scss | 15 +++++++++++++++ ui/src/router/routes.ts | 4 ++++ ui/src/utils/common.ts | 21 +++++++++++++++++++++ 5 files changed, 48 insertions(+) diff --git a/ui/package.json b/ui/package.json index 4dd402630..a26e51bbf 100644 --- a/ui/package.json +++ b/ui/package.json @@ -19,6 +19,7 @@ "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", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 78e838351..6f91fd637 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -25,6 +25,7 @@ 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 @@ -72,6 +73,7 @@ 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 @@ -4742,6 +4744,11 @@ 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'} diff --git a/ui/src/index.scss b/ui/src/index.scss index a767e0a00..66b3c2997 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -253,3 +253,18 @@ a { padding: 1px 0.5rem 2px; height: 24px; } + +.review-text-delete { + color: #842029; + background-color: #F8D7DA; + text-decoration: line-through; + .review-text-add { + text-decoration: none; + } +} + +.review-text-add { + color: #0F5132; + background-color: #D1E7DD; + text-decoration: none !important; +} diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index be7e61aca..3454738ad 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -201,6 +201,10 @@ const routes: RouteNode[] = [ return guard.forbidden(); }, }, + { + path: '/revision', + page: 'pages/Revision', + }, // for admin { path: 'admin', diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts index 41622307f..46d43ba0f 100644 --- a/ui/src/utils/common.ts +++ b/ui/src/utils/common.ts @@ -1,5 +1,7 @@ import i18next from 'i18next'; +const Diff = require('diff'); + function thousandthDivision(num) { const reg = /\d{1,3}(?=(\d{3})+$)/g; return `${num}`.replace(reg, '$&,'); @@ -163,6 +165,24 @@ function handleFormError( return data; } +function diffText(newText: string, oldText: string): string { + const diff = Diff.diffChars(newText, oldText); + const result = diff.map((part) => { + if (part.added) { + return `${part.value}`; + } + if (part.removed) { + return `${part.value}`; + } + return part.value; + }); + return result + .join('') + ?.replace(/\n/gi, '
') + ?.replace(/
{t('name')}{t('reputation')}{t('name')}{t('reputation')} {t('email')} + {t('created_at')} + {curFilter === 'deleted' ? t('delete_at') : t('suspend_at')} {t('status')}{t('action')}{t('status')}{t('action')}
{user.status !== 'deleted' && ( + + ); +}; +export const initFormData = (schema: JSONSchema): Type.FormDataType => { + const formData: Type.FormDataType = {}; + Object.keys(schema.properties).forEach((key) => { + formData[key] = { + value: '', + isInvalid: false, + errorMsg: '', + }; + }); + return formData; +}; + +export default SchemaForm; diff --git a/ui/src/components/index.ts b/ui/src/components/index.ts index 25bdefa75..4ff5e71cd 100644 --- a/ui/src/components/index.ts +++ b/ui/src/components/index.ts @@ -25,6 +25,7 @@ import Empty from './Empty'; import BaseUserCard from './BaseUserCard'; import FollowingTags from './FollowingTags'; import QueryGroup from './QueryGroup'; +import SchemaForm, { JSONSchema, UISchema, initFormData } from './SchemaForm'; export { Avatar, @@ -55,5 +56,7 @@ export { FollowingTags, htmlRender, QueryGroup, + SchemaForm, + initFormData, }; -export type { EditorRef }; +export type { EditorRef, JSONSchema, UISchema }; From c9c13a1331b1015c51b4a30543f6156f4c4a0243 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 10 Nov 2022 11:15:34 +0800 Subject: [PATCH 0231/3337] refactor(ui): use schemaForm --- ui/src/pages/Admin/General/index.tsx | 235 ++++++------------- ui/src/pages/Admin/Smtp/index.tsx | 337 ++++++++------------------- 2 files changed, 164 insertions(+), 408 deletions(-) diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 2ea2f4859..41ec1a14b 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -1,7 +1,7 @@ import React, { FC, useEffect, useState } from 'react'; -import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; +import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components'; import type * as Type from '@/common/interface'; import { useToast } from '@/hooks'; import { siteInfoStore } from '@/stores'; @@ -17,91 +17,70 @@ const General: FC = () => { const updateSiteInfo = siteInfoStore((state) => state.update); const { data: setting } = useGeneralSetting(); - const [formData, setFormData] = useState({ - name: { - value: '', - isInvalid: false, - errorMsg: '', + const schema: JSONSchema = { + title: t('title'), + description: t('description'), + required: ['name', 'site_url', 'contact_email'], + properties: { + name: { + type: 'string', + title: t('name.label'), + description: t('name.text'), + }, + site_url: { + type: 'string', + title: t('site_url.label'), + description: t('site_url.text'), + }, + short_description: { + type: 'string', + title: t('short_description.label'), + description: t('short_description.text'), + }, + description: { + type: 'string', + title: t('description.label'), + description: t('description.text'), + }, + contact_email: { + type: 'string', + title: t('contact_email.label'), + description: t('contact_email.text'), + }, }, + }; + const uiSchema: UISchema = { site_url: { - value: '', - isInvalid: false, - errorMsg: '', - }, - short_description: { - value: '', - isInvalid: false, - errorMsg: '', - }, - description: { - value: '', - isInvalid: false, - errorMsg: '', + 'ui:options': { + invalid: t('site_url.validate'), + validator: (value) => { + if (!/^(https?):\/\/([\w.]+\/?)\S*$/.test(value)) { + return false; + } + return true; + }, + }, }, contact_email: { - value: '', - isInvalid: false, - errorMsg: '', + 'ui:options': { + invalid: t('contact_email.validate'), + validator: (value) => { + if ( + !/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(value) + ) { + return false; + } + return true; + }, + }, }, - }); - const checkValidated = (): boolean => { - let ret = true; - const { name, site_url, contact_email } = formData; - if (!name.value) { - ret = false; - formData.name = { - value: '', - isInvalid: true, - errorMsg: t('name.msg'), - }; - } - if (!site_url.value) { - ret = false; - formData.site_url = { - value: '', - isInvalid: true, - errorMsg: t('site_url.msg'), - }; - } else if (!/^(https?):\/\/([\w.]+\/?)\S*$/.test(site_url.value)) { - ret = false; - formData.site_url = { - value: formData.site_url.value, - isInvalid: true, - errorMsg: t('site_url.validate'), - }; - } - - if (!contact_email.value) { - ret = false; - formData.contact_email = { - value: '', - isInvalid: true, - errorMsg: t('contact_email.msg'), - }; - } else if ( - !/^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test( - contact_email.value, - ) - ) { - ret = false; - formData.contact_email = { - value: formData.contact_email.value, - isInvalid: true, - errorMsg: t('contact_email.validate'), - }; - } - setFormData({ - ...formData, - }); - return ret; }; + const [formData, setFormData] = useState(initFormData(schema)); const onSubmit = (evt) => { evt.preventDefault(); evt.stopPropagation(); - if (checkValidated() === false) { - return; - } + const reqParams: Type.AdminSettingsGeneral = { name: formData.name.value, description: formData.description.value, @@ -126,19 +105,7 @@ const General: FC = () => { setFormData({ ...formData }); }); }; - const onFieldChange = (fieldName, fieldValue) => { - if (!formData[fieldName]) { - return; - } - const fieldData: Type.FormDataType = { - [fieldName]: { - value: fieldValue, - isInvalid: false, - errorMsg: '', - }, - }; - setFormData({ ...formData, ...fieldData }); - }; + useEffect(() => { if (!setting) { return; @@ -149,87 +116,21 @@ const General: FC = () => { }); setFormData({ ...formData, ...formMeta }); }, [setting]); + + const handleOnChange = (data) => { + setFormData(data); + }; + return ( <>

{t('page_title')}

-
- - {t('name.label')} - onFieldChange('name', evt.target.value)} - /> - {t('name.text')} - - {formData.name.errorMsg} - - - - {t('site_url.label')} - onFieldChange('site_url', evt.target.value)} - /> - {t('site_url.text')} - - {formData.site_url.errorMsg} - - - - {t('short_description.label')} - - onFieldChange('short_description', evt.target.value) - } - /> - {t('short_description.text')} - - {formData.short_description.errorMsg} - - - - {t('description.label')} - onFieldChange('description', evt.target.value)} - /> - {t('description.text')} - - {formData.description.errorMsg} - - - - {t('contact_email.label')} - onFieldChange('contact_email', evt.target.value)} - /> - {t('contact_email.text')} - - {formData.contact_email.errorMsg} - - - - -
+ ); }; diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx index 33de30ab3..0c3892bd6 100644 --- a/ui/src/pages/Admin/Smtp/index.tsx +++ b/ui/src/pages/Admin/Smtp/index.tsx @@ -1,11 +1,12 @@ import React, { FC, useEffect, useState } from 'react'; -import { Form, Button, Stack } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import type * as Type from '@/common/interface'; import { useToast } from '@/hooks'; import { useSmtpSetting, updateSmtpSetting } from '@/services'; import pattern from '@/common/pattern'; +import { SchemaForm, JSONSchema, UISchema } from '@/components'; +import { initFormData } from '../../../components/SchemaForm/index'; const Smtp: FC = () => { const { t } = useTranslation('translation', { @@ -13,90 +14,103 @@ const Smtp: FC = () => { }); const Toast = useToast(); const { data: setting } = useSmtpSetting(); - const [formData, setFormData] = useState({ - from_email: { - value: '', - isInvalid: false, - errorMsg: '', - }, - from_name: { - value: '', - isInvalid: false, - errorMsg: '', - }, - smtp_host: { - value: '', - isInvalid: false, - errorMsg: '', + const schema: JSONSchema = { + title: t('title'), + description: t('description'), + properties: { + from_email: { + type: 'string', + title: t('from_email.label'), + description: t('from_email.text'), + }, + from_name: { + type: 'string', + title: t('from_name.label'), + description: t('from_name.text'), + }, + smtp_host: { + type: 'string', + title: t('smtp_host.label'), + description: t('smtp_host.text'), + }, + encryption: { + type: 'boolean', + title: t('encryption.label'), + description: t('encryption.text'), + enum: [true, false], + enumNames: ['SSL', ''], + }, + smtp_port: { + type: 'string', + title: t('smtp_port.label'), + description: t('smtp_port.text'), + }, + smtp_authentication: { + type: 'boolean', + title: t('smtp_authentication.label'), + enum: [true, false], + enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')], + }, + smtp_username: { + type: 'string', + title: t('smtp_username.label'), + description: t('smtp_username.text'), + }, + smtp_password: { + type: 'string', + title: t('smtp_password.label'), + description: t('smtp_password.text'), + }, + test_email_recipient: { + type: 'string', + title: t('test_email_recipient.label'), + description: t('test_email_recipient.text'), + }, }, + }; + const uiSchema: UISchema = { encryption: { - value: '', - isInvalid: false, - errorMsg: '', + 'ui:widget': 'radio', }, - smtp_port: { - value: '', - isInvalid: false, - errorMsg: '', + smtp_password: { + 'ui:options': { + type: 'password', + }, }, smtp_authentication: { - value: 'yes', - isInvalid: false, - errorMsg: '', - }, - smtp_username: { - value: '', - isInvalid: false, - errorMsg: '', + 'ui:widget': 'radio', }, - smtp_password: { - value: '', - isInvalid: false, - errorMsg: '', + smtp_port: { + 'ui:options': { + invalid: t('smtp_port.msg'), + validator: (value) => { + if (!/^[1-9][0-9]*$/.test(value) || Number(value) > 65535) { + return false; + } + return true; + }, + }, }, test_email_recipient: { - value: '', - isInvalid: false, - errorMsg: '', + 'ui:options': { + invalid: t('test_email_recipient.msg'), + validator: (value) => { + if (value && !pattern.email.test(value)) { + return false; + } + return true; + }, + }, }, - }); - const checkValidated = (): boolean => { - let ret = true; - const { smtp_port, test_email_recipient } = formData; - if ( - !/^[1-9][0-9]*$/.test(smtp_port.value) || - Number(smtp_port.value) > 65535 - ) { - ret = false; - formData.smtp_port = { - value: smtp_port.value, - isInvalid: true, - errorMsg: t('smtp_port.msg'), - }; - } - if ( - test_email_recipient.value && - !pattern.email.test(test_email_recipient.value) - ) { - ret = false; - formData.test_email_recipient = { - value: test_email_recipient.value, - isInvalid: true, - errorMsg: t('test_email_recipient.msg'), - }; - } - setFormData({ - ...formData, - }); - return ret; }; + const [formData, setFormData] = useState( + initFormData(schema), + ); const onSubmit = (evt) => { evt.preventDefault(); evt.stopPropagation(); - if (!checkValidated()) { - return; - } + const reqParams: Type.AdminSettingsSmtp = { from_email: formData.from_email.value, from_name: formData.from_name.value, @@ -124,19 +138,7 @@ const Smtp: FC = () => { setFormData({ ...formData }); }); }; - const onFieldChange = (fieldName, fieldValue) => { - if (!formData[fieldName]) { - return; - } - const fieldData: Type.FormDataType = { - [fieldName]: { - value: fieldValue, - isInvalid: false, - errorMsg: '', - }, - }; - setFormData({ ...formData, ...fieldData }); - }; + useEffect(() => { if (!setting) { return; @@ -152,166 +154,19 @@ const Smtp: FC = () => { setFormData(formState); }, [setting]); + const handleOnChange = (data) => { + setFormData(data); + }; return ( <>

{t('page_title')}

-
- - {t('from_email.label')} - onFieldChange('from_email', evt.target.value)} - /> - {t('from_email.text')} - - {formData.from_email.errorMsg} - - - - {t('from_name.label')} - onFieldChange('from_name', evt.target.value)} - /> - {t('from_name.text')} - - {formData.from_name.errorMsg} - - - - {t('smtp_host.label')} - onFieldChange('smtp_host', evt.target.value)} - /> - {t('smtp_host.text')} - - {formData.smtp_host.errorMsg} - - - - {t('encryption.label')} - - onFieldChange('encryption', 'SSL')} - type="radio" - /> - onFieldChange('encryption', '')} - type="radio" - /> - - {t('encryption.text')} - - {formData.encryption.errorMsg} - - - - {t('smtp_port.label')} - onFieldChange('smtp_port', evt.target.value)} - /> - {t('smtp_port.text')} - - {formData.smtp_port.errorMsg} - - - - {t('smtp_authentication.label')} - - onFieldChange('smtp_authentication', true)} - type="radio" - /> - onFieldChange('smtp_authentication', false)} - type="radio" - /> - - - {formData.smtp_authentication.errorMsg} - - - - {t('smtp_username.label')} - onFieldChange('smtp_username', evt.target.value)} - /> - - {formData.smtp_username.errorMsg} - - - - {t('smtp_password.label')} - onFieldChange('smtp_password', evt.target.value)} - /> - - {formData.smtp_password.errorMsg} - - - - {t('test_email_recipient.label')} - - onFieldChange('test_email_recipient', evt.target.value) - } - /> - {t('test_email_recipient.text')} - - {formData.test_email_recipient.errorMsg} - - - - -
+ ); }; From 1604a74bf96f9616c76ea187f320831ced0ff717 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 10 Nov 2022 11:19:19 +0800 Subject: [PATCH 0232/3337] refactor(i18n): optimize i18n file import --- ui/config-overrides.js | 64 +- ui/package.json | 1 - ui/pnpm-lock.yaml | 2 - ui/scripts/i18n-locale-tool.js | 56 -- ui/src/i18n/DO_NOT_EDIT_dir_locales | 0 ui/src/i18n/init.ts | 4 +- ui/src/i18n/locales/en_US.yaml | 1111 --------------------------- ui/src/i18n/locales/i18n.yaml | 6 - ui/src/i18n/locales/it_IT.yaml | 170 ---- ui/src/i18n/locales/zh_CN.yaml | 920 ---------------------- ui/src/utils/localize.ts | 6 +- ui/tsconfig.json | 5 +- 12 files changed, 40 insertions(+), 2305 deletions(-) delete mode 100644 ui/scripts/i18n-locale-tool.js delete mode 100644 ui/src/i18n/DO_NOT_EDIT_dir_locales delete mode 100644 ui/src/i18n/locales/en_US.yaml delete mode 100644 ui/src/i18n/locales/i18n.yaml delete mode 100644 ui/src/i18n/locales/it_IT.yaml delete mode 100644 ui/src/i18n/locales/zh_CN.yaml diff --git a/ui/config-overrides.js b/ui/config-overrides.js index e8d2dceb8..0a5deeffd 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -1,49 +1,51 @@ -const path = require('path'); -const i18nLocaleTool = require('./scripts/i18n-locale-tool'); +const { + addWebpackModuleRule, + addWebpackAlias +} = require("customize-cra"); + +const path = require("path"); +const i18nPath = path.resolve(__dirname, "../i18n"); module.exports = { - webpack: function (config, env) { - if (env === 'production') { + webpack: function(config, env) { + if (env === "production") { config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH; - i18nLocaleTool.resolvePresetLocales(); } - for (let _rule of config.module.rules) { - if (_rule.oneOf) { - _rule.oneOf.unshift({ - test: /\.ya?ml$/, - use: 'yaml-loader' - }); - break; - } - } + addWebpackAlias({ + ["@"]: path.resolve(__dirname, "src"), + "@i18n": i18nPath + })(config); - config.resolve.alias = { - ...config.resolve.alias, - '@': path.resolve(__dirname, 'src'), - }; + addWebpackModuleRule({ + test: /\.ya?ml$/, + use: "yaml-loader" + })(config); + + // add i18n dir to ModuleScopePlugin allowedPaths + const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === "ModuleScopePlugin"); + if (moduleScopePlugin) { + moduleScopePlugin.allowedPaths.push(i18nPath); + } return config; }, - - devServer: function (configFunction) { - i18nLocaleTool.autoSync(); - - return function (proxy, allowedHost) { + devServer: function(configFunction) { + return function(proxy, allowedHost) { const config = configFunction(proxy, allowedHost); config.proxy = { - '/answer': { - target: 'http://10.0.10.98:2060', + "/answer": { + target: "http://10.0.10.98:2060", changeOrigin: true, - secure: false, + secure: false }, - '/installation': { - target: 'http://10.0.10.98:2060', + "/installation": { + target: "http://10.0.10.98:2060", changeOrigin: true, - secure: false, - }, + secure: false + } }; return config; }; - }, + } }; diff --git a/ui/package.json b/ui/package.json index fd2a34709..7bd56191c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -73,7 +73,6 @@ "@types/react-helmet": "^6.1.5", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.33.0", - "chokidar": "^3.5.3", "commitizen": "^4.2.5", "conventional-changelog-cli": "^2.2.2", "customize-cra": "^1.0.0", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index a4408b969..98ebd5898 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -26,7 +26,6 @@ specifiers: axios: ^0.27.2 bootstrap: ^5.2.0 bootstrap-icons: ^1.9.1 - chokidar: ^3.5.3 classnames: ^2.3.1 codemirror: 5.65.0 commitizen: ^4.2.5 @@ -133,7 +132,6 @@ devDependencies: '@types/react-helmet': 6.1.5 '@typescript-eslint/eslint-plugin': 5.38.0_wsb62dxj2oqwgas4kadjymcmry '@typescript-eslint/parser': 5.38.0_irgkl5vooow2ydyo6aokmferha - chokidar: 3.5.3 commitizen: 4.2.5 conventional-changelog-cli: 2.2.2 customize-cra: 1.0.0 diff --git a/ui/scripts/i18n-locale-tool.js b/ui/scripts/i18n-locale-tool.js deleted file mode 100644 index 05d55a50e..000000000 --- a/ui/scripts/i18n-locale-tool.js +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -const path = require('node:path'); -const fs = require('node:fs'); - -const chokidar = require('chokidar'); - -const SRC_PATH = path.resolve(__dirname, '../../i18n'); -const DEST_PATH = path.resolve(__dirname, '../src/i18n/locales'); -const PRESET_LANG = ['en_US', 'zh_CN']; - -const cleanLocales = () => { - fs.readdirSync(DEST_PATH).forEach((fp) => { - fs.rmSync(path.resolve(DEST_PATH, fp), { force: true, recursive: true }); - }); -}; - -const copyLocaleFile = (filePath) => { - const targetFilePath = path.resolve(DEST_PATH, path.basename(filePath)); - fs.copyFile( - filePath, - targetFilePath, - fs.constants.COPYFILE_FICLONE, - (err) => { - if (err) { - throw err; - } - }, - ); -}; - -const watchAndSync = () => { - chokidar - .watch(path.resolve(SRC_PATH, '*.yaml'), { - awaitWriteFinish: true, - }) - .on('all', (evt, filePath) => { - copyLocaleFile(filePath); - }); -}; - -const autoSync = () => { - cleanLocales(); - watchAndSync(); -}; - -const resolvePresetLocales = () => { - PRESET_LANG.forEach((lng) => { - const sp = path.resolve(SRC_PATH, `${lng}.yaml`); - copyLocaleFile(sp); - }); -}; - -module.exports = { - autoSync, - resolvePresetLocales, -}; diff --git a/ui/src/i18n/DO_NOT_EDIT_dir_locales b/ui/src/i18n/DO_NOT_EDIT_dir_locales deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/src/i18n/init.ts b/ui/src/i18n/init.ts index 6d75ddc0b..ec4bf3787 100644 --- a/ui/src/i18n/init.ts +++ b/ui/src/i18n/init.ts @@ -2,10 +2,10 @@ import { initReactI18next } from 'react-i18next'; import i18next from 'i18next'; import Backend from 'i18next-http-backend'; +import en_US from '@i18n/en_US.yaml'; +import zh_CN from '@i18n/zh_CN.yaml'; import { DEFAULT_LANG } from '@/common/constants'; -import en_US from '@/i18n/locales/en_US.yaml'; -import zh_CN from '@/i18n/locales/zh_CN.yaml'; i18next // load translation using http diff --git a/ui/src/i18n/locales/en_US.yaml b/ui/src/i18n/locales/en_US.yaml deleted file mode 100644 index fa6a1de34..000000000 --- a/ui/src/i18n/locales/en_US.yaml +++ /dev/null @@ -1,1111 +0,0 @@ -base: - success: - other: "Success." - unknown: - other: "Unknown error." - request_format_error: - other: "Request format is not valid." - unauthorized_error: - other: "Unauthorized." - database_error: - other: "Data server error." - -email: - other: "Email" -password: - other: "Password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email and password do not match." - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Answer do not found." - comment: - edit_without_permission: - other: "Comment are not allowed to edit." - not_found: - other: "Comment not found." - email: - duplicate: - other: "Email already exists." - need_to_be_verified: - other: "Email should be verified." - verify_url_expired: - other: "Email verified URL has expired, please resend the email." - lang: - not_found: - other: "Language file not found." - object: - captcha_verification_failed: - other: "Captcha wrong." - disallow_follow: - other: "You are not allowed to follow." - disallow_vote: - other: "You are not allowed to vote." - disallow_vote_your_self: - other: "You can't vote for your own post." - not_found: - other: "Object not found." - verification_failed: - other: "Verification failed." - email_or_password_incorrect: - other: "Email and password do not match." - old_password_verification_failed: - other: "The old password verification failed" - new_password_same_as_previous_setting: - other: "The new password is the same as the previous one." - question: - not_found: - other: "Question not found." - rank: - fail_to_meet_the_condition: - other: "Rank fail to meet the condition." - report: - handle_failed: - other: "Report handle failed." - not_found: - other: "Report not found." - tag: - not_found: - other: "Tag not found." - theme: - not_found: - other: "Theme not found." - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "User not found." - suspended: - other: "User has been suspended." - username_invalid: - other: "Username is invalid." - username_duplicate: - other: "Username is already in use." - set_avatar: - other: "Avatar set failed." - -report: - spam: - name: - other: "spam" - description: - other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic." - rude: - name: - other: "rude or abusive" - description: - other: "A reasonable person would find this content inappropriate for respectful discourse." - duplicate: - name: - other: "a duplicate" - description: - other: "This question has been asked before and already has an answer." - not_answer: - name: - other: "not an answer" - description: - other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether." - not_need: - name: - other: "no longer needed" - description: - other: "This comment is outdated, conversational or not relevant to this post." - other: - name: - other: "something else" - description: - other: "This post requires staff attention for another reason not listed above." - -question: - close: - duplicate: - name: - other: "spam" - description: - other: "This question has been asked before and already has an answer." - guideline: - name: - other: "a community-specific reason" - description: - other: "This question doesn't meet a community guideline." - multiple: - name: - other: "needs details or clarity" - description: - other: "This question currently includes multiple questions in one. It should focus on one problem only." - other: - name: - other: "something else" - description: - other: "This post requires another reason not listed above." - -notification: - action: - update_question: - other: "updated question" - answer_the_question: - other: "answered question" - update_answer: - other: "updated answer" - adopt_answer: - other: "accepted answer" - comment_question: - other: "commented question" - comment_answer: - other: "commented answer" - reply_to_you: - other: "replied to you" - mention_you: - other: "mentioned you" - your_question_is_closed: - other: "Your question has been closed" - your_question_was_deleted: - other: "Your question has been deleted" - your_answer_was_deleted: - other: "Your answer has been deleted" - your_comment_was_deleted: - other: "Your comment has been deleted" -# The following fields are used for interface presentation(Front-end) -ui: - how_to_format: - title: How to Format - description: >- -
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by - placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
- pagination: - prev: Prev - next: Next - page_title: - question: Question - questions: Questions - tag: Tag - tags: Tags - tag_wiki: tag wiki - edit_tag: Edit Tag - ask_a_question: Add Question - edit_question: Edit Question - edit_answer: Edit Answer - search: Search - posts_containing: Posts containing - settings: Settings - notifications: Notifications - login: Log In - sign_up: Sign Up - account_recovery: Account Recovery - account_activation: Account Activation - confirm_email: Confirm Email - account_suspended: Account Suspended - admin: Admin - change_email: Modify Email - install: Answer Installation - upgrade: Answer Upgrade - maintenance: Webite Maintenance - notifications: - title: Notifications - inbox: Inbox - achievement: Achievements - all_read: Mark all as read - show_more: Show more - suspended: - title: Your Account has been Suspended - until_time: 'Your account was suspended until {{ time }}.' - forever: This user was suspended forever. - end: You don't meet a community guideline. - editor: - blockquote: - text: Blockquote - bold: - text: Strong - chart: - text: Chart - flow_chart: Flow chart - sequence_diagram: Sequence diagram - class_diagram: Class diagram - state_diagram: State diagram - entity_relationship_diagram: Entity relationship diagram - user_defined_diagram: User defined diagram - gantt_chart: Gantt chart - pie_chart: Pie chart - code: - text: Code Sample - add_code: Add code sample - form: - fields: - code: - label: Code - msg: - empty: Code cannot be empty. - language: - label: Language (optional) - placeholder: Automatic detection - btn_cancel: Cancel - btn_confirm: Add - formula: - text: Formula - options: - inline: Inline formula - block: Block formula - heading: - text: Heading - options: - h1: Heading 1 - h2: Heading 2 - h3: Heading 3 - h4: Heading 4 - h5: Heading 5 - h6: Heading 6 - help: - text: Help - hr: - text: Horizontal Rule - image: - text: Image - add_image: Add image - tab_image: Upload image - form_image: - fields: - file: - label: Image File - btn: Select image - msg: - empty: File cannot be empty. - only_image: Only image files are allowed. - max_size: File size cannot exceed 4MB. - description: - label: Description (optional) - tab_url: Image URL - form_url: - fields: - url: - label: Image URL - msg: - empty: Image URL cannot be empty. - name: - label: Description (optional) - btn_cancel: Cancel - btn_confirm: Add - uploading: Uploading - indent: - text: Indent - outdent: - text: Outdent - italic: - text: Emphasis - link: - text: Hyperlink - add_link: Add hyperlink - form: - fields: - url: - label: URL - msg: - empty: URL cannot be empty. - name: - label: Description (optional) - btn_cancel: Cancel - btn_confirm: Add - ordered_list: - text: Numbered List - unordered_list: - text: Bulleted List - table: - text: Table - heading: Heading - cell: Cell - close_modal: - title: I am closing this post as... - btn_cancel: Cancel - btn_submit: Submit - remark: - empty: Cannot be empty. - msg: - empty: Please select a reason. - report_modal: - flag_title: I am flagging to report this post as... - close_title: I am closing this post as... - review_question_title: Review question - review_answer_title: Review answer - review_comment_title: Review comment - btn_cancel: Cancel - btn_submit: Submit - remark: - empty: Cannot be empty. - msg: - empty: Please select a reason. - tag_modal: - title: Create new tag - form: - fields: - display_name: - label: Display Name - msg: - empty: Display name cannot be empty. - range: Display name up to 35 characters. - slug_name: - label: URL Slug - description: 'Must use the character set "a-z", "0-9", "+ # - ."' - msg: - empty: URL slug cannot be empty. - range: URL slug up to 35 characters. - character: URL slug contains unallowed character set. - description: - label: Description (optional) - btn_cancel: Cancel - btn_submit: Submit - tag_info: - created_at: Created - edited_at: Edited - synonyms: - title: Synonyms - text: The following tags will be remapped to - empty: No synonyms found. - btn_add: Add a synonym - btn_edit: Edit - btn_save: Save - synonyms_text: The following tags will be remapped to - delete: - title: Delete this tag - content: >- -

We do not allowed deleting tag with posts.

Please remove this tag - from the posts first.

- content2: Are you sure you wish to delete? - close: Close - edit_tag: - title: Edit Tag - default_reason: Edit tag - form: - fields: - revision: - label: Revision - display_name: - label: Display Name - slug_name: - label: URL Slug - info: 'Must use the character set "a-z", "0-9", "+ # - ."' - description: - label: Description - edit_summary: - label: Edit Summary - placeholder: >- - Briefly explain your changes (corrected spelling, fixed grammar, - improved formatting) - btn_save_edits: Save edits - btn_cancel: Cancel - dates: - long_date: MMM D - long_date_with_year: 'MMM D, YYYY' - long_date_with_time: 'MMM D, YYYY [at] HH:mm' - now: now - x_seconds_ago: '{{count}}s ago' - x_minutes_ago: '{{count}}m ago' - x_hours_ago: '{{count}}h ago' - hour: hour - day: day - comment: - btn_add_comment: Add comment - reply_to: Reply to - btn_reply: Reply - btn_edit: Edit - btn_delete: Delete - btn_flag: Flag - btn_save_edits: Save edits - btn_cancel: Cancel - show_more: Show more comment - tip_question: >- - Use comments to ask for more information or suggest improvements. Avoid - answering questions in comments. - tip_answer: >- - Use comments to reply to other users or notify them of changes. If you are - adding new information, edit your post instead of commenting. - edit_answer: - title: Edit Answer - default_reason: Edit answer - form: - fields: - revision: - label: Revision - answer: - label: Answer - edit_summary: - label: Edit Summary - placeholder: >- - Briefly explain your changes (corrected spelling, fixed grammar, - improved formatting) - btn_save_edits: Save edits - btn_cancel: Cancel - tags: - title: Tags - sort_buttons: - popular: Popular - name: Name - newest: newest - button_follow: Follow - button_following: Following - tag_label: questions - search_placeholder: Filter by tag name - no_description: The tag has no description. - more: More - ask: - title: Add Question - edit_title: Edit Question - default_reason: Edit question - similar_questions: Similar questions - form: - fields: - revision: - label: Revision - title: - label: Title - placeholder: Be specific and imagine you're asking a question to another person - msg: - empty: Title cannot be empty. - range: Title up to 150 characters - body: - label: Body - msg: - empty: Body cannot be empty. - tags: - label: Tags - msg: - empty: Tags cannot be empty. - answer: - label: Answer - msg: - empty: Answer cannot be empty. - btn_post_question: Post your question - btn_save_edits: Save edits - answer_question: Answer your own question - post_question&answer: Post your question and answer - tag_selector: - add_btn: Add tag - create_btn: Create new tag - search_tag: Search tag - hint: 'Describe what your question is about, at least one tag is required.' - no_result: No tags matched - header: - nav: - question: Questions - tag: Tags - user: Users - profile: Profile - setting: Settings - logout: Log out - admin: Admin - search: - placeholder: Search - footer: - build_on: >- - Built on <1> Answer - the open-source software that power Q&A - communities
Made with love © 2022 Answer - upload_img: - name: Change - loading: loading... - pic_auth_code: - title: Captcha - placeholder: Type the text above - msg: - empty: Captcha cannot be empty. - inactive: - first: >- - You're almost done! We sent an activation mail to {{mail}}. - Please follow the instructions in the mail to activate your account. - info: 'If it doesn''t arrive, check your spam folder.' - another: >- - We sent another activation email to you at {{mail}}. It might - take a few minutes for it to arrive; be sure to check your spam folder. - btn_name: Resend activation email - change_btn_name: Change email - msg: - empty: Cannot be empty. - login: - page_title: Welcome to Answer - info_sign: Don't have an account? <1>Sign up - info_login: Already have an account? <1>Log in - forgot_pass: Forgot password? - name: - label: Name - msg: - empty: Name cannot be empty. - range: Name up to 30 characters. - email: - label: Email - msg: - empty: Email cannot be empty. - password: - label: Password - msg: - empty: Password cannot be empty. - different: The passwords entered on both sides are inconsistent - account_forgot: - page_title: Forgot Your Password - btn_name: Send me recovery email - send_success: >- - If an account matches {{mail}}, you should receive an email - with instructions on how to reset your password shortly. - email: - label: Email - msg: - empty: Email cannot be empty. - change_email: - page_title: Welcome to Answer - btn_cancel: Cancel - btn_update: Update email address - send_success: >- - If an account matches {{mail}}, you should receive an email - with instructions on how to reset your password shortly. - email: - label: New Email - msg: - empty: Email cannot be empty. - password_reset: - page_title: Password Reset - btn_name: Reset my password - reset_success: >- - You successfully changed your password; you will be redirected to the log in - page. - link_invalid: >- - Sorry, this password reset link is no longer valid. Perhaps your password is - already reset? - to_login: Continue to log in page - password: - label: Password - msg: - empty: Password cannot be empty. - length: The length needs to be between 8 and 32 - different: The passwords entered on both sides are inconsistent - password_confirm: - label: Confirm New Password - settings: - page_title: Settings - nav: - profile: Profile - notification: Notifications - account: Account - interface: Interface - profile: - btn_name: Update profile - display_name: - label: Display Name - msg: Display name cannot be empty. - msg_range: Display name up to 30 characters - username: - label: Username - caption: People can mention you as "@username". - msg: Username cannot be empty. - msg_range: Username up to 30 characters - character: 'Must use the character set "a-z", "0-9", " - . _"' - avatar: - label: Profile Image - gravatar: Gravatar - gravatar_text: You can change image on <1>gravatar.com - custom: Custom - btn_refresh: Refresh - custom_text: You can upload your image. - default: Default - msg: Please upload an avatar - bio: - label: About Me (optional) - website: - label: Website (optional) - placeholder: 'https://example.com' - msg: Website incorrect format - location: - label: Location (optional) - placeholder: 'City, Country' - notification: - email: - label: Email Notifications - radio: 'Answers to your questions, comments, and more' - account: - change_email_btn: Change email - change_pass_btn: Change password - change_email_info: >- - We've sent an email to that address. Please follow the confirmation - instructions. - email: - label: Email - msg: Email cannot be empty. - password_title: Password - current_pass: - label: Current Password - msg: - empty: Current Password cannot be empty. - length: The length needs to be between 8 and 32. - different: The two entered passwords do not match. - new_pass: - label: New Password - pass_confirm: - label: Confirm New Password - interface: - lang: - label: Interface Language - text: User interface language. It will change when you refresh the page. - toast: - update: update success - update_password: Password changed successfully. - flag_success: Thanks for flagging. - related_question: - title: Related Questions - btn: Add question - answers: answers - question_detail: - Asked: Asked - asked: asked - update: Modified - edit: edited - Views: Viewed - Follow: Follow - Following: Following - answered: answered - closed_in: Closed in - show_exist: Show existing question. - answers: - title: Answers - score: Score - newest: Newest - btn_accept: Accept - btn_accepted: Accepted - write_answer: - title: Your Answer - btn_name: Post your answer - confirm_title: Continue to answer - continue: Continue - confirm_info: >- -

Are you sure you want to add another answer?

You could use the - edit link to refine and improve your existing answer, instead.

- empty: Answer cannot be empty. - delete: - title: Delete this post - question: >- - We do not recommend deleting questions with answers because - doing so deprives future readers of this knowledge.

Repeated deletion - of answered questions can result in your account being blocked from asking. - Are you sure you wish to delete? - answer_accepted: >- -

We do not recommend deleting accepted answer because - doing so deprives future readers of this knowledge.

Repeated deletion - of accepted answers can result in your account being blocked from answering. - Are you sure you wish to delete? - other: Are you sure you wish to delete? - tip_question_deleted: This post has been deleted - tip_answer_deleted: This answer has been deleted - btns: - confirm: Confirm - cancel: Cancel - save: Save - delete: Delete - login: Log in - signup: Sign up - logout: Log out - verify: Verify - add_question: Add question - search: - title: Search Results - keywords: Keywords - options: Options - follow: Follow - following: Following - counts: '{{count}} Results' - more: More - sort_btns: - relevance: Relevance - newest: Newest - active: Active - score: Score - more: More - tips: - title: Advanced Search Tips - tag: '<1>[tag] search withing a tag' - user: '<1>user:username search by author' - answer: '<1>answers:0 unanswered questions' - score: '<1>score:3 posts with a 3+ score' - question: '<1>is:question search questions' - is_answer: '<1>is:answer search answers' - empty: We couldn't find anything.
Try different or less specific keywords. - share: - name: Share - copy: Copy link - via: Share post via... - copied: Copied - facebook: Share to Facebook - twitter: Share to Twitter - cannot_vote_for_self: You can't vote for your own post - modal_confirm: - title: Error... - account_result: - page_title: Welcome to Answer - success: Your new account is confirmed; you will be redirected to the home page. - link: Continue to homepage - invalid: >- - Sorry, this account confirmation link is no longer valid. Perhaps your - account is already active? - confirm_new_email: Your email has been updated. - confirm_new_email_invalid: >- - Sorry, this confirmation link is no longer valid. Perhaps your email was - already changed? - question: - following_tags: Following Tags - edit: Edit - save: Save - follow_tag_tip: Follow tags to curate your list of questions. - hot_questions: Hot Questions - all_questions: All Questions - x_questions: '{{ count }} Questions' - x_answers: '{{ count }} answers' - questions: Questions - answers: Answers - newest: Newest - active: Active - frequent: Frequent - score: Score - unanswered: Unanswered - modified: modified - answered: answered - asked: asked - closed: closed - follow_a_tag: Follow a tag - more: More - personal: - overview: Overview - answers: Answers - answer: answer - questions: Questions - question: question - bookmarks: Bookmarks - reputation: Reputation - comments: Comments - votes: Votes - newest: Newest - score: Score - edit_profile: Edit Profile - visited_x_days: 'Visited {{ count }} days' - viewed: Viewed - joined: Joined - last_login: Seen - about_me: About Me - about_me_empty: '// Hello, World !' - top_answers: Top Answers - top_questions: Top Questions - stats: Stats - list_empty: No posts found.
Perhaps you'd like to select a different tab? - accepted: Accepted - answered: answered - asked: asked - upvote: upvote - downvote: downvote - mod_short: Mod - mod_long: Moderators - x_reputation: reputation - x_votes: votes received - x_answers: answers - x_questions: questions - install: - title: Answer - next: Next - done: Done - config_yaml_error: Can’t create the config.yaml file. - lang: - label: Please Choose a Language - db_type: - label: Database Engine - db_username: - label: Username - placeholder: root - msg: Username cannot be empty. - db_password: - label: Password - placeholder: root - msg: Password cannot be empty. - db_host: - label: Database Host - placeholder: 'db:3306' - msg: Database Host cannot be empty. - db_name: - label: Database Name - placeholder: answer - msg: Database Name cannot be empty. - db_file: - label: Database File - placeholder: /data/answer.db - msg: Database File cannot be empty. - config_yaml: - title: Create config.yaml - label: The config.yaml file created. - description: >- - You can create the <1>config.yaml file manually in the - <1>/var/wwww/xxx/ directory and paste the following text into it. - info: 'After you’ve done that, click “Next” button.' - site_information: Site Information - admin_account: Admin Account - site_name: - label: Site Name - msg: Site Name cannot be empty. - site_url: - label: Site URL - text: The address of your site. - msg: - empty: Site URL cannot be empty. - incorrect: Site URL incorrect format. - contact_email: - label: Contact Email - text: Email address of key contact responsible for this site. - msg: - empty: Contact Email cannot be empty. - incorrect: Contact Email incorrect format. - admin_name: - label: Name - msg: Name cannot be empty. - admin_password: - label: Password - text: >- - You will need this password to log in. Please store it in a secure - location. - msg: Password cannot be empty. - admin_email: - label: Email - text: You will need this email to log in. - msg: - empty: Email cannot be empty. - incorrect: Email incorrect format. - ready_title: Your Answer is Ready! - ready_description: >- - If you ever feel like changing more settings, visit <1>admin section; - find it in the site menu. - good_luck: 'Have fun, and good luck!' - warn_title: Warning - warn_description: >- - The file <1>config.yaml already exists. If you need to reset any of the - configuration items in this file, please delete it first. - install_now: You may try <1>installing now. - installed: Already installed - installed_description: >- - You appear to have already installed. To reinstall please clear your old - database tables first. - page_404: - description: 'Unfortunately, this page doesn''t exist.' - back_home: Back to homepage - page_50X: - description: The server encountered an error and could not complete your request. - back_home: Back to homepage - page_maintenance: - description: 'We are under maintenance, we’ll be back soon.' - admin: - admin_header: - title: Admin - nav_menus: - dashboard: Dashboard - contents: Contents - questions: Questions - answers: Answers - users: Users - flags: Flags - settings: Settings - general: General - interface: Interface - smtp: SMTP - dashboard: - title: Dashboard - welcome: Welcome to Answer Admin! - site_statistics: Site Statistics - questions: 'Questions:' - answers: 'Answers:' - comments: 'Comments:' - votes: 'Votes:' - active_users: 'Active users:' - flags: 'Flags:' - site_health_status: Site Health Status - version: 'Version:' - https: 'HTTPS:' - uploading_files: 'Uploading files:' - smtp: 'SMTP:' - timezone: 'Timezone:' - system_info: System Info - storage_used: 'Storage used:' - uptime: 'Uptime:' - answer_links: Answer Links - documents: Documents - feedback: Feedback - review: Review - config: Config - update_to: Update to - latest: Latest - check_failed: Check failed - 'yes': 'Yes' - 'no': 'No' - not_allowed: Not allowed - allowed: Allowed - enabled: Enabled - disabled: Disabled - flags: - title: Flags - pending: Pending - completed: Completed - flagged: Flagged - created: Created - action: Action - review: Review - change_modal: - title: Change user status to... - btn_cancel: Cancel - btn_submit: Submit - normal_name: normal - normal_description: A normal user can ask and answer questions. - suspended_name: suspended - suspended_description: A suspended user can't log in. - deleted_name: deleted - deleted_description: 'Delete profile, authentication associations.' - inactive_name: inactive - inactive_description: An inactive user must re-validate their email. - confirm_title: Delete this user - confirm_content: Are you sure you want to delete this user? This is permanent! - confirm_btn: Delete - msg: - empty: Please select a reason. - status_modal: - title: 'Change {{ type }} status to...' - normal_name: normal - normal_description: A normal post available to everyone. - closed_name: closed - closed_description: 'A closed question can''t answer, but still can edit, vote and comment.' - deleted_name: deleted - deleted_description: All reputation gained and lost will be restored. - btn_cancel: Cancel - btn_submit: Submit - btn_next: Next - users: - title: Users - name: Name - email: Email - reputation: Reputation - created_at: Created Time - delete_at: Deleted Time - suspend_at: Suspended Time - status: Status - action: Action - change: Change - all: All - inactive: Inactive - suspended: Suspended - deleted: Deleted - normal: Normal - filter: - placeholder: 'Filter by name, user:id' - questions: - page_title: Questions - normal: Normal - closed: Closed - deleted: Deleted - post: Post - votes: Votes - answers: Answers - created: Created - status: Status - action: Action - change: Change - filter: - placeholder: 'Filter by title, question:id' - answers: - page_title: Answers - normal: Normal - deleted: Deleted - post: Post - votes: Votes - created: Created - status: Status - action: Action - change: Change - filter: - placeholder: 'Filter by title, answer:id' - general: - page_title: General - name: - label: Site Name - msg: Site name cannot be empty. - text: 'The name of this site, as used in the title tag.' - site_url: - label: Site URL - msg: Site url cannot be empty. - validate: Please enter a valid URL. - text: The address of your site. - short_description: - label: Short Site Description (optional) - msg: Short site description cannot be empty. - text: 'Short description, as used in the title tag on homepage.' - description: - label: Site Description (optional) - msg: Site description cannot be empty. - text: 'Describe this site in one sentence, as used in the meta description tag.' - contact_email: - label: Contact Email - msg: Contact email cannot be empty. - validate: Contact email is not valid. - text: Email address of key contact responsible for this site. - interface: - page_title: Interface - logo: - label: Logo (optional) - msg: Site logo cannot be empty. - text: You can upload your image or <1>reset it to the site title text. - theme: - label: Theme - msg: Theme cannot be empty. - text: Select an existing theme. - language: - label: Interface Language - msg: Interface language cannot be empty. - text: User interface language. It will change when you refresh the page. - time_zone: - label: Timezone - msg: Timezone cannot be empty. - text: Choose a city in the same timezone as you. - smtp: - page_title: SMTP - from_email: - label: From Email - msg: From email cannot be empty. - text: The email address which emails are sent from. - from_name: - label: From Name - msg: From name cannot be empty. - text: The name which emails are sent from. - smtp_host: - label: SMTP Host - msg: SMTP host cannot be empty. - text: Your mail server. - encryption: - label: Encryption - msg: Encryption cannot be empty. - text: For most servers SSL is the recommended option. - ssl: SSL - none: None - smtp_port: - label: SMTP Port - msg: SMTP port must be number 1 ~ 65535. - text: The port to your mail server. - smtp_username: - label: SMTP Username - msg: SMTP username cannot be empty. - smtp_password: - label: SMTP Password - msg: SMTP password cannot be empty. - test_email_recipient: - label: Test Email Recipients - text: Provide email address that will receive test sends. - msg: Test email recipients is invalid - smtp_authentication: - label: SMTP Authentication - msg: SMTP authentication cannot be empty. - 'yes': 'Yes' - 'no': 'No' diff --git a/ui/src/i18n/locales/i18n.yaml b/ui/src/i18n/locales/i18n.yaml deleted file mode 100644 index 13a6c5228..000000000 --- a/ui/src/i18n/locales/i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# all support language -language_options: - - label: "简体中文(CN)" - value: "zh_CN" - - label: "English(US)" - value: "en_US" diff --git a/ui/src/i18n/locales/it_IT.yaml b/ui/src/i18n/locales/it_IT.yaml deleted file mode 100644 index 0b9439412..000000000 --- a/ui/src/i18n/locales/it_IT.yaml +++ /dev/null @@ -1,170 +0,0 @@ -base: - success: - other: "Successo" - unknown: - other: "Errore sconosciuto" - request_format_error: - other: "Il formato della richiesta non è valido" - unauthorized_error: - other: "Non autorizzato" - database_error: - other: "Errore server dati" - -email: - other: "email" -password: - other: "password" - -email_or_password_wrong_error: &email_or_password_wrong - other: "Email o password errati" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "Risposta non trovata" - comment: - edit_without_permission: - other: "Non si hanno di privilegi sufficienti per modificare il commento" - not_found: - other: "Commento non trovato" - email: - duplicate: - other: "email già esistente" - need_to_be_verified: - other: "email deve essere verificata" - verify_url_expired: - other: "l'url di verifica email è scaduto, si prega di reinviare la email" - lang: - not_found: - other: "lingua non trovata" - object: - captcha_verification_failed: - other: "captcha errato" - disallow_follow: - other: "Non sei autorizzato a seguire" - disallow_vote: - other: "non sei autorizzato a votare" - disallow_vote_your_self: - other: "Non puoi votare un tuo post!" - not_found: - other: "oggetto non trovato" - verification_failed: - other: "verifica fallita" - email_or_password_incorrect: - other: "email o password incorretti" - old_password_verification_failed: - other: "la verifica della vecchia password è fallita" - new_password_same_as_previous_setting: - other: "La nuova password è identica alla precedente" - question: - not_found: - other: "domanda non trovata" - rank: - fail_to_meet_the_condition: - other: "Condizioni non valide per il grado" - report: - handle_failed: - other: "Gestione del report fallita" - not_found: - other: "Report non trovato" - tag: - not_found: - other: "Etichetta non trovata" - theme: - not_found: - other: "tema non trovato" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "utente non trovato" - suspended: - other: "utente sospeso" - username_invalid: - other: "utente non valido" - username_duplicate: - other: "utente già in uso" - -report: - spam: - name: - other: "spam" - description: - other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente" - rude: - name: - other: "scortese o violento" - description: - other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso" - duplicate: - name: - other: "duplicato" - description: - other: "Questa domanda è già stata posta e ha già una risposta." - not_answer: - name: - other: "non è una risposta" - description: - other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto." - not_need: - name: - other: "non più necessario" - description: - other: "Questo commento è datato, conversazionale o non rilevante a questo articolo." - other: - name: - other: "altro" - description: - other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra." - -question: - close: - duplicate: - name: - other: "spam" - description: - other: "Questa domanda è già stata chiesta o ha già una risposta." - guideline: - name: - other: "motivo legato alla community" - description: - other: "Questa domanda non soddisfa le linee guida della comunità." - multiple: - name: - other: "richiede maggiori dettagli o chiarezza" - description: - other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." - other: - name: - other: "altro" - description: - other: "Questo articolo richiede un'altro motivo non listato sopra." - -notification: - action: - update_question: - other: "domanda aggiornata" - answer_the_question: - other: "domanda risposta" - update_answer: - other: "risposta aggiornata" - adopt_answer: - other: "risposta accettata" - comment_question: - other: "domanda commentata" - comment_answer: - other: "risposta commentata" - reply_to_you: - other: "hai ricevuto risposta" - mention_you: - other: "sei stato menzionato" - your_question_is_closed: - other: "la tua domanda è stata chiusa" - your_question_was_deleted: - other: "la tua domanda è stata rimossa" - your_answer_was_deleted: - other: "la tua risposta è stata rimossa" - your_comment_was_deleted: - other: "il tuo commento è stato rimosso" diff --git a/ui/src/i18n/locales/zh_CN.yaml b/ui/src/i18n/locales/zh_CN.yaml deleted file mode 100644 index a02f42193..000000000 --- a/ui/src/i18n/locales/zh_CN.yaml +++ /dev/null @@ -1,920 +0,0 @@ -base: - success: - other: "成功" - unknown: - other: "未知错误" - request_format_error: - other: "请求格式错误" - unauthorized_error: - other: "未登录" - database_error: - other: "数据服务异常" - -email: - other: "邮箱" -password: - other: "密码" - -email_or_password_wrong_error: &email_or_password_wrong - other: "邮箱或密码错误" - -error: - admin: - email_or_password_wrong: *email_or_password_wrong - answer: - not_found: - other: "答案未找到" - comment: - edit_without_permission: - other: "不允许编辑评论" - not_found: - other: "评论未找到" - email: - duplicate: - other: "邮箱已经存在" - need_to_be_verified: - other: "邮箱需要验证" - verify_url_expired: - other: "邮箱验证的网址已过期,请重新发送邮件" - lang: - not_found: - other: "语言未找到" - object: - captcha_verification_failed: - other: "验证码错误" - disallow_follow: - other: "你不能关注" - disallow_vote: - other: "你不能投票" - disallow_vote_your_self: - other: "你不能为自己的帖子投票!" - not_found: - other: "对象未找到" - verification_failed: - other: "验证失败" - email_or_password_incorrect: - other: "邮箱或密码不正确" - old_password_verification_failed: - other: "旧密码验证失败" - new_password_same_as_previous_setting: - other: "新密码与之前的设置相同" - question: - not_found: - other: "问题未找到" - rank: - fail_to_meet_the_condition: - other: "级别不符合条件" - report: - handle_failed: - other: "报告处理失败" - not_found: - other: "报告未找到" - tag: - not_found: - other: "标签未找到" - theme: - not_found: - other: "主题未找到" - user: - email_or_password_wrong: - other: *email_or_password_wrong - not_found: - other: "用户未找到" - suspended: - other: "用户已被暂停" - username_invalid: - other: "用户名无效" - username_duplicate: - other: "用户名已被使用" - set_avatar: - other: "头像设置错误" - -report: - spam: - name: - other: "垃圾信息" - description: - other: "此帖子是一个广告贴,或是破坏性行为。它对当前的主题无用,也不相关。" - rude: - name: - other: "粗鲁或辱骂的" - description: - other: "有理智的人都会发现此内容不适合进行尊重的讨论。" - duplicate: - name: - other: "重复信息" - description: - other: "此问题以前就有人问过,而且已经有了答案。" - not_answer: - name: - other: "不是答案" - description: - other: "此帖子是作为一个答案发布的,但它并没有试图回答这个问题。总之,它可能应该是个编辑,评论,另一个问题或者被删除。" - not_need: - name: - other: "不再需要" - description: - other: "此条评论是过时的,对话性的或与本帖无关。" - other: - name: - other: "其他原因" - description: - other: "此帖子需要工作人员关注,因为是上述所列以外的其他理由。" - -question: - close: - duplicate: - name: - other: "垃圾信息" - description: - other: "此问题以前就有人问过,而且已经有了答案。" - guideline: - name: - other: "社区特定原因" - description: - other: "此问题不符合社区准则。" - multiple: - name: - other: "需要细节或澄清" - description: - other: "此问题目前涵盖多个问题。它应该只集中在一个问题上。" - other: - name: - other: "其他原因" - description: - other: "此帖子需要上述所列以外的其他理由。" - -notification: - action: - update_question: - other: "更新了问题" - answer_the_question: - other: "回答了问题" - update_answer: - other: "更新了答案" - adopt_answer: - other: "接受了答案" - comment_question: - other: "评论了问题" - comment_answer: - other: "评论了答案" - reply_to_you: - other: "回复了你" - mention_you: - other: "提到了你" - your_question_is_closed: - other: "你的问题已被关闭" - your_question_was_deleted: - other: "你的问题已被删除" - your_answer_was_deleted: - other: "你的答案已被删除" - your_comment_was_deleted: - other: "你的评论已被删除" -# The following fields are used for interface presentation(Front-end) -ui: - how_to_format: - title: 如何设定文本格式 - description: >- -
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 - **粗体**

  • 使用 4 - 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 - `像 _这样_`

  • 使用```创建代码块

    ```
    // - 这是代码
    ```
- pagination: - prev: 上一页 - next: 下一页 - page_title: - question: 问题 - questions: 问题 - tag: 标签 - tags: 标签 - tag_wiki: 标签 wiki - edit_tag: 编辑标签 - ask_a_question: 提问题 - edit_question: 编辑问题 - edit_answer: 编辑回答 - search: 搜索 - posts_containing: 包含 - settings: 设定 - notifications: 通知 - login: 登录 - sign_up: 注册 - account_recovery: 账号恢复 - account_activation: 账号激活 - confirm_email: 确认电子邮件 - account_suspended: 账号已封禁 - admin: 后台管理 - notifications: - title: 通知 - inbox: 收件箱 - achievement: 成就 - all_read: 全部标记为已读 - show_more: 显示更多 - suspended: - title: 账号已封禁 - until_time: '你的账号被封禁至{{ time }}。' - forever: 你的账号已被永久封禁。 - end: 违反了我们的社区准则。 - editor: - blockquote: - text: 引用 - bold: - text: 粗体 - chart: - text: 图表 - flow_chart: 流程图 - sequence_diagram: 时序图 - class_diagram: 类图 - state_diagram: 状态图 - entity_relationship_diagram: ER 图 - user_defined_diagram: User defined diagram - gantt_chart: 甘特图 - pie_chart: 饼图 - code: - text: 代码块 - add_code: 添加代码块 - form: - fields: - code: - label: 代码块 - msg: - empty: 代码块不能为空 - language: - label: 语言 (可选) - placeholder: 自动识别 - btn_cancel: 取消 - btn_confirm: 添加 - formula: - text: 公式 - options: - inline: 行内公式 - block: 公式块 - heading: - text: 标题 - options: - h1: 标题 1 - h2: 标题 2 - h3: 标题 3 - h4: 标题 4 - h5: 标题 5 - h6: 标题 6 - help: - text: 帮助 - hr: - text: 水平分割线 - image: - text: 图片 - add_image: 添加图片 - tab_image: 上传图片 - form_image: - fields: - file: - label: 图片文件 - btn: 选择图片 - msg: - empty: 请选择图片文件。 - only_image: 只能上传图片文件。 - max_size: 图片文件大小不能超过 4 MB。 - description: - label: 图片描述(可选) - tab_url: 网络图片 - form_url: - fields: - url: - label: 图片地址 - msg: - empty: 图片地址不能为空 - name: - label: 图片描述(可选) - btn_cancel: 取消 - btn_confirm: 添加 - uploading: 上传中... - indent: - text: 添加缩进 - outdent: - text: 减少缩进 - italic: - text: 斜体 - link: - text: 超链接 - add_link: 添加超链接 - form: - fields: - url: - label: 链接 - msg: - empty: 链接不能为空。 - name: - label: 链接描述(可选) - btn_cancel: 取消 - btn_confirm: 添加 - ordered_list: - text: 有编号列表 - unordered_list: - text: 无编号列表 - table: - text: 表格 - heading: 表头 - cell: 单元格 - close_modal: - title: 关闭原因是... - btn_cancel: 取消 - btn_submit: 提交 - remark: - empty: 不能为空。 - msg: - empty: 请选择一个原因。 - report_modal: - flag_title: 举报原因是... - close_title: 关闭原因是... - review_question_title: 审查问题 - review_answer_title: 审查回答 - review_comment_title: 审查评论 - btn_cancel: 取消 - btn_submit: 提交 - remark: - empty: 不能为空 - msg: - empty: 请选择一个原因。 - tag_modal: - title: 创建新标签 - form: - fields: - display_name: - label: 显示名称(别名) - msg: - empty: 不能为空 - range: 不能超过 35 个字符 - slug_name: - label: URL 固定链接 - description: '必须由 "a-z", "0-9", "+ # - ." 组成' - msg: - empty: 不能为空 - range: 不能超过 35 个字符 - character: 包含非法字符 - description: - label: 标签描述(可选) - btn_cancel: 取消 - btn_submit: 提交 - tag_info: - created_at: 创建于 - edited_at: 编辑于 - synonyms: - title: 同义词 - text: 以下标签等同于 - empty: 此标签目前没有同义词。 - btn_add: 添加同义词 - btn_edit: 编辑 - btn_save: 保存 - synonyms_text: 以下标签等同于 - delete: - title: 删除标签 - content:

不允许删除有关联问题的标签。

请先从关联的问题中删除此标签的引用。

- content2: 确定要删除吗? - close: 关闭 - edit_tag: - title: 编辑标签 - default_reason: 编辑标签 - form: - fields: - revision: - label: 编辑历史 - display_name: - label: 名称 - slug_name: - label: URL 固定链接 - info: '必须由 "a-z", "0-9", "+ # - ." 组成' - description: - label: 描述 - edit_summary: - label: 编辑概要 - placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) - btn_save_edits: 保存更改 - btn_cancel: 取消 - dates: - long_date: MM月DD日 - long_date_with_year: YYYY年MM月DD日 - long_date_with_time: 'YYYY年MM月DD日 HH:mm' - now: 刚刚 - x_seconds_ago: '{{count}} 秒前' - x_minutes_ago: '{{count}} 分钟前' - x_hours_ago: '{{count}} 小时前' - comment: - btn_add_comment: 添加评论 - reply_to: 回复 - btn_reply: 回复 - btn_edit: 编辑 - btn_delete: 删除 - btn_flag: 举报 - btn_save_edits: 保存 - btn_cancel: 取消 - show_more: 显示更多评论 - tip_question: 使用评论提问更多信息或者提出改进意见。尽量避免使用评论功能回答问题。 - tip_answer: 使用评论对回答者进行回复,或者通知回答者你已更新了问题的内容。如果要补充或者完善问题的内容,请在原问题中更改。 - edit_answer: - title: 编辑回答 - default_reason: 编辑回答 - form: - fields: - revision: - label: 编辑历史 - answer: - label: 回答内容 - edit_summary: - label: 编辑概要 - placeholder: 简单描述更改原因 (错别字、文字表达、格式等等) - btn_save_edits: 保存更改 - btn_cancel: 取消 - tags: - title: 标签 - sort_buttons: - popular: 热门 - name: 名称 - newest: 最新 - button_follow: 关注 - button_following: 已关注 - tag_label: 个问题 - search_placeholder: 通过标签名过滤 - no_description: 此标签无描述。 - more: 更多 - ask: - title: 提交新的问题 - edit_title: 编辑问题 - default_reason: 编辑问题 - similar_questions: 相似的问题 - form: - fields: - revision: - label: 编辑历史 - title: - label: 标题 - placeholder: 请详细描述你的问题 - msg: - empty: 标题不能为空 - range: 标题最多 150 个字符 - body: - label: 内容 - msg: - empty: 内容不能为空 - tags: - label: 标签 - msg: - empty: 必须选择一个标签 - answer: - label: 回答内容 - msg: - empty: 回答内容不能为空 - btn_post_question: 提交问题 - btn_save_edits: 保存更改 - answer_question: 直接发表回答 - post_question&answer: 提交问题和回答 - tag_selector: - add_btn: 添加标签 - create_btn: 创建新标签 - search_tag: 搜索标签 - hint: 选择至少一个与问题相关的标签。 - no_result: 没有匹配的标签 - header: - nav: - question: 问题 - tag: 标签 - user: 用户 - profile: 用户主页 - setting: 账号设置 - logout: 退出登录 - admin: 后台管理 - search: - placeholder: 搜索 - footer: - build_on: >- - Built on <1> Answer - the open-source software that power Q&A - communities
Made with love © 2022 Answer - upload_img: - name: 更改图片 - loading: 加载中... - pic_auth_code: - title: 验证码 - placeholder: 输入图片中的文字 - msg: - empty: 不能为空 - inactive: - first: '马上就好了!我们发送了一封激活邮件到 {{mail}}。请按照邮件中的说明激活您的帐户。' - info: 如果没有收到,请检查您的垃圾邮件文件夹。 - another: '我们向您发送了另一封激活电子邮件,地址为 {{mail}}。它可能需要几分钟才能到达;请务必检查您的垃圾邮件文件夹。' - btn_name: 重新发送激活邮件 - msg: - empty: 不能为空 - login: - page_title: 欢迎来到 Answer - info_sign: 没有帐户?<1>注册 - info_login: 已经有一个帐户?<1>登录 - forgot_pass: 忘记密码? - name: - label: 昵称 - msg: - empty: 昵称不能为空 - range: 昵称最多 30 个字符 - email: - label: 邮箱 - msg: - empty: 邮箱不能为空 - password: - label: 密码 - msg: - empty: 密码不能为空 - different: 两次输入密码不一致 - account_forgot: - page_title: 忘记密码 - btn_name: 发送恢复邮件 - send_success: '如无意外,你的邮箱 {{mail}} 将会收到一封重置密码的邮件,请根据指引重置你的密码。' - email: - label: 邮箱 - msg: - empty: 邮箱不能为空 - password_reset: - page_title: 密码重置 - btn_name: 重置我的密码 - reset_success: 你已经成功更改密码,将返回登录页面 - link_invalid: 抱歉,此密码重置链接已失效。也许是你已经重置过密码了? - to_login: 前往登录页面 - password: - label: 密码 - msg: - empty: 密码不能为空 - length: 密码长度在8-32个字符之间 - different: 两次输入密码不一致 - password_confirm: - label: 确认新密码 - settings: - page_title: 设置 - nav: - profile: 我的资料 - notification: 通知 - account: 账号 - interface: 界面 - profile: - btn_name: 保存更改 - display_name: - label: 昵称 - msg: 昵称不能为空 - msg_range: 昵称不能超过 30 个字符 - username: - label: 用户名 - caption: 用户之间可以通过 "@用户名" 进行交互。 - msg: 用户名不能为空 - msg_range: 用户名不能超过 30 个字符 - character: '用户名只能由 "a-z", "0-9", " - . _" 组成' - avatar: - label: 头像 - text: 您可以上传图片作为头像,也可以 <1>重置 为 - bio: - label: 关于我 (可选) - website: - label: 网站 (可选) - placeholder: 'https://example.com' - msg: 格式不正确 - location: - label: 位置 (可选) - placeholder: '城市, 国家' - notification: - email: - label: 邮件通知 - radio: 你的提问有新的回答,评论,和其他 - account: - change_email_btn: 更改邮箱 - change_pass_btn: 更改密码 - change_email_info: 邮件已发送。请根据指引完成验证。 - email: - label: 邮箱 - msg: 邮箱不能为空 - password_title: 密码 - current_pass: - label: 当前密码 - msg: - empty: 当前密码不能为空 - length: 密码长度必须在 8 至 32 之间 - different: 两次输入的密码不匹配 - new_pass: - label: 新密码 - pass_confirm: - label: 确认新密码 - interface: - lang: - label: 界面语言 - text: 设置用户界面语言,在刷新页面后生效。 - toast: - update: 更新成功 - update_password: 更改密码成功。 - flag_success: 感谢您的标记,我们会尽快处理。 - related_question: - title: 相关问题 - btn: 我要提问 - answers: 个回答 - question_detail: - Asked: 提问于 - asked: 提问于 - update: 修改于 - edit: 最后编辑于 - Views: 阅读次数 - Follow: 关注此问题 - Following: 已关注 - answered: 回答于 - closed_in: 关闭于 - show_exist: 查看相关问题。 - answers: - title: 个回答 - score: 评分 - newest: 最新 - btn_accept: 采纳 - btn_accepted: 已被采纳 - write_answer: - title: 你的回答 - btn_name: 提交你的回答 - confirm_title: 继续回答 - continue: 继续 - confirm_info:

您确定要提交一个新的回答吗?

您可以直接编辑和改善您之前的回答的。

- empty: 回答内容不能为空。 - delete: - title: 删除 - question: >- - 我们不建议删除有回答的帖子。因为这样做会使得后来的读者无法从该问题中获得帮助。

如果删除过多有回答的帖子,你的账号将会被禁止提问。你确定要删除吗? - answer_accepted: >- -

我们不建议删除被采纳的回答。因为这样做会使得后来的读者无法从该回答中获得帮助。

如果删除过多被采纳的回答,你的账号将会被禁止回答任何提问。你确定要删除吗? - other: 你确定要删除? - tip_question_deleted: 此问题已被删除 - tip_answer_deleted: 此回答已被删除 - btns: - confirm: 确认 - cancel: 取消 - save: 保存 - delete: 删除 - login: 登录 - signup: 注册 - logout: 退出登录 - verify: 验证 - add_question: 我要提问 - search: - title: 搜索结果 - keywords: 关键词 - options: 选项 - follow: 关注 - following: 已关注 - counts: '{{count}} 个结果' - more: 更多 - sort_btns: - relevance: 相关性 - newest: 最新的 - active: 活跃的 - score: 评分 - tips: - title: 高级搜索提示 - tag: '<1>[tag] 在指定标签中搜索' - user: '<1>user:username 根据作者搜索' - answer: '<1>answers:0 搜索未回答的问题' - score: '<1>score:3 评分 3 分或以上' - question: '<1>is:question 只搜索问题' - is_answer: '<1>is:answer 只搜索回答' - empty: 找不到任何相关的内容。
请尝试其他关键字,或者减少查找内容的长度。 - share: - name: 分享 - copy: 复制链接 - via: 分享在... - copied: 已复制 - facebook: 分享到 Facebook - twitter: 分享到 Twitter - cannot_vote_for_self: 不能给自己投票 - modal_confirm: - title: 发生错误... - account_result: - page_title: 欢迎来到 Answer - success: 你的账号已通过验证,即将返回首页。 - link: 返回首页 - invalid: 抱歉,此验证链接已失效。也许是你的账号已经通过验证了? - confirm_new_email: 你的电子邮箱已更新 - confirm_new_email_invalid: 抱歉,此验证链接已失效。也许是你的邮箱已经成功更改了? - question: - following_tags: 已关注的标签 - edit: 编辑 - save: 保存 - follow_tag_tip: 按照标签整理您的问题列表。 - hot_questions: 热点问题 - all_questions: 全部问题 - x_questions: '{{ count }} 个问题' - x_answers: '{{ count }} 个回答' - questions: 个问题 - answers: 回答 - newest: 最新 - active: 活跃 - frequent: 浏览量 - score: 评分 - unanswered: 未回答 - modified: 修改于 - answered: 回答于 - asked: 提问于 - closed: 已关闭 - follow_a_tag: 关注一个标签 - more: 更多 - personal: - overview: 概览 - answers: 回答 - answer: 回答 - questions: 问题 - question: 问题 - bookmarks: 收藏 - reputation: 声望 - comments: 评论 - votes: 得票 - newest: 最新 - score: 评分 - edit_profile: 编辑我的资料 - visited_x_days: 'Visited {{ count }} days' - viewed: Viewed - joined: 加入于 - last_login: 上次登录 - about_me: 关于我 - about_me_empty: '// Hello, World !' - top_answers: 热门回答 - top_questions: 热门问题 - stats: 状态 - list_empty: 没有找到相关的内容。
试试看其他标签? - accepted: 已采纳 - answered: 回答于 - asked: 提问于 - upvote: 赞同 - downvote: 反对 - mod_short: 管理员 - mod_long: 管理员 - x_reputation: 声望 - x_votes: 得票 - x_answers: 个回答 - x_questions: 个问题 - page_404: - description: 页面不存在 - back_home: 回到主页 - page_50X: - description: 服务器遇到了一个错误,无法完成你的请求。 - back_home: 回到主页 - admin: - admin_header: - title: 后台管理 - nav_menus: - dashboard: 后台管理 - contents: 内容管理 - questions: 问题 - answers: 回答 - users: 用户管理 - flags: 举报管理 - settings: 站点设置 - general: 一般 - interface: 界面 - smtp: SMTP - dashboard: - title: 后台管理 - welcome: 欢迎来到 Answer 后台管理! - version: 版本 - flags: - title: 举报 - pending: 等待处理 - completed: 已完成 - flagged: 被举报内容 - created: 创建于 - action: 操作 - review: 审查 - change_modal: - title: 更改用户状态为... - btn_cancel: 取消 - btn_submit: 提交 - normal_name: 正常 - normal_description: 正常状态的用户可以提问和回答。 - suspended_name: 封禁 - suspended_description: 被封禁的用户将无法登录。 - deleted_name: 删除 - deleted_description: 删除用户的个人信息,认证等等。 - inactive_name: 不活跃 - inactive_description: 不活跃的用户必须重新验证邮箱。 - confirm_title: 删除此用户 - confirm_content: 确定要删除此用户?此操作无法撤销! - confirm_btn: 删除 - msg: - empty: 请选择一个原因 - status_modal: - title: '更改 {{ type }} 状态为...' - normal_name: 正常 - normal_description: 所有用户都可以访问 - closed_name: 关闭 - closed_description: 不能回答,但仍然可以编辑、投票和评论。 - deleted_name: 删除 - deleted_description: 所有获得/损失的声望将会恢复。 - btn_cancel: 取消 - btn_submit: 提交 - btn_next: 下一步 - users: - title: 用户 - name: 名称 - email: 邮箱 - reputation: 声望 - created_at: 创建时间 - delete_at: 删除时间 - suspend_at: 封禁时间 - status: 状态 - action: 操作 - change: 更改 - all: 全部 - inactive: 不活跃 - suspended: 已封禁 - deleted: 已删除 - normal: 正常 - questions: - page_title: 问题 - normal: 正常 - closed: 已关闭 - deleted: 已删除 - post: 标题 - votes: 得票数 - answers: 回答数 - created: 创建于 - status: 状态 - action: 操作 - change: 更改 - answers: - page_title: 回答 - normal: 正常 - deleted: 已删除 - post: 标题 - votes: 得票数 - created: 创建于 - status: 状态 - action: 操作 - change: 更改 - general: - page_title: 一般 - name: - label: 站点名称 - msg: 不能为空 - text: 站点的名称,作为站点的标题(HTML 的 title 标签)。 - short_description: - label: 简短的站点标语 (可选) - msg: 不能为空 - text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。 - description: - label: 网站描述 (可选) - msg: 不能为空 - text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。 - interface: - page_title: 界面 - logo: - label: Logo (可选) - msg: 不能为空 - text: 可以上传图片,或者<1>重置为站点标题。 - theme: - label: 主题 - msg: 不能为空 - text: 选择一个主题 - language: - label: 界面语言 - msg: 不能为空 - text: 设置用户界面语言,在刷新页面后生效。 - smtp: - page_title: SMTP - from_email: - label: 发件人地址 - msg: 不能为空 - text: 用于发送邮件的地址。 - from_name: - label: 发件人名称 - msg: 不能为空 - text: 发件人的名称 - smtp_host: - label: SMTP 主机 - msg: 不能为空 - text: 邮件服务器 - encryption: - label: 加密 - msg: 不能为空 - text: 对于大多数服务器而言,SSL 是推荐开启的。 - ssl: SSL - none: 无加密 - smtp_port: - label: SMTP 端口 - msg: SMTP 端口必须在 1 ~ 65535 之间。 - text: 邮件服务器的端口号。 - smtp_username: - label: SMTP 用户名 - msg: 不能为空 - smtp_password: - label: SMTP 密码 - msg: 不能为空 - test_email_recipient: - label: 测试邮件收件人 - text: 提供用于接收测试邮件的邮箱地址。 - msg: 地址无效 - smtp_authentication: - label: SMTP 认证 - msg: 不能为空 - 'yes': 是 - 'no': 否 - diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts index 86f93d6f3..5071ee9d8 100644 --- a/ui/src/utils/localize.ts +++ b/ui/src/utils/localize.ts @@ -21,7 +21,7 @@ export const loadLanguageOptions = async (forAdmin = false) => { ? await getAdminLanguageOptions() : await getLanguageOptions(); if (process.env.NODE_ENV === 'development') { - const { default: optConf } = await import('@/i18n/locales/i18n.yaml'); + const { default: optConf } = await import('@i18n/i18n.yaml'); optConf?.language_options.forEach((opt) => { if (!languageOptions.find((_) => opt.label === _.label)) { languageOptions.push(opt); @@ -35,9 +35,7 @@ const addI18nResource = async (langName) => { const res = { lng: langName, resources: undefined }; if (process.env.NODE_ENV === 'development') { try { - const { default: resConf } = await import( - `@/i18n/locales/${langName}.yaml` - ); + const { default: resConf } = await import(`@i18n/${langName}.yaml`); res.resources = resConf.ui; } catch (ex) { console.log('ex: ', ex); diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 000cc227a..afe1b5b92 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -20,8 +20,9 @@ "jsx": "react-jsx", "baseUrl": "./", "paths": { - "@/*": ["src/*"] + "@/*": ["src/*"], + "@i18n/*": ["../i18n/*"] } }, - "include": ["src", "node_modules/@testing-library/jest-dom", "scripts"] + "include": ["src", "node_modules/@testing-library/jest-dom" ] } From 17bac93737ca802a23b620c7d286c2fd8bc875e3 Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Thu, 10 Nov 2022 11:21:42 +0800 Subject: [PATCH 0233/3337] fix(i18n): typo & indent --- i18n/en_US.yaml | 24 ++++++++++++------------ i18n/zh_CN.yaml | 22 +++++++++++----------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index fa6a1de34..9892f9456 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -173,17 +173,17 @@ notification: # The following fields are used for interface presentation(Front-end) ui: how_to_format: - title: How to Format - description: >- -
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by - placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
+ title: How to Format + description: >- +
  • to make links

    <https://url.com>

    [Title](https://url.com)
  • put returns between paragraphs

  • _italic_ or **bold**

  • indent code by 4 spaces

  • quote by + placing > at start of line

  • backtick escapes `like _this_`

  • create code fences with backticks `

    ```
    code here
    ```
pagination: prev: Prev next: Next @@ -211,7 +211,7 @@ ui: change_email: Modify Email install: Answer Installation upgrade: Answer Upgrade - maintenance: Webite Maintenance + maintenance: Website Maintenance notifications: title: Notifications inbox: Inbox diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index a02f42193..703788958 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -173,17 +173,17 @@ notification: # The following fields are used for interface presentation(Front-end) ui: how_to_format: - title: 如何设定文本格式 - description: >- -
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 - **粗体**

  • 使用 4 - 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 - `像 _这样_`

  • 使用```创建代码块

    ```
    // - 这是代码
    ```
+ title: 如何设定文本格式 + description: >- +
  • 添加链接:

    <https://url.com>

    [标题](https://url.com)
  • 段落之间使用空行分隔

  • _斜体_ 或者 + **粗体**

  • 使用 4 + 个空格缩进代码

  • 在行首添加>表示引用

  • 反引号进行转义 + `像 _这样_`

  • 使用```创建代码块

    ```
    // + 这是代码
    ```
pagination: prev: 上一页 next: 下一页 From 555c1936ade4a447158cee374d15c0bf707ff88b Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 10 Nov 2022 11:22:07 +0800 Subject: [PATCH 0234/3337] =?UTF-8?q?fix:=20/install=20path=20will=20be=20?= =?UTF-8?q?redirected=20to=20/=CB=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/router/ui.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/router/ui.go b/internal/router/ui.go index 3498265c6..f1296d9f7 100644 --- a/internal/router/ui.go +++ b/internal/router/ui.go @@ -78,6 +78,9 @@ func (a *UIRouter) Register(r *gin.Engine) { filePath = UIRootFilePath + name case "/manifest.json": filePath = UIRootFilePath + name + case "/install": + c.Redirect(http.StatusFound, "/") + return default: filePath = UIIndexFilePath c.Header("content-type", "text/html;charset=utf-8") From 42f3154233824459b57c06d64a3aeb02e2bb18f7 Mon Sep 17 00:00:00 2001 From: kumfo Date: Thu, 10 Nov 2022 11:23:07 +0800 Subject: [PATCH 0235/3337] fix: fetch excerpt, if contain link, show link name --- pkg/htmltext/htmltext.go | 4 ++-- pkg/htmltext/htmltext_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/htmltext/htmltext.go b/pkg/htmltext/htmltext.go index 31ae1a223..e0463e109 100644 --- a/pkg/htmltext/htmltext.go +++ b/pkg/htmltext/htmltext.go @@ -17,8 +17,8 @@ func ClearText(html string) (text string) { re *regexp.Regexp codeReg = `(?ism)<(pre)>.*<\/pre>` codeRepl = "{code...}" - linkReg = `(?ism).*?<\/a>` - linkRepl = "[link]" + linkReg = `(?ism)(.*)?<\/a>` + linkRepl = " [$1] " spaceReg = ` +` spaceRepl = " " ) diff --git a/pkg/htmltext/htmltext_test.go b/pkg/htmltext/htmltext_test.go index 71aafbb6e..a320ef128 100644 --- a/pkg/htmltext/htmltext_test.go +++ b/pkg/htmltext/htmltext_test.go @@ -17,8 +17,8 @@ func TestClearText(t *testing.T) { assert.Equal(t, expected, clearedText) // test link clear text - expected = "hello[link]" - clearedText = ClearText("

helloexample.com

") + expected = "hello [example.com]" + clearedText = ClearText("

hello example.com

") assert.Equal(t, expected, clearedText) clearedText = ClearText("

helloexample.com

") assert.Equal(t, expected, clearedText) From 3f5ac0a6144892416367f57c3b15b163a40adcbd Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 10 Nov 2022 11:31:53 +0800 Subject: [PATCH 0236/3337] feat: add /50x web page --- internal/install/install_server.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/install/install_server.go b/internal/install/install_server.go index b2ada51eb..c0c38a7f4 100644 --- a/internal/install/install_server.go +++ b/internal/install/install_server.go @@ -35,16 +35,16 @@ func NewInstallHTTPServer() *gin.Engine { installApi := r.Group("") installApi.GET("/install", WebPage) - + installApi.GET("/50x", WebPage) installApi.GET("/installation/language/options", LangOptions) - installApi.POST("/installation/db/check", CheckDatabase) - installApi.POST("/installation/config-file/check", CheckConfigFile) - installApi.POST("/installation/init", InitEnvironment) - installApi.POST("/installation/base-info", InitBaseInfo) + + r.NoRoute(func(ctx *gin.Context) { + ctx.Redirect(http.StatusFound, "/50x") + }) return r } From 7e11d6b82c2c0fb704da85d190e976016bd81bdf Mon Sep 17 00:00:00 2001 From: LinkinStar Date: Thu, 10 Nov 2022 12:13:42 +0800 Subject: [PATCH 0237/3337] fix: install check database add log --- internal/cli/install_check.go | 6 ++++++ internal/install/install_req.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/cli/install_check.go b/internal/cli/install_check.go index 947d71e07..4526ccd80 100644 --- a/internal/cli/install_check.go +++ b/internal/cli/install_check.go @@ -1,6 +1,8 @@ package cli import ( + "fmt" + "github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/pkg/dir" @@ -19,9 +21,11 @@ func CheckUploadDir() bool { func CheckDB(dataConf *data.Database, mustInstalled bool) bool { db, err := data.NewDB(false, dataConf) if err != nil { + fmt.Printf("connection database failed: %s\n", err) return false } if err = db.Ping(); err != nil { + fmt.Printf("connection ping database failed: %s\n", err) return false } if !mustInstalled { @@ -30,9 +34,11 @@ func CheckDB(dataConf *data.Database, mustInstalled bool) bool { exist, err := db.IsTableExist(&entity.Version{}) if err != nil { + fmt.Printf("check table exist failed: %s\n", err) return false } if !exist { + fmt.Printf("check table not exist\n") return false } return true diff --git a/internal/install/install_req.go b/internal/install/install_req.go index 7c836d5ad..a3add9ca8 100644 --- a/internal/install/install_req.go +++ b/internal/install/install_req.go @@ -35,7 +35,7 @@ func (r *CheckDatabaseReq) GetConnection() string { } if r.DbType == string(schemas.POSTGRES) { host, port := parsePgSQLHostPort(r.DbHost) - return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s", + return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", host, port, r.DbUsername, r.DbPassword, r.DbName) } return "" From acd263a9db3fba1c183dc9a4f5976ad43b04df22 Mon Sep 17 00:00:00 2001 From: shuai Date: Thu, 10 Nov 2022 12:19:34 +0800 Subject: [PATCH 0238/3337] feat: add brand upload component --- i18n/en_US.yaml | 3 ++ ui/src/common/constants.ts | 7 +++- ui/src/components/BrandUpload/index.tsx | 39 +++++++++++++++++++ ui/src/components/UploadImg/index.tsx | 8 ++-- ui/src/components/index.ts | 2 + ui/src/i18n/locales/en_US.yaml | 3 ++ ui/src/index.scss | 10 +++++ ui/src/pages/Admin/Branding/index.tsx | 37 ++++++++++++++++++ ui/src/pages/Admin/Interface/index.tsx | 4 +- ui/src/pages/Admin/index.tsx | 7 +++- ui/src/pages/Users/Settings/Profile/index.tsx | 6 ++- ui/src/router/routes.ts | 4 ++ 12 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 ui/src/components/BrandUpload/index.tsx create mode 100644 ui/src/pages/Admin/Branding/index.tsx diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index fa6a1de34..0ade02a18 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -915,6 +915,7 @@ ui: general: General interface: Interface smtp: SMTP + branding: Branding dashboard: title: Dashboard welcome: Welcome to Answer Admin! @@ -1109,3 +1110,5 @@ ui: msg: SMTP authentication cannot be empty. 'yes': 'Yes' 'no': 'No' + branding: + page_title: Branding diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 1bf1f355d..3ed5e37c3 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -54,7 +54,12 @@ export const ADMIN_NAV_MENUS = [ }, { name: 'settings', - child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], + child: [ + { name: 'general' }, + { name: 'interface' }, + { name: 'branding' }, + { name: 'smtp' }, + ], }, ]; diff --git a/ui/src/components/BrandUpload/index.tsx b/ui/src/components/BrandUpload/index.tsx new file mode 100644 index 000000000..477efea0e --- /dev/null +++ b/ui/src/components/BrandUpload/index.tsx @@ -0,0 +1,39 @@ +import { FC } from 'react'; +import { ButtonGroup, Button } from 'react-bootstrap'; + +import { Icon, UploadImg } from '@/components'; + +interface Props { + type: string; + imgPath: string; + uploadCallback: (data: FormData) => Promise; + deleteCallback: (type: string) => void; +} + +const Index: FC = ({ + type, + imgPath, + uploadCallback, + deleteCallback, +}) => { + return ( +
+
+ +
+ + + + + + + +
+ ); +}; + +export default Index; diff --git a/ui/src/components/UploadImg/index.tsx b/ui/src/components/UploadImg/index.tsx index 3054fac48..90cca3e4b 100644 --- a/ui/src/components/UploadImg/index.tsx +++ b/ui/src/components/UploadImg/index.tsx @@ -3,10 +3,12 @@ import { useTranslation } from 'react-i18next'; interface IProps { type: string; + className?: string; + children?: React.ReactNode; upload: (data: FormData) => Promise; } -const Index: React.FC = ({ type, upload }) => { +const Index: React.FC = ({ type, upload, children, className }) => { const { t } = useTranslation(); const [status, setStatus] = useState(false); @@ -35,8 +37,8 @@ const Index: React.FC = ({ type, upload }) => { }; return ( -
{li.vote_count}