├── .gitattributes ├── .gitignore ├── LICENSE ├── Makefile ├── README.MD ├── api └── v1 │ ├── api.go │ ├── article.go │ ├── ask.go │ ├── captcha.go │ ├── category.go │ ├── content.go │ ├── file.go │ ├── index.go │ ├── interact.go │ ├── login.go │ ├── profile.go │ ├── register.go │ ├── reply.go │ ├── search.go │ ├── topic.go │ └── user.go ├── go.mod ├── go.sum ├── hack └── config.yaml ├── internal ├── cmd │ └── cmd.go ├── consts │ ├── consts.go │ ├── content.go │ ├── interact.go │ ├── openapi.go │ ├── session.go │ └── user.go ├── controller │ ├── article.go │ ├── ask.go │ ├── captcha.go │ ├── category.go │ ├── content.go │ ├── file.go │ ├── index.go │ ├── interact.go │ ├── login.go │ ├── profile.go │ ├── register.go │ ├── reply.go │ ├── search.go │ ├── topic.go │ └── user.go ├── dao │ ├── category.go │ ├── content.go │ ├── file.go │ ├── interact.go │ ├── internal │ │ ├── category.go │ │ ├── content.go │ │ ├── file.go │ │ ├── interact.go │ │ ├── reply.go │ │ ├── setting.go │ │ └── user.go │ ├── reply.go │ ├── setting.go │ └── user.go ├── logic │ ├── bizctx │ │ └── bizctx.go │ ├── captcha │ │ └── captcha.go │ ├── category │ │ └── category.go │ ├── content │ │ └── content.go │ ├── file │ │ └── file.go │ ├── interact │ │ └── interact.go │ ├── logic.go │ ├── menu │ │ └── menu.go │ ├── middleware │ │ └── middleware.go │ ├── reply │ │ └── reply.go │ ├── session │ │ └── session.go │ ├── setting │ │ └── setting.go │ ├── user │ │ └── user.go │ └── view │ │ ├── view.go │ │ └── view_buildin.go ├── model │ ├── category.go │ ├── content.go │ ├── context.go │ ├── do │ │ ├── category.go │ │ ├── content.go │ │ ├── file.go │ │ ├── interact.go │ │ ├── reply.go │ │ ├── setting.go │ │ └── user.go │ ├── entity │ │ ├── category.go │ │ ├── content.go │ │ ├── file.go │ │ ├── interact.go │ │ ├── reply.go │ │ ├── setting.go │ │ └── user.go │ ├── file.go │ ├── menu.go │ ├── reply.go │ ├── session.go │ ├── user.go │ └── view.go ├── packed │ └── packed.go └── service │ ├── bizctx.go │ ├── captcha.go │ ├── category.go │ ├── content.go │ ├── file.go │ ├── interact.go │ ├── menu.go │ ├── middleware.go │ ├── reply.go │ ├── session.go │ ├── setting.go │ ├── user.go │ └── view.go ├── main.go ├── manifest ├── config │ └── config.example.yaml ├── deploy │ └── kustomize │ │ ├── base │ │ ├── deployment.yaml │ │ ├── kustomization.yaml │ │ └── service.yaml │ │ └── overlays │ │ └── develop │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ └── kustomization.yaml ├── docker │ ├── Dockerfile │ └── docker.sh └── document │ ├── design │ ├── focus-v0.1.0-beta.png │ ├── focus-v0.2.0.png │ ├── focus-v0.2.0.xmind │ └── focus-v1.0.xmind │ ├── focus.sql │ ├── images │ ├── databases.png │ ├── demo1.png │ ├── demo2.png │ ├── demo3.png │ ├── demo4.png │ ├── demo5.png │ ├── demo6.png │ ├── demo7.png │ ├── demo8.png │ └── plan.png │ ├── mwb │ ├── gf.mwb │ └── gf.mwb.bak │ └── sqlite │ ├── focus.db │ └── focus.sql ├── resource ├── public │ ├── admin │ │ ├── css │ │ │ ├── .gitkeep │ │ │ └── common.css │ │ ├── images │ │ │ └── .gitkeep │ │ └── js │ │ │ ├── .gitkeep │ │ │ └── common.js │ ├── css │ │ └── common.css │ ├── favicon.ico │ ├── images │ │ ├── avatar │ │ │ ├── avatar1.jpg │ │ │ ├── avatar2.jpg │ │ │ ├── avatar3.jpg │ │ │ └── avatar4.jpg │ │ ├── error │ │ │ ├── error_401.png │ │ │ ├── error_403.png │ │ │ ├── error_404.png │ │ │ └── error_500.png │ │ └── logo.png │ ├── js │ │ ├── common.js │ │ └── encrypt │ │ │ ├── base64.js │ │ │ └── md5.js │ └── plugin │ │ ├── bootstrap-dialog │ │ ├── bootstrap-dialog-common.js │ │ ├── bootstrap-dialog.css │ │ ├── bootstrap-dialog.js │ │ ├── bootstrap-dialog.min.css │ │ ├── bootstrap-dialog.min.js │ │ ├── common-demo.html │ │ └── index.html │ │ ├── bootstrap │ │ ├── bootstrap.css │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.js │ │ └── popper.min.js │ │ ├── iconfont │ │ ├── demo.css │ │ ├── demo_index.html │ │ ├── iconfont.css │ │ ├── iconfont.eot │ │ ├── iconfont.js │ │ ├── iconfont.json │ │ ├── iconfont.svg │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ └── iconfont.woff2 │ │ ├── jquery │ │ ├── jquery.form.min.js │ │ ├── jquery.min.js │ │ ├── jquery.validate.min.js │ │ └── sweetalert.min.js │ │ ├── markdown-css │ │ └── github-markdown.css │ │ ├── marked │ │ └── marked.min.js │ │ ├── md-toc │ │ ├── md-toc.js │ │ └── md-toc.min.js │ │ ├── prism │ │ ├── prism.css │ │ └── prism.js │ │ └── vditor │ │ └── dist │ │ ├── css │ │ └── content-theme │ │ │ ├── ant-design.css │ │ │ ├── dark.css │ │ │ ├── light.css │ │ │ └── wechat.css │ │ ├── images │ │ ├── emoji │ │ │ ├── b3log.png │ │ │ ├── chainbook.png │ │ │ ├── doge.png │ │ │ ├── hacpai.png │ │ │ ├── huaji.gif │ │ │ ├── latke.png │ │ │ ├── lute.png │ │ │ ├── octocat.png │ │ │ ├── pipe.png │ │ │ ├── solo.png │ │ │ ├── sym.png │ │ │ ├── trollface.png │ │ │ ├── vditor.png │ │ │ ├── wide.png │ │ │ └── wulian.png │ │ ├── img-loading.svg │ │ └── logo.png │ │ ├── index.css │ │ ├── index.d.ts │ │ ├── index.min.js │ │ ├── js │ │ ├── abcjs │ │ │ └── abcjs_basic.min.js │ │ ├── echarts │ │ │ └── echarts.min.js │ │ ├── flowchart.js │ │ │ └── flowchart.min.js │ │ ├── graphviz │ │ │ ├── full.render.js │ │ │ └── viz.js │ │ ├── highlight.js │ │ │ ├── highlight.pack.js │ │ │ └── styles │ │ │ │ ├── abap.css │ │ │ │ ├── algol.css │ │ │ │ ├── algol_nu.css │ │ │ │ ├── ant-design.css │ │ │ │ ├── arduino.css │ │ │ │ ├── autumn.css │ │ │ │ ├── borland.css │ │ │ │ ├── bw.css │ │ │ │ ├── colorful.css │ │ │ │ ├── dracula.css │ │ │ │ ├── emacs.css │ │ │ │ ├── friendly.css │ │ │ │ ├── fruity.css │ │ │ │ ├── github.css │ │ │ │ ├── igor.css │ │ │ │ ├── lovelace.css │ │ │ │ ├── manni.css │ │ │ │ ├── monokai.css │ │ │ │ ├── monokailight.css │ │ │ │ ├── murphy.css │ │ │ │ ├── native.css │ │ │ │ ├── paraiso-dark.css │ │ │ │ ├── paraiso-light.css │ │ │ │ ├── pastie.css │ │ │ │ ├── perldoc.css │ │ │ │ ├── pygments.css │ │ │ │ ├── rainbow_dash.css │ │ │ │ ├── rrt.css │ │ │ │ ├── solarized-dark.css │ │ │ │ ├── solarized-dark256.css │ │ │ │ ├── solarized-light.css │ │ │ │ ├── swapoff.css │ │ │ │ ├── tango.css │ │ │ │ ├── trac.css │ │ │ │ ├── vim.css │ │ │ │ ├── vs.css │ │ │ │ └── xcode.css │ │ ├── icons │ │ │ ├── ant.js │ │ │ └── material.js │ │ ├── katex │ │ │ ├── fonts │ │ │ │ ├── KaTeX_AMS-Regular.ttf │ │ │ │ ├── KaTeX_AMS-Regular.woff │ │ │ │ ├── KaTeX_AMS-Regular.woff2 │ │ │ │ ├── KaTeX_Caligraphic-Bold.ttf │ │ │ │ ├── KaTeX_Caligraphic-Bold.woff │ │ │ │ ├── KaTeX_Caligraphic-Bold.woff2 │ │ │ │ ├── KaTeX_Caligraphic-Regular.ttf │ │ │ │ ├── KaTeX_Caligraphic-Regular.woff │ │ │ │ ├── KaTeX_Caligraphic-Regular.woff2 │ │ │ │ ├── KaTeX_Fraktur-Bold.ttf │ │ │ │ ├── KaTeX_Fraktur-Bold.woff │ │ │ │ ├── KaTeX_Fraktur-Bold.woff2 │ │ │ │ ├── KaTeX_Fraktur-Regular.ttf │ │ │ │ ├── KaTeX_Fraktur-Regular.woff │ │ │ │ ├── KaTeX_Fraktur-Regular.woff2 │ │ │ │ ├── KaTeX_Main-Bold.ttf │ │ │ │ ├── KaTeX_Main-Bold.woff │ │ │ │ ├── KaTeX_Main-Bold.woff2 │ │ │ │ ├── KaTeX_Main-BoldItalic.ttf │ │ │ │ ├── KaTeX_Main-BoldItalic.woff │ │ │ │ ├── KaTeX_Main-BoldItalic.woff2 │ │ │ │ ├── KaTeX_Main-Italic.ttf │ │ │ │ ├── KaTeX_Main-Italic.woff │ │ │ │ ├── KaTeX_Main-Italic.woff2 │ │ │ │ ├── KaTeX_Main-Regular.ttf │ │ │ │ ├── KaTeX_Main-Regular.woff │ │ │ │ ├── KaTeX_Main-Regular.woff2 │ │ │ │ ├── KaTeX_Math-BoldItalic.ttf │ │ │ │ ├── KaTeX_Math-BoldItalic.woff │ │ │ │ ├── KaTeX_Math-BoldItalic.woff2 │ │ │ │ ├── KaTeX_Math-Italic.ttf │ │ │ │ ├── KaTeX_Math-Italic.woff │ │ │ │ ├── KaTeX_Math-Italic.woff2 │ │ │ │ ├── KaTeX_SansSerif-Bold.ttf │ │ │ │ ├── KaTeX_SansSerif-Bold.woff │ │ │ │ ├── KaTeX_SansSerif-Bold.woff2 │ │ │ │ ├── KaTeX_SansSerif-Italic.ttf │ │ │ │ ├── KaTeX_SansSerif-Italic.woff │ │ │ │ ├── KaTeX_SansSerif-Italic.woff2 │ │ │ │ ├── KaTeX_SansSerif-Regular.ttf │ │ │ │ ├── KaTeX_SansSerif-Regular.woff │ │ │ │ ├── KaTeX_SansSerif-Regular.woff2 │ │ │ │ ├── KaTeX_Script-Regular.ttf │ │ │ │ ├── KaTeX_Script-Regular.woff │ │ │ │ ├── KaTeX_Script-Regular.woff2 │ │ │ │ ├── KaTeX_Size1-Regular.ttf │ │ │ │ ├── KaTeX_Size1-Regular.woff │ │ │ │ ├── KaTeX_Size1-Regular.woff2 │ │ │ │ ├── KaTeX_Size2-Regular.ttf │ │ │ │ ├── KaTeX_Size2-Regular.woff │ │ │ │ ├── KaTeX_Size2-Regular.woff2 │ │ │ │ ├── KaTeX_Size3-Regular.ttf │ │ │ │ ├── KaTeX_Size3-Regular.woff │ │ │ │ ├── KaTeX_Size3-Regular.woff2 │ │ │ │ ├── KaTeX_Size4-Regular.ttf │ │ │ │ ├── KaTeX_Size4-Regular.woff │ │ │ │ ├── KaTeX_Size4-Regular.woff2 │ │ │ │ ├── KaTeX_Typewriter-Regular.ttf │ │ │ │ ├── KaTeX_Typewriter-Regular.woff │ │ │ │ └── KaTeX_Typewriter-Regular.woff2 │ │ │ ├── katex.min.css │ │ │ └── katex.min.js │ │ ├── lute │ │ │ └── lute.min.js │ │ ├── mathjax │ │ │ ├── input │ │ │ │ ├── asciimath.js │ │ │ │ ├── mml.js │ │ │ │ ├── mml │ │ │ │ │ └── entities.js │ │ │ │ ├── tex-base.js │ │ │ │ ├── tex-full.js │ │ │ │ ├── tex.js │ │ │ │ └── tex │ │ │ │ │ └── extensions │ │ │ │ │ ├── action.js │ │ │ │ │ ├── all-packages.js │ │ │ │ │ ├── ams.js │ │ │ │ │ ├── amscd.js │ │ │ │ │ ├── autoload.js │ │ │ │ │ ├── bbox.js │ │ │ │ │ ├── boldsymbol.js │ │ │ │ │ ├── braket.js │ │ │ │ │ ├── bussproofs.js │ │ │ │ │ ├── cancel.js │ │ │ │ │ ├── color.js │ │ │ │ │ ├── colorV2.js │ │ │ │ │ ├── configMacros.js │ │ │ │ │ ├── enclose.js │ │ │ │ │ ├── extpfeil.js │ │ │ │ │ ├── html.js │ │ │ │ │ ├── mhchem.js │ │ │ │ │ ├── newcommand.js │ │ │ │ │ ├── noerrors.js │ │ │ │ │ ├── noundefined.js │ │ │ │ │ ├── physics.js │ │ │ │ │ ├── require.js │ │ │ │ │ ├── tagFormat.js │ │ │ │ │ ├── unicode.js │ │ │ │ │ └── verb.js │ │ │ └── tex-svg.js │ │ └── mermaid │ │ │ └── mermaid.min.js │ │ ├── method.d.ts │ │ ├── method.min.js │ │ ├── ts │ │ ├── constants.d.ts │ │ ├── devtools │ │ │ └── index.d.ts │ │ ├── export │ │ │ └── index.d.ts │ │ ├── hint │ │ │ └── index.d.ts │ │ ├── i18n │ │ │ └── index.d.ts │ │ ├── ir │ │ │ ├── expandMarker.d.ts │ │ │ ├── highlightToolbarIR.d.ts │ │ │ ├── index.d.ts │ │ │ ├── input.d.ts │ │ │ ├── process.d.ts │ │ │ └── processKeydown.d.ts │ │ ├── markdown │ │ │ ├── abcRender.d.ts │ │ │ ├── anchorRender.d.ts │ │ │ ├── chartRender.d.ts │ │ │ ├── codeRender.d.ts │ │ │ ├── flowchartRender.d.ts │ │ │ ├── getHTML.d.ts │ │ │ ├── getMarkdown.d.ts │ │ │ ├── graphvizRender.d.ts │ │ │ ├── highlightRender.d.ts │ │ │ ├── lazyLoadImageRender.d.ts │ │ │ ├── mathRender.d.ts │ │ │ ├── mediaRender.d.ts │ │ │ ├── mermaidRender.d.ts │ │ │ ├── mindmapRender.d.ts │ │ │ ├── outlineRender.d.ts │ │ │ ├── previewRender.d.ts │ │ │ ├── setLute.d.ts │ │ │ └── speechRender.d.ts │ │ ├── outline │ │ │ └── index.d.ts │ │ ├── preview │ │ │ └── index.d.ts │ │ ├── resize │ │ │ └── index.d.ts │ │ ├── sv │ │ │ ├── index.d.ts │ │ │ ├── inputEvent.d.ts │ │ │ ├── process.d.ts │ │ │ └── processKeydown.d.ts │ │ ├── tip │ │ │ └── index.d.ts │ │ ├── toolbar │ │ │ ├── Both.d.ts │ │ │ ├── Br.d.ts │ │ │ ├── CodeTheme.d.ts │ │ │ ├── ContentTheme.d.ts │ │ │ ├── Counter.d.ts │ │ │ ├── Custom.d.ts │ │ │ ├── Devtools.d.ts │ │ │ ├── Divider.d.ts │ │ │ ├── EditMode.d.ts │ │ │ ├── Emoji.d.ts │ │ │ ├── Export.d.ts │ │ │ ├── Fullscreen.d.ts │ │ │ ├── Headings.d.ts │ │ │ ├── Help.d.ts │ │ │ ├── Indent.d.ts │ │ │ ├── Info.d.ts │ │ │ ├── InsertAfter.d.ts │ │ │ ├── InsertBefore.d.ts │ │ │ ├── MenuItem.d.ts │ │ │ ├── Outdent.d.ts │ │ │ ├── Outline.d.ts │ │ │ ├── Preview.d.ts │ │ │ ├── Record.d.ts │ │ │ ├── Redo.d.ts │ │ │ ├── Undo.d.ts │ │ │ ├── Upload.d.ts │ │ │ ├── index.d.ts │ │ │ └── setToolbar.d.ts │ │ ├── ui │ │ │ ├── initUI.d.ts │ │ │ ├── setCodeTheme.d.ts │ │ │ ├── setContentTheme.d.ts │ │ │ ├── setPreviewMode.d.ts │ │ │ └── setTheme.d.ts │ │ ├── undo │ │ │ └── index.d.ts │ │ ├── util │ │ │ ├── Options.d.ts │ │ │ ├── RecordMedia.d.ts │ │ │ ├── addScript.d.ts │ │ │ ├── addStyle.d.ts │ │ │ ├── code160to32.d.ts │ │ │ ├── compatibility.d.ts │ │ │ ├── editorCommonEvent.d.ts │ │ │ ├── fixBrowserBehavior.d.ts │ │ │ ├── getSelectText.d.ts │ │ │ ├── hasClosest.d.ts │ │ │ ├── hasClosestByHeadings.d.ts │ │ │ ├── highlightToolbar.d.ts │ │ │ ├── hotKey.d.ts │ │ │ ├── log.d.ts │ │ │ ├── merge.d.ts │ │ │ ├── processCode.d.ts │ │ │ ├── selection.d.ts │ │ │ └── toc.d.ts │ │ └── wysiwyg │ │ │ ├── afterRenderEvent.d.ts │ │ │ ├── highlightToolbarWYSIWYG.d.ts │ │ │ ├── index.d.ts │ │ │ ├── inlineTag.d.ts │ │ │ ├── input.d.ts │ │ │ ├── processKeydown.d.ts │ │ │ ├── renderDomByMd.d.ts │ │ │ ├── setHeading.d.ts │ │ │ ├── showCode.d.ts │ │ │ └── toolbarEvent.d.ts │ │ └── types │ │ └── index.d.ts └── template │ ├── admin │ └── index.html │ └── index │ ├── article │ ├── detail.html │ └── index.html │ ├── ask │ ├── detail.html │ └── index.html │ ├── content │ ├── create.html │ ├── detail.html │ ├── index.html │ └── update.html │ ├── footer.html │ ├── header.html │ ├── index.html │ ├── index │ ├── index.html │ └── page_link.html │ ├── login │ └── index.html │ ├── message.html │ ├── navbar.html │ ├── pages │ ├── 401.html │ ├── 403.html │ ├── 404.html │ └── 500.html │ ├── profile │ ├── avatar.html │ ├── index.html │ ├── message.html │ └── password.html │ ├── register │ └── index.html │ ├── reply.html │ ├── search │ └── index.html │ ├── topic │ ├── detail.html │ └── index.html │ └── user │ ├── article.html │ ├── ask.html │ ├── detail.html │ ├── profile.html │ ├── topic.html │ ├── user_content_list.html │ └── user_menu.html └── utility ├── response └── response.go └── utils ├── utils.go └── utils_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=GO 2 | *.css linguist-language=GO 3 | *.html linguist-language=GO -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .buildpath 3 | .hgignore.swp 4 | .project 5 | .orig 6 | .swp 7 | .idea/ 8 | .settings/ 9 | .vscode/ 10 | vender/ 11 | log/ 12 | logs/ 13 | composer.lock 14 | gitpush.sh 15 | pkg/ 16 | upload/ 17 | bin/ 18 | cbuild 19 | */.DS_Store 20 | config/config.toml 21 | main 22 | .vscode 23 | node_modules/ 24 | temp/ 25 | logs/ 26 | packed/data.go 27 | focus 28 | manifest/config/config.yaml 29 | 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ROOT_DIR = $(shell pwd) 2 | NAMESPACE = "default" 3 | DEPLOY_NAME = "focus-single" 4 | DOCKER_NAME = "focus-single" 5 | 6 | # Install/Update to the latest CLI tool. 7 | .PHONY: cli 8 | cli: 9 | @set -e; \ 10 | wget -O gf https://github.com/gogf/gf/releases/latest/download/gf_$(shell go env GOOS)_$(shell go env GOARCH) && \ 11 | chmod +x gf && \ 12 | ./gf install -y && \ 13 | rm ./gf 14 | 15 | 16 | # Check and install CLI tool. 17 | .PHONY: cli.install 18 | cli.install: 19 | @set -e; \ 20 | gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \ 21 | echo "GoFame CLI is not installed, start proceeding auto installation..."; \ 22 | make cli; \ 23 | fi; 24 | 25 | 26 | # Generate Go files for DAO/DO/Entity. 27 | .PHONY: dao 28 | dao: cli.install 29 | @gf gen dao 30 | 31 | # Generate Go files for Service. 32 | .PHONY: service 33 | service: cli.install 34 | @gf gen service 35 | 36 | # Build image, deploy image and yaml to current kubectl environment and make port forward to local machine. 37 | .PHONY: start 38 | start: 39 | @set -e; \ 40 | make image; \ 41 | make deploy; \ 42 | make port; 43 | 44 | # Build docker image. 45 | .PHONY: image 46 | image: cli.install 47 | $(eval _TAG = $(shell git log -1 --format="%cd.%h" --date=format:"%Y%m%d%H%M%S")) 48 | ifneq (, $(shell git status --porcelain 2>/dev/null)) 49 | $(eval _TAG = $(_TAG).dirty) 50 | endif 51 | $(eval _TAG = $(if ${TAG}, ${TAG}, $(_TAG))) 52 | $(eval _PUSH = $(if ${PUSH}, ${PUSH}, )) 53 | @gf docker -p -b "-a amd64 -s linux -p temp" -tn $(DOCKER_NAME):${_TAG}; 54 | 55 | 56 | # Build docker image and automatically push to docker repo. 57 | .PHONY: image.push 58 | image.push: 59 | @make image PUSH=-p; 60 | 61 | 62 | # Deploy image and yaml to current kubectl environment. 63 | .PHONY: deploy 64 | deploy: 65 | $(eval _TAG = $(if ${TAG}, ${TAG}, develop)) 66 | 67 | @set -e; \ 68 | mkdir -p $(ROOT_DIR)/temp/kustomize;\ 69 | cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_TAG};\ 70 | kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\ 71 | kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \ 72 | kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}"; 73 | 74 | 75 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # 1. Focus Single 2 | 3 | A `MVC` project using `GoFrame`. 4 | 5 | # 2. Quick start 6 | 7 | ## 2.1 If you use SQLite 8 | 9 | 1. Download the source code 10 | ``` 11 | git clone https://github.com/gogf/focus-single.git 12 | cd focus-single 13 | ``` 14 | 15 | 2. Run Focus Single 16 | ``` 17 | cp manifest/config/config.example.yaml manifest/config/config.yaml 18 | gf run main.go 19 | ``` 20 | 21 | 3. Enjoy it 22 | then open http://127.0.0.1:8199/ and enjoy it. 23 | 24 | ``` 25 | user: goframe 26 | password: 123456 27 | ``` 28 | 29 | ## 2.2 If you use MySQL 30 | 31 | 1. Download the source code 32 | ``` 33 | git clone https://github.com/gogf/focus-single.git 34 | cd focus-single 35 | ``` 36 | 37 | 2. Update config 38 | copy manifest/config/config.yaml and edit database.default config 39 | ``` 40 | cp manifest/config/config.example.yaml manifest/config/config.yaml 41 | ``` 42 | 43 | 3. Import Db 44 | Import manifest/document/focus.sql to your Mysql 45 | 46 | 4. Run Focus Single And Enjoy 47 | ``` 48 | gf run main.go 49 | ``` 50 | 51 | then open http://127.0.0.1:8199/ and enjoy it. 52 | ``` 53 | user: goframe 54 | password: 123456 55 | ``` 56 | 57 | -------------------------------------------------------------------------------- /api/v1/api.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | type CommonPaginationReq struct { 4 | Page int `json:"page" in:"query" d:"1" v:"min:0#分页号码错误" dc:"分页号码,默认1"` 5 | Size int `json:"size" in:"query" d:"10" v:"max:50#分页数量最大50条" dc:"分页数量,最大50"` 6 | } 7 | 8 | type CommonPaginationRes struct { 9 | Total int `dc:"总数"` 10 | } 11 | -------------------------------------------------------------------------------- /api/v1/article.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type ArticleIndexReq struct { 8 | g.Meta `path:"/article" method:"get" tags:"文章" summary:"展示Article列表页面"` 9 | ContentGetListCommonReq 10 | } 11 | type ArticleIndexRes struct { 12 | ContentGetListCommonRes 13 | } 14 | 15 | type ArticleDetailReq struct { 16 | g.Meta `path:"/article/{Id}" method:"get" tags:"文章" summary:"展示Article详情页面" ` 17 | Id uint `in:"path" v:"min:1#请选择查看的内容" dc:"内容id"` 18 | } 19 | type ArticleDetailRes struct { 20 | g.Meta `mime:"text/html" type:"string" example:"
"` 21 | } 22 | -------------------------------------------------------------------------------- /api/v1/ask.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type AskIndexReq struct { 8 | g.Meta `path:"/ask" method:"get" tags:"问答" summary:"展示Ask列表页面"` 9 | ContentGetListCommonReq 10 | } 11 | type AskIndexRes struct { 12 | ContentGetListCommonRes 13 | } 14 | 15 | type AskDetailReq struct { 16 | g.Meta `path:"/ask/{Id}" method:"get" tags:"问答" summary:"展示Ask详情页面" ` 17 | Id uint `in:"path" v:"min:1#请选择查看的内容" dc:"内容id"` 18 | } 19 | type AskDetailRes struct { 20 | g.Meta `mime:"text/html" type:"string" example:" 26 |{{ .Error}}
33 |"` 21 | } 22 | -------------------------------------------------------------------------------- /api/v1/captcha.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type CaptchaIndexReq struct { 8 | g.Meta `path:"/captcha" method:"get" tags:"工具" summary:"获取默认的验证码" dc:"注意直接返回的是图片二进制内容"` 9 | } 10 | type CaptchaIndexRes struct { 11 | g.Meta `mime:"png" dc:"验证码二进制内容" ` 12 | } 13 | -------------------------------------------------------------------------------- /api/v1/category.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "focus-single/internal/model" 5 | "github.com/gogf/gf/v2/frame/g" 6 | ) 7 | 8 | type CategoryTreeReq struct { 9 | g.Meta `path:"/category/tree" method:"get" tags:"分类" summary:"获取分类列表" dc:"获取分类列表,构造成树形结构返回"` 10 | ContentType string `json:"contentType" dc:"栏目类型:topic/question/article。当传递空时表示获取所有类型的栏目"` 11 | } 12 | type CategoryTreeRes struct { 13 | List []*model.CategoryTreeItem `json:"list" dc:"栏目列表"` 14 | } 15 | -------------------------------------------------------------------------------- /api/v1/file.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | "github.com/gogf/gf/v2/net/ghttp" 6 | ) 7 | 8 | type FileUploadReq struct { 9 | g.Meta `path:"/file" method:"post" mime:"multipart/form-data" tags:"工具" summary:"上传文件"` 10 | File *ghttp.UploadFile `json:"file" type:"file" dc:"选择上传文件"` 11 | } 12 | type FileUploadRes struct { 13 | Name string `json:"name" dc:"文件名称"` 14 | Url string `json:"url" dc:"访问URL,可能只是URI"` 15 | } 16 | -------------------------------------------------------------------------------- /api/v1/index.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type IndexReq struct { 8 | g.Meta `path:"/" method:"get" tags:"首页" summary:"首页"` 9 | ContentGetListCommonReq 10 | } 11 | type IndexRes struct { 12 | ContentGetListCommonRes 13 | } 14 | -------------------------------------------------------------------------------- /api/v1/interact.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "github.com/gogf/gf/v2/frame/g" 4 | 5 | type InteractZanReq struct { 6 | g.Meta `path:"/interact/zan" method:"put" summary:"赞" tags:"交互"` 7 | Id uint `json:"id" v:"min:1#请选择需要赞的内容"` 8 | Type string `json:"type" v:"required#请提交需要赞的内容类型" dc:"content/reply"` 9 | } 10 | type InteractZanRes struct{} 11 | 12 | type InteractCancelZanReq struct { 13 | g.Meta `path:"/interact/zan" method:"delete" summary:"取消赞" tags:"交互"` 14 | Id uint `json:"id" v:"min:1#请选择需要取消赞的内容"` 15 | Type string `json:"type" v:"required#请提交需要取消赞的内容类型" ` 16 | } 17 | type InteractCancelZanRes struct{} 18 | 19 | type InteractCaiReq struct { 20 | g.Meta `path:"/interact/cai" method:"put" summary:"踩" tags:"交互"` 21 | Id uint `json:"id" v:"min:1#请选择需要踩的内容"` 22 | Type string `json:"type" v:"required#请提交需要踩的内容类型" dc:"content/reply"` 23 | } 24 | type InteractCaiRes struct{} 25 | 26 | type InteractCancelCaiReq struct { 27 | g.Meta `path:"/interact/cai" method:"delete" summary:"取消踩" tags:"交互"` 28 | Id uint `json:"id" v:"min:1#请选择需要取消踩的内容"` 29 | Type string `json:"type" v:"required#请提交需要取消踩的内容类型" dc:"content/reply"` 30 | } 31 | type InteractCancelCaiRes struct{} 32 | -------------------------------------------------------------------------------- /api/v1/login.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "github.com/gogf/gf/v2/frame/g" 4 | 5 | type LoginIndexReq struct { 6 | g.Meta `path:"/login" method:"get" summary:"展示登录页面" tags:"登录"` 7 | } 8 | type LoginIndexRes struct { 9 | g.Meta `mime:"text/html" type:"string" example:"
"` 10 | } 11 | 12 | type LoginDoReq struct { 13 | g.Meta `path:"/login" method:"post" summary:"执行登录请求" tags:"登录"` 14 | Passport string `json:"passport" v:"required#请输入账号" dc:"账号"` 15 | Password string `json:"password" v:"required#请输入密码" dc:"密码(明文)"` 16 | Captcha string `json:"captcha" v:"required#请输入验证码" dc:"验证码"` 17 | } 18 | type LoginDoRes struct { 19 | Referer string `json:"referer" dc:"引导客户端跳转地址"` 20 | } 21 | -------------------------------------------------------------------------------- /api/v1/profile.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type ProfileIndexReq struct { 8 | g.Meta `path:"/profile" method:"get" summary:"展示个人资料页面" tags:"个人"` 9 | } 10 | type ProfileIndexRes struct { 11 | g.Meta `mime:"text/html" type:"string" example:"
"` 12 | } 13 | 14 | type ProfileUpdateReq struct { 15 | g.Meta `path:"/profile" method:"post" summary:"修改个人资料" tags:"个人"` 16 | Id uint `json:"id" dc:"用户ID"` 17 | Avatar string `json:"avatar" dc:"头像地址"` 18 | Gender int `json:"gender" dc:"性别 0: 未设置 1: 男 2: 女"` 19 | Nickname string `json:"nickname" v:"required#请输入昵称信息" dc:"昵称"` 20 | } 21 | type ProfileUpdateRes struct{} 22 | 23 | type ProfileAvatarReq struct { 24 | g.Meta `path:"/profile/avatar" method:"get" summary:"展示头像管理页面" tags:"个人"` 25 | } 26 | type ProfileAvatarRes struct { 27 | g.Meta `mime:"text/html" type:"string" example:"
"` 28 | } 29 | 30 | type ProfileUpdateAvatarReq struct { 31 | g.Meta `path:"/profile/avatar" method:"post" summary:"修改个人头像" tags:"个人"` 32 | Id uint `json:"id" dc:"用户ID"` 33 | Avatar string `json:"avatar" dc:"头像地址"` 34 | } 35 | type ProfileUpdateAvatarRes struct{} 36 | 37 | type ProfilePasswordReq struct { 38 | g.Meta `path:"/profile/password" method:"get" summary:"展示密码修改页面" tags:"个人"` 39 | } 40 | type ProfilePasswordRes struct { 41 | g.Meta `mime:"text/html" type:"string" example:"
"` 42 | } 43 | 44 | type ProfileUpdatePasswordReq struct { 45 | g.Meta `path:"/profile/password" method:"post" summary:"修改个人密码" tags:"个人"` 46 | OldPassword string `json:"oldPassword" v:"required#请输入原始密码" dc:"原密码"` 47 | NewPassword string `json:"newPassword" v:"required#请输入新密码" dc:"新密码"` 48 | } 49 | type ProfileUpdatePasswordRes struct{} 50 | 51 | type ProfileMessageReq struct { 52 | g.Meta `path:"/profile/message" method:"get" summary:"展示查询用户消息列表页面" tags:"个人"` 53 | CommonPaginationReq 54 | TargetType string `json:"targetType" dc:"数据类型"` 55 | TargetId uint `json:"targetId" dc:"数据ID"` 56 | } 57 | type ProfileMessageRes struct { 58 | g.Meta `mime:"text/html" type:"string" example:"
"` 59 | } 60 | -------------------------------------------------------------------------------- /api/v1/register.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import "github.com/gogf/gf/v2/frame/g" 4 | 5 | type RegisterIndexReq struct { 6 | g.Meta `path:"/register" method:"get" summary:"展示注册页面" tags:"注册"` 7 | } 8 | type RegisterIndexRes struct { 9 | g.Meta `mime:"text/html" type:"string" example:"
"` 10 | } 11 | 12 | type RegisterDoReq struct { 13 | g.Meta `path:"/register" method:"post" summary:"执行注册请求" tags:"注册"` 14 | Passport string `json:"passport" v:"required#请输入账号" dc:"账号"` 15 | Password string `json:"password" v:"required#请输入密码" dc:"密码"` 16 | Nickname string `json:"nickname" v:"required#请输入昵称" dc:"昵称"` 17 | Captcha string `json:"captcha" v:"required#请输入验证码" dc:"验证码"` 18 | } 19 | type RegisterDoRes struct{} 20 | -------------------------------------------------------------------------------- /api/v1/reply.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type ReplyGetListContentReq struct { 8 | g.Meta `path:"/reply" method:"get" summary:"查询回复列表" tags:"回复"` 9 | Page int `json:"page" dc:"分页码"` 10 | Size int `json:"size" dc:"分页数量"` 11 | TargetType string `json:"targetType" v:"required#评论内容类型错误" dc:"评论类型: topic/ask/article/reply"` 12 | TargetId uint `json:"targetId" v:"required#评论目标ID错误" dc:"对应内容ID"` 13 | } 14 | type ReplyGetListContentRes struct { 15 | Content string `json:"content" dc:"HTML内容"` 16 | } 17 | 18 | type ReplyCreateReq struct { 19 | g.Meta `path:"/reply" method:"put" summary:"执行回复接口" tags:"回复"` 20 | Title string `json:"title" dc:"回复标题"` 21 | ParentId uint `json:"parentId" dc:"回复对应的上一级回复ID(没有的话默认为0)"` 22 | TargetType string `json:"targetType" v:"required#评论内容类型错误" dc:"评论类型: topic/ask/article/reply"` 23 | TargetId uint `json:"targetId" v:"required#评论目标ID错误" dc:"对应内容ID"` 24 | Content string `json:"content" v:"required#评论内容不能为空" dc:"回复内容"` 25 | } 26 | type ReplyCreateRes struct{} 27 | 28 | type ReplyDeleteReq struct { 29 | g.Meta `path:"/reply" method:"delete" summary:"删除回复接口" tags:"回复"` 30 | Id uint `v:"min:1#请选择需要删除的内容" dc:"删除时ID不能为空"` 31 | } 32 | type ReplyDeleteRes struct{} 33 | -------------------------------------------------------------------------------- /api/v1/search.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type SearchIndexReq struct { 8 | g.Meta `path:"/search" method:"get" summary:"展示内容搜索页面" tags:"搜索"` 9 | ContentGetListCommonReq 10 | Key string `json:"key" v:"required#请输入搜索关键字" dc:"关键字"` 11 | Type string `json:"type" dc:"内容模型"` 12 | CategoryId uint `json:"cate" dc:"栏目ID"` 13 | Sort int `json:"sort" dc:"排序类型(0:最新, 默认。1:活跃, 2:热度)"` 14 | } 15 | type SearchIndexRes struct { 16 | g.Meta `mime:"text/html" type:"string" example:"
"` 17 | } 18 | -------------------------------------------------------------------------------- /api/v1/topic.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type TopicIndexReq struct { 8 | g.Meta `path:"/topic" method:"get" tags:"话题" summary:"展示Topic列表页面"` 9 | ContentGetListCommonReq 10 | } 11 | type TopicIndexRes struct { 12 | ContentGetListCommonRes 13 | } 14 | 15 | type TopicDetailReq struct { 16 | g.Meta `path:"/topic/{Id}" method:"get" tags:"话题" summary:"展示Topic详情页面" ` 17 | Id uint `in:"path" v:"min:1#请选择查看的内容" dc:"内容id"` 18 | } 19 | type TopicDetailRes struct { 20 | g.Meta `mime:"text/html" type:"string" example:"
"` 21 | } 22 | -------------------------------------------------------------------------------- /api/v1/user.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type UserIndexReq struct { 8 | g.Meta `path:"/user/{UserId}" method:"get" summary:"展示查询用户内容列表页面" tags:"用户"` 9 | ContentGetListCommonReq 10 | UserId uint `json:"userId" in:"path" dc:"用户ID"` 11 | } 12 | type UserIndexRes struct { 13 | g.Meta `mime:"text/html" type:"string" example:"
"` 14 | } 15 | 16 | type UserArticleReq struct { 17 | g.Meta `path:"/user/article" method:"get" summary:"展示查询用户Article列表页面" tags:"用户"` 18 | ContentGetListCommonReq 19 | UserId uint `json:"userId" in:"path" dc:"用户ID"` 20 | } 21 | type UserArticleRes struct { 22 | g.Meta `mime:"text/html" type:"string" example:"
"` 23 | } 24 | 25 | type UserTopicReq struct { 26 | g.Meta `path:"/user/topic" method:"get" summary:"展示查询用户Topic列表页面" tags:"用户"` 27 | ContentGetListCommonReq 28 | UserId uint `json:"userId" in:"path" dc:"用户ID"` 29 | } 30 | type UserTopicRes struct { 31 | g.Meta `mime:"text/html" type:"string" example:"
"` 32 | } 33 | 34 | type UserAskReq struct { 35 | g.Meta `path:"/user/ask" method:"get" summary:"展示查询用户Ask列表页面" tags:"用户"` 36 | ContentGetListCommonReq 37 | UserId uint `json:"userId" in:"path" dc:"用户ID"` 38 | } 39 | type UserAskRes struct { 40 | g.Meta `mime:"text/html" type:"string" example:"
"` 41 | } 42 | 43 | type UserLogoutReq struct { 44 | g.Meta `path:"/user/logout" method:"get" summary:"执行用户注销接口" tags:"个人"` 45 | } 46 | 47 | type UserLogoutRes struct { 48 | g.Meta `mime:"text/html" type:"string" example:"
"` 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module focus-single 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.2.1 // indirect 7 | github.com/clbanning/mxj/v2 v2.5.7 // indirect 8 | github.com/fatih/color v1.15.0 // indirect 9 | github.com/fsnotify/fsnotify v1.6.0 // indirect 10 | github.com/glebarez/go-sqlite v1.21.0 // indirect 11 | github.com/go-sql-driver/mysql v1.7.0 // indirect 12 | github.com/gogf/gf/contrib/drivers/mysql/v2 v2.3.3 13 | github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.3.3 14 | github.com/gogf/gf/v2 v2.4.0-beta.0.20230328132849-d3c96b5e6b45 15 | github.com/kr/text v0.2.0 // indirect 16 | github.com/magiconair/properties v1.8.7 // indirect 17 | github.com/mattn/go-isatty v0.0.18 // indirect 18 | github.com/mattn/go-runewidth v0.0.14 // indirect 19 | github.com/mojocn/base64Captcha v1.3.1 20 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 21 | github.com/o1egl/govatar v0.3.0 22 | github.com/rivo/uniseg v0.4.4 // indirect 23 | github.com/russross/blackfriday/v2 v2.1.0 24 | go.opentelemetry.io/otel/sdk v1.14.0 // indirect 25 | golang.org/x/net v0.8.0 // indirect 26 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 27 | modernc.org/sqlite v1.21.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /hack/config.yaml: -------------------------------------------------------------------------------- 1 | 2 | # 工具相关配置 3 | gfcli: 4 | # 工具编译配置 5 | build: 6 | name: "focus" 7 | arch: "amd64" 8 | system: "linux,darwin,windows" 9 | mod: "" 10 | cgo: 0 11 | 12 | # dao生成 13 | gen: 14 | dao: 15 | - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/focus" 16 | removePrefix: "gf_" 17 | descriptionTag: true 18 | noModelComment: true 19 | -------------------------------------------------------------------------------- /internal/consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | Version = "v0.2.0" // 当前服务版本(用于模板展示) 5 | CaptchaDefaultName = "CaptchaDefaultName" // 验证码默认存储空间名称 6 | ContextKey = "ContextKey" // 上下文变量存储键名,前后端系统共享 7 | FileMaxUploadCountMinute = 10 // 同一用户1分钟之内最大上传数量 8 | ) 9 | -------------------------------------------------------------------------------- /internal/consts/content.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | ContentListDefaultSize = 10 5 | ContentListMaxSize = 50 6 | ContentSortDefault = 0 // 排序:按照创建时间 7 | ContentSortActive = 1 // 排序:按照更新时间 8 | ContentSortHot = 2 // 排序:按照浏览量 9 | ContentSortScore = 3 // 排序:按照搜索结果关联性 10 | ContentTypeArticle = "article" 11 | ContentTypeAsk = "ask" 12 | ContentTypeTopic = "topic" 13 | ) 14 | -------------------------------------------------------------------------------- /internal/consts/interact.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | InteractTypeZan = 0 5 | InteractTypeCai = 1 6 | InteractTargetTypeContent = "content" 7 | InteractTargetTypeReply = "reply" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/consts/openapi.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | OpenAPITagNameIndex = `首页` 5 | OpenAPITagNameLogin = `登录` 6 | OpenAPITagNameRegister = `注册` 7 | OpenAPITagNameArticle = `文章` 8 | OpenAPITagNameTopic = `话题` 9 | OpenAPITagNameAsk = `问答` 10 | OpenAPITagNameReply = `回复` 11 | OpenAPITagNameContent = `内容` 12 | OpenAPITagNameSearch = `搜索` 13 | OpenAPITagNameInteract = `交互` 14 | OpenAPITagNameCategory = `分类` 15 | OpenAPITagNameProfile = `个人` 16 | OpenAPITagNameUser = `用户` 17 | OpenAPITagNameMess = `工具` 18 | ) 19 | -------------------------------------------------------------------------------- /internal/consts/session.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | SessionNoticeTypeSuccess = "success" 5 | SessionNoticeTypeInfo = "primary" 6 | SessionNoticeTypeWarn = "warning" 7 | SessionNoticeTypeError = "danger" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/consts/user.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | const ( 4 | UserStatusOk = 0 // 用户状态正常 5 | UserStatusDisabled = 1 // 用户状态禁用 6 | UserGenderUnknown = 0 // 性别: 未知 7 | UserGenderMale = 1 // 性别: 男 8 | UserGenderFemale = 2 // 性别: 女 9 | UserLoginUrl = "/login" 10 | ) 11 | -------------------------------------------------------------------------------- /internal/controller/article.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/consts" 8 | "focus-single/internal/model" 9 | "focus-single/internal/service" 10 | ) 11 | 12 | // Article 文章管理 13 | var Article = cArticle{} 14 | 15 | type cArticle struct{} 16 | 17 | // Index article list 18 | func (a *cArticle) Index(ctx context.Context, req *v1.ArticleIndexReq) (res *v1.ArticleIndexRes, err error) { 19 | req.Type = consts.ContentTypeArticle 20 | getListRes, err := service.Content().GetList(ctx, model.ContentGetListInput{ 21 | Type: req.Type, 22 | CategoryId: req.CategoryId, 23 | Page: req.Page, 24 | Size: req.Size, 25 | Sort: req.Sort, 26 | }) 27 | if err != nil { 28 | return nil, err 29 | } 30 | service.View().Render(ctx, model.View{ 31 | ContentType: req.Type, 32 | Data: getListRes, 33 | Title: service.View().GetTitle(ctx, &model.ViewGetTitleInput{ 34 | ContentType: req.Type, 35 | CategoryId: req.CategoryId, 36 | }), 37 | }) 38 | return 39 | } 40 | 41 | // Detail .article details 42 | func (a *cArticle) Detail(ctx context.Context, req *v1.ArticleDetailReq) (res *v1.ArticleDetailRes, err error) { 43 | getDetailRes, err := service.Content().GetDetail(ctx, req.Id) 44 | if err != nil { 45 | return nil, err 46 | } 47 | if getDetailRes == nil { 48 | service.View().Render404(ctx) 49 | return nil, nil 50 | } 51 | if err = service.Content().AddViewCount(ctx, req.Id, 1); err != nil { 52 | return res, err 53 | } 54 | service.View().Render(ctx, model.View{ 55 | ContentType: consts.ContentTypeArticle, 56 | Data: getDetailRes, 57 | Title: service.View().GetTitle(ctx, &model.ViewGetTitleInput{ 58 | ContentType: getDetailRes.Content.Type, 59 | CategoryId: getDetailRes.Content.CategoryId, 60 | CurrentName: getDetailRes.Content.Title, 61 | }), 62 | BreadCrumb: service.View().GetBreadCrumb(ctx, &model.ViewGetBreadCrumbInput{ 63 | ContentId: getDetailRes.Content.Id, 64 | ContentType: getDetailRes.Content.Type, 65 | CategoryId: getDetailRes.Content.CategoryId, 66 | }), 67 | }) 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /internal/controller/ask.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/consts" 8 | "focus-single/internal/model" 9 | "focus-single/internal/service" 10 | ) 11 | 12 | // Ask 问答管理 13 | var Ask = cAak{} 14 | 15 | type cAak struct{} 16 | 17 | func (a *cAak) Index(ctx context.Context, req *v1.AskIndexReq) (res *v1.AskIndexRes, err error) { 18 | req.Type = consts.ContentTypeAsk 19 | getListRes, err := service.Content().GetList(ctx, model.ContentGetListInput{ 20 | Type: req.Type, 21 | CategoryId: req.CategoryId, 22 | Page: req.Page, 23 | Size: req.Size, 24 | Sort: req.Sort, 25 | }) 26 | if err != nil { 27 | return nil, err 28 | } 29 | service.View().Render(ctx, model.View{ 30 | ContentType: req.Type, 31 | Data: getListRes, 32 | Title: service.View().GetTitle(ctx, &model.ViewGetTitleInput{ 33 | ContentType: req.Type, 34 | CategoryId: req.CategoryId, 35 | }), 36 | }) 37 | return 38 | } 39 | 40 | func (a *cAak) Detail(ctx context.Context, req *v1.AskDetailReq) (res *v1.AskDetailRes, err error) { 41 | getDetailRes, err := service.Content().GetDetail(ctx, req.Id) 42 | if err != nil { 43 | return nil, err 44 | } 45 | if getDetailRes == nil { 46 | service.View().Render404(ctx) 47 | return nil, nil 48 | } 49 | if err = service.Content().AddViewCount(ctx, req.Id, 1); err != nil { 50 | return nil, err 51 | } 52 | var ( 53 | title = service.View().GetTitle(ctx, &model.ViewGetTitleInput{ 54 | ContentType: getDetailRes.Content.Type, 55 | CategoryId: getDetailRes.Content.CategoryId, 56 | CurrentName: getDetailRes.Content.Title, 57 | }) 58 | breadCrumb = service.View().GetBreadCrumb(ctx, &model.ViewGetBreadCrumbInput{ 59 | ContentId: getDetailRes.Content.Id, 60 | ContentType: getDetailRes.Content.Type, 61 | CategoryId: getDetailRes.Content.CategoryId, 62 | }) 63 | ) 64 | service.View().Render(ctx, model.View{ 65 | ContentType: consts.ContentTypeAsk, 66 | Data: getDetailRes, 67 | Title: title, 68 | BreadCrumb: breadCrumb, 69 | }) 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /internal/controller/captcha.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/consts" 8 | "focus-single/internal/service" 9 | ) 10 | 11 | // 图形验证码 12 | var Captcha = cCaptcha{} 13 | 14 | type cCaptcha struct{} 15 | 16 | func (a *cCaptcha) Index(ctx context.Context, req *v1.CaptchaIndexReq) (res *v1.CaptchaIndexRes, err error) { 17 | err = service.Captcha().NewAndStore(ctx, consts.CaptchaDefaultName) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /internal/controller/category.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/service" 8 | ) 9 | 10 | // 栏目管理 11 | var Category = cCategory{} 12 | 13 | type cCategory struct{} 14 | 15 | func (a *cCategory) Tree(ctx context.Context, req *v1.CategoryTreeReq) (res *v1.CategoryTreeRes, err error) { 16 | res = &v1.CategoryTreeRes{} 17 | res.List, err = service.Category().GetTree(ctx, req.ContentType) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /internal/controller/file.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/model" 8 | "focus-single/internal/service" 9 | "github.com/gogf/gf/v2/errors/gcode" 10 | "github.com/gogf/gf/v2/errors/gerror" 11 | ) 12 | 13 | // 文件管理 14 | var File = cFile{} 15 | 16 | type cFile struct{} 17 | 18 | func (a *cFile) Upload(ctx context.Context, req *v1.FileUploadReq) (res *v1.FileUploadRes, err error) { 19 | if req.File == nil { 20 | return nil, gerror.NewCode(gcode.CodeMissingParameter, "请选择需要上传的文件") 21 | } 22 | result, err := service.File().Upload(ctx, model.FileUploadInput{ 23 | File: req.File, 24 | RandomName: true, 25 | }) 26 | if err != nil { 27 | return nil, err 28 | } 29 | res = &v1.FileUploadRes{ 30 | Name: result.Name, 31 | Url: result.Url, 32 | } 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /internal/controller/index.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/model" 8 | "focus-single/internal/service" 9 | ) 10 | 11 | // 首页接口 12 | var Index = cIndex{} 13 | 14 | type cIndex struct{} 15 | 16 | func (a *cIndex) Index(ctx context.Context, req *v1.IndexReq) (res *v1.IndexRes, err error) { 17 | if getListRes, err := service.Content().GetList(ctx, model.ContentGetListInput{ 18 | Type: req.Type, 19 | CategoryId: req.CategoryId, 20 | Page: req.Page, 21 | Size: req.Size, 22 | Sort: req.Sort, 23 | }); err != nil { 24 | return nil, err 25 | } else { 26 | service.View().Render(ctx, model.View{ 27 | ContentType: req.Type, 28 | Data: getListRes, 29 | Title: "首页", 30 | }) 31 | } 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /internal/controller/interact.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/service" 8 | ) 9 | 10 | // 赞踩控制器 11 | var Interact = cInteract{} 12 | 13 | type cInteract struct{} 14 | 15 | func (a *cInteract) Zan(ctx context.Context, req *v1.InteractZanReq) (res *v1.InteractZanRes, err error) { 16 | err = service.Interact().Zan(ctx, req.Type, req.Id) 17 | return 18 | } 19 | 20 | func (a *cInteract) CancelZan(ctx context.Context, req *v1.InteractCancelZanReq) (res *v1.InteractCancelZanRes, err error) { 21 | err = service.Interact().CancelZan(ctx, req.Type, req.Id) 22 | return 23 | } 24 | 25 | func (a *cInteract) Cai(ctx context.Context, req *v1.InteractCaiReq) (res *v1.InteractCaiRes, err error) { 26 | err = service.Interact().Cai(ctx, req.Type, req.Id) 27 | return 28 | } 29 | 30 | func (a *cInteract) CancelCai(ctx context.Context, req *v1.InteractCancelCaiReq) (res *v1.InteractCancelCaiRes, err error) { 31 | err = service.Interact().CancelCai(ctx, req.Type, req.Id) 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /internal/controller/login.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/consts" 8 | "focus-single/internal/model" 9 | "focus-single/internal/service" 10 | "github.com/gogf/gf/v2/errors/gcode" 11 | "github.com/gogf/gf/v2/errors/gerror" 12 | "github.com/gogf/gf/v2/frame/g" 13 | ) 14 | 15 | // 登录管理 16 | var Login = cLogin{} 17 | 18 | type cLogin struct{} 19 | 20 | func (a *cLogin) Index(ctx context.Context, req *v1.LoginIndexReq) (res *v1.LoginIndexRes, err error) { 21 | service.View().Render(ctx, model.View{}) 22 | return 23 | } 24 | 25 | func (a *cLogin) Login(ctx context.Context, req *v1.LoginDoReq) (res *v1.LoginDoRes, err error) { 26 | res = &v1.LoginDoRes{} 27 | if !service.Captcha().VerifyAndClear(g.RequestFromCtx(ctx), consts.CaptchaDefaultName, req.Captcha) { 28 | return res, gerror.NewCode(gcode.CodeBusinessValidationFailed, "请输入正确的验证码") 29 | } 30 | err = service.User().Login(ctx, model.UserLoginInput{ 31 | Passport: req.Passport, 32 | Password: req.Password, 33 | }) 34 | if err != nil { 35 | return 36 | } 37 | // 识别并跳转到登录前页面 38 | loginReferer := service.Session().GetLoginReferer(ctx) 39 | if loginReferer != "" { 40 | _ = service.Session().RemoveLoginReferer(ctx) 41 | } 42 | res.Referer = loginReferer 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /internal/controller/register.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/consts" 8 | "focus-single/internal/model" 9 | "focus-single/internal/service" 10 | "github.com/gogf/gf/v2/errors/gcode" 11 | "github.com/gogf/gf/v2/errors/gerror" 12 | "github.com/gogf/gf/v2/frame/g" 13 | ) 14 | 15 | // 注册控制器 16 | var Register = cRegister{} 17 | 18 | type cRegister struct{} 19 | 20 | func (a *cRegister) Index(ctx context.Context, req *v1.RegisterIndexReq) (res *v1.RegisterIndexRes, err error) { 21 | service.View().Render(ctx, model.View{}) 22 | return 23 | } 24 | 25 | func (a *cRegister) Register(ctx context.Context, req *v1.RegisterDoReq) (res *v1.RegisterDoRes, err error) { 26 | if !service.Captcha().VerifyAndClear(g.RequestFromCtx(ctx), consts.CaptchaDefaultName, req.Captcha) { 27 | return nil, gerror.NewCode(gcode.CodeBusinessValidationFailed, "请输入正确的验证码") 28 | } 29 | if err = service.User().Register(ctx, model.UserRegisterInput{ 30 | Passport: req.Passport, 31 | Password: req.Password, 32 | Nickname: req.Nickname, 33 | }); err != nil { 34 | return 35 | } 36 | 37 | // 自动登录 38 | err = service.User().Login(ctx, model.UserLoginInput{ 39 | Passport: req.Passport, 40 | Password: req.Password, 41 | }) 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /internal/controller/reply.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/model" 8 | "focus-single/internal/service" 9 | "github.com/gogf/gf/v2/frame/g" 10 | ) 11 | 12 | // Reply 回复控制器 13 | var Reply = cReply{} 14 | 15 | type cReply struct{} 16 | 17 | func (a *cReply) GetListContent(ctx context.Context, req *v1.ReplyGetListContentReq) (res *v1.ReplyGetListContentRes, err error) { 18 | out, err := service.Reply().GetList(ctx, model.ReplyGetListInput{ 19 | Page: req.Page, 20 | Size: req.Size, 21 | TargetType: req.TargetType, 22 | TargetId: req.TargetId, 23 | }) 24 | if err != nil { 25 | return nil, err 26 | } 27 | request := g.RequestFromCtx(ctx) 28 | service.View().RenderTpl(ctx, "index/reply.html", model.View{Data: out}) 29 | tplContent := request.Response.BufferString() 30 | request.Response.ClearBuffer() 31 | return &v1.ReplyGetListContentRes{Content: tplContent}, nil 32 | } 33 | 34 | func (a *cReply) Create(ctx context.Context, req *v1.ReplyCreateReq) (res *v1.ReplyCreateRes, err error) { 35 | err = service.Reply().Create(ctx, model.ReplyCreateInput{ 36 | Title: req.Title, 37 | ParentId: req.ParentId, 38 | TargetType: req.TargetType, 39 | TargetId: req.TargetId, 40 | Content: req.Content, 41 | UserId: service.Session().GetUser(ctx).Id, 42 | }) 43 | return 44 | } 45 | 46 | func (a *cReply) Delete(ctx context.Context, req *v1.ReplyDeleteReq) (res *v1.ReplyDeleteRes, err error) { 47 | err = service.Reply().Delete(ctx, req.Id) 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /internal/controller/search.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/model" 8 | "focus-single/internal/service" 9 | ) 10 | 11 | // 搜索管理 12 | var Search = cSearch{} 13 | 14 | type cSearch struct{} 15 | 16 | func (a *cSearch) Index(ctx context.Context, req *v1.SearchIndexReq) (res *v1.SearchIndexRes, err error) { 17 | out, err := service.Content().Search(ctx, model.ContentSearchInput{ 18 | Key: req.Key, 19 | Type: req.Type, 20 | CategoryId: req.CategoryId, 21 | Page: req.Page, 22 | Size: req.Size, 23 | Sort: req.Sort, 24 | }) 25 | if err != nil { 26 | return nil, err 27 | } 28 | service.View().Render(ctx, model.View{ 29 | Data: out, 30 | Title: service.View().GetTitle(ctx, &model.ViewGetTitleInput{}), 31 | }) 32 | return nil, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/controller/topic.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/api/v1" 7 | "focus-single/internal/consts" 8 | "focus-single/internal/model" 9 | "focus-single/internal/service" 10 | ) 11 | 12 | // Topic 主题管理 13 | var Topic = cTopic{} 14 | 15 | type cTopic struct{} 16 | 17 | func (a *cTopic) Index(ctx context.Context, req *v1.TopicIndexReq) (res *v1.TopicIndexRes, err error) { 18 | req.Type = consts.ContentTypeTopic 19 | out, err := service.Content().GetList(ctx, model.ContentGetListInput{ 20 | Type: req.Type, 21 | CategoryId: req.CategoryId, 22 | Page: req.Page, 23 | Size: req.Size, 24 | Sort: req.Sort, 25 | }) 26 | if err != nil { 27 | return nil, err 28 | } 29 | title := service.View().GetTitle(ctx, &model.ViewGetTitleInput{ 30 | ContentType: req.Type, 31 | CategoryId: req.CategoryId, 32 | }) 33 | service.View().Render(ctx, model.View{ 34 | ContentType: req.Type, 35 | Data: out, 36 | Title: title, 37 | }) 38 | return 39 | } 40 | 41 | func (a *cTopic) Detail(ctx context.Context, req *v1.TopicDetailReq) (res *v1.TopicDetailRes, err error) { 42 | out, err := service.Content().GetDetail(ctx, req.Id) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if out == nil { 47 | service.View().Render404(ctx) 48 | return 49 | } 50 | err = service.Content().AddViewCount(ctx, req.Id, 1) 51 | service.View().Render(ctx, model.View{ 52 | ContentType: consts.ContentTypeTopic, 53 | Data: out, 54 | Title: service.View().GetTitle(ctx, &model.ViewGetTitleInput{ 55 | ContentType: out.Content.Type, 56 | CategoryId: out.Content.CategoryId, 57 | CurrentName: out.Content.Title, 58 | }), 59 | BreadCrumb: service.View().GetBreadCrumb(ctx, &model.ViewGetBreadCrumbInput{ 60 | ContentId: out.Content.Id, 61 | ContentType: out.Content.Type, 62 | CategoryId: out.Content.CategoryId, 63 | }), 64 | }) 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /internal/dao/category.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. 3 | // ================================================================================= 4 | 5 | package dao 6 | 7 | import ( 8 | "focus-single/internal/dao/internal" 9 | ) 10 | 11 | // categoryDao is the data access object for table gf_category. 12 | // You can define custom methods on it to extend its functionality as you wish. 13 | type categoryDao struct { 14 | *internal.CategoryDao 15 | } 16 | 17 | var ( 18 | // Category is globally public accessible object for table gf_category operations. 19 | Category = categoryDao{ 20 | internal.NewCategoryDao(), 21 | } 22 | ) 23 | 24 | // Fill with you ideas below. 25 | -------------------------------------------------------------------------------- /internal/dao/content.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. 3 | // ================================================================================= 4 | 5 | package dao 6 | 7 | import ( 8 | "focus-single/internal/dao/internal" 9 | ) 10 | 11 | // contentDao is the data access object for table gf_content. 12 | // You can define custom methods on it to extend its functionality as you wish. 13 | type contentDao struct { 14 | *internal.ContentDao 15 | } 16 | 17 | var ( 18 | // Content is globally public accessible object for table gf_content operations. 19 | Content = contentDao{ 20 | internal.NewContentDao(), 21 | } 22 | ) 23 | 24 | // Fill with you ideas below. 25 | -------------------------------------------------------------------------------- /internal/dao/file.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. 3 | // ================================================================================= 4 | 5 | package dao 6 | 7 | import ( 8 | "focus-single/internal/dao/internal" 9 | ) 10 | 11 | // fileDao is the data access object for table gf_file. 12 | // You can define custom methods on it to extend its functionality as you wish. 13 | type fileDao struct { 14 | *internal.FileDao 15 | } 16 | 17 | var ( 18 | // File is globally public accessible object for table gf_file operations. 19 | File = fileDao{ 20 | internal.NewFileDao(), 21 | } 22 | ) 23 | 24 | // Fill with you ideas below. 25 | -------------------------------------------------------------------------------- /internal/dao/interact.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. 3 | // ================================================================================= 4 | 5 | package dao 6 | 7 | import ( 8 | "focus-single/internal/dao/internal" 9 | ) 10 | 11 | // interactDao is the data access object for table gf_interact. 12 | // You can define custom methods on it to extend its functionality as you wish. 13 | type interactDao struct { 14 | *internal.InteractDao 15 | } 16 | 17 | var ( 18 | // Interact is globally public accessible object for table gf_interact operations. 19 | Interact = interactDao{ 20 | internal.NewInteractDao(), 21 | } 22 | ) 23 | 24 | // Fill with you ideas below. 25 | -------------------------------------------------------------------------------- /internal/dao/reply.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. 3 | // ================================================================================= 4 | 5 | package dao 6 | 7 | import ( 8 | "focus-single/internal/dao/internal" 9 | ) 10 | 11 | // replyDao is the data access object for table gf_reply. 12 | // You can define custom methods on it to extend its functionality as you wish. 13 | type replyDao struct { 14 | *internal.ReplyDao 15 | } 16 | 17 | var ( 18 | // Reply is globally public accessible object for table gf_reply operations. 19 | Reply = replyDao{ 20 | internal.NewReplyDao(), 21 | } 22 | ) 23 | 24 | // Fill with you ideas below. 25 | -------------------------------------------------------------------------------- /internal/dao/setting.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. 3 | // ================================================================================= 4 | 5 | package dao 6 | 7 | import ( 8 | "focus-single/internal/dao/internal" 9 | ) 10 | 11 | // settingDao is the data access object for table gf_setting. 12 | // You can define custom methods on it to extend its functionality as you wish. 13 | type settingDao struct { 14 | *internal.SettingDao 15 | } 16 | 17 | var ( 18 | // Setting is globally public accessible object for table gf_setting operations. 19 | Setting = settingDao{ 20 | internal.NewSettingDao(), 21 | } 22 | ) 23 | 24 | // Fill with you ideas below. 25 | -------------------------------------------------------------------------------- /internal/dao/user.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. 3 | // ================================================================================= 4 | 5 | package dao 6 | 7 | import ( 8 | "focus-single/internal/dao/internal" 9 | ) 10 | 11 | // userDao is the data access object for table gf_user. 12 | // You can define custom methods on it to extend its functionality as you wish. 13 | type userDao struct { 14 | *internal.UserDao 15 | } 16 | 17 | var ( 18 | // User is globally public accessible object for table gf_user operations. 19 | User = userDao{ 20 | internal.NewUserDao(), 21 | } 22 | ) 23 | 24 | // Fill with you ideas below. 25 | -------------------------------------------------------------------------------- /internal/logic/bizctx/bizctx.go: -------------------------------------------------------------------------------- 1 | package bizctx 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/internal/service" 7 | "github.com/gogf/gf/v2/frame/g" 8 | "github.com/gogf/gf/v2/net/ghttp" 9 | 10 | "focus-single/internal/consts" 11 | "focus-single/internal/model" 12 | ) 13 | 14 | type sBizCtx struct{} 15 | 16 | func init() { 17 | service.RegisterBizCtx(New()) 18 | } 19 | 20 | func New() *sBizCtx { 21 | return &sBizCtx{} 22 | } 23 | 24 | // Init 初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改。 25 | func (s *sBizCtx) Init(r *ghttp.Request, customCtx *model.Context) { 26 | r.SetCtxVar(consts.ContextKey, customCtx) 27 | } 28 | 29 | // Get 获得上下文变量,如果没有设置,那么返回nil 30 | func (s *sBizCtx) Get(ctx context.Context) *model.Context { 31 | value := ctx.Value(consts.ContextKey) 32 | if value == nil { 33 | return nil 34 | } 35 | if localCtx, ok := value.(*model.Context); ok { 36 | return localCtx 37 | } 38 | return nil 39 | } 40 | 41 | // SetUser 将上下文信息设置到上下文请求中,注意是完整覆盖 42 | func (s *sBizCtx) SetUser(ctx context.Context, ctxUser *model.ContextUser) { 43 | s.Get(ctx).User = ctxUser 44 | } 45 | 46 | // SetData 将上下文信息设置到上下文请求中,注意是完整覆盖 47 | func (s *sBizCtx) SetData(ctx context.Context, data g.Map) { 48 | s.Get(ctx).Data = data 49 | } 50 | -------------------------------------------------------------------------------- /internal/logic/captcha/captcha.go: -------------------------------------------------------------------------------- 1 | package captcha 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/internal/service" 7 | "github.com/gogf/gf/v2/frame/g" 8 | "github.com/gogf/gf/v2/net/ghttp" 9 | "github.com/gogf/gf/v2/util/guid" 10 | "github.com/mojocn/base64Captcha" 11 | ) 12 | 13 | type sCaptcha struct{} 14 | 15 | var ( 16 | captchaStore = base64Captcha.DefaultMemStore 17 | captchaDriver = newDriver() 18 | ) 19 | 20 | func init() { 21 | service.RegisterCaptcha(New()) 22 | } 23 | 24 | // Captcha 验证码管理服务 25 | func New() *sCaptcha { 26 | return &sCaptcha{} 27 | } 28 | 29 | func newDriver() *base64Captcha.DriverString { 30 | driver := &base64Captcha.DriverString{ 31 | Height: 44, 32 | Width: 126, 33 | NoiseCount: 5, 34 | ShowLineOptions: base64Captcha.OptionShowSineLine | base64Captcha.OptionShowSlimeLine | base64Captcha.OptionShowHollowLine, 35 | Length: 4, 36 | Source: "1234567890", 37 | Fonts: []string{"wqy-microhei.ttc"}, 38 | } 39 | return driver.ConvertFonts() 40 | } 41 | 42 | // NewAndStore 创建验证码,直接输出验证码图片内容到HTTP Response. 43 | func (s *sCaptcha) NewAndStore(ctx context.Context, name string) error { 44 | var ( 45 | request = g.RequestFromCtx(ctx) 46 | captcha = base64Captcha.NewCaptcha(captchaDriver, captchaStore) 47 | ) 48 | _, content, answer := captcha.Driver.GenerateIdQuestionAnswer() 49 | item, _ := captcha.Driver.DrawCaptcha(content) 50 | captchaStoreKey := guid.S() 51 | request.Session.Set(name, captchaStoreKey) 52 | captcha.Store.Set(captchaStoreKey, answer) 53 | _, err := item.WriteTo(request.Response.Writer) 54 | return err 55 | } 56 | 57 | // VerifyAndClear 校验验证码,并清空缓存的验证码信息 58 | func (s *sCaptcha) VerifyAndClear(r *ghttp.Request, name string, value string) bool { 59 | defer r.Session.Remove(name) 60 | captchaStoreKey := r.Session.MustGet(name).String() 61 | return captchaStore.Verify(captchaStoreKey, value, true) 62 | } 63 | -------------------------------------------------------------------------------- /internal/logic/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "focus-single/internal/service" 8 | "github.com/gogf/gf/v2/errors/gerror" 9 | "github.com/gogf/gf/v2/frame/g" 10 | "github.com/gogf/gf/v2/os/gfile" 11 | "github.com/gogf/gf/v2/os/gtime" 12 | 13 | "focus-single/internal/consts" 14 | "focus-single/internal/dao" 15 | "focus-single/internal/model" 16 | "focus-single/internal/model/entity" 17 | ) 18 | 19 | type sFile struct{} 20 | 21 | func init() { 22 | service.RegisterFile(New()) 23 | } 24 | 25 | func New() *sFile { 26 | return &sFile{} 27 | } 28 | 29 | // 同一上传文件 30 | func (s *sFile) Upload(ctx context.Context, in model.FileUploadInput) (*model.FileUploadOutput, error) { 31 | uploadPath := g.Cfg().MustGet(ctx, "upload.path").String() 32 | if uploadPath == "" { 33 | return nil, gerror.New("上传文件路径配置不存在") 34 | } 35 | if in.Name != "" { 36 | in.File.Filename = in.Name 37 | } 38 | // 同一用户1分钟之内只能上传10张图片 39 | count, err := dao.File.Ctx(ctx). 40 | Where(dao.File.Columns().UserId, service.BizCtx().Get(ctx).User.Id). 41 | WhereGTE(dao.File.Columns().CreatedAt, gtime.Now().Add(time.Minute)). 42 | Count() 43 | if err != nil { 44 | return nil, err 45 | } 46 | if count >= consts.FileMaxUploadCountMinute { 47 | return nil, gerror.New("您上传得太频繁,请稍后再操作") 48 | } 49 | dateDirName := gtime.Now().Format("Ymd") 50 | fileName, err := in.File.Save(gfile.Join(uploadPath, dateDirName), in.RandomName) 51 | if err != nil { 52 | return nil, err 53 | } 54 | // 记录到数据表 55 | data := entity.File{ 56 | Name: fileName, 57 | Src: gfile.Join(uploadPath, dateDirName, fileName), 58 | Url: "/upload/" + dateDirName + "/" + fileName, 59 | UserId: service.BizCtx().Get(ctx).User.Id, 60 | } 61 | result, err := dao.File.Ctx(ctx).Data(data).OmitEmpty().Insert() 62 | if err != nil { 63 | return nil, err 64 | } 65 | id, _ := result.LastInsertId() 66 | return &model.FileUploadOutput{ 67 | Id: uint(id), 68 | Name: data.Name, 69 | Path: data.Src, 70 | Url: data.Url, 71 | }, nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/logic/logic.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package logic 6 | 7 | import ( 8 | _ "focus-single/internal/logic/bizctx" 9 | _ "focus-single/internal/logic/captcha" 10 | _ "focus-single/internal/logic/category" 11 | _ "focus-single/internal/logic/content" 12 | _ "focus-single/internal/logic/file" 13 | _ "focus-single/internal/logic/interact" 14 | _ "focus-single/internal/logic/menu" 15 | _ "focus-single/internal/logic/middleware" 16 | _ "focus-single/internal/logic/reply" 17 | _ "focus-single/internal/logic/session" 18 | _ "focus-single/internal/logic/setting" 19 | _ "focus-single/internal/logic/user" 20 | _ "focus-single/internal/logic/view" 21 | ) 22 | -------------------------------------------------------------------------------- /internal/logic/menu/menu.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "focus-single/internal/model" 8 | "focus-single/internal/service" 9 | ) 10 | 11 | type sMenu struct{} 12 | 13 | const settingTopMenusKey = "TopMenus" 14 | 15 | func init() { 16 | service.RegisterMenu(New()) 17 | } 18 | 19 | func New() *sMenu { 20 | return &sMenu{} 21 | } 22 | 23 | // 获取顶部菜单 24 | func (s *sMenu) SetTopMenus(ctx context.Context, menus []*model.MenuItem) error { 25 | b, err := json.Marshal(menus) 26 | if err != nil { 27 | return err 28 | } 29 | return service.Setting().Set(ctx, settingTopMenusKey, string(b)) 30 | } 31 | 32 | // 获取顶部菜单 33 | func (s *sMenu) GetTopMenus(ctx context.Context) ([]*model.MenuItem, error) { 34 | var topMenus []*model.MenuItem 35 | v, err := service.Setting().GetVar(ctx, settingTopMenusKey) 36 | if err != nil { 37 | return nil, err 38 | } 39 | err = v.Structs(&topMenus) 40 | return topMenus, err 41 | } 42 | 43 | // 根据给定的Url检索顶部菜单,给定的Url可能只是一个Url Path。 44 | func (s *sMenu) GetTopMenuByUrl(ctx context.Context, url string) (*model.MenuItem, error) { 45 | items, _ := s.GetTopMenus(ctx) 46 | for _, v := range items { 47 | if v.Url == url { 48 | return v, nil 49 | } 50 | } 51 | return nil, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/logic/setting/setting.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "context" 5 | 6 | "focus-single/internal/service" 7 | "github.com/gogf/gf/v2/frame/g" 8 | 9 | "focus-single/internal/dao" 10 | "focus-single/internal/model/entity" 11 | ) 12 | 13 | type sSetting struct{} 14 | 15 | func init() { 16 | service.RegisterSetting(New()) 17 | } 18 | 19 | func New() *sSetting { 20 | return &sSetting{} 21 | } 22 | 23 | // 设置KV。 24 | func (s *sSetting) Set(ctx context.Context, key, value string) error { 25 | _, err := dao.Setting.Ctx(ctx).Data(entity.Setting{ 26 | K: key, 27 | V: value, 28 | }).Save() 29 | return err 30 | } 31 | 32 | // 查询KV。 33 | func (s *sSetting) Get(ctx context.Context, key string) (string, error) { 34 | v, err := s.GetVar(ctx, key) 35 | return v.String(), err 36 | } 37 | 38 | // 查询KV,返回泛型,便于转换。 39 | func (s *sSetting) GetVar(ctx context.Context, key string) (*g.Var, error) { 40 | v, err := dao.Setting.Ctx(ctx).Fields(dao.Setting.Columns().V).Where(dao.Setting.Columns().K, key).Value() 41 | return v, err 42 | } 43 | -------------------------------------------------------------------------------- /internal/model/category.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // CategoryTreeItem 栏目树形列表 4 | type CategoryTreeItem struct { 5 | Id uint `json:"id"` // 分类ID,自增主键 6 | ParentId uint `json:"parent_id"` // 父级分类ID,用于层级管理 7 | Name string `json:"name"` // 分类名称 8 | Thumb string `json:"thumb"` // 封面图 9 | Brief string `json:"brief"` // 简述 10 | Content string `json:"content"` // 详细介绍 11 | Indent string `json:"indent"` // 缩进字符串,包含: , " │", " ├", " └" 12 | Items []*CategoryTreeItem `json:"items,omitempty"` // 子级数据项 13 | } 14 | -------------------------------------------------------------------------------- /internal/model/context.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | "github.com/gogf/gf/v2/net/ghttp" 6 | ) 7 | 8 | // Context 请求上下文结构 9 | type Context struct { 10 | Session *ghttp.Session // 当前Session管理对象 11 | User *ContextUser // 上下文用户信息 12 | Data g.Map // 自定KV变量,业务模块根据需要设置,不固定 13 | } 14 | 15 | // ContextUser 请求上下文中的用户信息 16 | type ContextUser struct { 17 | Id uint // 用户ID 18 | Passport string // 用户账号 19 | Nickname string // 用户名称 20 | Avatar string // 用户头像 21 | IsAdmin bool // 是否是管理员 22 | } 23 | -------------------------------------------------------------------------------- /internal/model/do/category.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package do 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/frame/g" 9 | "github.com/gogf/gf/v2/os/gtime" 10 | ) 11 | 12 | // Category is the golang structure of table gf_category for DAO operations like Where/Data. 13 | type Category struct { 14 | g.Meta `orm:"table:gf_category, do:true"` 15 | Id interface{} // 分类ID,自增主键 16 | ContentType interface{} // 内容类型:topic, ask, article, reply 17 | Key interface{} // 栏目唯一键名,用于程序部分场景硬编码,一般不会用得到 18 | ParentId interface{} // 父级分类ID,用于层级管理 19 | UserId interface{} // 创建的用户ID 20 | Name interface{} // 分类名称 21 | Sort interface{} // 排序,数值越低越靠前,默认为添加时的时间戳,可用于置顶 22 | Thumb interface{} // 封面图 23 | Brief interface{} // 简述 24 | Content interface{} // 详细介绍 25 | CreatedAt *gtime.Time // 创建时间 26 | UpdatedAt *gtime.Time // 修改时间 27 | } 28 | -------------------------------------------------------------------------------- /internal/model/do/content.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package do 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/frame/g" 9 | "github.com/gogf/gf/v2/os/gtime" 10 | ) 11 | 12 | // Content is the golang structure of table gf_content for DAO operations like Where/Data. 13 | type Content struct { 14 | g.Meta `orm:"table:gf_content, do:true"` 15 | Id interface{} // 自增ID 16 | Key interface{} // 唯一键名,用于程序硬编码,一般不常用 17 | Type interface{} // 内容模型: topic, ask, article等,具体由程序定义 18 | CategoryId interface{} // 栏目ID 19 | UserId interface{} // 用户ID 20 | AdoptedReplyId interface{} // 采纳的回复ID,问答模块有效 21 | Title interface{} // 标题 22 | Content interface{} // 内容 23 | Sort interface{} // 排序,数值越低越靠前,默认为添加时的时间戳,可用于置顶 24 | Brief interface{} // 摘要 25 | Thumb interface{} // 缩略图 26 | Tags interface{} // 标签名称列表,以JSON存储 27 | Referer interface{} // 内容来源,例如github/gitee 28 | Status interface{} // 状态 0: 正常, 1: 禁用 29 | ReplyCount interface{} // 回复数量 30 | ViewCount interface{} // 浏览数量 31 | ZanCount interface{} // 赞 32 | CaiCount interface{} // 踩 33 | CreatedAt *gtime.Time // 创建时间 34 | UpdatedAt *gtime.Time // 修改时间 35 | } 36 | -------------------------------------------------------------------------------- /internal/model/do/file.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package do 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/frame/g" 9 | "github.com/gogf/gf/v2/os/gtime" 10 | ) 11 | 12 | // File is the golang structure of table gf_file for DAO operations like Where/Data. 13 | type File struct { 14 | g.Meta `orm:"table:gf_file, do:true"` 15 | Id interface{} // 自增ID 16 | Name interface{} // 文件名称 17 | Src interface{} // 本地文件存储路径 18 | Url interface{} // URL地址,可能为空 19 | UserId interface{} // 操作用户 20 | CreatedAt *gtime.Time // 21 | } 22 | -------------------------------------------------------------------------------- /internal/model/do/interact.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package do 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/frame/g" 9 | "github.com/gogf/gf/v2/os/gtime" 10 | ) 11 | 12 | // Interact is the golang structure of table gf_interact for DAO operations like Where/Data. 13 | type Interact struct { 14 | g.Meta `orm:"table:gf_interact, do:true"` 15 | Id interface{} // 自增ID 16 | Type interface{} // 操作类型。0:赞,1:踩。 17 | UserId interface{} // 操作用户 18 | TargetId interface{} // 对应内容ID,该内容可能是content, reply 19 | TargetType interface{} // 内容模型: content, reply, 具体由程序定义 20 | Count interface{} // 操作数据值 21 | CreatedAt *gtime.Time // 22 | UpdatedAt *gtime.Time // 23 | } 24 | -------------------------------------------------------------------------------- /internal/model/do/reply.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package do 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/frame/g" 9 | "github.com/gogf/gf/v2/os/gtime" 10 | ) 11 | 12 | // Reply is the golang structure of table gf_reply for DAO operations like Where/Data. 13 | type Reply struct { 14 | g.Meta `orm:"table:gf_reply, do:true"` 15 | Id interface{} // 回复ID 16 | ParentId interface{} // 回复对应的上一级回复ID(没有的话默认为0) 17 | Title interface{} // 回复标题 18 | Content interface{} // 回复内容 19 | TargetType interface{} // 评论类型: content, reply 20 | TargetId interface{} // 对应内容ID,可能回复的是另一个回复,所以这里没有使用content_id 21 | UserId interface{} // 网站用户ID 22 | ZanCount interface{} // 赞 23 | CaiCount interface{} // 踩 24 | CreatedAt *gtime.Time // 创建时间 25 | UpdatedAt *gtime.Time // 26 | } 27 | -------------------------------------------------------------------------------- /internal/model/do/setting.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package do 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/frame/g" 9 | "github.com/gogf/gf/v2/os/gtime" 10 | ) 11 | 12 | // Setting is the golang structure of table gf_setting for DAO operations like Where/Data. 13 | type Setting struct { 14 | g.Meta `orm:"table:gf_setting, do:true"` 15 | K interface{} // 键名 16 | V interface{} // 键值 17 | CreatedAt *gtime.Time // 创建时间 18 | UpdatedAt *gtime.Time // 更新时间 19 | } 20 | -------------------------------------------------------------------------------- /internal/model/do/user.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package do 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/frame/g" 9 | "github.com/gogf/gf/v2/os/gtime" 10 | ) 11 | 12 | // User is the golang structure of table gf_user for DAO operations like Where/Data. 13 | type User struct { 14 | g.Meta `orm:"table:gf_user, do:true"` 15 | Id interface{} // UID 16 | Passport interface{} // 账号 17 | Password interface{} // MD5密码 18 | Nickname interface{} // 昵称 19 | Avatar interface{} // 头像地址 20 | Status interface{} // 状态 0:启用 1:禁用 21 | Gender interface{} // 性别 0: 未设置 1: 男 2: 女 22 | CreatedAt *gtime.Time // 注册时间 23 | UpdatedAt *gtime.Time // 更新时间 24 | } 25 | -------------------------------------------------------------------------------- /internal/model/entity/category.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package entity 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/os/gtime" 9 | ) 10 | 11 | // Category is the golang structure for table category. 12 | type Category struct { 13 | Id uint `json:"id" description:"分类ID,自增主键"` 14 | ContentType string `json:"contentType" description:"内容类型:topic, ask, article, reply"` 15 | Key string `json:"key" description:"栏目唯一键名,用于程序部分场景硬编码,一般不会用得到"` 16 | ParentId uint `json:"parentId" description:"父级分类ID,用于层级管理"` 17 | UserId uint `json:"userId" description:"创建的用户ID"` 18 | Name string `json:"name" description:"分类名称"` 19 | Sort uint `json:"sort" description:"排序,数值越低越靠前,默认为添加时的时间戳,可用于置顶"` 20 | Thumb string `json:"thumb" description:"封面图"` 21 | Brief string `json:"brief" description:"简述"` 22 | Content string `json:"content" description:"详细介绍"` 23 | CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"` 24 | UpdatedAt *gtime.Time `json:"updatedAt" description:"修改时间"` 25 | } 26 | -------------------------------------------------------------------------------- /internal/model/entity/content.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package entity 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/os/gtime" 9 | ) 10 | 11 | // Content is the golang structure for table content. 12 | type Content struct { 13 | Id uint `json:"id" description:"自增ID"` 14 | Key string `json:"key" description:"唯一键名,用于程序硬编码,一般不常用"` 15 | Type string `json:"type" description:"内容模型: topic, ask, article等,具体由程序定义"` 16 | CategoryId uint `json:"categoryId" description:"栏目ID"` 17 | UserId uint `json:"userId" description:"用户ID"` 18 | AdoptedReplyId uint `json:"adoptedReplyId" description:"采纳的回复ID,问答模块有效"` 19 | Title string `json:"title" description:"标题"` 20 | Content string `json:"content" description:"内容"` 21 | Sort uint `json:"sort" description:"排序,数值越低越靠前,默认为添加时的时间戳,可用于置顶"` 22 | Brief string `json:"brief" description:"摘要"` 23 | Thumb string `json:"thumb" description:"缩略图"` 24 | Tags string `json:"tags" description:"标签名称列表,以JSON存储"` 25 | Referer string `json:"referer" description:"内容来源,例如github/gitee"` 26 | Status uint `json:"status" description:"状态 0: 正常, 1: 禁用"` 27 | ReplyCount uint `json:"replyCount" description:"回复数量"` 28 | ViewCount uint `json:"viewCount" description:"浏览数量"` 29 | ZanCount uint `json:"zanCount" description:"赞"` 30 | CaiCount uint `json:"caiCount" description:"踩"` 31 | CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"` 32 | UpdatedAt *gtime.Time `json:"updatedAt" description:"修改时间"` 33 | } 34 | -------------------------------------------------------------------------------- /internal/model/entity/file.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package entity 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/os/gtime" 9 | ) 10 | 11 | // File is the golang structure for table file. 12 | type File struct { 13 | Id uint `json:"id" description:"自增ID"` 14 | Name string `json:"name" description:"文件名称"` 15 | Src string `json:"src" description:"本地文件存储路径"` 16 | Url string `json:"url" description:"URL地址,可能为空"` 17 | UserId uint `json:"userId" description:"操作用户"` 18 | CreatedAt *gtime.Time `json:"createdAt" description:""` 19 | } 20 | -------------------------------------------------------------------------------- /internal/model/entity/interact.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package entity 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/os/gtime" 9 | ) 10 | 11 | // Interact is the golang structure for table interact. 12 | type Interact struct { 13 | Id uint `json:"id" description:"自增ID"` 14 | Type int `json:"type" description:"操作类型。0:赞,1:踩。"` 15 | UserId uint `json:"userId" description:"操作用户"` 16 | TargetId uint `json:"targetId" description:"对应内容ID,该内容可能是content, reply"` 17 | TargetType string `json:"targetType" description:"内容模型: content, reply, 具体由程序定义"` 18 | Count uint `json:"count" description:"操作数据值"` 19 | CreatedAt *gtime.Time `json:"createdAt" description:""` 20 | UpdatedAt *gtime.Time `json:"updatedAt" description:""` 21 | } 22 | -------------------------------------------------------------------------------- /internal/model/entity/reply.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package entity 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/os/gtime" 9 | ) 10 | 11 | // Reply is the golang structure for table reply. 12 | type Reply struct { 13 | Id uint `json:"id" description:"回复ID"` 14 | ParentId uint `json:"parentId" description:"回复对应的上一级回复ID(没有的话默认为0)"` 15 | Title string `json:"title" description:"回复标题"` 16 | Content string `json:"content" description:"回复内容"` 17 | TargetType string `json:"targetType" description:"评论类型: content, reply"` 18 | TargetId uint `json:"targetId" description:"对应内容ID,可能回复的是另一个回复,所以这里没有使用content_id"` 19 | UserId uint `json:"userId" description:"网站用户ID"` 20 | ZanCount uint `json:"zanCount" description:"赞"` 21 | CaiCount uint `json:"caiCount" description:"踩"` 22 | CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"` 23 | UpdatedAt *gtime.Time `json:"updatedAt" description:""` 24 | } 25 | -------------------------------------------------------------------------------- /internal/model/entity/setting.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package entity 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/os/gtime" 9 | ) 10 | 11 | // Setting is the golang structure for table setting. 12 | type Setting struct { 13 | K string `json:"k" description:"键名"` 14 | V string `json:"v" description:"键值"` 15 | CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"` 16 | UpdatedAt *gtime.Time `json:"updatedAt" description:"更新时间"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/model/entity/user.go: -------------------------------------------------------------------------------- 1 | // ================================================================================= 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ================================================================================= 4 | 5 | package entity 6 | 7 | import ( 8 | "github.com/gogf/gf/v2/os/gtime" 9 | ) 10 | 11 | // User is the golang structure for table user. 12 | type User struct { 13 | Id uint `json:"id" description:"UID"` 14 | Passport string `json:"passport" description:"账号"` 15 | Password string `json:"password" description:"MD5密码"` 16 | Nickname string `json:"nickname" description:"昵称"` 17 | Avatar string `json:"avatar" description:"头像地址"` 18 | Status int `json:"status" description:"状态 0:启用 1:禁用"` 19 | Gender int `json:"gender" description:"性别 0: 未设置 1: 男 2: 女"` 20 | CreatedAt *gtime.Time `json:"createdAt" description:"注册时间"` 21 | UpdatedAt *gtime.Time `json:"updatedAt" description:"更新时间"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/model/file.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/gogf/gf/v2/net/ghttp" 4 | 5 | // FileUploadInput 上传文件输入参数 6 | type FileUploadInput struct { 7 | File *ghttp.UploadFile // 上传文件对象 8 | Name string // 自定义文件名称 9 | RandomName bool // 是否随机命名文件 10 | } 11 | 12 | // FileUploadOutput 上传文件返回参数 13 | type FileUploadOutput struct { 14 | Id uint // 数据表ID 15 | Name string // 文件名称 16 | Path string // 本地路径 17 | Url string // 访问URL,可能只是URI 18 | } 19 | -------------------------------------------------------------------------------- /internal/model/menu.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // MenuItem 菜单数据结构 4 | type MenuItem struct { 5 | Name string // 显示名称 6 | Url string // 链接地址 7 | Icon string // 图标,可能是class,也可能是iconfont 8 | Target string // 打开方式: 空, _blank 9 | Active bool // 是否被选中 10 | Items []*MenuItem // 子级菜单 11 | } 12 | -------------------------------------------------------------------------------- /internal/model/reply.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/os/gtime" 5 | ) 6 | 7 | // ReplyCreateInput 创建内容 8 | type ReplyCreateInput struct { 9 | Title string 10 | ParentId uint // 回复对应的上一级回复ID(没有的话默认为0) 11 | TargetType string // 评论类型: topic, ask, article, reply 12 | TargetId uint // 对应内容ID 13 | Content string // 回复内容 14 | UserId uint 15 | } 16 | 17 | // ReplyGetListInput 查询回复列表 18 | type ReplyGetListInput struct { 19 | Page int // 分页码 20 | Size int // 分页数量 21 | TargetType string // 数据类型 22 | TargetId uint // 数据ID 23 | UserId uint // 用户ID 24 | } 25 | 26 | // ReplyGetListOutput 查询列表结果 27 | type ReplyGetListOutput struct { 28 | List []ReplyGetListOutputItem `json:"list"` // 列表 29 | Page int `json:"page"` // 分页码 30 | Size int `json:"size"` // 分页数量 31 | Total int `json:"total"` // 数据总数 32 | } 33 | 34 | // ReplyGetListOutputItem 查询列表结果项 35 | type ReplyGetListOutputItem struct { 36 | Reply *ReplyListItem `json:"reply"` 37 | User *ReplyListUserItem `json:"user"` 38 | Content *ContentListItem `json:"content"` 39 | Category *ContentListCategoryItem `json:"category"` 40 | } 41 | 42 | // ReplyListItem 评论列表项 43 | type ReplyListItem struct { 44 | Id uint `json:"id"` // 回复ID 45 | ParentId uint `json:"parent_id"` // 回复对应的上一级回复ID(没有的话默认为0) 46 | TargetType string `json:"target_type"` // 评论类型: topic, ask, article, reply 47 | TargetId uint `json:"target_id"` // 对应内容ID 48 | UserId uint `json:"user_id"` // 网站用户ID 49 | ZanCount uint `json:"zan_count"` // 赞 50 | CaiCount uint `json:"cai_count"` // 踩 51 | Title string `json:"title"` // 回复标题 52 | Content string `json:"content"` // 回复内容 53 | CreatedAt *gtime.Time `json:"created_at"` // 创建时间 54 | UpdatedAt *gtime.Time `json:"updated_at"` // 55 | } 56 | 57 | // ReplyListUserItem 绑定到Content列表中的用户信息 58 | type ReplyListUserItem struct { 59 | Id uint `json:"id"` // UID 60 | Nickname string `json:"nickname"` // 昵称 61 | Avatar string `json:"avatar"` // 头像地址 62 | } 63 | -------------------------------------------------------------------------------- /internal/model/session.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // SessionNotice 存放在Session中的提示信息,往往使用后则删除 4 | type SessionNotice struct { 5 | Type string // 消息类型 6 | Content string // 消息内容 7 | } 8 | -------------------------------------------------------------------------------- /internal/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // UserRegisterInput 创建用户 4 | type UserRegisterInput struct { 5 | Passport string // 账号 6 | Password string // 密码(明文) 7 | Nickname string // 昵称 8 | } 9 | 10 | type UserPasswordInput struct { 11 | OldPassword string // 原密码 12 | NewPassword string // 新密码 13 | } 14 | 15 | // UserLoginInput 用户登录 16 | type UserLoginInput struct { 17 | Passport string // 账号 18 | Password string // 密码(明文) 19 | } 20 | 21 | type UserGetProfileOutput struct { 22 | Id uint // 用户ID 23 | Nickname string // 昵称 24 | Avatar string // 头像地址 25 | Gender int // 性别 0: 未设置 1: 男 2: 女 26 | Stats map[string]int // 发布内容数量 27 | } 28 | 29 | type UserUpdateAvatarInput struct { 30 | UserId uint // 用户ID 31 | Avatar string // 头像地址 32 | } 33 | 34 | type UserUpdateProfileInput struct { 35 | UserId uint // 用户ID 36 | Nickname string // 昵称 37 | Avatar string // 头像地址 38 | Gender int // 性别 0: 未设置 1: 男 2: 女 39 | } 40 | 41 | // UserGetContentListInput 查询用户列表输入 42 | type UserGetContentListInput struct { 43 | ContentGetListInput 44 | } 45 | 46 | // UserGetListOutput 查询用户详情结果 47 | type UserGetListOutput struct { 48 | Content *ContentGetListOutput `json:"content"` // 查询用户 49 | User *UserGetProfileOutput `json:"user"` // 查询用户 50 | Stats map[string]int // 发布内容数量 51 | } 52 | 53 | type UserGetMessageListInput struct { 54 | Page int // 分页码 55 | Size int // 分页数量 56 | TargetType string // 数据类型 57 | TargetId uint // 数据ID 58 | UserId uint // 用户ID 59 | } 60 | 61 | // 查询用户列表查询结果 62 | type UserGetMessageListOutput struct { 63 | List []ReplyGetListOutputItem `json:"list"` // 列表 64 | Page int `json:"page"` // 分页码 65 | Size int `json:"size"` // 分页数量 66 | Total int `json:"total"` // 数据总数 67 | Stats map[string]int // 发布内容数量 68 | } 69 | -------------------------------------------------------------------------------- /internal/model/view.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // This is auto-generated by gf cli tool. Fill this file as you wish. 3 | // ========================================================================== 4 | 5 | package model 6 | 7 | // View 视图渲染内容对象 8 | type View struct { 9 | Title string // 页面标题 10 | Keywords string // 页面Keywords 11 | Description string // 页面Description 12 | Error string // 错误信息 13 | MainTpl string // 自定义MainTpl展示模板文件 14 | Redirect string // 引导页面跳转 15 | ContentType string // 内容模型 16 | BreadCrumb []ViewBreadCrumb // 面包屑 17 | Data interface{} // 页面参数 18 | } 19 | 20 | // ViewBreadCrumb 视图面包屑结构 21 | type ViewBreadCrumb struct { 22 | Name string // 显示名称 23 | Url string // 链接地址,当为空时表示被选中 24 | } 25 | 26 | // ViewGetBreadCrumbInput 获取面包屑请求 27 | type ViewGetBreadCrumbInput struct { 28 | ContentId uint // (可选)内容ID 29 | ContentType string // (可选)内容类型 30 | CategoryId uint // (可选)栏目ID 31 | } 32 | 33 | // ViewGetTitleInput 获取title请求 34 | type ViewGetTitleInput struct { 35 | ContentType string // (可选)内容类型 36 | CategoryId uint // (可选)栏目ID 37 | CurrentName string // (可选)当前名称 38 | } 39 | -------------------------------------------------------------------------------- /internal/packed/packed.go: -------------------------------------------------------------------------------- 1 | package packed 2 | -------------------------------------------------------------------------------- /internal/service/bizctx.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | "focus-single/internal/model" 10 | 11 | "github.com/gogf/gf/v2/frame/g" 12 | "github.com/gogf/gf/v2/net/ghttp" 13 | ) 14 | 15 | type IBizCtx interface { 16 | Init(r *ghttp.Request, customCtx *model.Context) 17 | Get(ctx context.Context) *model.Context 18 | SetUser(ctx context.Context, ctxUser *model.ContextUser) 19 | SetData(ctx context.Context, data g.Map) 20 | } 21 | 22 | var localBizCtx IBizCtx 23 | 24 | func BizCtx() IBizCtx { 25 | if localBizCtx == nil { 26 | panic("implement not found for interface IBizCtx, forgot register?") 27 | } 28 | return localBizCtx 29 | } 30 | 31 | func RegisterBizCtx(i IBizCtx) { 32 | localBizCtx = i 33 | } 34 | -------------------------------------------------------------------------------- /internal/service/captcha.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/gogf/gf/v2/net/ghttp" 11 | ) 12 | 13 | type ICaptcha interface { 14 | NewAndStore(ctx context.Context, name string) error 15 | VerifyAndClear(r *ghttp.Request, name string, value string) bool 16 | } 17 | 18 | var localCaptcha ICaptcha 19 | 20 | func Captcha() ICaptcha { 21 | if localCaptcha == nil { 22 | panic("implement not found for interface ICaptcha, forgot register?") 23 | } 24 | return localCaptcha 25 | } 26 | 27 | func RegisterCaptcha(i ICaptcha) { 28 | localCaptcha = i 29 | } 30 | -------------------------------------------------------------------------------- /internal/service/category.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | "focus-single/internal/model" 10 | "focus-single/internal/model/entity" 11 | ) 12 | 13 | type ICategory interface { 14 | GetTree(ctx context.Context, contentType string) ([]*model.CategoryTreeItem, error) 15 | GetSubIdList(ctx context.Context, id uint) ([]uint, error) 16 | GetList(ctx context.Context) (list []*entity.Category, err error) 17 | GetItem(ctx context.Context, id uint) (*entity.Category, error) 18 | GetMap(ctx context.Context) (map[uint]*entity.Category, error) 19 | } 20 | 21 | var localCategory ICategory 22 | 23 | func Category() ICategory { 24 | if localCategory == nil { 25 | panic("implement not found for interface ICategory, forgot register?") 26 | } 27 | return localCategory 28 | } 29 | 30 | func RegisterCategory(i ICategory) { 31 | localCategory = i 32 | } 33 | -------------------------------------------------------------------------------- /internal/service/content.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | "focus-single/internal/model" 10 | ) 11 | 12 | type IContent interface { 13 | GetList(ctx context.Context, in model.ContentGetListInput) (out *model.ContentGetListOutput, err error) 14 | Search(ctx context.Context, in model.ContentSearchInput) (out *model.ContentSearchOutput, err error) 15 | GetDetail(ctx context.Context, id uint) (out *model.ContentGetDetailOutput, err error) 16 | Create(ctx context.Context, in model.ContentCreateInput) (out model.ContentCreateOutput, err error) 17 | Update(ctx context.Context, in model.ContentUpdateInput) error 18 | Delete(ctx context.Context, id uint) error 19 | AddViewCount(ctx context.Context, id uint, count int) error 20 | AddReplyCount(ctx context.Context, id uint, count int) error 21 | AdoptReply(ctx context.Context, id uint, replyID uint) error 22 | UnacceptedReply(ctx context.Context, id uint) error 23 | } 24 | 25 | var localContent IContent 26 | 27 | func Content() IContent { 28 | if localContent == nil { 29 | panic("implement not found for interface IContent, forgot register?") 30 | } 31 | return localContent 32 | } 33 | 34 | func RegisterContent(i IContent) { 35 | localContent = i 36 | } 37 | -------------------------------------------------------------------------------- /internal/service/file.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | "focus-single/internal/model" 10 | ) 11 | 12 | type IFile interface { 13 | Upload(ctx context.Context, in model.FileUploadInput) (*model.FileUploadOutput, error) 14 | } 15 | 16 | var localFile IFile 17 | 18 | func File() IFile { 19 | if localFile == nil { 20 | panic("implement not found for interface IFile, forgot register?") 21 | } 22 | return localFile 23 | } 24 | 25 | func RegisterFile(i IFile) { 26 | localFile = i 27 | } 28 | -------------------------------------------------------------------------------- /internal/service/interact.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import "context" 8 | 9 | type IInteract interface { 10 | Zan(ctx context.Context, targetType string, targetId uint) error 11 | CancelZan(ctx context.Context, targetType string, targetId uint) error 12 | DidIZan(ctx context.Context, targetType string, targetId uint) (bool, error) 13 | Cai(ctx context.Context, targetType string, targetId uint) error 14 | CancelCai(ctx context.Context, targetType string, targetId uint) error 15 | DidICai(ctx context.Context, targetType string, targetId uint) (bool, error) 16 | } 17 | 18 | var localInteract IInteract 19 | 20 | func Interact() IInteract { 21 | if localInteract == nil { 22 | panic("implement not found for interface IInteract, forgot register?") 23 | } 24 | return localInteract 25 | } 26 | 27 | func RegisterInteract(i IInteract) { 28 | localInteract = i 29 | } 30 | -------------------------------------------------------------------------------- /internal/service/menu.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | "focus-single/internal/model" 10 | ) 11 | 12 | type IMenu interface { 13 | SetTopMenus(ctx context.Context, menus []*model.MenuItem) error 14 | GetTopMenus(ctx context.Context) ([]*model.MenuItem, error) 15 | GetTopMenuByUrl(ctx context.Context, url string) (*model.MenuItem, error) 16 | } 17 | 18 | var localMenu IMenu 19 | 20 | func Menu() IMenu { 21 | if localMenu == nil { 22 | panic("implement not found for interface IMenu, forgot register?") 23 | } 24 | return localMenu 25 | } 26 | 27 | func RegisterMenu(i IMenu) { 28 | localMenu = i 29 | } 30 | -------------------------------------------------------------------------------- /internal/service/middleware.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import "github.com/gogf/gf/v2/net/ghttp" 8 | 9 | type IMiddleware interface { 10 | ResponseHandler(r *ghttp.Request) 11 | Ctx(r *ghttp.Request) 12 | Auth(r *ghttp.Request) 13 | } 14 | 15 | var localMiddleware IMiddleware 16 | 17 | func Middleware() IMiddleware { 18 | if localMiddleware == nil { 19 | panic("implement not found for interface IMiddleware, forgot register?") 20 | } 21 | return localMiddleware 22 | } 23 | 24 | func RegisterMiddleware(i IMiddleware) { 25 | localMiddleware = i 26 | } 27 | -------------------------------------------------------------------------------- /internal/service/reply.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | "focus-single/internal/model" 10 | ) 11 | 12 | type IReply interface { 13 | Create(ctx context.Context, in model.ReplyCreateInput) error 14 | Delete(ctx context.Context, id uint) error 15 | DeleteByUserContentId(ctx context.Context, userId, contentId uint) error 16 | GetList(ctx context.Context, in model.ReplyGetListInput) (out *model.ReplyGetListOutput, err error) 17 | } 18 | 19 | var localReply IReply 20 | 21 | func Reply() IReply { 22 | if localReply == nil { 23 | panic("implement not found for interface IReply, forgot register?") 24 | } 25 | return localReply 26 | } 27 | 28 | func RegisterReply(i IReply) { 29 | localReply = i 30 | } 31 | -------------------------------------------------------------------------------- /internal/service/session.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | "focus-single/internal/model" 10 | "focus-single/internal/model/entity" 11 | ) 12 | 13 | type ISession interface { 14 | SetUser(ctx context.Context, user *entity.User) error 15 | GetUser(ctx context.Context) *entity.User 16 | RemoveUser(ctx context.Context) error 17 | SetLoginReferer(ctx context.Context, referer string) error 18 | GetLoginReferer(ctx context.Context) string 19 | RemoveLoginReferer(ctx context.Context) error 20 | SetNotice(ctx context.Context, message *model.SessionNotice) error 21 | GetNotice(ctx context.Context) (*model.SessionNotice, error) 22 | RemoveNotice(ctx context.Context) error 23 | } 24 | 25 | var localSession ISession 26 | 27 | func Session() ISession { 28 | if localSession == nil { 29 | panic("implement not found for interface ISession, forgot register?") 30 | } 31 | return localSession 32 | } 33 | 34 | func RegisterSession(i ISession) { 35 | localSession = i 36 | } 37 | -------------------------------------------------------------------------------- /internal/service/setting.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/gogf/gf/v2/frame/g" 11 | ) 12 | 13 | type ISetting interface { 14 | Set(ctx context.Context, key, value string) error 15 | Get(ctx context.Context, key string) (string, error) 16 | GetVar(ctx context.Context, key string) (*g.Var, error) 17 | } 18 | 19 | var localSetting ISetting 20 | 21 | func Setting() ISetting { 22 | if localSetting == nil { 23 | panic("implement not found for interface ISetting, forgot register?") 24 | } 25 | return localSetting 26 | } 27 | 28 | func RegisterSetting(i ISetting) { 29 | localSetting = i 30 | } 31 | -------------------------------------------------------------------------------- /internal/service/user.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | "focus-single/internal/model" 10 | "focus-single/internal/model/entity" 11 | ) 12 | 13 | type IUser interface { 14 | GetAvatarUploadPath() string 15 | GetAvatarUploadUrlPrefix() string 16 | Login(ctx context.Context, in model.UserLoginInput) error 17 | Logout(ctx context.Context) error 18 | EncryptPassword(passport, password string) string 19 | GetUserByPassportAndPassword(ctx context.Context, passport, password string) (user *entity.User, err error) 20 | CheckPassportUnique(ctx context.Context, passport string) error 21 | CheckNicknameUnique(ctx context.Context, nickname string) error 22 | Register(ctx context.Context, in model.UserRegisterInput) error 23 | UpdatePassword(ctx context.Context, in model.UserPasswordInput) error 24 | GetProfileById(ctx context.Context, userId uint) (out *model.UserGetProfileOutput, err error) 25 | GetProfile(ctx context.Context) (*model.UserGetProfileOutput, error) 26 | UpdateAvatar(ctx context.Context, in model.UserUpdateAvatarInput) error 27 | UpdateProfile(ctx context.Context, in model.UserUpdateProfileInput) error 28 | Disable(ctx context.Context, id uint) error 29 | GetList(ctx context.Context, in model.UserGetContentListInput) (out *model.UserGetListOutput, err error) 30 | GetMessageList(ctx context.Context, in model.UserGetMessageListInput) (out *model.UserGetMessageListOutput, err error) 31 | GetUserStats(ctx context.Context, userId uint) (map[string]int, error) 32 | IsCtxAdmin(ctx context.Context) bool 33 | IsAdmin(ctx context.Context, userId uint) bool 34 | } 35 | 36 | var localUser IUser 37 | 38 | func User() IUser { 39 | if localUser == nil { 40 | panic("implement not found for interface IUser, forgot register?") 41 | } 42 | return localUser 43 | } 44 | 45 | func RegisterUser(i IUser) { 46 | localUser = i 47 | } 48 | -------------------------------------------------------------------------------- /internal/service/view.go: -------------------------------------------------------------------------------- 1 | // ========================================================================== 2 | // Code generated by GoFrame CLI tool. DO NOT EDIT. 3 | // ========================================================================== 4 | 5 | package service 6 | 7 | import ( 8 | "context" 9 | "focus-single/internal/model" 10 | ) 11 | 12 | type IView interface { 13 | GetBreadCrumb(ctx context.Context, in *model.ViewGetBreadCrumbInput) []model.ViewBreadCrumb 14 | GetTitle(ctx context.Context, in *model.ViewGetTitleInput) string 15 | RenderTpl(ctx context.Context, tpl string, data ...model.View) 16 | Render(ctx context.Context, data ...model.View) 17 | Render302(ctx context.Context, data ...model.View) 18 | Render401(ctx context.Context, data ...model.View) 19 | Render403(ctx context.Context, data ...model.View) 20 | Render404(ctx context.Context, data ...model.View) 21 | Render500(ctx context.Context, data ...model.View) 22 | } 23 | 24 | var localView IView 25 | 26 | func View() IView { 27 | if localView == nil { 28 | panic("implement not found for interface IView, forgot register?") 29 | } 30 | return localView 31 | } 32 | 33 | func RegisterView(i IView) { 34 | localView = i 35 | } 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/gogf/gf/contrib/drivers/mysql/v2" 5 | 6 | _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" 7 | 8 | _ "focus-single/internal/packed" 9 | 10 | _ "focus-single/internal/logic" 11 | 12 | "focus-single/internal/cmd" 13 | 14 | "github.com/gogf/gf/v2/os/gctx" 15 | ) 16 | 17 | func main() { 18 | cmd.Main.Run(gctx.New()) 19 | } 20 | -------------------------------------------------------------------------------- /manifest/config/config.example.yaml: -------------------------------------------------------------------------------- 1 | # 配置文件的键名命名方式统一使用小驼峰。 2 | 3 | # HTTP Server. 4 | server: 5 | address: ":8199" 6 | serverRoot: "resource/public" 7 | dumpRouterMap: true 8 | routeOverWrite: true 9 | accessLogEnabled: false 10 | accessLogPattern: "access-{Ymd}.log" 11 | sessionPath: "temp/sessions" # Session文件存储目录 12 | openapiPath: "/api.json" 13 | swaggerPath: "/swagger" 14 | 15 | # 数据库连接配置 16 | database: 17 | logger: 18 | path: "temp/logs/sql" 19 | level: "all" 20 | stdout: true 21 | ctxKeys: ["RequestId"] 22 | 23 | default: 24 | # link: "mysql:root:12345678@tcp(127.0.0.1:3306)/focus" 25 | link: "sqlite::@file(manifest/document/sqlite/focus.db)" 26 | debug: true 27 | 28 | # 内容设置 29 | setting: 30 | title: "Focus聚焦社区 - Beta!" 31 | keywords: "Go,MVC,Cookie,Session,ORM,golang,cache,goframe,go frame,gf,go框架,web框架,高性能" 32 | description: "模块化、高性能、生产级的Go基础开发框架。实现了比较完善的基础设施建设,包括常用的核心开发组件, 如:缓存、日志、文件、时间、队列、数组、集合、字符串、定时器、命令行、文件锁、内存锁、对象池、连接池、资源管理、数据校验、数据编码、文件监控、 定时任务、数据库ORM、TCP/UDP组件、进程管理/通信、并发安全容器等等。 并提供了Web服务开发的系列核心组件,如:Router、Cookie、Session、Middleware、服务注册、配置管理、模板引擎等等, 支持热重启、热更新、多域名、多端口、多服务、HTTPS、Rewrite等特性。" 33 | adminIds: [1] # 管理员ID 34 | 35 | # 文件上传设置 36 | upload: 37 | path: "temp/upload" 38 | 39 | # Logger configurations. 40 | logger: 41 | path: "temp/logs/default" 42 | level: "all" 43 | stdout: true 44 | ctxKeys: ["RequestId"] 45 | 46 | # 模板引擎配置 47 | viewer: 48 | indexLayout: "index/index.html" 49 | adminHomeLayout: "admin/home.html" 50 | adminLayout: "admin/index.html" 51 | 52 | -------------------------------------------------------------------------------- /manifest/deploy/kustomize/base/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: focus-single 5 | labels: 6 | app: focus-single 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: focus-single 12 | template: 13 | metadata: 14 | labels: 15 | app: focus-single 16 | spec: 17 | containers: 18 | - name : main 19 | image: focus-single 20 | imagePullPolicy: Always 21 | 22 | -------------------------------------------------------------------------------- /manifest/deploy/kustomize/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | - service.yaml 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /manifest/deploy/kustomize/base/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: focus-single 5 | spec: 6 | ports: 7 | - port: 80 8 | protocol: TCP 9 | targetPort: 8000 10 | selector: 11 | app: focus-single 12 | 13 | -------------------------------------------------------------------------------- /manifest/deploy/kustomize/overlays/develop/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: focus-single-configmap 5 | data: 6 | config.yaml: | 7 | server: 8 | address: ":8000" 9 | openapiPath: "/api.json" 10 | swaggerPath: "/swagger" 11 | 12 | logger: 13 | level : "all" 14 | stdout: true 15 | -------------------------------------------------------------------------------- /manifest/deploy/kustomize/overlays/develop/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: focus-single 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name : main 10 | image: focus-single:develop -------------------------------------------------------------------------------- /manifest/deploy/kustomize/overlays/develop/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../../base 6 | - configmap.yaml 7 | 8 | patchesStrategicMerge: 9 | - deployment.yaml 10 | 11 | namespace: default 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /manifest/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM loads/alpine:3.8 2 | 3 | ############################################################################### 4 | # INSTALLATION 5 | ############################################################################### 6 | 7 | ENV WORKDIR /app 8 | ADD resource $WORKDIR/ 9 | ADD ./temp/linux_amd64/main $WORKDIR/main 10 | RUN chmod +x $WORKDIR/main 11 | 12 | ############################################################################### 13 | # START 14 | ############################################################################### 15 | WORKDIR $WORKDIR 16 | CMD ./main 17 | -------------------------------------------------------------------------------- /manifest/docker/docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This shell is executed before docker build. 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /manifest/document/design/focus-v0.1.0-beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/design/focus-v0.1.0-beta.png -------------------------------------------------------------------------------- /manifest/document/design/focus-v0.2.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/design/focus-v0.2.0.png -------------------------------------------------------------------------------- /manifest/document/design/focus-v0.2.0.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/design/focus-v0.2.0.xmind -------------------------------------------------------------------------------- /manifest/document/design/focus-v1.0.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/design/focus-v1.0.xmind -------------------------------------------------------------------------------- /manifest/document/images/databases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/databases.png -------------------------------------------------------------------------------- /manifest/document/images/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/demo1.png -------------------------------------------------------------------------------- /manifest/document/images/demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/demo2.png -------------------------------------------------------------------------------- /manifest/document/images/demo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/demo3.png -------------------------------------------------------------------------------- /manifest/document/images/demo4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/demo4.png -------------------------------------------------------------------------------- /manifest/document/images/demo5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/demo5.png -------------------------------------------------------------------------------- /manifest/document/images/demo6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/demo6.png -------------------------------------------------------------------------------- /manifest/document/images/demo7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/demo7.png -------------------------------------------------------------------------------- /manifest/document/images/demo8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/demo8.png -------------------------------------------------------------------------------- /manifest/document/images/plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/images/plan.png -------------------------------------------------------------------------------- /manifest/document/mwb/gf.mwb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/mwb/gf.mwb -------------------------------------------------------------------------------- /manifest/document/mwb/gf.mwb.bak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/mwb/gf.mwb.bak -------------------------------------------------------------------------------- /manifest/document/sqlite/focus.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/manifest/document/sqlite/focus.db -------------------------------------------------------------------------------- /resource/public/admin/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/admin/css/.gitkeep -------------------------------------------------------------------------------- /resource/public/admin/css/common.css: -------------------------------------------------------------------------------- 1 | 2 | .modal-body, .modal-body input { 3 | font-size:15px; 4 | } 5 | 6 | .modal-footer button { 7 | /*padding:5px 12px 5px 12px !important;*/ 8 | } 9 | 10 | .btn-gf { 11 | padding:5px 12px 5px 12px; 12 | } 13 | 14 | .required-mark{ 15 | color:red; 16 | } 17 | .nav-sidebar .nav-link p { 18 | transition: none !important; 19 | -webkit-animation-name: none !important; 20 | animation-name: none !important; 21 | -webkit-animation-fill-mode: none !important; 22 | animation-fill-mode: none !important; 23 | } 24 | 25 | .table thead th { 26 | font-size:15px; 27 | padding: .75rem .75rem .75rem 0; 28 | border-bottom: 1px solid #dee2e6 !important; 29 | } 30 | 31 | .table tbody td { 32 | font-size: 15px; 33 | padding: .75rem .75rem .75rem 0; 34 | border-top: none !important; 35 | border-bottom: 1px solid #dee2e6 !important; 36 | } 37 | 38 | .pagination { 39 | margin-top:20px; 40 | } 41 | 42 | section.content { 43 | padding-top:10px !important; 44 | } 45 | 46 | .brand-link { 47 | color: rgba(255,255,255,.9) !important; 48 | } 49 | 50 | .nav-treeview a, .nav-treeview a:visited, .nav-treeview a:active { 51 | padding-left:25px; 52 | font-size:15px; 53 | } 54 | 55 | .brand-link img { 56 | width: 34px !important; 57 | } 58 | 59 | .user-panel img { 60 | width: 34px !important; 61 | } 62 | 63 | /*************** 表单校验 *********************/ 64 | .validation-error-block { 65 | color: red; 66 | padding: 2px 0 0 12px; 67 | width: 100%; 68 | } -------------------------------------------------------------------------------- /resource/public/admin/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/admin/images/.gitkeep -------------------------------------------------------------------------------- /resource/public/admin/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/admin/js/.gitkeep -------------------------------------------------------------------------------- /resource/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/favicon.ico -------------------------------------------------------------------------------- /resource/public/images/avatar/avatar1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/images/avatar/avatar1.jpg -------------------------------------------------------------------------------- /resource/public/images/avatar/avatar2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/images/avatar/avatar2.jpg -------------------------------------------------------------------------------- /resource/public/images/avatar/avatar3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/images/avatar/avatar3.jpg -------------------------------------------------------------------------------- /resource/public/images/avatar/avatar4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/images/avatar/avatar4.jpg -------------------------------------------------------------------------------- /resource/public/images/error/error_401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/images/error/error_401.png -------------------------------------------------------------------------------- /resource/public/images/error/error_403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/images/error/error_403.png -------------------------------------------------------------------------------- /resource/public/images/error/error_404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/images/error/error_404.png -------------------------------------------------------------------------------- /resource/public/images/error/error_500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/images/error/error_500.png -------------------------------------------------------------------------------- /resource/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gogf/focus-single/d9034a0b417e8cf311fae2157f08fa29b5dd9372/resource/public/images/logo.png -------------------------------------------------------------------------------- /resource/public/plugin/bootstrap-dialog/bootstrap-dialog-common.js: -------------------------------------------------------------------------------- 1 | // 提示框 2 | dialog = { 3 | Alert: function (message, handler) { 4 | handler = handler || null; 5 | BootstrapDialog.alert({ 6 | title: '提示', 7 | message: message, 8 | callback: function () { 9 | if (handler != null) dialog.f(handler); 10 | } 11 | }); 12 | }, 13 | //成功信息SucceedInfo 14 | Succeed: function (message, handler) { 15 | handler = handler || null; 16 | BootstrapDialog.show({ 17 | type: BootstrapDialog.TYPE_SUCCESS, 18 | title: '信息', 19 | message: message, 20 | buttons: [{ 21 | label: '确认', 22 | action: function (dialog) { 23 | if (handler != null) dialog.f(handler); 24 | dialog.close(); 25 | } 26 | }] 27 | }); 28 | }, 29 | //失败信息ErrorInfo 30 | Error: function (message, handler) { 31 | handler = handler || null; 32 | BootstrapDialog.show({ 33 | type: BootstrapDialog.TYPE_DANGER, 34 | title: '信息', 35 | message: message, 36 | buttons: [{ 37 | label: '确认', 38 | action: function (dialog) { 39 | if (handler != null) dialog.f(handler); 40 | dialog.close(); 41 | } 42 | }] 43 | }); 44 | }, 45 | //询问信息 46 | Confirm: function (message, ok_fun, cancel_fun) { 47 | BootstrapDialog.confirm(message, function (result) { 48 | if (result) { 49 | dialog.f(ok_fun); 50 | } else { 51 | if (cancel_fun != null) { 52 | dialog.f(cancel_fun); 53 | } 54 | } 55 | }); 56 | }, 57 | EmptyFunc: function () { 58 | return; 59 | }, 60 | f: function (fn) { 61 | fn(); 62 | } 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /resource/public/plugin/bootstrap-dialog/bootstrap-dialog.min.css: -------------------------------------------------------------------------------- 1 | .bootstrap-dialog .modal-header{border-top-left-radius:4px;border-top-right-radius:4px}.bootstrap-dialog .bootstrap-dialog-title{color:#fff;display:inline-block;font-size:16px}.bootstrap-dialog .bootstrap-dialog-message{font-size:14px}.bootstrap-dialog .bootstrap-dialog-button-icon{margin-right:3px}.bootstrap-dialog .bootstrap-dialog-close-button{font-size:20px;float:right;opacity:.9;filter:alpha(opacity=90)}.bootstrap-dialog .bootstrap-dialog-close-button:hover{cursor:pointer;opacity:1;filter:alpha(opacity=100)}@media(min-width:1172px){.bootstrap-dialog .modal-xl{max-width:95%}}.bootstrap-dialog .modal-lg .bootstrap4-dialog-button:first-child{margin-top:8px}.bootstrap-dialog.type-default .modal-header{background-color:#fff}.bootstrap-dialog.type-default .bootstrap-dialog-title{color:#333}.bootstrap-dialog.type-info .modal-header{background-color:#17a2b8}.bootstrap-dialog.type-primary .modal-header{background-color:#007bff}.bootstrap-dialog.type-secondary .modal-header{background-color:#6c757d}.bootstrap-dialog.type-success .modal-header{background-color:#28a745}.bootstrap-dialog.type-warning .modal-header{background-color:#ffc107}.bootstrap-dialog.type-danger .modal-header{background-color:#dc3545}.bootstrap-dialog.type-light .modal-header{background-color:#f8f9fa}.bootstrap-dialog.type-dark .modal-header{background-color:#343a40}.bootstrap-dialog.size-large .bootstrap-dialog-title{font-size:24px}.bootstrap-dialog.size-large .bootstrap-dialog-close-button{font-size:30px}.bootstrap-dialog.size-large .bootstrap-dialog-message{font-size:18px}.bootstrap-dialog .icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}.bootstrap-dialog-footer-buttons{display:flex}@-moz-keyframes spin{0{-moz-transform:rotate(0)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0{-webkit-transform:rotate(0)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0{-o-transform:rotate(0)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0{-ms-transform:rotate(0)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0{transform:rotate(0)}100%{transform:rotate(359deg)}}.bootstrap-dialog-header{display:contents} -------------------------------------------------------------------------------- /resource/public/plugin/bootstrap-dialog/common-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 |