├── .github
└── ISSUE_TEMPLATE
│ ├── ✨-新功能建议.md
│ ├── 系统问题解答.md
│ └── 🐞-bug反馈.md
├── .gitignore
├── LICENSE
├── README.md
├── blog.sql
├── client
├── env
│ ├── .env
│ ├── .env.development
│ └── index.js
├── next-env.d.ts
├── next.config.ts
├── package.json
├── postcss.config.js
├── prettier.config.js
├── public
│ ├── favicon.ico
│ ├── favicon.svg
│ ├── icon
│ │ ├── admin
│ │ │ ├── QQ.svg
│ │ │ ├── fork.svg
│ │ │ ├── github.svg
│ │ │ ├── homepage.svg
│ │ │ ├── issues.svg
│ │ │ ├── star.svg
│ │ │ ├── watch.svg
│ │ │ ├── 数据看板.svg
│ │ │ └── 邮箱.svg
│ │ └── client
│ │ │ ├── briefcase.png
│ │ │ ├── collection-blue.png
│ │ │ ├── collection-fill.png
│ │ │ ├── collection.png
│ │ │ ├── comment.png
│ │ │ ├── comments.png
│ │ │ ├── delete-fill.png
│ │ │ ├── delete.png
│ │ │ ├── drafts.svg
│ │ │ ├── email-fill.png
│ │ │ ├── email.png
│ │ │ ├── emoji.png
│ │ │ ├── github-fill.png
│ │ │ ├── github.png
│ │ │ ├── likes-fill.png
│ │ │ ├── likes.png
│ │ │ ├── picture.png
│ │ │ ├── police.png
│ │ │ ├── postcard.png
│ │ │ ├── problem-collection-black.png
│ │ │ ├── problem-collection-white.png
│ │ │ ├── problem-follow-black.png
│ │ │ ├── problem-follow-white.png
│ │ │ ├── problem-like-black.png
│ │ │ ├── problem-like-white.png
│ │ │ ├── problem.svg
│ │ │ ├── qq.png
│ │ │ ├── share.png
│ │ │ ├── small-bell.png
│ │ │ ├── unit.png
│ │ │ ├── view-blue.png
│ │ │ ├── view-white.png
│ │ │ ├── view.png
│ │ │ ├── website.png
│ │ │ ├── wechat.png
│ │ │ └── write-article.svg
│ ├── image
│ │ ├── admin
│ │ │ ├── bg.svg
│ │ │ └── statistics
│ │ │ │ ├── bg.jpg
│ │ │ │ ├── border_bg.jpg
│ │ │ │ ├── title_left_bg.png
│ │ │ │ └── title_right_bg.png
│ │ └── client
│ │ │ ├── collection-bg.jpg
│ │ │ ├── github.jpg
│ │ │ ├── load-error.png
│ │ │ ├── qq-qrcode.jpg
│ │ │ └── wechat-qrcode.jpg
│ ├── robots-template.txt
│ └── vditor
│ │ ├── ant.js
│ │ ├── github.min.css
│ │ ├── highlight.min.js
│ │ ├── light.css
│ │ ├── lute.min.js
│ │ ├── third-languages.js
│ │ └── zh_CN.js
├── scripts
│ └── build-output
│ │ ├── index.js
│ │ └── nodemon.json
├── src
│ ├── app
│ │ ├── (main)
│ │ │ └── page.tsx
│ │ ├── (route)
│ │ │ ├── ads.txt
│ │ │ │ └── route.ts
│ │ │ ├── sitemap
│ │ │ │ └── [type]
│ │ │ │ │ ├── index.xml
│ │ │ │ │ └── route.tsx
│ │ │ │ │ └── index[index].xml
│ │ │ │ │ └── route.tsx
│ │ │ └── static
│ │ │ │ ├── antd
│ │ │ │ └── [name]
│ │ │ │ │ └── route.tsx
│ │ │ │ ├── editor
│ │ │ │ └── [...path]
│ │ │ │ │ └── route.tsx
│ │ │ │ ├── high-light
│ │ │ │ └── [type]
│ │ │ │ │ └── route.tsx
│ │ │ │ └── theme
│ │ │ │ └── [id]
│ │ │ │ └── route.tsx
│ │ ├── admin
│ │ │ ├── [...all]
│ │ │ │ └── page.tsx
│ │ │ ├── advertisement
│ │ │ │ ├── [id]
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── list
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── article
│ │ │ │ ├── [id]
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── list
│ │ │ │ │ └── page.tsx
│ │ │ │ └── write
│ │ │ │ │ └── page.tsx
│ │ │ ├── comment
│ │ │ │ └── page.tsx
│ │ │ ├── external-link
│ │ │ │ └── page.tsx
│ │ │ ├── friendly-link
│ │ │ │ ├── create
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── login
│ │ │ │ └── page.tsx
│ │ │ ├── oss
│ │ │ │ └── page.tsx
│ │ │ ├── page.tsx
│ │ │ ├── statistics
│ │ │ │ └── page.tsx
│ │ │ ├── theme
│ │ │ │ ├── create
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── type
│ │ │ │ ├── page.tsx
│ │ │ │ ├── tag
│ │ │ │ │ └── [id]
│ │ │ │ │ │ └── page.tsx
│ │ │ │ └── type
│ │ │ │ │ └── [id]
│ │ │ │ │ └── page.tsx
│ │ │ └── user
│ │ │ │ ├── [id]
│ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ ├── article
│ │ │ ├── [id]
│ │ │ │ ├── (main)
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ └── not-found.tsx
│ │ │ └── editor
│ │ │ │ ├── [id]
│ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ ├── collection
│ │ │ └── [id]
│ │ │ │ ├── not-found.tsx
│ │ │ │ └── page.tsx
│ │ ├── creator
│ │ │ ├── content
│ │ │ │ ├── article
│ │ │ │ │ └── page.tsx
│ │ │ │ └── problem
│ │ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── error.tsx
│ │ ├── forget-password
│ │ │ └── page.tsx
│ │ ├── friendly-link
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── link
│ │ │ └── page.tsx
│ │ ├── not-found.tsx
│ │ ├── notification
│ │ │ ├── [type]
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── oauth
│ │ │ └── github
│ │ │ │ └── page.tsx
│ │ ├── problem
│ │ │ ├── (main)
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── [id]
│ │ │ │ ├── (main)
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ └── not-found.tsx
│ │ │ └── editor
│ │ │ │ ├── [id]
│ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ ├── result
│ │ │ └── page.tsx
│ │ ├── robots.ts
│ │ ├── search
│ │ │ └── page.tsx
│ │ ├── sitemap
│ │ │ └── [type]
│ │ │ │ └── page.tsx
│ │ ├── tag
│ │ │ └── article
│ │ │ │ └── [id]
│ │ │ │ └── page.tsx
│ │ ├── theme
│ │ │ └── page.tsx
│ │ └── user
│ │ │ ├── [id]
│ │ │ └── page.tsx
│ │ │ └── settings
│ │ │ ├── account
│ │ │ └── page.tsx
│ │ │ ├── destroy
│ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── profile
│ │ │ └── page.tsx
│ ├── common
│ │ ├── hooks
│ │ │ ├── useComputed.ts
│ │ │ ├── useFetch.tsx
│ │ │ ├── useGetRawPath.tsx
│ │ │ └── useWatchEffect.tsx
│ │ ├── modules
│ │ │ ├── cookie
│ │ │ │ └── index.ts
│ │ │ ├── readingRecords
│ │ │ │ ├── index.ts
│ │ │ │ ├── redis.ts
│ │ │ │ ├── setReferer.ts
│ │ │ │ └── setSpider.ts
│ │ │ └── sitemap
│ │ │ │ ├── sitemap-index.ts
│ │ │ │ └── sitemap.ts
│ │ └── utils
│ │ │ ├── HtmlToMarkDown.ts
│ │ │ └── vw.ts
│ ├── components
│ │ ├── admin
│ │ │ ├── common
│ │ │ │ ├── Footer
│ │ │ │ │ └── index.tsx
│ │ │ │ └── Header
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── items.tsx
│ │ │ └── page
│ │ │ │ ├── advertisement
│ │ │ │ └── AdvertisementForm.tsx
│ │ │ │ ├── article
│ │ │ │ └── list
│ │ │ │ │ ├── Header.tsx
│ │ │ │ │ └── Table.tsx
│ │ │ │ ├── index
│ │ │ │ └── notice.tsx
│ │ │ │ ├── statistics
│ │ │ │ ├── Bottom
│ │ │ │ │ ├── Occupation.tsx
│ │ │ │ │ ├── Referer.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Container
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Statistics.tsx
│ │ │ │ └── Top
│ │ │ │ │ ├── Article.tsx
│ │ │ │ │ ├── ArticleRanking.tsx
│ │ │ │ │ ├── Header
│ │ │ │ │ ├── HeaderItem.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── Visits.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── theme
│ │ │ │ └── CodeEdite.tsx
│ │ │ │ └── type
│ │ │ │ ├── AddTagModal.tsx
│ │ │ │ ├── AddTypeModal.tsx
│ │ │ │ ├── TagForm.tsx
│ │ │ │ └── TypeForm.tsx
│ │ ├── common
│ │ │ ├── AdSense
│ │ │ │ ├── ADS.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Advertisement
│ │ │ │ └── index.tsx
│ │ │ ├── ArticleEditor
│ │ │ │ ├── DraftsButton.tsx
│ │ │ │ ├── Modal
│ │ │ │ │ ├── Cover.tsx
│ │ │ │ │ ├── Reprint.tsx
│ │ │ │ │ ├── Type.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── ArticleList
│ │ │ │ ├── ArticleItem.tsx
│ │ │ │ ├── Cover.tsx
│ │ │ │ ├── index.module.scss
│ │ │ │ └── index.tsx
│ │ │ ├── Avatar
│ │ │ │ ├── Menu.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── BackTop
│ │ │ │ └── index.tsx
│ │ │ ├── CollectionModal
│ │ │ │ ├── CreateFrom.tsx
│ │ │ │ ├── List.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── CreateTheme
│ │ │ │ └── index.tsx
│ │ │ ├── Editor
│ │ │ │ ├── LanguageListPlugin.tsx
│ │ │ │ ├── Skeleton.tsx
│ │ │ │ ├── StyleLink.tsx
│ │ │ │ ├── VditorEditor.tsx
│ │ │ │ ├── index.scss
│ │ │ │ ├── index.tsx
│ │ │ │ └── upload.ts
│ │ │ ├── Header
│ │ │ │ ├── Navigation.tsx
│ │ │ │ ├── News.tsx
│ │ │ │ ├── Search.tsx
│ │ │ │ ├── Sign
│ │ │ │ │ ├── ForgetPassword
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── LogIn
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── LogOn
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── User
│ │ │ │ │ ├── NotLogin
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── UserData
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── UpLoad
│ │ │ │ ├── Image.tsx
│ │ │ │ ├── Modal.tsx
│ │ │ │ ├── getCroppedImg.ts
│ │ │ │ ├── index.tsx
│ │ │ │ └── upload.ts
│ │ │ └── _Editor
│ │ │ │ ├── Editor.tsx
│ │ │ │ ├── MarkDownEditor
│ │ │ │ ├── LanguageListPlugin.tsx
│ │ │ │ ├── ThemeSelect.tsx
│ │ │ │ ├── UseRichTextPlugin.tsx
│ │ │ │ └── index.tsx
│ │ │ │ ├── RichTextEditor
│ │ │ │ ├── CodeEditor.tsx
│ │ │ │ └── index.tsx
│ │ │ │ ├── StyleLink.tsx
│ │ │ │ ├── index.scss
│ │ │ │ ├── index.tsx
│ │ │ │ └── upload.ts
│ │ ├── next
│ │ │ ├── ActiveLink.tsx
│ │ │ ├── Head.tsx
│ │ │ ├── Image.tsx
│ │ │ └── NoFollowLink.tsx
│ │ └── page
│ │ │ ├── article
│ │ │ ├── Aside
│ │ │ │ ├── Catalogue
│ │ │ │ │ ├── Catalogue.tsx
│ │ │ │ │ ├── index.module.scss
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Repository
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Comments
│ │ │ │ ├── Comment
│ │ │ │ │ ├── Item.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Editor.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── ImagePreview
│ │ │ │ ├── index.module.scss
│ │ │ │ └── index.tsx
│ │ │ ├── Layout.tsx
│ │ │ ├── NoFound.tsx
│ │ │ ├── Recommend.tsx
│ │ │ ├── Reprint.tsx
│ │ │ ├── Store.tsx
│ │ │ ├── ToolBar
│ │ │ │ ├── Collection
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Comment.tsx
│ │ │ │ ├── Like.tsx
│ │ │ │ ├── Share.tsx
│ │ │ │ ├── class.ts
│ │ │ │ └── index.tsx
│ │ │ ├── UserData
│ │ │ │ ├── FollowButton.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── View
│ │ │ │ └── index.tsx
│ │ │ └── editor
│ │ │ │ └── ArticleRemoveWarn.tsx
│ │ │ ├── collection
│ │ │ ├── Brow.tsx
│ │ │ ├── Collection.tsx
│ │ │ ├── FavoritesModal.tsx
│ │ │ └── ToolsBar.tsx
│ │ │ ├── creator
│ │ │ ├── ArticleList
│ │ │ │ ├── ArticleListItem.tsx
│ │ │ │ └── index.tsx
│ │ │ └── Layout
│ │ │ │ ├── Aside.tsx
│ │ │ │ ├── Header.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── friendly-link
│ │ │ └── FriendlyLink.tsx
│ │ │ ├── index
│ │ │ ├── Footer.tsx
│ │ │ ├── Layout.tsx
│ │ │ ├── Ranking
│ │ │ │ ├── AuthorRanking.tsx
│ │ │ │ ├── FunsRanking.tsx
│ │ │ │ ├── RankingList.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── SortSelect
│ │ │ │ └── index.tsx
│ │ │ ├── TypeHeader
│ │ │ │ └── index.tsx
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ │ ├── notification
│ │ │ ├── Layout.tsx
│ │ │ └── Notice
│ │ │ │ ├── Answer.tsx
│ │ │ │ ├── Comment.tsx
│ │ │ │ ├── Follow.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── problem
│ │ │ ├── Answer
│ │ │ │ └── index.tsx
│ │ │ ├── Aside.tsx
│ │ │ ├── Comments
│ │ │ │ ├── Editor.tsx
│ │ │ │ ├── Item.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Editor
│ │ │ │ ├── Message.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Layout.tsx
│ │ │ ├── List
│ │ │ │ ├── Item.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── NoFound.tsx
│ │ │ ├── ProblemDetail.tsx
│ │ │ ├── ProblemList.tsx
│ │ │ ├── ToolBar
│ │ │ │ └── index.tsx
│ │ │ └── write
│ │ │ │ └── Tag.tsx
│ │ │ ├── tag
│ │ │ ├── Layout.tsx
│ │ │ └── List.tsx
│ │ │ └── user
│ │ │ ├── index
│ │ │ ├── NotFind.tsx
│ │ │ └── UserData
│ │ │ │ ├── Aside
│ │ │ │ └── index.tsx
│ │ │ │ ├── Header
│ │ │ │ ├── FollowButton.tsx
│ │ │ │ └── index.tsx
│ │ │ │ ├── Main
│ │ │ │ ├── Favorites
│ │ │ │ │ ├── Modal.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── FollowList
│ │ │ │ │ ├── FollowButton.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ └── setting
│ │ │ ├── UpdateEmailModal.tsx
│ │ │ ├── UpdatePasswordModal.tsx
│ │ │ └── UploadAvatar
│ │ │ └── index.tsx
│ ├── layout
│ │ ├── Admin
│ │ │ └── Base.tsx
│ │ ├── Base
│ │ │ └── index.tsx
│ │ ├── Content
│ │ │ ├── HightLight
│ │ │ │ ├── index.scss
│ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ ├── Main
│ │ │ └── index.tsx
│ │ └── Sidebar
│ │ │ └── index.tsx
│ ├── plugin
│ │ ├── antd.tsx
│ │ ├── axios.ts
│ │ └── dayjs.ts
│ ├── request
│ │ ├── advertisement.ts
│ │ ├── article
│ │ │ └── article-list.ts
│ │ ├── collection
│ │ │ ├── collection.ts
│ │ │ ├── index.ts
│ │ │ └── uncollection.ts
│ │ ├── follow
│ │ │ ├── follow.ts
│ │ │ ├── index.ts
│ │ │ └── unfollow.ts
│ │ ├── like
│ │ │ ├── index.ts
│ │ │ ├── like.ts
│ │ │ └── unlike.ts
│ │ ├── load-static.ts
│ │ └── type
│ │ │ ├── getTag.ts
│ │ │ ├── getTagArticleLData.ts
│ │ │ └── type-tree-index.ts
│ ├── store
│ │ ├── admin
│ │ │ ├── admin-article-list
│ │ │ │ └── index.ts
│ │ │ ├── admin-search-option
│ │ │ │ └── index.ts
│ │ │ ├── admin-statistics-data
│ │ │ │ ├── index.tsx
│ │ │ │ └── store.ts
│ │ │ └── admin-table-option
│ │ │ │ └── index.ts
│ │ ├── common
│ │ │ └── editor-mode
│ │ │ │ └── index.ts
│ │ └── user
│ │ │ ├── user-article-comment
│ │ │ └── index.ts
│ │ │ ├── user-current-article-data
│ │ │ └── index.ts
│ │ │ ├── user-data
│ │ │ ├── index.tsx
│ │ │ └── store.ts
│ │ │ ├── user-sign-model-state
│ │ │ └── index.ts
│ │ │ └── user-write-article
│ │ │ └── index.ts
│ └── styles
│ │ ├── content.module.scss
│ │ ├── genAntdCss.tsx
│ │ ├── globals.scss
│ │ └── reset.css
├── tailwind.config.js
├── tsconfig.json
├── types
│ ├── common
│ │ ├── response.ts
│ │ └── user-data.ts
│ ├── folder.ts
│ ├── index.d.ts
│ ├── model-attribute.ts
│ ├── model
│ │ ├── advertisement.ts
│ │ ├── article-comment.ts
│ │ ├── article-list-item.ts
│ │ ├── favorites-collection-list.ts
│ │ └── problem.ts
│ ├── response.ts
│ ├── statistics-type.ts
│ └── type.ts
└── yarn.lock
├── dev.bat
├── intall.bat
├── lint.bat
└── server
├── ecosystem.config.js
├── env
└── .env.template
├── package.json
├── prettier.config.js
├── public
├── prism.zip
└── robots.txt
├── scripts
├── build.ts
├── dev
│ └── index.ts
├── modules
│ ├── compile.ts
│ ├── copyDir.ts
│ ├── countFile.ts
│ └── deleteDir.ts
└── start.ts
├── src
├── app.ts
├── common
│ ├── middleware
│ │ ├── auth
│ │ │ ├── getUserId.ts
│ │ │ └── index.ts
│ │ ├── cache
│ │ │ └── index.ts
│ │ └── verify
│ │ │ ├── validator.ts
│ │ │ └── validatorAsync.ts
│ ├── modules
│ │ ├── article
│ │ │ ├── get
│ │ │ │ ├── external-link.ts
│ │ │ │ ├── html-to-markdown.ts
│ │ │ │ ├── img-add-prefix.ts
│ │ │ │ ├── set-code-block-language.ts
│ │ │ │ ├── set-description.ts
│ │ │ │ ├── set-tag-data.ts
│ │ │ │ └── set-title-id.ts
│ │ │ └── select
│ │ │ │ ├── getBloggerList.ts
│ │ │ │ ├── getTypeChildrenTag.ts
│ │ │ │ ├── option.ts
│ │ │ │ └── search.ts
│ │ ├── cache
│ │ │ ├── advertisement
│ │ │ │ └── index.ts
│ │ │ ├── email
│ │ │ │ └── index.ts
│ │ │ ├── external-link
│ │ │ │ └── index.ts
│ │ │ └── type
│ │ │ │ └── index.ts
│ │ ├── comment
│ │ │ └── get-comment-childrnen-list.ts
│ │ ├── getAllRouter.ts
│ │ ├── getFilePath.ts
│ │ ├── github
│ │ │ ├── getGithubName.ts
│ │ │ └── updata.ts
│ │ ├── notice
│ │ │ ├── answer.ts
│ │ │ ├── comment
│ │ │ │ ├── comment-answer.ts
│ │ │ │ ├── comment-article.ts
│ │ │ │ └── comment-problem.ts
│ │ │ ├── follow
│ │ │ │ ├── follow-article.ts
│ │ │ │ └── follow-problem.ts
│ │ │ └── index.ts
│ │ ├── tasks
│ │ │ ├── set-recommend-data.ts
│ │ │ └── sortArticleList.ts
│ │ └── user
│ │ │ ├── destroy.ts
│ │ │ └── isEmailDestroy.ts
│ ├── tasks
│ │ ├── advertisement.ts
│ │ ├── auto-delete-recommend.ts
│ │ ├── friendly-link-response-time.ts
│ │ ├── index.ts
│ │ ├── oss
│ │ │ └── delete-redis-cache.ts
│ │ ├── ranking
│ │ │ └── index.ts
│ │ ├── recommend
│ │ │ ├── cache-recommend-data.ts
│ │ │ └── index.ts
│ │ ├── set-article-view-count.ts
│ │ └── visualization
│ │ │ ├── github.ts
│ │ │ ├── history.ts
│ │ │ └── load.ts
│ ├── transaction
│ │ ├── answer
│ │ │ ├── create.ts
│ │ │ └── delete.ts
│ │ ├── article
│ │ │ ├── create-article.ts
│ │ │ └── delete-article.ts
│ │ ├── comment
│ │ │ ├── create-comment.ts
│ │ │ └── delete-comment.ts
│ │ ├── follow
│ │ │ └── unfollow
│ │ │ │ ├── index.ts
│ │ │ │ ├── problem.ts
│ │ │ │ └── user.ts
│ │ └── problem
│ │ │ ├── adopt.ts
│ │ │ ├── cancel.ts
│ │ │ └── delete.ts
│ ├── utils
│ │ ├── auth
│ │ │ ├── destroy-session.ts
│ │ │ ├── sign-jwt.ts
│ │ │ ├── sign-session.ts
│ │ │ ├── sign.ts
│ │ │ ├── verify-jwt.ts
│ │ │ ├── verify-session.ts
│ │ │ └── verify.ts
│ │ ├── email
│ │ │ └── index.ts
│ │ ├── id.ts
│ │ ├── map.ts
│ │ ├── random.ts
│ │ ├── redis.ts
│ │ ├── sha256.ts
│ │ ├── sleep.ts
│ │ ├── static
│ │ │ ├── ali
│ │ │ │ ├── deleteFile.ts
│ │ │ │ ├── exist.ts
│ │ │ │ ├── imageInfo.ts
│ │ │ │ ├── listPrefix.ts
│ │ │ │ ├── refreshUrls.ts
│ │ │ │ ├── upload.ts
│ │ │ │ └── utils
│ │ │ │ │ └── oss.ts
│ │ │ ├── folderList.ts
│ │ │ ├── index.ts
│ │ │ └── qiniu
│ │ │ │ ├── deleteFile.ts
│ │ │ │ ├── exist.ts
│ │ │ │ ├── imageInfo.ts
│ │ │ │ ├── listPrefix.ts
│ │ │ │ ├── refreshUrls.ts
│ │ │ │ ├── upload.ts
│ │ │ │ └── utils
│ │ │ │ ├── Mac.ts
│ │ │ │ ├── bucketManager.ts
│ │ │ │ └── zone.ts
│ │ └── xss
│ │ │ ├── article.ts
│ │ │ └── comment.ts
│ └── verify
│ │ ├── api-verify
│ │ ├── advertisement
│ │ │ └── create-update.ts
│ │ ├── answer
│ │ │ ├── create.ts
│ │ │ ├── delete.ts
│ │ │ ├── problem-md.ts
│ │ │ └── update.ts
│ │ ├── article
│ │ │ ├── common.module.ts
│ │ │ ├── create-article.ts
│ │ │ ├── list.ts
│ │ │ ├── search.ts
│ │ │ └── update-article.ts
│ │ ├── collection
│ │ │ ├── create.ts
│ │ │ └── update.ts
│ │ ├── comment
│ │ │ └── create.ts
│ │ ├── external-link
│ │ │ └── create.ts
│ │ ├── favorites
│ │ │ └── create.ts
│ │ ├── follow
│ │ │ └── create
│ │ │ │ ├── index.ts
│ │ │ │ └── map.ts
│ │ ├── friendly-link
│ │ │ └── create.ts
│ │ ├── like
│ │ │ └── create.ts
│ │ ├── problem
│ │ │ ├── adopt.ts
│ │ │ ├── cancel.ts
│ │ │ ├── create.ts
│ │ │ ├── list.ts
│ │ │ ├── questions.ts
│ │ │ └── update.ts
│ │ ├── theme
│ │ │ ├── create.ts
│ │ │ ├── remove.ts
│ │ │ └── update.ts
│ │ └── user
│ │ │ └── update.ts
│ │ ├── integer.ts
│ │ └── modules
│ │ ├── file-name.ts
│ │ ├── tag.ts
│ │ ├── type.ts
│ │ └── url.ts
├── db
│ ├── config
│ │ └── index.ts
│ ├── hooks
│ │ ├── advertisement.ts
│ │ ├── external-link.ts
│ │ ├── type.ts
│ │ └── utils
│ │ │ └── init.ts
│ ├── index.ts
│ ├── models
│ │ ├── advertisement.ts
│ │ ├── answer.ts
│ │ ├── article.ts
│ │ ├── article_tag.ts
│ │ ├── collection.ts
│ │ ├── comment.ts
│ │ ├── external_link.ts
│ │ ├── favorites.ts
│ │ ├── follow.ts
│ │ ├── friendly_link.ts
│ │ ├── init-models.ts
│ │ ├── likes.ts
│ │ ├── notice.ts
│ │ ├── problem.ts
│ │ ├── recommend.ts
│ │ ├── tag.ts
│ │ ├── theme.ts
│ │ └── user.ts
│ └── utils
│ │ └── stringArrayReplace.ts
├── index.ts
├── routes
│ ├── advertisement
│ │ ├── create.ts
│ │ ├── delete.ts
│ │ ├── get-all.ts
│ │ ├── id.ts
│ │ └── update.ts
│ ├── answer
│ │ ├── create.ts
│ │ ├── delete.ts
│ │ ├── problem-md.ts
│ │ └── update.ts
│ ├── article
│ │ ├── create.ts
│ │ ├── delete-article.ts
│ │ ├── get-data
│ │ │ ├── article-list-search.ts
│ │ │ ├── article-list-tag.ts
│ │ │ ├── article-list.ts
│ │ │ ├── get-id.ts
│ │ │ └── get-list-page.ts
│ │ ├── recommend.ts
│ │ └── update.ts
│ ├── auth
│ │ └── github.ts
│ ├── collection
│ │ ├── collection-state.ts
│ │ ├── collection-user-list.ts
│ │ ├── create.ts
│ │ ├── delete-favorites.ts
│ │ ├── delete.ts
│ │ ├── favorites-list.ts
│ │ └── update.ts
│ ├── comment
│ │ ├── create.ts
│ │ ├── delete.ts
│ │ ├── list-article.ts
│ │ ├── list.ts
│ │ └── review.ts
│ ├── external-link
│ │ ├── create.ts
│ │ ├── delete.ts
│ │ ├── find.ts
│ │ └── list.ts
│ ├── favorites
│ │ ├── create.ts
│ │ ├── delete.ts
│ │ ├── id.ts
│ │ ├── list.ts
│ │ └── update.ts
│ ├── follow
│ │ ├── follow.ts
│ │ ├── followers-list.ts
│ │ ├── following-list.ts
│ │ ├── unfollow.ts
│ │ └── user-follow-state.ts
│ ├── friendly-link
│ │ ├── adopt.ts
│ │ ├── apply.ts
│ │ ├── create.ts
│ │ ├── delete.ts
│ │ └── list.ts
│ ├── high-light
│ │ ├── high-light.ts
│ │ └── language-list.ts
│ ├── like
│ │ ├── create.ts
│ │ ├── delete.ts
│ │ └── state.ts
│ ├── notice
│ │ ├── list-notice.ts
│ │ ├── notice-count.ts
│ │ └── update-notice.ts
│ ├── problem
│ │ ├── adopt.ts
│ │ ├── cancel.ts
│ │ ├── create.ts
│ │ ├── delete.ts
│ │ ├── list-user.ts
│ │ ├── list.ts
│ │ ├── questions.ts
│ │ ├── read-update.ts
│ │ └── update.ts
│ ├── ranking
│ │ ├── author.ts
│ │ └── funs.ts
│ ├── sitemap
│ │ ├── index.ts
│ │ ├── list-xml.ts
│ │ └── list.ts
│ ├── static
│ │ └── upload-image.ts
│ ├── statistics
│ │ ├── index.ts
│ │ └── visualization.ts
│ ├── tag
│ │ ├── create-tag.ts
│ │ ├── delete-tag.ts
│ │ ├── get-data-id.ts
│ │ ├── list.ts
│ │ ├── reception-tag-tree.ts
│ │ └── update.ts
│ ├── theme
│ │ ├── create.ts
│ │ ├── item.ts
│ │ ├── list.ts
│ │ ├── remove.ts
│ │ └── update.ts
│ └── user
│ │ ├── destroy
│ │ ├── destroy-id.ts
│ │ ├── destroy.ts
│ │ └── recovery.ts
│ │ ├── forget-password
│ │ ├── email-update.ts
│ │ └── email.ts
│ │ ├── get-data
│ │ ├── achievement.ts
│ │ ├── user-data.ts
│ │ ├── user-info.ts
│ │ └── user-list.ts
│ │ ├── login
│ │ └── email-password.ts
│ │ ├── logon
│ │ ├── email-link.ts
│ │ └── logn-email-link.ts
│ │ ├── tools
│ │ └── location.ts
│ │ └── update
│ │ ├── email
│ │ ├── email-link.ts
│ │ └── email.ts
│ │ ├── password.ts
│ │ └── update.ts
├── socket
│ ├── index.ts
│ └── oss.ts
└── views
│ ├── forget-password.ejs
│ ├── friendly-apply.ejs
│ ├── friendly-fail.ejs
│ ├── logn-up.ejs
│ └── update-email.ejs
├── tsconfig.json
├── types
├── NodeJS
│ └── index.d.ts
├── identicon.js
│ └── index.d.ts
├── koa
│ └── index.d.ts
└── turndown-plugin-gfm
│ └── index.d.ts
└── yarn.lock
/.github/ISSUE_TEMPLATE/✨-新功能建议.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "✨ 新功能建议"
3 | about: 建议出一个XX新功能
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/系统问题解答.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 系统问题解答
3 | about: 对于系统内的功能实现,你有什么不懂的可以再这里提问
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | 1.管理系统、用户端、服务器哪里的哪个问题不懂
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/🐞-bug反馈.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F41E BUG反馈"
3 | about: 反馈系统漏洞
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ** 环境 **
11 | 1. 浏览器以及版本:
12 | 2. Node.js版本:
13 | 3. MySQL版本:
14 | 4. Redis版本:
15 |
16 | ** 报错情况 **
17 |
18 |
19 | ** 我该如何复现 **
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .next
4 | .idea
5 | .vscode
6 | .DS_Store
7 | *.local
8 |
9 | server/**/.env.development
10 | server/**/.env.production
11 | server/log
12 | server/blog_server
13 |
14 | client/env/.env.production
--------------------------------------------------------------------------------
/client/env/.env:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_SITE_NAME=网络日志
2 | # 站长信息配置
3 | QQ=1974109227
4 | EMAIL=1974109227@qq.com
5 | GITHUB=https://github.com/Lrunlin
6 | ICP=辽ICP备2020014377号
7 | # Google ADS
8 | NEXT_PUBLIC_GOOGLE_ADS_CLIENT_ID=1577114276814290
9 | NEXT_PUBLIC_GOOGLE_ADS_SLOT=3214779798
10 |
11 | # 上传图片最大多MB
12 | UPLOAD_MAX_SIZE=5
13 |
14 | # 鉴权方式
15 | AUTH_MODE=session
--------------------------------------------------------------------------------
/client/env/.env.development:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_HOST=http://localhost:5678
2 | NEXT_PUBLIC_API_HOST=http://localhost:3000
3 | NEXT_PUBLIC_GITHUB_CLIENT_ID=b1099c9c62ebb6ff2d87
4 |
5 | CDN=http://localhost:5678
6 |
7 | # Redis配置
8 | REDIS_HOST=localhost
9 | REDIS_USER=
10 | REDIS_POST=6379
11 | REDIS_PASSWORD=""
12 |
13 |
--------------------------------------------------------------------------------
/client/env/index.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const dotenv = require("dotenv");
4 | const md5 = require("md5");
5 | const { globSync } = require("glob");
6 |
7 | let envObject = dotenv.parse(
8 | `${fs.readFileSync(path.join(__dirname, "./.env"))}
9 | \n
10 | ${fs.readFileSync(
11 | path.join(
12 | __dirname,
13 | `./.env.${process.env.NEXT_PUBLIC_ISPRO ? "production" : "development"}`,
14 | ),
15 | )}
16 | `,
17 | );
18 |
19 | module.exports = {
20 | buildid: () => {
21 | return md5(
22 | globSync(["src/**/*.*", "./**.js", "./**.json", "./**.ts", "./**.lock"])
23 | .filter((item) => {
24 | const filePath = path.resolve(process.cwd(), item);
25 | return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
26 | })
27 | .map((item) => {
28 | let str = fs.readFileSync(item).toString();
29 | return md5(item + str);
30 | })
31 | .join(""),
32 | );
33 | },
34 | env: envObject,
35 | };
36 |
--------------------------------------------------------------------------------
/client/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | // autoprefixer: {},//自动前缀
5 | ...(process.env.NEXT_PUBLIC_ISPRO
6 | ? {
7 | cssnano: {},
8 | }
9 | : {}),
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/icon/admin/QQ.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/icon/admin/fork.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/icon/admin/数据看板.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/icon/client/briefcase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/briefcase.png
--------------------------------------------------------------------------------
/client/public/icon/client/collection-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/collection-blue.png
--------------------------------------------------------------------------------
/client/public/icon/client/collection-fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/collection-fill.png
--------------------------------------------------------------------------------
/client/public/icon/client/collection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/collection.png
--------------------------------------------------------------------------------
/client/public/icon/client/comment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/comment.png
--------------------------------------------------------------------------------
/client/public/icon/client/comments.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/comments.png
--------------------------------------------------------------------------------
/client/public/icon/client/delete-fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/delete-fill.png
--------------------------------------------------------------------------------
/client/public/icon/client/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/delete.png
--------------------------------------------------------------------------------
/client/public/icon/client/email-fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/email-fill.png
--------------------------------------------------------------------------------
/client/public/icon/client/email.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/email.png
--------------------------------------------------------------------------------
/client/public/icon/client/emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/emoji.png
--------------------------------------------------------------------------------
/client/public/icon/client/github-fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/github-fill.png
--------------------------------------------------------------------------------
/client/public/icon/client/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/github.png
--------------------------------------------------------------------------------
/client/public/icon/client/likes-fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/likes-fill.png
--------------------------------------------------------------------------------
/client/public/icon/client/likes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/likes.png
--------------------------------------------------------------------------------
/client/public/icon/client/picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/picture.png
--------------------------------------------------------------------------------
/client/public/icon/client/police.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/police.png
--------------------------------------------------------------------------------
/client/public/icon/client/postcard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/postcard.png
--------------------------------------------------------------------------------
/client/public/icon/client/problem-collection-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/problem-collection-black.png
--------------------------------------------------------------------------------
/client/public/icon/client/problem-collection-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/problem-collection-white.png
--------------------------------------------------------------------------------
/client/public/icon/client/problem-follow-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/problem-follow-black.png
--------------------------------------------------------------------------------
/client/public/icon/client/problem-follow-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/problem-follow-white.png
--------------------------------------------------------------------------------
/client/public/icon/client/problem-like-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/problem-like-black.png
--------------------------------------------------------------------------------
/client/public/icon/client/problem-like-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/problem-like-white.png
--------------------------------------------------------------------------------
/client/public/icon/client/qq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/qq.png
--------------------------------------------------------------------------------
/client/public/icon/client/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/share.png
--------------------------------------------------------------------------------
/client/public/icon/client/small-bell.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/small-bell.png
--------------------------------------------------------------------------------
/client/public/icon/client/unit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/unit.png
--------------------------------------------------------------------------------
/client/public/icon/client/view-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/view-blue.png
--------------------------------------------------------------------------------
/client/public/icon/client/view-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/view-white.png
--------------------------------------------------------------------------------
/client/public/icon/client/view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/view.png
--------------------------------------------------------------------------------
/client/public/icon/client/website.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/website.png
--------------------------------------------------------------------------------
/client/public/icon/client/wechat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/icon/client/wechat.png
--------------------------------------------------------------------------------
/client/public/image/admin/statistics/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/image/admin/statistics/bg.jpg
--------------------------------------------------------------------------------
/client/public/image/admin/statistics/border_bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/image/admin/statistics/border_bg.jpg
--------------------------------------------------------------------------------
/client/public/image/admin/statistics/title_left_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/image/admin/statistics/title_left_bg.png
--------------------------------------------------------------------------------
/client/public/image/admin/statistics/title_right_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/image/admin/statistics/title_right_bg.png
--------------------------------------------------------------------------------
/client/public/image/client/collection-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/image/client/collection-bg.jpg
--------------------------------------------------------------------------------
/client/public/image/client/github.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/image/client/github.jpg
--------------------------------------------------------------------------------
/client/public/image/client/load-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/image/client/load-error.png
--------------------------------------------------------------------------------
/client/public/image/client/qq-qrcode.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/image/client/qq-qrcode.jpg
--------------------------------------------------------------------------------
/client/public/image/client/wechat-qrcode.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/client/public/image/client/wechat-qrcode.jpg
--------------------------------------------------------------------------------
/client/public/robots-template.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow:*
3 | # 文章修改和发布
4 | Disallow:/article/editor
5 | # 开发者中心
6 | Disallow:/creator
7 | # 文章搜索
8 | Disallow:/search
9 | # 用户信息
10 | Disallow:/user
11 | # 消息提示
12 | Disallow:/notification
13 | # oauth登录重定向
14 | Disallow:/oauth
15 | # 部分链接的结果展示
16 | Disallow:/result
17 | # 友链页面
18 | Disallow:/friendly-link
19 | #中转页面
20 | Disallow:/link
21 | # 提问
22 | Disallow:/problem/editor
23 | # 收藏页面
24 | Disallow:/collection
25 | # 管理员
26 | Disallow:/admin
27 | # 找回密码
28 | Disallow:/forget-password
29 |
30 | Sitemap:https://blogweb.cn/sitemap/index.xml
--------------------------------------------------------------------------------
/client/scripts/build-output/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": [],
3 | "ext": "js",
4 | "ignore": ["*"],
5 | "restartable": "rs",
6 | "exec": "node ./server.js",
7 | "env": {
8 | "PORT": "5678",
9 | "NEXT_PUBLIC_ISPRO": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/app/(route)/ads.txt/route.ts:
--------------------------------------------------------------------------------
1 | export async function GET() {
2 | if (process.env.NEXT_PUBLIC_GOOGLE_ADS_CLIENT_ID) {
3 | return new Response(
4 | `google.com, pub-${process.env.NEXT_PUBLIC_GOOGLE_ADS_CLIENT_ID}, DIRECT, f08c47fec0942fa0`,
5 | {
6 | status: 200,
7 | headers: {
8 | "content-type": "text/txt; charset=utf-8",
9 | },
10 | },
11 | );
12 | } else {
13 | return new Response(undefined, {
14 | status: 404,
15 | });
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/app/(route)/sitemap/[type]/index.xml/route.tsx:
--------------------------------------------------------------------------------
1 | import { NextRequest } from "next/server";
2 | import axios from "@axios";
3 | import setSiteMap from "@/common/modules/sitemap/sitemap-index";
4 |
5 | export async function GET(
6 | res: NextRequest,
7 | props: { params: Promise<{ type: string }> },
8 | ) {
9 | const params = await props.params;
10 | let xml = await axios
11 | .get("/sitemap/" + params.type)
12 | .then((res) => setSiteMap(res.data.data));
13 | return new Response(xml, {
14 | status: 200,
15 | headers: {
16 | "content-type": "text/xml; charset=utf-8",
17 | },
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/app/(route)/sitemap/[type]/index[index].xml/route.tsx:
--------------------------------------------------------------------------------
1 | import { NextRequest } from "next/server";
2 | import axios from "@axios";
3 | import setSiteMap from "@/common/modules/sitemap/sitemap";
4 |
5 | export async function GET(
6 | res: NextRequest,
7 | props: { params: Promise<{ type: string }> },
8 | ) {
9 | const params = await props.params;
10 | let match = res.nextUrl.pathname.match(/index(\d+)\.xml/);
11 |
12 | if (!match || !["article", "problem"].includes(params.type)) {
13 | return new Response(undefined, {
14 | status: 400,
15 | });
16 | }
17 |
18 | let sitemap = await axios
19 | .get(`/sitemap/${params.type}/${match[1]}`)
20 | .then((res) => setSiteMap(res.data.data))
21 | .catch((err) => {
22 | console.log(err);
23 | return ``;
24 | });
25 |
26 | return new Response(sitemap, {
27 | status: 200,
28 | headers: {
29 | "content-type": "text/xml; charset=utf-8",
30 | },
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/app/(route)/static/antd/[name]/route.tsx:
--------------------------------------------------------------------------------
1 | import { NextRequest } from "next/server";
2 | import fs from "fs";
3 |
4 | type Params = {
5 | name: string;
6 | };
7 |
8 | export async function GET(
9 | res: NextRequest,
10 | context: { params: Promise },
11 | ) {
12 | let name = (await context.params)!.name as string | undefined;
13 | if (typeof name == "string" && name?.endsWith(".css")) {
14 | try {
15 | let content = fs.readFileSync(`.next/css/${name}`).toString();
16 |
17 | return new Response(content, {
18 | status: 200,
19 | headers: {
20 | "content-type": "text/css; charset=utf-8",
21 | },
22 | });
23 | } catch (error) {
24 | console.log(error);
25 | return new Response(undefined, {
26 | status: 404,
27 | });
28 | }
29 | } else {
30 | return new Response(undefined, {
31 | status: 404,
32 | });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/app/(route)/static/high-light/[type]/route.tsx:
--------------------------------------------------------------------------------
1 | import type { NextRequest } from "next/server";
2 | import axios from "@axios";
3 |
4 | export async function GET(
5 | res: NextRequest,
6 | props: { params: Promise<{ type: string }> },
7 | ) {
8 | const params = await props.params;
9 |
10 | const { type } = params;
11 |
12 | const searchParams = res.nextUrl.searchParams;
13 | const languages = searchParams.get("languages");
14 | if (["js", "css"].includes(String(type))) {
15 | let content = await axios
16 | .get(`/high-light/${type}`, {
17 | params: { languages: languages },
18 | })
19 | .then((res) => res.data);
20 | return new Response(content, {
21 | status: 200,
22 | headers: {
23 | "content-type": `text/${type}; charset=utf-8`,
24 | "Cache-Control": `public, max-age=9999999999, must-revalidate`,
25 | },
26 | });
27 | } else {
28 | return new Response(undefined, {
29 | status: 404,
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/app/(route)/static/theme/[id]/route.tsx:
--------------------------------------------------------------------------------
1 | import type { NextRequest } from "next/server";
2 | import axios from "@axios";
3 |
4 | export async function GET(
5 | res: NextRequest,
6 | props: { params: Promise<{ id: string }> },
7 | ) {
8 | const params = await props.params;
9 |
10 | const { id } = params;
11 |
12 | if (!(id as string).endsWith(".css")) {
13 | return new Response(undefined, {
14 | status: 404,
15 | });
16 | }
17 |
18 | let content = await axios
19 | .get(`/theme/${(id as string).replace(/.css/, "")}`)
20 | .then((res) => res.data?.data?.content)
21 | .catch(() => false);
22 |
23 | if (!content) {
24 | return new Response(undefined, {
25 | status: 404,
26 | });
27 | }
28 |
29 | return new Response(content, {
30 | status: 200,
31 | headers: {
32 | "content-type": `text/css; charset=utf-8`,
33 | "Cache-Control": `public, max-age=9999999999, must-revalidate`,
34 | },
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/app/admin/[...all]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { Button, Result } from "antd";
5 | import AdminLayout from "@/layout/Admin/Base";
6 | import Head from "@/components/next/Head";
7 |
8 | const NoFound = () => {
9 | let router = useRouter();
10 | return (
11 |
12 |
13 | router.replace("/admin")}>
19 | 首页
20 |
21 | }
22 | />
23 |
24 | );
25 | };
26 | export default NoFound;
27 |
--------------------------------------------------------------------------------
/client/src/app/admin/advertisement/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { message } from "antd";
5 | import axios from "@axios";
6 | import AdminLayout from "@/layout/Admin/Base";
7 | import AdvertisementForm from "@/components/admin/page/advertisement/AdvertisementForm";
8 |
9 | const APP = () => {
10 | const [key, setKey] = useState(`key-AdvertisementForm-1`);
11 |
12 | function onFinish(values: any) {
13 | axios.post("/advertisement", values).then((res) => {
14 | if (res.data.success) {
15 | message.success(res.data.message);
16 | setKey(`key-AdvertisementForm-${Math.random()}`);
17 | } else {
18 | message.error(res.data.message);
19 | }
20 | });
21 | }
22 | return (
23 |
24 |
27 |
28 | );
29 | };
30 | export default APP;
31 |
--------------------------------------------------------------------------------
/client/src/app/admin/layout.tsx:
--------------------------------------------------------------------------------
1 | export default async function RootLayout({
2 | children,
3 | }: Readonly<{
4 | children: React.ReactNode;
5 | }>) {
6 | return <>{children}>;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/app/admin/theme/create/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import AdminLayout from "@/layout/Admin/Base";
4 | import CreateTheme from "@/components/common/CreateTheme";
5 |
6 | export default () => (
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/client/src/app/article/[id]/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode } from "react";
2 | import Read from "@/layout/Content";
3 | import Aside from "@/components/page/article/Aside";
4 | import ToolBar from "@/components/page/article/ToolBar";
5 |
6 | const Layout: FC<{ children: ReactNode }> = ({ children }) => {
7 | return (
8 | <>
9 | } Aside={}>
10 | {children}
11 |
12 | >
13 | );
14 | };
15 | export default Layout;
16 |
--------------------------------------------------------------------------------
/client/src/app/article/[id]/not-found.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { Button, Result } from "antd";
5 | import Header from "@/components/common/Header";
6 | import Head from "@/components/next/Head";
7 |
8 | const Error = () => {
9 | let router = useRouter();
10 |
11 | return (
12 | <>
13 |
18 |
19 | router.replace("/")}>
25 | 回到首页
26 |
27 | }
28 | />
29 | >
30 | );
31 | };
32 |
33 | export default Error;
34 |
--------------------------------------------------------------------------------
/client/src/app/collection/[id]/not-found.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { Button, Result } from "antd";
5 | import Base from "@/layout/Base";
6 |
7 | const NoFound = () => {
8 | let router = useRouter();
9 |
10 | return (
11 | <>
12 |
13 |
14 | router.replace("/")}>
20 | 返回首页
21 |
22 | }
23 | />
24 |
25 |
26 | >
27 | );
28 | };
29 | export default NoFound;
30 |
--------------------------------------------------------------------------------
/client/src/app/error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { Button, Result } from "antd";
5 | import Header from "@/components/common/Header";
6 | import Head from "@/components/next/Head";
7 |
8 | const Error = (err: any) => {
9 | let router = useRouter();
10 |
11 | return (
12 | <>
13 |
14 |
15 | router.replace("/")}>
22 | 回到首页
23 |
24 | }
25 | >
26 | >
27 | );
28 | };
29 |
30 | export default Error;
31 |
--------------------------------------------------------------------------------
/client/src/app/friendly-link/page.tsx:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/common/response";
3 | import type { userDataType } from "@type/common/user-data";
4 | import type { LinkAttributes } from "@type/model-attribute";
5 | import Header from "@/components/common/Header";
6 | import Head from "@/components/next/Head";
7 | import FriendlyLink from "@/components/page/friendly-link/FriendlyLink";
8 |
9 | type linkItem = Pick<
10 | LinkAttributes,
11 | "id" | "name" | "logo_file_name" | "logo_url" | "url"
12 | > & {
13 | user_data?: userDataType;
14 | };
15 |
16 | const Links = async () => {
17 | let data = await axios
18 | .get>("/friendly-link")
19 | .then((res) => res.data.data);
20 |
21 | return (
22 | <>
23 |
24 |
25 |
26 | >
27 | );
28 | };
29 | export default Links;
30 |
--------------------------------------------------------------------------------
/client/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { Button, Result } from "antd";
5 | import Header from "@/components/common/Header";
6 | import Head from "@/components/next/Head";
7 |
8 | const NotFound = () => {
9 | let router = useRouter();
10 |
11 | return (
12 | <>
13 |
14 |
15 | router.replace("/")}>
21 | 回到首页
22 |
23 | }
24 | />
25 | >
26 | );
27 | };
28 |
29 | export default NotFound;
30 |
--------------------------------------------------------------------------------
/client/src/app/notification/page.tsx:
--------------------------------------------------------------------------------
1 | import { permanentRedirect } from "next/navigation";
2 |
3 | const Notification = () => {
4 | permanentRedirect("/notification/notice");
5 | return <>>;
6 | };
7 | export default Notification;
8 |
--------------------------------------------------------------------------------
/client/src/app/problem/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode } from "react";
2 | import ProblemLayout from "@/components/page/problem/Layout";
3 |
4 | const Layout: FC<{ children: ReactNode }> = ({ children }) => {
5 | return {children};
6 | };
7 | export default Layout;
8 |
--------------------------------------------------------------------------------
/client/src/app/problem/(main)/page.tsx:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/common/response";
3 | import type { ProblemAttributes } from "@type/model-attribute";
4 | import Head from "@/components/next/Head";
5 | import ProblemList from "@/components/page/problem/ProblemList";
6 |
7 | function getProblemList(page: number, type: "newest" | "noanswer") {
8 | return axios
9 | .get>(
10 | `/problem/page/${page}`,
11 | {
12 | params: { type: type },
13 | },
14 | )
15 | .then((res) => res.data.data)
16 | .catch((err) => null);
17 | }
18 |
19 | const Problem = async () => {
20 | let response = (await getProblemList(1, "newest")) as {
21 | total: number;
22 | list: ProblemAttributes[];
23 | };
24 | return (
25 | <>
26 |
27 |
28 | >
29 | );
30 | };
31 | export default Problem;
32 |
--------------------------------------------------------------------------------
/client/src/app/problem/[id]/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode } from "react";
2 | import Read from "@/layout/Content";
3 | import Aside from "@/components/page/problem/Aside";
4 |
5 | const Layout: FC<{ children: ReactNode }> = ({ children }) => {
6 | return (
7 | <>
8 | }>
9 | {children}
10 |
11 | >
12 | );
13 | };
14 | export default Layout;
15 |
--------------------------------------------------------------------------------
/client/src/app/problem/[id]/not-found.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { Button, Result } from "antd";
5 | import Header from "@/components/common/Header";
6 | import Head from "@/components/next/Head";
7 |
8 | const Error = () => {
9 | let router = useRouter();
10 |
11 | return (
12 | <>
13 |
18 |
19 | router.replace("/")}>
25 | 回到首页
26 |
27 | }
28 | />
29 | >
30 | );
31 | };
32 |
33 | export default Error;
34 |
--------------------------------------------------------------------------------
/client/src/app/theme/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Result } from "antd";
4 | import { SmileOutlined } from "@ant-design/icons";
5 | import Base from "@/layout/Base";
6 | import CreateTheme from "@/components/common/CreateTheme";
7 | import Head from "@/components/next/Head";
8 | import useUserData from "@/store/user/user-data";
9 |
10 | const Theme = () => {
11 | let userData = useUserData((s) => s.data);
12 |
13 | return (
14 |
15 |
16 |
17 | {userData ? (
18 |
19 | ) : (
20 | } title="请登录后在提交主题" />
21 | )}
22 |
23 |
24 | );
25 | };
26 | export default Theme;
27 |
--------------------------------------------------------------------------------
/client/src/common/hooks/useComputed.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import type { DependencyList } from "react";
5 |
6 | const useComputed = (
7 | factory: () => T,
8 | deps: DependencyList,
9 | ): [T, (v: T) => void] => {
10 | const [state, setState] = useState(factory);
11 |
12 | useEffect(() => {
13 | setState(factory);
14 | }, deps);
15 |
16 | return [state, setState];
17 | };
18 |
19 | export default useComputed;
20 |
--------------------------------------------------------------------------------
/client/src/common/modules/cookie/index.ts:
--------------------------------------------------------------------------------
1 | import cookie from "js-cookie";
2 |
3 | const getDomain = (host: string) => {
4 | // 处理不同的环境和域名
5 | if (
6 | host.startsWith("http://localhost") ||
7 | host.startsWith("http://127.0.0.1") ||
8 | host.startsWith("http://192.168")
9 | ) {
10 | return "localhost"; // 开发环境使用 localhost
11 | }
12 |
13 | const url = new URL(host);
14 | const domainParts = url.hostname.split(".").slice(-2);
15 |
16 | // 返回带点的域名,例如 .example.com
17 | return `.${domainParts.join(".")}`;
18 | };
19 |
20 | const option = {
21 | expires: 365,
22 | domain: getDomain(process.env.NEXT_PUBLIC_HOST),
23 | };
24 |
25 | function setToken(token: string) {
26 | cookie.set("token", token, option);
27 | }
28 |
29 | function removeToken() {
30 | cookie.remove("token");
31 | }
32 |
33 | function getToken() {
34 | return cookie.get("token");
35 | }
36 |
37 | export { setToken, removeToken, getToken };
38 |
--------------------------------------------------------------------------------
/client/src/common/modules/readingRecords/redis.ts:
--------------------------------------------------------------------------------
1 | import ioredis from "ioredis";
2 |
3 | /**
4 | * 创建Redis链接
5 | */
6 | const redis = new ioredis({
7 | host: process.env.REDIS_HOST || "127.0.0.1",
8 | port: process.env.REDIS_PORT ? +process.env.REDIS_PORT : 6379,
9 | password: process.env.REDIS_PASSWORD,
10 | db: 0,
11 | username: process.env.REDIS_USER,
12 | retryStrategy: function (times) {
13 | return Math.min(times * 50, 5000);
14 | },
15 | });
16 | export default redis;
17 |
--------------------------------------------------------------------------------
/client/src/common/modules/readingRecords/setReferer.ts:
--------------------------------------------------------------------------------
1 | export const list = [
2 | { key: -1, label: "Other", color: "yellow" },
3 | { key: 0, label: "直接进入", color: "purple" },
4 | { key: 1, keyword: "google.com", label: "Google", color: "#ea4335" },
5 | { key: 2, keyword: "so.com", label: "360", color: "#19b955" },
6 | { key: 3, keyword: "baidu.com", label: "Baidu", color: "#4e6ef2" },
7 | { key: 4, keyword: "bing.com", label: "Bing", color: "#007daa" },
8 | { key: 5, keyword: "github.com", label: "GitHub", color: "#24292f" },
9 | { key: 6, keyword: "spider", label: "搜索引擎爬虫", color: "red" },
10 | ];
11 |
12 | /**
13 | * 根据referer判断访问来源
14 | * @params referer {string} req.headers.referer
15 | * @return Referrer number 处理好的访问来源的key
16 | */
17 | function setReferer(referer: string | undefined) {
18 | // 没有referer,直接进入的
19 | if (referer == undefined || !/^[\s\S]*.*[^\s][\s\S]*$/.test(referer)) {
20 | return 0;
21 | }
22 | let result = list.find((item) => referer.includes(item.keyword as string));
23 | return result?.key || -1;
24 | }
25 | export default setReferer;
26 |
--------------------------------------------------------------------------------
/client/src/common/modules/readingRecords/setSpider.ts:
--------------------------------------------------------------------------------
1 | function setSpider(ua: string | undefined) {
2 | if (!ua) return false as false;
3 |
4 | // 通过 UA 判断是否是搜索引擎
5 | return ua
6 | .toLocaleLowerCase()
7 | .match(
8 | /googlebot|bingbot|baiduspider|linkedinbot|slackbot|Applebot|360spider|Sosospider|YoudaoBot|Sogou web spider/i,
9 | )
10 | ? 6
11 | : false;
12 | }
13 | export default setSpider;
14 |
--------------------------------------------------------------------------------
/client/src/common/modules/sitemap/sitemap-index.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "@dayjs";
2 |
3 | interface sitemapItemType {
4 | href: string | number;
5 | }
6 | /** 处理sitemap-list*/
7 | function setSiteMap(list: sitemapItemType[]) {
8 | const header = `
9 | `;
10 | let body = list.map((item) => {
11 | return `
12 |
13 | ${item.href}
14 | ${dayjs().format("YYYY-MM-DD")}
15 | `;
16 | });
17 | const footer = `\n`;
18 | return header + body.join("") + footer;
19 | }
20 | export default setSiteMap;
21 |
--------------------------------------------------------------------------------
/client/src/common/modules/sitemap/sitemap.ts:
--------------------------------------------------------------------------------
1 | interface sitemapItemType {
2 | href: string | number;
3 | priority?: number;
4 | create_time?: Date;
5 | }
6 | /** 处理文章的sitemap*/
7 | function setSiteMap(list: sitemapItemType[]) {
8 | const header = `
9 | `;
15 | let body = list.map((item) => {
16 | return `
17 |
18 | ${item.href}
19 | ${item.priority || 0.9}
20 | ${item.create_time}
21 | weekly
22 | `;
23 | });
24 | const footer = `\n`;
25 | return header + body.join("") + footer;
26 | }
27 | export default setSiteMap;
28 |
--------------------------------------------------------------------------------
/client/src/common/utils/vw.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 用于文章大屏页面,传入px转为当前设备下对应的值
3 | * @params px {number} 1920屏幕下的像素值
4 | * @params min {number|undefined} 最小值
5 | * @params max {number|undefined} 最大值
6 | * @returns px {number} 当前设备下的像素值
7 | */
8 | function vw(px: number, min?: number, max?: number) {
9 | // 需要注意是否服务器环境
10 | let _px =
11 | (px / 1920) *
12 | (typeof window == "undefined"
13 | ? 1200
14 | : document.documentElement.clientWidth);
15 | if (min) {
16 | return Math.max(_px, min);
17 | }
18 | if (max) {
19 | return Math.min(_px, max);
20 | }
21 | return _px;
22 | }
23 | export default vw;
24 |
--------------------------------------------------------------------------------
/client/src/components/admin/page/statistics/Bottom/index.tsx:
--------------------------------------------------------------------------------
1 | import vw from "@/common/utils/vw";
2 | import Container from "../Container";
3 | import Occupation from "./Occupation";
4 | import Referer from "./Referer";
5 |
6 | /** 大屏统计页面顶部展示部分*/
7 | const Bottom = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 | export default Bottom;
20 |
--------------------------------------------------------------------------------
/client/src/components/admin/page/statistics/Top/Header/HeaderItem.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode } from "react";
2 | import Container from "../../Container";
3 |
4 | interface propsType {
5 | title: string | ReactNode;
6 | data: string | ReactNode;
7 | }
8 | /** 大屏页面顶部的数据展示框*/
9 | const HeaderItem: FC = ({ title, data }) => {
10 | return (
11 |
12 |
13 |
{title}
14 |
15 | {data}
16 |
17 |
18 |
19 | );
20 | };
21 | export default HeaderItem;
22 |
--------------------------------------------------------------------------------
/client/src/components/admin/page/statistics/Top/index.tsx:
--------------------------------------------------------------------------------
1 | import Container from "../Container";
2 | import Article from "./Article";
3 | import ArticleRanking from "./ArticleRanking";
4 | import Header from "./Header";
5 | import Visits from "./Visits";
6 |
7 | /** 大屏统计页面顶部展示部分*/
8 | const Top = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
28 | );
29 | };
30 | export default Top;
31 |
--------------------------------------------------------------------------------
/client/src/components/common/AdSense/ADS.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | const ADS = () => {
4 | useEffect(() => {
5 | let script = document.createElement("script");
6 | script.src = `//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-${process.env.NEXT_PUBLIC_GOOGLE_ADS_CLIENT_ID}`;
7 | script.crossOrigin = "anonymous";
8 | document.head.append(script);
9 | ((window as any).adsbygoogle = (window as any).adsbygoogle || []).push({});
10 | return () => {
11 | script.remove();
12 | };
13 | }, []);
14 | return (
15 | <>
16 |
24 | >
25 | );
26 | };
27 | export default ADS;
28 |
--------------------------------------------------------------------------------
/client/src/components/common/ArticleEditor/Modal/Cover.tsx:
--------------------------------------------------------------------------------
1 | import useUserWriteArticle from "@/store/user/user-write-article";
2 | import Upload from "../../UpLoad";
3 |
4 | const Cover = () => {
5 | let articleData = useUserWriteArticle((s) => s.data);
6 | let updateData = useUserWriteArticle((s) => s.updateData);
7 |
8 | return (
9 | <>
10 |
16 | updateData({
17 | cover_file_name: data.file_name,
18 | cover_url: data.file_href,
19 | })
20 | }
21 | onDelete={() => updateData({ cover_file_name: null, cover_url: null })}
22 | />
23 | >
24 | );
25 | };
26 | export default Cover;
27 |
--------------------------------------------------------------------------------
/client/src/components/common/ArticleList/Cover.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { Image } from "antd";
3 |
4 | interface propsType {
5 | cover_url: string;
6 | }
7 |
8 | export const CoverSkeleton = () => (
9 |
10 | );
11 |
12 | /** 单个的ArticleListItem的封面展示*/
13 | const Cover: FC = ({ cover_url }) => {
14 | return (
15 | <>
16 |
17 | }
24 | />
25 |
26 | >
27 | );
28 | };
29 | export default Cover;
30 |
--------------------------------------------------------------------------------
/client/src/components/common/ArticleList/index.module.scss:
--------------------------------------------------------------------------------
1 | .adorn {
2 | }
3 | @mixin line {
4 | transform: translateY(2px);
5 | display: inline-block;
6 | width: 1px;
7 | height: 14px;
8 | background: #e5e6eb;
9 | margin: 0px 4px;
10 | content: " ";
11 | }
12 | .adorn::after {
13 | @include line;
14 | }
15 | .adorn::before {
16 | @include line;
17 | }
18 | .tag:not(:last-child):after {
19 | display: inline-block;
20 | content: " ";
21 | width: 2px;
22 | height: 2px;
23 | border-radius: 50%;
24 | margin: 0px 2px;
25 | transform: translateY(-4px);
26 | background: #4e5969;
27 | }
28 |
29 |
30 |
--------------------------------------------------------------------------------
/client/src/components/common/BackTop/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FloatButton } from "antd";
4 |
5 | const BackTop = () => {
6 | let Btn = FloatButton.BackTop;
7 | return ;
8 | };
9 | export default BackTop;
10 |
--------------------------------------------------------------------------------
/client/src/components/common/Editor/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | const Skeleton = () => {
2 | return ;
3 | };
4 | export default Skeleton;
5 |
--------------------------------------------------------------------------------
/client/src/components/common/Editor/StyleLink.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react";
2 | import type { FC } from "react";
3 |
4 | const StyleLink: FC<{ id: number }> = memo(({ id }) => {
5 | return (
6 |
10 | );
11 | });
12 | export default StyleLink;
13 |
--------------------------------------------------------------------------------
/client/src/components/common/Header/User/NotLogin/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "antd";
2 | import useUserSignModel from "@/store/user/user-sign-model-state";
3 |
4 | const NotLogin = () => {
5 | let setModalState = useUserSignModel((s) => s.setData);
6 | return (
7 | <>
8 |
11 | >
12 | );
13 | };
14 | export default NotLogin;
15 |
--------------------------------------------------------------------------------
/client/src/components/common/Header/User/index.tsx:
--------------------------------------------------------------------------------
1 | import NotLogin from "@/components/common/Header/User/NotLogin";
2 | import UserData from "@/components/common/Header/User/UserData";
3 | import useUserData from "@/store/user/user-data";
4 |
5 | /** 顶部Header的右侧部分*/
6 | const User = () => {
7 | let userData = useUserData((s) => s.data);
8 | return (
9 |
10 | {userData ? (
11 | <>
12 |
13 | >
14 | ) : (
15 | <>
16 |
17 | >
18 | )}
19 |
20 | );
21 | };
22 | export default User;
23 |
--------------------------------------------------------------------------------
/client/src/components/common/_Editor/StyleLink.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react";
2 | import type { FC } from "react";
3 |
4 | const StyleLink: FC<{ id: number }> = memo(({ id }) => {
5 | return (
6 |
10 | );
11 | });
12 | export default StyleLink;
13 |
--------------------------------------------------------------------------------
/client/src/components/common/_Editor/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 | import dynamic from "next/dynamic";
5 | import "./index.scss";
6 |
7 | const Editor = dynamic(() => import("./Editor"), {
8 | ssr: false,
9 | loading: () => ,
10 | });
11 |
12 | export interface propsType {
13 | className?: string;
14 | /** 上传至哪个文件夹*/
15 | target: "article" | "problem" | "answer";
16 | /** 初始化数据*/
17 | initValue?: string;
18 | onChange?: (html: string) => any;
19 | height?: number;
20 | /** 是否使用主题*/
21 | theme?: boolean;
22 | /** 设置主题*/
23 | onSetTheme?: (id: number) => void;
24 | defaultTheme?: number;
25 | }
26 |
27 | export const Skeleton = () => (
28 |
29 | );
30 |
31 | const ComponentName: FC = (props) => {
32 | return (
33 | <>
34 |
35 | >
36 | );
37 | };
38 | export default ComponentName;
39 |
--------------------------------------------------------------------------------
/client/src/components/next/Head.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode } from "react";
2 |
3 | interface props {
4 | title?: string;
5 | description?: string;
6 | keywords?: string[];
7 | children?: ReactNode;
8 | }
9 |
10 | /**Head组件封装 title keywords description children*/
11 | const Head: FC = (props) => {
12 | let { title, description, keywords } = props;
13 | return (
14 | <>
15 | {title && {title}}
16 |
20 | {description && }
21 | {keywords && }
22 |
23 | {props.children}
24 | >
25 | );
26 | };
27 |
28 | export default Head;
29 |
--------------------------------------------------------------------------------
/client/src/components/next/Image.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { FC } from "react";
4 | import ImageNext from "next/image";
5 | import type { ImageProps } from "next/image";
6 |
7 | const Image: FC = (props) => {
8 | return (
9 | <>
10 | {
14 | return `${process.env.CDN}${src}?w=${width}&q=${100}`;
15 | }}
16 | priority={true}
17 | />
18 | >
19 | );
20 | };
21 |
22 | export default Image;
23 |
--------------------------------------------------------------------------------
/client/src/components/next/NoFollowLink.tsx:
--------------------------------------------------------------------------------
1 | import type { AnchorHTMLAttributes, DetailedHTMLProps, FC } from "react";
2 |
3 | type propsType = DetailedHTMLProps<
4 | AnchorHTMLAttributes,
5 | HTMLAnchorElement
6 | >;
7 |
8 | /** 不需要搜索引擎抓取的外站链接*/
9 | const NoFollowLink: FC = (props) => {
10 | return (
11 |
12 | {props.children}
13 |
14 | );
15 | };
16 | export default NoFollowLink;
17 |
--------------------------------------------------------------------------------
/client/src/components/page/article/Aside/Catalogue/index.module.scss:
--------------------------------------------------------------------------------
1 | .article-page-catalogue {
2 | ol {
3 | padding-left: 0px;
4 | list-style: none;
5 | width: 240px;
6 | li {
7 | position: relative;
8 | width: 100%;
9 | a {
10 | overflow: hidden;
11 | text-overflow: ellipsis;
12 | white-space: nowrap;
13 | padding: 8px 16px;
14 | display: block;
15 | &:hover {
16 | background-color: rgba(214, 214, 214, 0.2);
17 | }
18 | }
19 | }
20 | }
21 | a {
22 | color: black;
23 | }
24 | }
25 | .is-active-link {
26 | color: #007fff !important;
27 | }
28 | .is-active-link::before {
29 | content: "";
30 | position: absolute;
31 | top: 10px;
32 | left: 0px;
33 | width: 4px;
34 | height: 16px;
35 | background: #1e80ff;
36 | border-radius: 0 4px 4px 0;
37 | }
38 | .is-collapsible {
39 | a {
40 | padding-left: 32px !important;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/client/src/components/page/article/Aside/Catalogue/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import userUserCurrentArticleData from "@/store/user/user-current-article-data";
4 | import Catalogue_ from "./Catalogue";
5 |
6 | /** 文章页面侧边目录*/
7 | const Catalogue = () => {
8 | let articleData = userUserCurrentArticleData((s) => s.data);
9 | return <>{articleData.display_directory && }>;
10 | };
11 | export default Catalogue;
12 |
--------------------------------------------------------------------------------
/client/src/components/page/article/Aside/Repository/index.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "react";
2 | import classNames from "classnames";
3 | import Image from "@/components/next/Image";
4 | import NoFollowLink from "@/components/next/NoFollowLink";
5 |
6 | interface propsType {
7 | className?: string;
8 | }
9 | /** 侧边栏张贴项目地址的*/
10 | const Repository: FC = ({ className }) => {
11 | return (
12 | <>
13 |
16 |
22 |
23 | 项目地址
24 | 一个简单的博客社区
25 |
26 |
27 | >
28 | );
29 | };
30 | export default Repository;
31 |
--------------------------------------------------------------------------------
/client/src/components/page/article/Aside/index.tsx:
--------------------------------------------------------------------------------
1 | import AdSense from "@/components/common/AdSense";
2 | import Advertisement from "@/components/common/Advertisement";
3 | import Repository from "@/components/page/article/Aside/Repository";
4 | import Catalogue from "./Catalogue";
5 |
6 | /** 文章页面的右侧推广内容*/
7 | const Aside = () => {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 | >
15 | );
16 | };
17 | export default Aside;
18 |
--------------------------------------------------------------------------------
/client/src/components/page/article/ImagePreview/index.module.scss:
--------------------------------------------------------------------------------
1 | .preview {
2 | img {
3 | max-width: 80% !important;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/components/page/article/NoFound.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { Button, Result } from "antd";
5 | import Header from "@/components/common/Header";
6 | import Head from "@/components/next/Head";
7 |
8 | /** 文章页面404*/
9 | const NoFound = () => {
10 | let router = useRouter();
11 |
12 | return (
13 | <>
14 |
15 |
16 | router.replace("/")}>
22 | 返回主页
23 |
24 | }
25 | />
26 | >
27 | );
28 | };
29 | export default NoFound;
30 |
--------------------------------------------------------------------------------
/client/src/components/page/article/Reprint.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "react";
2 |
3 | interface propsType {
4 | /** 转载地址*/
5 | reprint?: string | null;
6 | }
7 | /** 文章底部的转载链接组件*/
8 | const Reprint: FC = ({ reprint }) => {
9 | return (
10 | <>
11 | {reprint && (
12 |
13 | 转载自:{reprint}
14 |
15 | )}
16 | >
17 | );
18 | };
19 | export default Reprint;
20 |
--------------------------------------------------------------------------------
/client/src/components/page/article/Store.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC, useEffect } from "react";
4 | import type { ArticleAttributes } from "@type/model-attribute";
5 | import userUserCurrentArticleData from "@/store/user/user-current-article-data";
6 |
7 | const Store: FC<{ data: ArticleAttributes }> = ({ data }) => {
8 | let setUserCurrentArticleData = userUserCurrentArticleData(
9 | (s) => s.setArticleData,
10 | );
11 | let resetUserCurrentArticleData = userUserCurrentArticleData(
12 | (s) => s.resetData,
13 | );
14 |
15 | useEffect(() => {
16 | setUserCurrentArticleData(data);
17 | return () => {
18 | resetUserCurrentArticleData();
19 | };
20 | }, []);
21 | return <>>;
22 | };
23 | export default Store;
24 |
--------------------------------------------------------------------------------
/client/src/components/page/article/ToolBar/Comment.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "antd";
2 | import classNames from "classnames";
3 | import Image from "@/components/next/Image";
4 | import userUserCurrentArticleData from "@/store/user/user-current-article-data";
5 | import itemClassName from "./class";
6 |
7 | const Comment = () => {
8 | let currentArticleData = userUserCurrentArticleData((s) => s.data);
9 |
10 | return (
11 | <>
12 |
13 |
18 |
24 |
25 |
26 | >
27 | );
28 | };
29 | export default Comment;
30 |
--------------------------------------------------------------------------------
/client/src/components/page/article/ToolBar/class.ts:
--------------------------------------------------------------------------------
1 | import classNames from "classnames";
2 |
3 | /** className*/
4 | const itemClassName = classNames([
5 | "sm:hidden",
6 | "w-14",
7 | "h-14",
8 | "text-[#707070]",
9 | "flex",
10 | "items-center",
11 | "justify-center",
12 | "bg-white",
13 | "rounded-full",
14 | "shadow-md",
15 | "hover:shadow-xl",
16 | "duration-300",
17 | "cursor-pointer",
18 | "opacity-70",
19 | "hover:opacity-100",
20 | "text-xl",
21 | "mt-4",
22 | ]);
23 | export default itemClassName;
24 |
--------------------------------------------------------------------------------
/client/src/components/page/article/ToolBar/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Collection from "./Collection";
4 | import Comment from "./Comment";
5 | import Likes from "./Like";
6 | import Share from "./Share";
7 |
8 | const ToolBar = () => {
9 | return (
10 | <>
11 |
12 | {/* 点赞信息 */}
13 |
14 | {/* 评论 */}
15 |
16 | {/* 收藏信息 */}
17 |
18 | {/* 分享 */}
19 |
20 |
21 | >
22 | );
23 | };
24 | export default ToolBar;
25 |
--------------------------------------------------------------------------------
/client/src/components/page/article/View/index.tsx:
--------------------------------------------------------------------------------
1 | // https://nextjs.org/docs/messages/react-hydration-error#solution-3-using-suppresshydrationwarning
2 | // 有script标签,防止水合报错
3 | import type { FC } from "react";
4 | import style from "@/styles/content.module.scss";
5 |
6 | interface prposType {
7 | content: string;
8 | }
9 | /** 文章页面主题内容显示*/
10 | const View: FC = (props) => {
11 | return (
12 |
17 | );
18 | };
19 | export default View;
20 |
--------------------------------------------------------------------------------
/client/src/components/page/index/Layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { FC } from "react";
4 | import Base, { propsType as basePropsType } from "@/layout/Base";
5 | import AdSense from "@/components/common/AdSense";
6 | import Advertisement from "@/components/common/Advertisement";
7 | import Repository from "@/components/page/article/Aside/Repository";
8 | import Footer from "./Footer";
9 | import Ranking from "./Ranking";
10 |
11 | /**
12 | * 首页的基本嵌套布局
13 | * 需要传递右侧推广信息的数据
14 | */
15 | const Layout: FC = (props) => {
16 | return (
17 |
18 |
19 | {props.children}
20 |
21 |
28 |
29 | );
30 | };
31 | export default Layout;
32 |
--------------------------------------------------------------------------------
/client/src/components/page/index/Ranking/AuthorRanking.tsx:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import useFetch from "@/common/hooks/useFetch";
3 | import RankingList from "./RankingList";
4 |
5 | const AuthorRanking = () => {
6 | let { data, isLoading, error } = useFetch(() =>
7 | axios.get("/ranking/author").then((res) => res.data.data),
8 | );
9 | return (
10 | <>
11 |
12 |
17 |
18 | >
19 | );
20 | };
21 | export default AuthorRanking;
22 |
--------------------------------------------------------------------------------
/client/src/components/page/index/Ranking/FunsRanking.tsx:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import useFetch from "@/common/hooks/useFetch";
3 | import RankingList from "./RankingList";
4 |
5 | const FunsRanking = () => {
6 | let { data, isLoading, error } = useFetch(() =>
7 | axios.get("/ranking/funs").then((res) => res.data.data),
8 | );
9 | return (
10 | <>
11 |
12 |
17 |
18 | >
19 | );
20 | };
21 | export default FunsRanking;
22 |
--------------------------------------------------------------------------------
/client/src/components/page/index/index.module.scss:
--------------------------------------------------------------------------------
1 | .type-active {
2 | color: #007fff;
3 | }
4 | .tag-active {
5 | color: white;
6 | background-color: #007fff;
7 | }
8 | .type-header {
9 | transition: all 0.2s;
10 | }
11 | .child-a-gray a {
12 | color: #9aa3ab;
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/components/page/problem/Aside.tsx:
--------------------------------------------------------------------------------
1 | import AdSense from "@/components/common/AdSense";
2 | import Advertisement from "@/components/common/Advertisement";
3 |
4 | const Aside = () => {
5 | return (
6 | <>
7 |
8 |
9 | >
10 | );
11 | };
12 | export default Aside;
13 |
--------------------------------------------------------------------------------
/client/src/components/page/problem/Comments/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { FC } from "react";
4 | import type { problemCommentType } from "@type/model/problem";
5 | import classNames from "classnames";
6 | import Item from "./Item";
7 |
8 | interface propsType {
9 | belong_id: number;
10 | className?: string;
11 | data: problemCommentType[];
12 | type: "problem" | "answer";
13 | }
14 | /** 单个答案的回复组件*/
15 | const Comments: FC = (props) => {
16 | return (
17 |
23 | {props.data.map((item) => {
24 | return (
25 |
32 | );
33 | })}
34 |
35 | );
36 | };
37 | export default Comments;
38 |
--------------------------------------------------------------------------------
/client/src/components/page/problem/Layout.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode } from "react";
2 | import Read from "@/layout/Content";
3 | import Aside from "./Aside";
4 |
5 | interface propsType {
6 | children?: ReactNode;
7 | language?: string[];
8 | }
9 |
10 | const Layout: FC = (props) => {
11 | return (
12 | }>
13 | {props.children}
14 |
15 | );
16 | };
17 | export default Layout;
18 |
--------------------------------------------------------------------------------
/client/src/components/page/problem/List/index.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "react";
2 | import type { ProblemAttributes } from "@type/model-attribute";
3 | import classNames from "classnames";
4 | import Item from "./Item";
5 | import type { childrenPropsType } from "./Item";
6 |
7 | export type dataItemPropsType = Pick<
8 | ProblemAttributes,
9 | "id" | "answer_count" | "view_count" | "title" | "tag" | "answer_id"
10 | >;
11 | const List: FC<
12 | {
13 | data: dataItemPropsType[];
14 | } & childrenPropsType
15 | > = ({ data, className, topRight }) => {
16 | return (
17 | <>
18 |
19 | {data.map((item, index) => (
20 |
26 | ))}
27 |
28 | >
29 | );
30 | };
31 | export default List;
32 |
--------------------------------------------------------------------------------
/client/src/components/page/problem/NoFound.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { Button, Result } from "antd";
5 | import Head from "@/components/next/Head";
6 |
7 | const NoFound = () => {
8 | let router = useRouter();
9 |
10 | return (
11 | <>
12 |
13 | router.replace("/")}>
19 | 返回首页
20 |
21 | }
22 | />
23 | >
24 | );
25 | };
26 | export default NoFound;
27 |
--------------------------------------------------------------------------------
/client/src/components/page/tag/Layout.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode } from "react";
2 | import Sidebar from "@/layout/Sidebar";
3 | import AdSense from "@/components/common/AdSense";
4 | import Advertisement from "@/components/common/Advertisement";
5 | import Repository from "../article/Aside/Repository";
6 |
7 | interface propsType {
8 | children: ReactNode;
9 | }
10 | const Layout: FC = ({ children }) => {
11 | return (
12 |
15 |
16 |
17 |
18 | >
19 | }
20 | >
21 | {children}
22 |
23 | );
24 | };
25 | export default Layout;
26 |
--------------------------------------------------------------------------------
/client/src/components/page/user/index/NotFind.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/navigation";
2 | import { Button, Result } from "antd";
3 | import Head from "@/components/next/Head";
4 |
5 | const NotFind = () => {
6 | let router = useRouter();
7 | return (
8 |
9 |
10 | router.replace("/")}>
14 | 返回首页
15 |
16 | }
17 | />
18 |
19 | );
20 | };
21 | export default NotFind;
22 |
--------------------------------------------------------------------------------
/client/src/components/page/user/index/UserData/Main/Favorites/Modal.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "react";
2 | import { Modal } from "antd";
3 | import type { ModalProps } from "antd";
4 | import CreateFrom, {
5 | propsType as CollectionModalPropsType,
6 | } from "@/components/common/CollectionModal/CreateFrom";
7 |
8 | type propsType = Pick &
9 | CollectionModalPropsType;
10 |
11 | const CollectionModal: FC = (props) => {
12 | return (
13 | <>
14 | props.onCancel()}
19 | >
20 |
21 |
22 | >
23 | );
24 | };
25 | export default CollectionModal;
26 |
--------------------------------------------------------------------------------
/client/src/components/page/user/index/UserData/index.tsx:
--------------------------------------------------------------------------------
1 | import type { FC } from "react";
2 | import { Alert } from "antd";
3 | import type { UserAttributes } from "@type/model-attribute";
4 | import Head from "@/components/next/Head";
5 | import Aside from "./Aside";
6 | import Header from "./Header";
7 | import Main from "./Main";
8 |
9 | const UserData: FC<{ data: UserAttributes }> = ({ data }) => {
10 | return (
11 | <>
12 |
13 |
14 | {data.state == 0 && (
15 |
20 | )}
21 |
22 |
23 |
24 |
25 |
26 |
29 | >
30 | );
31 | };
32 | export default UserData;
33 |
--------------------------------------------------------------------------------
/client/src/components/page/user/setting/UploadAvatar/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import type { FC } from "react";
3 | import Upload from "@/components/common/UpLoad";
4 |
5 | interface propsType {
6 | avatar_file_name: string;
7 | avatar_url: string;
8 | onChange: (value: string) => void;
9 | }
10 |
11 | const UploadAvatar: FC = (props) => {
12 | let first = useRef(true);
13 | useEffect(() => {
14 | if (first.current) {
15 | props.onChange(props.avatar_file_name);
16 | first.current = false;
17 | }
18 | }, [props.avatar_file_name]);
19 |
20 | return (
21 | <>
22 | {
27 | props.onChange(data.file_name);
28 | }}
29 | onDelete={() => {
30 | props.onChange("");
31 | }}
32 | />
33 | >
34 | );
35 | };
36 | export default UploadAvatar;
37 |
--------------------------------------------------------------------------------
/client/src/layout/Content/HightLight/index.scss:
--------------------------------------------------------------------------------
1 | .toolbar-item {
2 | margin-right: 6px;
3 | & > a,
4 | & > button,
5 | & > span {
6 | border: 1px solid #bbb !important;
7 | border-radius: 3px !important;
8 | cursor: pointer;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/layout/Content/index.tsx:
--------------------------------------------------------------------------------
1 | import { type FC, type ReactNode, Suspense } from "react";
2 | import classNames from "classnames";
3 | import Sidebar from "@/layout/Sidebar";
4 | import ImagePreview from "@/components/page/article/ImagePreview";
5 | import HightLight from "./HightLight";
6 |
7 | export interface propsType {
8 | children: ReactNode;
9 | ToolBar?: ReactNode;
10 | Aside?: ReactNode;
11 | language?: string[] | null;
12 | className?: string;
13 | }
14 |
15 | /** 内容页面(文章、问答)布局*/
16 | const Layout: FC = (props) => {
17 | return (
18 | <>
19 | {props.language && }
20 |
24 | {props.ToolBar}
25 | {props.children}
26 |
27 |
28 |
29 |
30 | >
31 | );
32 | };
33 | export default Layout;
34 |
--------------------------------------------------------------------------------
/client/src/layout/Main/index.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode } from "react";
2 | import classNames from "classnames";
3 |
4 | const Main: FC<{
5 | children: ReactNode;
6 | mainClassName?: classNames.ArgumentArray;
7 | containerClassName?: classNames.ArgumentArray;
8 | }> = (props) => {
9 | return (
10 |
13 |
23 | {props.children}
24 |
25 |
26 | );
27 | };
28 | export default Main;
29 |
--------------------------------------------------------------------------------
/client/src/layout/Sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode } from "react";
2 | import classNames from "classnames";
3 | import Base from "@/layout/Base";
4 |
5 | /** Sidebar布局所需类型*/
6 | export interface propsType {
7 | children: ReactNode;
8 | Aside: ReactNode;
9 | className?: string;
10 | }
11 |
12 | /** 顶部Header,侧边携带Aside*/
13 | const Sidebar: FC = (props) => {
14 | return (
15 |
16 | {/* 1160-16(margin right)-240(aside width) */}
17 | {props.children}
18 |
22 |
23 | );
24 | };
25 | export default Sidebar;
26 |
--------------------------------------------------------------------------------
/client/src/plugin/axios.ts:
--------------------------------------------------------------------------------
1 | import axiosPlugin from "axios";
2 | import { getToken } from "@/common/modules/cookie";
3 |
4 | // 创建 axios 实例
5 | const apiClient = axiosPlugin.create({
6 | baseURL: process.env.NEXT_PUBLIC_API_HOST,
7 | });
8 |
9 | // 请求拦截器
10 | apiClient.interceptors.request.use(
11 | (config) => {
12 | // 客户端才修改请求头
13 | if (typeof window !== "undefined") {
14 | config.headers.authorization = getToken();
15 | }
16 | config.headers["Cache-Control"] = "no-cache";
17 | return config;
18 | },
19 | (error) => {
20 | return Promise.reject(error);
21 | },
22 | );
23 |
24 | // 响应拦截器
25 | apiClient.interceptors.response.use(
26 | (response) => {
27 | /**访问成功**/
28 | return response;
29 | },
30 | (error) => {
31 | if (axiosPlugin.isCancel(error)) {
32 | return new Promise(() => {}); // 返回一个空 Promise 取消请求不触发 catch
33 | }
34 | return Promise.reject({
35 | ...error.response?.data,
36 | status: error.response?.status,
37 | });
38 | },
39 | );
40 |
41 | export default apiClient;
42 |
--------------------------------------------------------------------------------
/client/src/plugin/dayjs.ts:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import "dayjs/locale/zh-cn";
3 | import localeData from "dayjs/plugin/localeData";
4 | import relativeTime from "dayjs/plugin/relativeTime";
5 | import weekday from "dayjs/plugin/weekday";
6 |
7 | dayjs.extend(relativeTime);
8 | dayjs.locale("zh-cn");
9 | dayjs.extend(weekday);
10 | dayjs.extend(localeData);
11 |
12 | export default dayjs;
13 |
--------------------------------------------------------------------------------
/client/src/request/advertisement.ts:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import { response } from "@type/common/response";
3 |
4 | export interface advertisementType {
5 | id: number;
6 | url: string;
7 | poster_file_name: string;
8 | poster_url: string;
9 | image_size: { width: number; height: number };
10 | }
11 |
12 | export type responseType = advertisementType[];
13 |
14 | /** 获取推广信息*/
15 | function getAdvertisementList(position: string) {
16 | return axios
17 | .get>("/advertisement", { params: { position } })
18 | .then((res) => res.data.data)
19 | .catch((err) => []);
20 | }
21 | export default getAdvertisementList;
22 |
--------------------------------------------------------------------------------
/client/src/request/collection/collection.ts:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/common/response";
3 |
4 | /** 收藏*/
5 | function collection(id: number | string, type: "problem" | "article") {
6 | return axios.post(`/collection/${id}`, { type });
7 | }
8 | export default collection;
9 |
--------------------------------------------------------------------------------
/client/src/request/collection/index.ts:
--------------------------------------------------------------------------------
1 | import collection from "./collection";
2 | import uncollection from "./uncollection";
3 |
4 | export { collection, uncollection };
5 |
--------------------------------------------------------------------------------
/client/src/request/collection/uncollection.ts:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/common/response";
3 |
4 | /** 取消点赞*/
5 | function unLike(id: number | string) {
6 | return axios.delete(`/collection/${id}`);
7 | }
8 | export default unLike;
9 |
--------------------------------------------------------------------------------
/client/src/request/follow/follow.ts:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/common/response";
3 |
4 | /** 关注*/
5 | function follow(id: number | string, type: "problem" | "user") {
6 | return axios.post(`/follow/${id}`, { type });
7 | }
8 | export default follow;
9 |
--------------------------------------------------------------------------------
/client/src/request/follow/index.ts:
--------------------------------------------------------------------------------
1 | import follow from "./follow";
2 | import unfollow from "./unfollow";
3 |
4 | export { follow, unfollow };
5 |
--------------------------------------------------------------------------------
/client/src/request/follow/unfollow.ts:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/common/response";
3 |
4 | /** 取消关注*/
5 | function unFollow(id: number | string) {
6 | return axios.delete(`/follow/${id}`);
7 | }
8 | export default unFollow;
9 |
--------------------------------------------------------------------------------
/client/src/request/like/index.ts:
--------------------------------------------------------------------------------
1 | import like from "./like";
2 | import unlike from "./unlike";
3 |
4 | export { like, unlike };
5 |
--------------------------------------------------------------------------------
/client/src/request/like/like.ts:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/common/response";
3 |
4 | /** 点赞*/
5 | function like(id: number | string, type: "problem" | "answer" | "article") {
6 | return axios.post(`/like/${id}`, { type });
7 | }
8 | export default like;
9 |
--------------------------------------------------------------------------------
/client/src/request/like/unlike.ts:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/common/response";
3 |
4 | /** 取消点赞*/
5 | function unLike(id: number | string) {
6 | return axios.delete(`/like/${id}`);
7 | }
8 | export default unLike;
9 |
--------------------------------------------------------------------------------
/client/src/request/load-static.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import axios from "@axios";
3 | import { response } from "@type/common/response";
4 |
5 | type target = "type" | "article" | "cover" | "avatar" | "comment";
6 |
7 | export interface responseType {
8 | file_name: string;
9 | file_href: string;
10 | }
11 |
12 | function loadStatic(target: target, file: File) {
13 | let formData = new FormData();
14 | formData.append("image", file);
15 | return axios
16 | .post>(`/static/${target}`, formData)
17 | .then((res) => res.data);
18 | }
19 | export default loadStatic;
20 |
--------------------------------------------------------------------------------
/client/src/request/type/getTag.ts:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/response";
3 | import type { TagAttributes, TagTreeAttributes } from "@type/type";
4 |
5 | async function getTag(
6 | type: T,
7 | ): Promise {
8 | const res = await axios.get<
9 | response
10 | >("/tag", { params: { type } });
11 | return res.data.data;
12 | }
13 |
14 | export default getTag;
15 |
--------------------------------------------------------------------------------
/client/src/request/type/getTagArticleLData.ts:
--------------------------------------------------------------------------------
1 | import { propsType } from "@/app/tag/article/[id]/page";
2 | import axios from "@axios";
3 | import { response } from "@type/common/response";
4 |
5 | const getTagArticleLData = (page: number, id: string) =>
6 | axios
7 | .get<
8 | response
9 | >(`/article/tag/${id}`, { params: { page } })
10 | .then((res) => res.data.data);
11 |
12 | export default getTagArticleLData;
13 |
--------------------------------------------------------------------------------
/client/src/request/type/type-tree-index.ts:
--------------------------------------------------------------------------------
1 | import axios from "@axios";
2 | import type { response } from "@type/common/response";
3 |
4 | export interface responseType {
5 | id: string;
6 | name: string;
7 | children: responseType[];
8 | isLogin?: boolean;
9 | }
10 |
11 | /**
12 | * 首页用于获取类型树
13 | */
14 | function getTypeTreeIndex() {
15 | return axios
16 | .get>("/tag-tree-client")
17 | .then((res) => res.data.data);
18 | }
19 | export default getTypeTreeIndex;
20 |
--------------------------------------------------------------------------------
/client/src/store/admin/admin-article-list/index.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | const initValues = {
4 | list: [] as any[],
5 | total_count: 0,
6 | };
7 |
8 | type actionType = {
9 | setData: (data: typeof initValues) => any;
10 | };
11 |
12 | const useAdminArticleList = create<{ data: typeof initValues } & actionType>(
13 | (set) => ({
14 | data: initValues,
15 | setData: (data: typeof initValues) => set(() => ({ data: data })),
16 | }),
17 | );
18 | export default useAdminArticleList;
19 |
--------------------------------------------------------------------------------
/client/src/store/admin/admin-search-option/index.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | const initValues = {
4 | /** 发布人 管理员或者用户*/
5 | auth: undefined,
6 | /** 设置正序还是倒序*/
7 | sort: ["create_time", "desc"],
8 | /** 时间线,查询某某之后create_time的文章*/
9 | deadline: undefined,
10 | /** 根据ID进行查询*/
11 | article_id: undefined,
12 | /** 发布者ID*/
13 | author_id: undefined,
14 | /** 是否仅原创文章*/
15 | only_original: undefined,
16 | };
17 |
18 | type actionType = {
19 | setData: (data: typeof initValues) => any;
20 | };
21 |
22 | const useAdminArticleSearch = create<{ data: typeof initValues } & actionType>(
23 | (set) => ({
24 | data: initValues,
25 | setData: (data: typeof initValues) => set(() => ({ data: data })),
26 | }),
27 | );
28 | export default useAdminArticleSearch;
29 |
--------------------------------------------------------------------------------
/client/src/store/admin/admin-table-option/index.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | const initValues = {
4 | key: 0, //用于刷新表格
5 | page:
6 | typeof window != "undefined" && !isNaN(+window?.sessionStorage.page)
7 | ? +window?.sessionStorage.page
8 | : 1,
9 | page_size:
10 | typeof window != "undefined" && !isNaN(+window?.sessionStorage.page_size)
11 | ? +window?.sessionStorage.page_size
12 | : 10,
13 | };
14 |
15 | type actionType = {
16 | setData: (data: typeof initValues) => any;
17 | };
18 |
19 | const useAdminTableOption = create<{ data: typeof initValues } & actionType>(
20 | (set) => ({
21 | data: initValues,
22 | setData: (data: typeof initValues) => set(() => ({ data: data })),
23 | }),
24 | );
25 | export default useAdminTableOption;
26 |
--------------------------------------------------------------------------------
/client/src/store/common/editor-mode/index.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | type dataType = "markdown" | "rich-text";
4 |
5 | type actionType = {
6 | setData: (data: dataType) => any;
7 | };
8 |
9 | /** 文章编辑器切换*/
10 | const useEditorMode = create<{ data: dataType } & actionType>((set) => ({
11 | data: (typeof window != "undefined"
12 | ? window.localStorage.getItem("editor-mode") || "markdown"
13 | : "markdown") as dataType,
14 | setData: (data: dataType) => set({ data: data }),
15 | }));
16 | export default useEditorMode;
17 |
--------------------------------------------------------------------------------
/client/src/store/user/user-article-comment/index.ts:
--------------------------------------------------------------------------------
1 | import { articleCommentType } from "@type/model/article-comment";
2 | import { create } from "zustand";
3 |
4 | const initValue: {
5 | activeEmojiID: null | string | number;
6 | activeInputID: null | string | number;
7 | list: articleCommentType[] | null;
8 | } = {
9 | activeEmojiID: null,
10 | activeInputID: null,
11 | list: null,
12 | };
13 | type actionType = {
14 | setData: (
15 | data: Partial<{ [K in keyof typeof initValue]: (typeof initValue)[K] }>,
16 | ) => any;
17 | };
18 |
19 | /** 文章评论编辑*/
20 | const useUserArticleComment = create<
21 | {
22 | data: typeof initValue;
23 | } & actionType
24 | >((set) => ({
25 | data: initValue,
26 | setData: (data) => set((state) => ({ data: { ...state.data, ...data } })),
27 | }));
28 | export default useUserArticleComment;
29 |
--------------------------------------------------------------------------------
/client/src/store/user/user-current-article-data/index.ts:
--------------------------------------------------------------------------------
1 | import { ArticleAttributes } from "@type/model-attribute";
2 | import { create } from "zustand";
3 |
4 | export const initValue = {} as ArticleAttributes;
5 |
6 | type dataType = typeof initValue;
7 |
8 | type actionType = {
9 | setArticleData: (data: dataType) => any;
10 | updateData: (
11 | data: Partial<{ [K in keyof typeof initValue]: (typeof initValue)[K] }>,
12 | ) => any;
13 | resetData: () => any;
14 | };
15 |
16 | /** 更新用户端文章编辑数据*/
17 | const userUserCurrentArticleData = create<{ data: dataType } & actionType>(
18 | (set) => ({
19 | data: initValue,
20 | /** 更新整体对象*/
21 | setArticleData: (data: dataType) => set({ data: data }),
22 | /** 更新单一属性*/
23 | updateData: (state) => set((s) => ({ data: { ...s.data, ...state } })),
24 | /** 重置属性*/
25 | resetData: () => set({ data: initValue }),
26 | }),
27 | );
28 | export default userUserCurrentArticleData;
29 |
--------------------------------------------------------------------------------
/client/src/store/user/user-sign-model-state/index.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { componentsList } from "@/components/common/Header/Sign";
3 |
4 | type dataType = false | keyof typeof componentsList;
5 |
6 | type actionType = {
7 | setData: (data: dataType) => any;
8 | };
9 |
10 | /** 弹窗组件显示的状态管理管理*/
11 | const useUserSignModel = create<{ data: dataType } & actionType>((set) => ({
12 | data: false,
13 | setData: (data: dataType) => set({ data: data }),
14 | }));
15 | export default useUserSignModel;
16 |
--------------------------------------------------------------------------------
/client/src/store/user/user-write-article/index.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | export const initValue = {
4 | title: "",
5 | content: "",
6 | tag: [] as number[],
7 | reprint: null as null | string,
8 | description: null as null | string,
9 | cover_file_name: null as null | string,
10 | cover_url: null as null | string,
11 | theme_id: 0,
12 | };
13 |
14 | type dataType = typeof initValue;
15 |
16 | type actionType = {
17 | setArticleData: (data: dataType) => void;
18 | resetData: () => void;
19 | updateData: (data: Partial) => void;
20 | };
21 |
22 | /** 更新用户端文章编辑数据 */
23 | const useUserWriteArticle = create<{ data: dataType } & actionType>((set) => ({
24 | data: initValue,
25 | /** 更新整体对象 */
26 | setArticleData: (data: dataType) => set({ data: data }),
27 | /** 重置文章数据 */
28 | resetData: () => set({ data: initValue }),
29 | /** 更新单一属性 */
30 | updateData: (state: Partial) =>
31 | set((s) => {
32 | return { data: { ...s.data, ...state } };
33 | }),
34 | }));
35 |
36 | export default useUserWriteArticle;
37 |
--------------------------------------------------------------------------------
/client/src/styles/content.module.scss:
--------------------------------------------------------------------------------
1 | .content_body {
2 | img {
3 | display: block !important;
4 | cursor: zoom-in !important;
5 | max-width: 100% !important;
6 | }
7 | h1,
8 | h2,
9 | h3,
10 | h4,
11 | h5,
12 | h6 {
13 | margin-top: 15px !important;
14 | margin-bottom: 8px !important;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/styles/genAntdCss.tsx:
--------------------------------------------------------------------------------
1 | import { extractStyle } from "@ant-design/cssinjs";
2 | import type Entity from "@ant-design/cssinjs/lib/Cache";
3 | import { createHash } from "crypto";
4 |
5 | export type DoExtraStyleOptions = {
6 | cache: Entity;
7 | };
8 | export async function doExtraStyle({ cache }: DoExtraStyleOptions) {
9 | if (typeof window == "undefined") {
10 | const fs = await import(`fs`);
11 | const path = await import(`path`);
12 |
13 | if (!fs.existsSync(`.next/css`)) {
14 | fs.mkdirSync(`.next/css`, { recursive: true });
15 | }
16 |
17 | const css = extractStyle(cache, true);
18 |
19 | if (!css) return "";
20 |
21 | const md5 = createHash("md5");
22 | const hash = md5.update(css).digest("hex");
23 |
24 | const fileName = `${hash.substring(0, 16)}.css`;
25 | const fullpath = path.join(`.next/css`, fileName);
26 |
27 | if (fs.existsSync(fullpath)) return `/static/antd/${fileName}`;
28 |
29 | fs.writeFileSync(fullpath, css);
30 |
31 | return `/static/antd/${fileName}`;
32 | }
33 | return "/";
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/styles/globals.scss:
--------------------------------------------------------------------------------
1 | a:hover {
2 | color: inherit;
3 | }
4 | .container {
5 | @apply mx-auto;
6 | & > * {
7 | @apply mx-auto max-w-[1160px];
8 | }
9 | }
10 | .container-xs {
11 | @apply mx-auto;
12 | & > * {
13 | @apply mx-auto max-w-[960px];
14 | }
15 | }
16 |
17 | .border-b-solid {
18 | @apply border-b;
19 | border-bottom-style: solid;
20 | }
21 |
22 | .border-l-solid {
23 | @apply border-l;
24 | border-left-style: solid;
25 | }
26 | .border-r-solid {
27 | @apply border-r;
28 | border-right-style: solid;
29 | }
30 | .border-t-solid {
31 | @apply border-t;
32 | border-top-style: solid;
33 | }
34 |
35 | .piece {
36 | @apply shadow-sm bg-white p-4;
37 | }
38 |
39 | /* @tailwind base;不使用初始化CSS */
40 | /* @tailwind components;组件也不用 */
41 | @tailwind utilities;
42 |
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | mode: "jit",
4 | content: ["./src/**/*.{tsx,html,ts}"],
5 | theme: {
6 | extend: {
7 | colors: {
8 | "statistics-cyan-color": "rgba(14, 253, 255, 1)",
9 | "statistics-cyan-border-color": "rgba(14, 253, 255, 0.5)",
10 | },
11 | },
12 | screens: {
13 | // 设置PC端优先
14 | sm: { max: "768px" },
15 | },
16 | },
17 | plugins: [require("tailwind-scrollbar")],
18 | };
19 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["src/*"],
20 | "@type/*": ["types/*"],
21 | "@axios": ["src/plugin/axios.ts"],
22 | "@dayjs": ["src/plugin/dayjs.ts"]
23 | },
24 | "plugins": [
25 | {
26 | "name": "next"
27 | }
28 | ]
29 | },
30 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "env/index.js", ".next/types/**/*.ts"],
31 | "exclude": ["node_modules"]
32 | }
33 |
--------------------------------------------------------------------------------
/client/types/common/response.ts:
--------------------------------------------------------------------------------
1 | /** 服务端返回数据模板*/
2 | interface response {
3 | success: boolean;
4 | message: string;
5 | data: T;
6 | }
7 | export type { response };
8 |
--------------------------------------------------------------------------------
/client/types/common/user-data.ts:
--------------------------------------------------------------------------------
1 | import type { UserAttributes } from "../model-attribute";
2 |
3 | type userDataType = Pick<
4 | UserAttributes,
5 | "id" | "name" | "auth" | "avatar_file_name" | "avatar_url"
6 | >;
7 | export type { userDataType };
--------------------------------------------------------------------------------
/client/types/folder.ts:
--------------------------------------------------------------------------------
1 | // upload组件上传时上传的文件夹
2 | type target = "type" | "article" | "cover" | "avatar" | "comment" | "advertisement" | "link";
3 | export type { target };
4 |
--------------------------------------------------------------------------------
/client/types/model/advertisement.ts:
--------------------------------------------------------------------------------
1 | export interface advertisementType {
2 | id: number;
3 | position: string;
4 | indexes: number;
5 | poster_file_name: string;
6 | url?: string;
7 | create_time: Date;
8 | }
9 |
--------------------------------------------------------------------------------
/client/types/model/article-comment.ts:
--------------------------------------------------------------------------------
1 | import type { CommentAttributes, UserAttributes } from "@type/model-attribute";
2 | export type user_data = Pick<
3 | UserAttributes,
4 | "id" | "name" | "auth" | "avatar_file_name" | "avatar_url"
5 | >;
6 | export type reply = {
7 | content: string;
8 | user_data: user_data;
9 | };
10 | /** 文章评论中的单个评论类型*/
11 | export interface articleCommentType extends Omit {
12 | reply: null | reply;
13 | user_data: user_data;
14 | children: articleCommentType[];
15 | }
16 |
--------------------------------------------------------------------------------
/client/types/model/article-list-item.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | ArticleAttributes,
3 | TagAttributes,
4 | UserAttributes,
5 | } from "@type/model-attribute";
6 |
7 | /** 文章列表,单个文章主要的字段类型*/
8 | type articleListItemType = Pick<
9 | ArticleAttributes,
10 | | "id"
11 | | "title"
12 | | "description"
13 | | "view_count"
14 | | "cover_url"
15 | | "update_time"
16 | | "create_time"
17 | | "comment_count"
18 | | "like_count"
19 | | "state"
20 | > & {
21 | tag: Pick[];
22 | author_data: Pick;
23 | };
24 | export type { articleListItemType };
25 |
--------------------------------------------------------------------------------
/client/types/response.ts:
--------------------------------------------------------------------------------
1 | interface response {
2 | success:boolean;
3 | message:string;
4 | data:T
5 | }
6 | export type { response };
7 |
--------------------------------------------------------------------------------
/client/types/type.ts:
--------------------------------------------------------------------------------
1 | interface TagAttributes {
2 | id: number;
3 | name: string;
4 | belong_id?: number;
5 | icon_file_name?: string;
6 | description?: string;
7 | create_time: Date;
8 | }
9 | interface TagTreeAttributes extends TagAttributes {
10 | children?: TagAttributes[];
11 | }
12 | export type { TagAttributes, TagTreeAttributes };
13 |
--------------------------------------------------------------------------------
/dev.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | start cmd /k "cd server&&yarn dev"
3 | start cmd /k "cd client&&yarn dev"
4 |
--------------------------------------------------------------------------------
/intall.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | start cmd /k "cd server &&npm remove -g pm2&&npm i cross-env @socket.io/pm2 husky ts-node -g&& yarn config set sharp_binary_host https://npmmirror.com/mirrors/sharp && yarn config set sharp_libvips_binary_host https://npmmirror.com/mirrors/sharp-libvips && yarn&& exit"
3 | start cmd /k "cd client&&yarn&& exit"
4 |
--------------------------------------------------------------------------------
/lint.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | start cmd /k "cd server &&yarn prettier --write "src/**/*.{js,ts}"&& exit"
3 | start cmd /k "cd client&&yarn prettier --write "src/**/*.{js,jsx,ts,tsx}"&& exit"
4 |
--------------------------------------------------------------------------------
/server/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 |
4 | const logDirectory = path.join(__dirname, "log");
5 |
6 | if (!fs.existsSync(logDirectory)) {
7 | fs.mkdirSync(logDirectory);
8 | }
9 |
10 | module.exports = {
11 | apps: [
12 | {
13 | name: "blog-server", //不要修改
14 | args:
15 | process.env.npm_lifecycle_event == "start:debug"
16 | ? "--inspect=0.0.0.0:9229"
17 | : undefined,
18 | script: "./src/index.js",
19 | exec_mode: "cluster",
20 | instances: 2,
21 | max_memory_restart: "1500M",
22 | listen_timeout: 3000,
23 | min_uptime: "60s",
24 | max_restarts: 0,
25 | error_file: path.join(logDirectory, "error.log"), // 错误日志文件
26 | out_file: path.join(logDirectory, "out.log"), // 标准输出日志文件
27 | // log_date_format: "YYYY-MM-DD HH:mm:ss", // 设置日志时间戳格式
28 | env: {
29 | ENV: "production",
30 | },
31 | },
32 | ],
33 | };
34 |
--------------------------------------------------------------------------------
/server/env/.env.template:
--------------------------------------------------------------------------------
1 | # 站点信息
2 | SITE_API_HOST=http://localhost:3000
3 | SITE_NAME=网络日志
4 | CLIENT_HOST=http://localhost:5678
5 | CLIENT_CDN=http://localhost:5678
6 |
7 | # 验证方式jwt|session
8 | AUTH_MODE=session
9 |
10 | # MySQL数据库内容
11 | DB_MYSQL_HOST=localhost
12 | DB_MYSQL_USER=root
13 | DB_MYSQL_PORT=3306
14 | DB_MYSQL_PASSWORD=123456
15 |
16 | # 云服务器(OSS/CDN)
17 | CDN=
18 |
19 | # ali/qiniu
20 | CLOUD_SERVER=qiniu
21 |
22 | CLOUD_SERVER_ACCESS_KEY_ID=
23 | CLOUD_SERVER_ACCESS_KEY_SECRET=
24 | # 七牛云无需填写
25 | OSS_REGION=huabei
26 | OSS_BUCKET=
27 |
28 | # 百度地图AK
29 | BAIDU_MAP_AK=
30 |
31 | # GitHub的ID和CS
32 | GITHUB_CLIENT_ID=b1099c9c62ebb6ff2d87
33 | GITHUB_CLIENT_SECRETS=
34 |
35 | # Redis内容
36 | DB_REDIS_HOST=localhost
37 | DB_REDIS_USER=
38 | DB_REDIS_PORT=6379
39 | DB_REDIS_PASSWORD=""
40 |
41 | # 邮箱内容
42 | EMAIL_USER=
43 | EMAIL_KEY=
44 |
45 | # 秘钥
46 | KEY=
47 |
48 |
49 |
--------------------------------------------------------------------------------
/server/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 80,
3 | tabWidth: 2,
4 | trailingComma: "all",
5 | singleQuote: false,
6 | semi: true,
7 | plugins: ["@trivago/prettier-plugin-sort-imports"],
8 | importOrder: [
9 | "^./app$",//配置要第一行引入
10 | "^module-alias$", // Ensure module-alias is imported first
11 | 'moduleAlias.addAlias("@", __dirname);', // Ensure any submodules of module-alias are imported next
12 | "^node$", // Node.js 模块
13 | "^koa$", // Koa
14 | "^@koa/(.*)$", // Koa 相关模块
15 | "", // 其他第三方模块
16 | "^sequelize$", // Sequelize
17 | "^@/db/(.*)$", // @/db 模块
18 | "^@types/(.*)$", // 类型声明
19 | "^@/common/(.*)$", // @/common 模块
20 | "^[./]", // 当前目录及更深层的相对路径
21 | "^[../]", // 父目录及更深层的相对路径
22 | ],
23 | importOrderSeparation: false, // 不进行换行
24 | importOrderSortSpecifiers: true,
25 | importOrderParserPlugins: ["typescript"],
26 | };
27 |
--------------------------------------------------------------------------------
/server/public/prism.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lrunlin/blog/342bb962c6f7b94b798311ee934315fc1b924157/server/public/prism.zip
--------------------------------------------------------------------------------
/server/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:/
3 | Sitemap:https://blogweb.cn/sitemap.xml
--------------------------------------------------------------------------------
/server/scripts/modules/compile.ts:
--------------------------------------------------------------------------------
1 | import * as ts from "typescript";
2 | import fs from "fs";
3 |
4 | /** 编译单个TS文件为JS和map*/
5 | function compile(path: string) {
6 | let content = fs.readFileSync(path).toString();
7 | const { outputText } = ts.transpileModule(content, {
8 | compilerOptions: {
9 | module: ts.ModuleKind.CommonJS,
10 | target: 4, //4是2017,2是2015
11 | moduleResolution: 2,
12 | esModuleInterop: true,
13 | strict: true,
14 | removeComments: true,
15 | },
16 | });
17 | return outputText;
18 | }
19 |
20 | export default compile;
21 |
22 |
23 | // const { readFileSync, writeFileSync } = require("fs");
24 | // const { transform, transformSync } = require("@swc/core");
25 | // const code = readFileSync("./advertisement.ts", "utf-8");
26 | // let a = transform(code, {
27 | // jsc: {
28 | // parser: {
29 | // syntax: "typescript",
30 | // decorators: true,
31 | // },
32 | // target: "es2015",
33 | // },
34 | // });
35 |
--------------------------------------------------------------------------------
/server/scripts/modules/copyDir.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs/promises";
2 |
3 | async function copyDir(src: string, dist: string): Promise {
4 | // 复制目录
5 | return await fs.cp(src, dist, { recursive: true });
6 | }
7 |
8 | export default copyDir;
9 |
--------------------------------------------------------------------------------
/server/scripts/modules/countFile.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from "path";
3 |
4 | /**
5 | * 获取文件夹内文件数量
6 | */
7 | function countFile(filePath:string) {
8 | let fileCount = 0;
9 | function count(filePath:string) {
10 | let files = fs.readdirSync(filePath);
11 | for (let index = 0; index < files.length; index++) {
12 | let filename = files[index];
13 | let filedir = path.join(filePath, filename); //拼接路径用于app.use
14 | let stats = fs.statSync(filedir);
15 | let isFile = stats.isFile();
16 | let isDir = stats.isDirectory();
17 | if (isFile) {
18 | fileCount++
19 | }
20 | if (isDir) {
21 | count(filedir);
22 | }
23 | }
24 | }
25 | count(filePath)
26 | return fileCount;
27 | }
28 | export default countFile;
--------------------------------------------------------------------------------
/server/scripts/modules/deleteDir.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs/promises";
2 | import path from "path";
3 |
4 | async function directoryExists(dirPath: string): Promise {
5 | try {
6 | await fs.access(dirPath);
7 | return true;
8 | } catch {
9 | return false;
10 | }
11 | }
12 |
13 | /** 递归删除文件夹 */
14 | async function deleteDir(dirpath: string): Promise {
15 | try {
16 | // 检查目录是否存在
17 | if (!(await directoryExists(dirpath))) {
18 | return;
19 | }
20 |
21 | const fileList = await fs.readdir(dirpath);
22 | await Promise.all(
23 | fileList.map(async file => {
24 | const filePath = path.resolve(dirpath, file);
25 | const fileInfo = await fs.stat(filePath);
26 | if (fileInfo.isFile()) {
27 | await fs.unlink(filePath);
28 | } else if (fileInfo.isDirectory()) {
29 | await deleteDir(filePath);
30 | }
31 | })
32 | );
33 |
34 | await fs.rmdir(dirpath);
35 | } catch (err) {
36 | console.error(`Error deleting directory ${dirpath}:`, err);
37 | }
38 | }
39 |
40 | export default deleteDir;
41 |
--------------------------------------------------------------------------------
/server/scripts/start.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "child_process";
2 | import * as os from "os";
3 |
4 | const processName = "blog-server"; // 替换为你的进程名称
5 |
6 | try {
7 | // 根据操作系统选择命令
8 | const command =
9 | os.platform() === "win32"
10 | ? `pm2 list | findstr "${processName}"`
11 | : `pm2 list | grep "${processName}"`;
12 |
13 | // 执行命令并获取输出
14 | const stdout = execSync(command).toString();
15 |
16 | // 检查输出是否包含进程名称
17 | if (stdout) {
18 | // 执行 pm2 关闭
19 | execSync("pm2 stop blog-server");
20 | execSync("pm2 delete blog-server");
21 | } else {
22 | }
23 | } catch (error) {
24 | console.error(`Error: ${(error as Error).message}`);
25 | }
26 |
--------------------------------------------------------------------------------
/server/src/common/middleware/auth/getUserId.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Next } from "koa";
2 | import verify from "../../utils/auth/verify";
3 |
4 | /**
5 | * 获取用户ID以及身份
6 | *
7 | */
8 | const getUserId = async (ctx: Context, next: Next) => {
9 | let token = ctx.headers.authorization;
10 | if (!token) {
11 | await next();
12 | return;
13 | }
14 | await verify(token as string)
15 | .then(async (decoded: any) => {
16 | ctx.id = decoded.id;
17 | ctx.auth = decoded.auth;
18 | ctx.token = token;
19 | })
20 | .catch(() => {})
21 | .finally(async () => {
22 | await next();
23 | });
24 | };
25 | export default getUserId;
26 |
--------------------------------------------------------------------------------
/server/src/common/middleware/auth/index.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Next } from "koa";
2 | import verify from "@/common/utils/auth/verify";
3 |
4 | type authCode = 1 | 0;
5 |
6 | /**
7 | * 传递数字或者数组进行权限判断,如果通过设置id和status
8 | * @params auth {number[] | number} 身份代码
9 | */
10 | function auth(auth?: authCode[] | authCode) {
11 | return async (ctx: Context, next: Next) => {
12 | let token = ctx.headers.authorization;
13 | if (!token) {
14 | ctx.status = 401;
15 | return;
16 | }
17 | // ?管理员可以访问全部接口
18 | let authList = typeof auth == "number" ? [1, auth] : [1, ...(auth || [])];
19 | await verify(token as string)
20 | .then(async (decoded: any) => {
21 | if (authList.includes(decoded.auth)) {
22 | ctx.id = decoded.id;
23 | ctx.auth = decoded.auth;
24 | ctx.token = token;
25 | await next();
26 | } else {
27 | ctx.status = 401;
28 | }
29 | })
30 | .catch((err) => {
31 | // console.log(ctx.path, err);
32 | ctx.status = 401;
33 | });
34 | };
35 | }
36 | export default auth;
37 |
--------------------------------------------------------------------------------
/server/src/common/middleware/cache/index.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Next } from "koa";
2 | import redis from "@/common/utils/redis";
3 |
4 | /**
5 | * 传递数字或者数组进行权限判断,如果通过设置id和status
6 | * @params auth {number[] | number} 身份代码
7 | */
8 | async function cache(
9 | key: (ctx: Context) => string,
10 | allowSaveValue: (ctx: Context) => any,
11 | seconds?: number,
12 | ) {
13 | return async (ctx: Context, next: Next) => {
14 | let _key = key(ctx);
15 | let data = await redis.get("cache-" + _key);
16 | if (data) {
17 | ctx.body = JSON.parse(data);
18 | } else {
19 | await next();
20 | let _allowSaveValue = allowSaveValue(ctx);
21 | if (_allowSaveValue) {
22 | redis.set(_key, JSON.stringify(ctx.body), "EX", seconds || 300);
23 | }
24 | }
25 | };
26 | }
27 | export default cache;
28 |
--------------------------------------------------------------------------------
/server/src/common/middleware/verify/validator.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Next } from "koa";
2 | import type { ObjectSchema } from "joi";
3 |
4 | /** 对query和body的参数进行验证*/
5 | export default function validator(
6 | /** Joi配置*/
7 | schema: ObjectSchema,
8 | /** 是否ctx.params参数*/
9 | isParams?: boolean,
10 | /** 是否允许其他参数*/
11 | allowUnknown?: boolean,
12 | ) {
13 | return async (ctx: Context, next: Next) => {
14 | let validate = schema.validate(
15 | isParams
16 | ? ctx.params
17 | : "DELETE,GET".includes(ctx.method)
18 | ? ctx.request.query
19 | : ctx.request.body,
20 | { allowUnknown: !!allowUnknown },
21 | );
22 | if (validate.error) {
23 | ctx.status = 400;
24 | ctx.body = {
25 | success: false,
26 | message: validate.error?.message || "请求参数错误",
27 | };
28 | } else {
29 | await next();
30 | }
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/server/src/common/middleware/verify/validatorAsync.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Next } from "koa";
2 | import type { ObjectSchema } from "joi";
3 |
4 | /** 对query和body的参数进行验证(支持异步)*/
5 | export default function validator(
6 | schema: ObjectSchema,
7 | isParams?: boolean,
8 | allowUnknown?: boolean,
9 | ) {
10 | return async (ctx: Context, next: Next) => {
11 | await schema
12 | .validateAsync(
13 | isParams
14 | ? ctx.params
15 | : "DELETE,GET".includes(ctx.method)
16 | ? ctx.request.query
17 | : ctx.request.body,
18 | { allowUnknown: allowUnknown },
19 | )
20 | .then(async (res) => {
21 | await next();
22 | })
23 | .catch((err) => {
24 | ctx.status = 400;
25 | ctx.body = { success: false, message: err + "" || "请求参数错误" };
26 | });
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/server/src/common/modules/article/get/img-add-prefix.ts:
--------------------------------------------------------------------------------
1 | import { load } from "cheerio";
2 |
3 | /**对于文章中的图片标签进行处理*/
4 | function setImageTag(
5 | content: string,
6 | prefix: "article" | "problem" | "answer",
7 | alt?: string,
8 | ) {
9 | let $ = load(content);
10 | $("img").each((i, el) => {
11 | $(el)
12 | .attr("src", `${process.env.CDN}/${prefix}/${$(el).attr("src")}`)
13 | .attr("loading", "lazy")
14 | .attr("alt", alt);
15 | });
16 | return $("body").html() as string;
17 | }
18 | export default setImageTag;
19 |
--------------------------------------------------------------------------------
/server/src/common/modules/article/get/set-code-block-language.ts:
--------------------------------------------------------------------------------
1 | import { load } from "cheerio";
2 |
3 | /**
4 | * todo 为文章表设置language字段,用来判断代码块使用到了哪些语言
5 | * TODO 并且设置代码高亮使用的插件
6 | * ?用于用户查询时
7 | */
8 | function getCodeBlockLanguage(content: string) {
9 | let $ = load(content);
10 | let languages: string[] = [];
11 | $("pre").each((_, el) => {
12 | /**获取pre和code的class转为数组*/
13 | let allClassNames = `${$(el).attr("class")} ${$(el)
14 | .children("code")
15 | .eq(0)
16 | .attr("class")}`.split(" ");
17 | let hasClassNames = allClassNames.find((item) =>
18 | item.includes("language-"),
19 | );
20 | let language = hasClassNames
21 | ? hasClassNames.replace("language-", "")
22 | : false;
23 | if (language) {
24 | if (!languages.includes(language)) languages.push(language);
25 | }
26 |
27 | // 开启代码行数的显示
28 | $(el).addClass("line-numbers");
29 | });
30 | return {
31 | content: $("body").html() as string,
32 | language: languages.length ? languages : null,
33 | };
34 | }
35 | export default getCodeBlockLanguage;
36 |
--------------------------------------------------------------------------------
/server/src/common/modules/article/get/set-description.ts:
--------------------------------------------------------------------------------
1 | import { load } from "cheerio";
2 | import type { ArticleAttributes } from "@/db/models/article";
3 |
4 | type paramsType = Pick;
5 | /**
6 | * todo对文章表的description字段进行加工,在没有description时设置为前200的text
7 | * ?用于用户查询时
8 | * @params content {string} 文章内容
9 | * @params length {number} description长度(默认200)
10 | * @return article {Article} 处理好的文章数据
11 | */
12 | function setDescription(content: string, length: number = 200) {
13 | let $ = load(content);
14 | $("pre,code,a,table,ul,ol").remove();
15 |
16 | let description = $("body")
17 | .text()
18 | .replace(/ /g, "")
19 | .replace(/\s/g, "")
20 | .replace(/\n/g, "")
21 | .substring(0, length);
22 |
23 | return description;
24 | }
25 | export default setDescription;
26 |
--------------------------------------------------------------------------------
/server/src/common/modules/article/get/set-title-id.ts:
--------------------------------------------------------------------------------
1 | import { load } from "cheerio";
2 | import { ArticleAttributes } from "@/db/models/article";
3 |
4 | /** 设置h1-h6的id*/
5 | function setTitleId(content: string) {
6 | let $ = load(content);
7 | let title = $("h1,h2,h3,h4,h5,h6");
8 | title.each((index, item) => {
9 | $(item).attr("id", `heading-${index}`);
10 | });
11 |
12 | return {
13 | content: $("body").html() as string,
14 | display_directory: title.length > 3,
15 | };
16 | }
17 | export default setTitleId;
18 |
--------------------------------------------------------------------------------
/server/src/common/modules/article/select/getBloggerList.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 |
3 | /**
4 | * 查询某用户关注的博主
5 | * @params id {number} 用户ID
6 | * @return bloggerList {number[]} 博主的id列表
7 | */
8 | async function getBloggerList(id: number) {
9 | return await DB.Follow.findAll({
10 | where: { user_id: id, type: "user" },
11 | attributes: ["belong_id"],
12 | })
13 | .then(
14 | (rows) =>
15 | rows.map((item) => item.toJSON().belong_id) as unknown as number[],
16 | )
17 | .catch(() => [] as number[]);
18 | }
19 | export default getBloggerList;
20 |
--------------------------------------------------------------------------------
/server/src/common/modules/article/select/getTypeChildrenTag.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 |
3 | /**
4 | * 根据type返回所属的tag
5 | * @params id {number} type的id
6 | * @return tagID {number[]} 归属于该type的tag_id
7 | */
8 | async function getTypeChildrenTag(id: number) {
9 | return await DB.Tag.findAll({ where: { belong_id: id }, attributes: ["id"] })
10 | .then((rows) => rows.map((item) => item.toJSON().id) as unknown as number[])
11 | .catch(() => [] as number[]);
12 | }
13 |
14 | export default getTypeChildrenTag;
15 |
--------------------------------------------------------------------------------
/server/src/common/modules/cache/external-link/index.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import { LRUCache } from "lru-cache";
3 | import { ExternalLinkAttributes } from "@/db/models/external_link";
4 |
5 | /**
6 | * 存储外链列表
7 | */
8 | const cache = new LRUCache<"list", ExternalLinkAttributes["href"][]>({
9 | max: 20,
10 | });
11 |
12 | /** 刷新缓存数据*/
13 | function setData() {
14 | DB.ExternalLink.findAll({ raw: true }).then((rows) => {
15 | cache.set(
16 | "list",
17 | rows.map((item) => item.href),
18 | );
19 | });
20 | }
21 |
22 | setTimeout(() => {
23 | setData();
24 | }, 0);
25 |
26 | function getData() {
27 | return cache.get("list");
28 | }
29 |
30 | export default cache;
31 | export { getData, setData, cache };
32 |
--------------------------------------------------------------------------------
/server/src/common/modules/comment/get-comment-childrnen-list.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 |
3 | /** 根据传递的评论ID查找出全部子评论的ID*/
4 | async function getCommentChildrenList(id: number) {
5 | let idHub: number[] = [id];
6 | async function _collectCommentID(_id: number) {
7 | await DB.Comment.findAll({
8 | where: { reply: _id },
9 | attributes: ["id"],
10 | }).then(async (rows) => {
11 | for (let index = 0; index < rows.length; index++) {
12 | const id = rows[index].id;
13 | idHub.push(id);
14 | await _collectCommentID(id);
15 | }
16 | });
17 | }
18 | await _collectCommentID(id);
19 | return idHub;
20 | }
21 | export default getCommentChildrenList;
22 |
--------------------------------------------------------------------------------
/server/src/common/modules/getAllRouter.ts:
--------------------------------------------------------------------------------
1 | import { globSync } from "glob";
2 | import path from "path";
3 | import getFilePath from "./getFilePath";
4 |
5 | export default getFilePath("getAllRouter", [__dirname, "../../routes"], () =>
6 | globSync([`**/*.js`, `**/*.ts`], {
7 | cwd: path.join(__dirname, "../../routes"),
8 | }),
9 | );
10 |
--------------------------------------------------------------------------------
/server/src/common/modules/getFilePath.ts:
--------------------------------------------------------------------------------
1 | import * as OS from "os";
2 | import * as path from "path";
3 | import { pathToFileURL } from "url";
4 |
5 | let pathMap: { [key: string]: string[] } = {};
6 |
7 | function getFilePath(
8 | key: string,
9 | pathSegments: string[],
10 | getPath: () => string[],
11 | ) {
12 | // 根据当前操作系统生成正确的文件路径
13 | const resolvePath = (item: string) => {
14 | // 动态拼接路径
15 | const fullPath = path.join(...pathSegments, item);
16 |
17 | return process.env.ENV === "development"
18 | ? pathToFileURL(fullPath).href
19 | : fullPath;
20 | };
21 |
22 | // 检查当前环境是否为开发环境
23 | if (process.env.ENV === "development") {
24 | // 每次都生成最新的路径数组
25 | const paths = getPath().map(resolvePath);
26 | return paths;
27 | } else {
28 | // 在生产环境中,缓存路径数组
29 | if (!pathMap[key]) {
30 | const paths = getPath().map(resolvePath);
31 | pathMap[key] = paths;
32 | }
33 | // 返回缓存的路径数组
34 | return pathMap[key];
35 | }
36 | }
37 |
38 | export default getFilePath;
39 |
--------------------------------------------------------------------------------
/server/src/common/modules/github/getGithubName.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | /** 根据code获取github用户名*/
4 | async function getGithubName(code: string) {
5 | const accessToken = await axios
6 | .post(
7 | "https://github.com/login/oauth/access_token",
8 | {
9 | client_id: process.env.GITHUB_CLIENT_ID,
10 | client_secret: process.env.GITHUB_CLIENT_SECRETS,
11 | code: code,
12 | },
13 | {
14 | headers: {
15 | accept: "application/json",
16 | },
17 | },
18 | )
19 | .then((res) => res.data.access_token)
20 | .catch(() => false as false);
21 |
22 | if (!accessToken) {
23 | return false;
24 | }
25 |
26 | const githubData = await axios({
27 | method: "get",
28 | url: `https://api.github.com/user`,
29 | headers: {
30 | accept: "application/json",
31 | Authorization: `token ${accessToken}`,
32 | },
33 | })
34 | .then((res) => res.data.login)
35 | .catch(() => false as false);
36 |
37 | return githubData || false;
38 | }
39 | export default getGithubName;
40 |
--------------------------------------------------------------------------------
/server/src/common/modules/github/updata.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 |
3 | /** 更新用户的github字段*/
4 | async function updateGithub(user_id: number, github_name: string) {
5 | return await DB.User.update(
6 | { github: github_name },
7 | { where: { id: user_id } },
8 | )
9 | .then((res) => {
10 | if (res[0]) {
11 | return {
12 | success: true,
13 | message: `成功将Github用户绑定为:${github_name}`,
14 | };
15 | } else {
16 | return { success: false, message: "请不要重复绑定" };
17 | }
18 | })
19 | .catch(() => {
20 | return { success: false, message: "绑定失败" };
21 | });
22 | }
23 | export default updateGithub;
24 |
--------------------------------------------------------------------------------
/server/src/common/modules/notice/follow/follow-article.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import type { NoticeAttributes } from "@/db/models/notice";
3 |
4 | // 转换 follow-article 类型的通知
5 | async function switchNoticeFollowProblem(data: NoticeAttributes) {
6 | /** 查询文章数据*/
7 | let articleData = await DB.Article.findByPk(data.relation_id, {
8 | attributes: ["id", "author", "title"],
9 | raw: true,
10 | })
11 | .then((row) => row)
12 | .catch((err) => {
13 | return false as false;
14 | });
15 |
16 | if (!articleData) return false;
17 |
18 | let userData = await DB.User.findByPk(articleData.author, {
19 | attributes: ["id", "name", "avatar_file_name", "avatar_url"],
20 | });
21 |
22 | return {
23 | ...data,
24 | label: {
25 | type: "article",
26 | user_data: userData,
27 | content_data: articleData,
28 | },
29 | };
30 | }
31 | export default switchNoticeFollowProblem;
32 |
--------------------------------------------------------------------------------
/server/src/common/modules/notice/follow/follow-problem.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import type { NoticeAttributes } from "@/db/models/notice";
3 |
4 | // 转换 follow-problem 类型的通知
5 | async function switchNoticeFollowProblem(data: NoticeAttributes) {
6 | /** 查询问题的id和标题*/
7 | let problemData = await DB.Problem.findByPk(data.relation_id, {
8 | attributes: ["id", "title", "author"],
9 | raw: true,
10 | })
11 | .then((row) => row)
12 | .catch(() => false as false);
13 | if (!problemData) return false;
14 |
15 | let userData = await DB.User.findByPk(problemData.author, {
16 | attributes: ["id", "name", "avatar_file_name", "avatar_url"],
17 | });
18 |
19 | return {
20 | ...data,
21 | label: {
22 | type: "problem",
23 | user_data: userData,
24 | content_data: problemData,
25 | },
26 | };
27 | }
28 | export default switchNoticeFollowProblem;
29 |
--------------------------------------------------------------------------------
/server/src/common/modules/user/isEmailDestroy.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import sha256 from "@/common/utils/sha256";
3 |
4 | async function isEmailDestroy(email: string) {
5 | let decode = sha256(email);
6 | let exist = await DB.User.findOne({
7 | where: { email: `${decode}@destroy.com` },
8 | attributes: ["id"],
9 | raw: true,
10 | });
11 | return !!exist;
12 | }
13 | export default isEmailDestroy;
14 |
--------------------------------------------------------------------------------
/server/src/common/tasks/advertisement.ts:
--------------------------------------------------------------------------------
1 | import { setData } from "../modules/cache/advertisement";
2 |
3 | export default setData;
4 |
--------------------------------------------------------------------------------
/server/src/common/tasks/auto-delete-recommend.ts:
--------------------------------------------------------------------------------
1 | import sequelize from "@/db/config";
2 |
3 | /** 删除推荐表中存在但是文章表中不存在的数据*/
4 | //?管理员有手动删除文章表中数据的可能
5 | async function autoDeleteRecommend() {
6 | await sequelize.query(`
7 | DELETE
8 | FROM recommend
9 | WHERE NOT EXISTS (
10 | SELECT 1
11 | FROM article
12 | WHERE article.id = recommend.id
13 | );
14 | `);
15 | }
16 |
17 | export default () => {
18 | autoDeleteRecommend();
19 | setInterval(() => {
20 | autoDeleteRecommend();
21 | }, 3_600_000);
22 | };
23 |
--------------------------------------------------------------------------------
/server/src/common/tasks/index.ts:
--------------------------------------------------------------------------------
1 | import { globSync } from "glob";
2 | import getFilePath from "../modules/getFilePath";
3 |
4 | function start() {
5 | if ([undefined, "0"].includes(process.env.NODE_APP_INSTANCE)) {
6 | getFilePath("getTasks", [__dirname], () =>
7 | globSync([`**/*.js`, `**/*.ts`], {
8 | ignore: ["index.js", "index.ts"],
9 | cwd: __dirname,
10 | }),
11 | ).forEach((item) => {
12 | import(item).then((res) => {
13 | res.default && res.default();
14 | });
15 | });
16 | }
17 | }
18 | export default start;
19 |
--------------------------------------------------------------------------------
/server/src/common/tasks/oss/delete-redis-cache.ts:
--------------------------------------------------------------------------------
1 | import redis from "@/common/utils/redis";
2 |
3 | /** 删除OSS模块中缓存的状态*/
4 | async function deleteRedisCache() {
5 | await redis.del([
6 | "oss-key-code",
7 | "oss-key-delete_code",
8 | "oss-key-last_time",
9 | "oss-key-list",
10 | ]);
11 | }
12 |
13 | export default () => {
14 | deleteRedisCache();
15 | };
16 |
--------------------------------------------------------------------------------
/server/src/common/tasks/recommend/index.ts:
--------------------------------------------------------------------------------
1 | //更新文章推荐表
2 | import DB from "@/db";
3 | import { setArticleListWrite } from "@/common/modules/tasks/set-recommend-data";
4 |
5 | export default async () => {
6 | // 只有在文章表有内容,但是推荐表没内容时才进行初始化
7 | Promise.all([
8 | DB.Article.count({ where: { state: 1 } }),
9 | DB.Recommend.count(),
10 | ]).then(async (res) => {
11 | if (res[0] && !res[1]) {
12 | setArticleListWrite();
13 | }
14 | });
15 | setInterval(() => {
16 | setArticleListWrite();
17 | }, 7_200_000);
18 | };
19 |
--------------------------------------------------------------------------------
/server/src/common/tasks/set-article-view-count.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import redis from "@/common/utils/redis";
3 |
4 | function setArticleViewCount() {
5 | setInterval(() => {
6 | redis
7 | .keys("*-unentered", (err, keys) => {
8 | if (err) {
9 | console.log(`获取文章历史记录keys错误:${err}`);
10 | return [];
11 | }
12 | })
13 | .then(async (keys) => {
14 | for (const key of keys) {
15 | let type = key.split("-")[1] as "article" | "problem"; //article、problem
16 | let id: string = key.split("-")[3];
17 | await (DB[type == "article" ? "Article" : "Problem"] as any)
18 | .increment("view_count", { where: { id } })
19 | .then(async () => {
20 | await redis
21 | .rename(key, key.replace("-unentered", ""))
22 | .catch(() => {});
23 | })
24 | .catch((err: any) => {
25 | console.log(err);
26 | });
27 | }
28 | });
29 | }, 21_600_000);
30 | }
31 | export default setArticleViewCount;
32 |
--------------------------------------------------------------------------------
/server/src/common/tasks/visualization/github.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import moment from "moment";
3 | import redis from "@/common/utils/redis";
4 |
5 | function getGtiHubData() {
6 | axios
7 | .get("https://api.github.com/repos/Lrunlin/blog")
8 | .then((res) => {
9 | const data = {
10 | star_count: res.data.stargazers_count,
11 | fork_count: res.data.forks_count,
12 | issues_count: res.data.open_issues,
13 | watch_count: res.data.subscribers_count,
14 | html_url: res.data.html_url,
15 | homepage: res.data.homepage,
16 | refresh_time: moment().format("MM-DD HH:mm:ss"),
17 | };
18 | redis.set("visualization-github", JSON.stringify(data));
19 | })
20 | .catch((err) => {
21 | //开发环境经常因为代码更新快而限制请求频率
22 | if (process.env.ENV == "production") {
23 | console.log(err);
24 | }
25 | });
26 | }
27 |
28 | export default () => {
29 | getGtiHubData();
30 | setInterval(() => {
31 | getGtiHubData();
32 | }, 3_600_000);
33 | };
34 |
--------------------------------------------------------------------------------
/server/src/common/transaction/comment/delete-comment.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import type { Transaction } from "sequelize/types";
3 |
4 | async function transaction(id: number[], t: Transaction) {
5 | return await DB.Notice.destroy({
6 | where: {
7 | relation_id: id,
8 | },
9 | transaction: t,
10 | })
11 | .then(() => true)
12 | .catch(() => false);
13 | }
14 | export default transaction;
15 |
--------------------------------------------------------------------------------
/server/src/common/transaction/follow/unfollow/index.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import type { Transaction } from "sequelize/types";
3 | import problem from "./problem";
4 | import user from "./user";
5 |
6 | let map = {
7 | user,
8 | problem,
9 | };
10 |
11 | /** 取消关注时的事务处理 */
12 | async function transaction(belong_id: number, user_id: number, t: Transaction) {
13 | /** 获取follow的type属性*/
14 | let type = await DB.Follow.findOne({
15 | where: {
16 | belong_id,
17 | user_id,
18 | },
19 | })
20 | .then((row) => row?.type as keyof typeof map)
21 | .catch(() => false as false);
22 | if (!type) return;
23 | return map[type](belong_id, user_id, t);
24 | }
25 | export default transaction;
26 |
--------------------------------------------------------------------------------
/server/src/common/transaction/follow/unfollow/problem.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import type { Transaction } from "sequelize/types";
3 |
4 | /** 删除关注时删除problem类型*/
5 | /** 删除所有该问题对本用户引起的通知*/
6 | async function problem(belong_id: number, user_id: number, t: Transaction) {
7 | let deleteNoticeRedult = await DB.Notice.destroy({
8 | where: {
9 | relation_id: belong_id,
10 | user_id,
11 | },
12 | transaction: t,
13 | })
14 | .then(() => true)
15 | .catch(() => false);
16 |
17 | return deleteNoticeRedult;
18 | }
19 | export default problem;
20 |
--------------------------------------------------------------------------------
/server/src/common/transaction/follow/unfollow/user.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import type { Transaction } from "sequelize/types";
3 |
4 | /** 删除关注时删除user类型*/
5 | /** 获取用户发布的文章、删除follow_article通知*/
6 | async function user(belong_id: number, user_id: number, t: Transaction) {
7 | /** 获取关注的用户发布的文章列表*/
8 | let articleList = await DB.Article.findAll({
9 | where: { author: belong_id },
10 | raw: true,
11 | attributes: ["id"],
12 | })
13 | .then((rows) => rows.map((item) => item.id))
14 | .catch(() => false as false);
15 | if (!articleList) return false;
16 | let deleteNoticeRedult = await DB.Notice.destroy({
17 | where: {
18 | relation_id: articleList,
19 | user_id,
20 | },
21 | transaction: t,
22 | })
23 | .then(() => true)
24 | .catch(() => false);
25 | return deleteNoticeRedult;
26 | }
27 | export default user;
28 |
--------------------------------------------------------------------------------
/server/src/common/utils/auth/destroy-session.ts:
--------------------------------------------------------------------------------
1 | import redis from "@/common/utils/redis";
2 |
3 | async function removeSession(id: number) {
4 | let retult = true;
5 | if (process.env.AUTH_MODE == "session") {
6 | try {
7 | let keys = await redis.keys(`auth_${id}_*`);
8 |
9 | await redis.del(keys);
10 | } catch (error) {
11 | retult = false;
12 | }
13 | }
14 | return retult;
15 | }
16 | export default removeSession;
17 |
--------------------------------------------------------------------------------
/server/src/common/utils/auth/sign-jwt.ts:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | interface decodeType {
4 | id: number;
5 | auth: number;
6 | [key: string]: any;
7 | }
8 |
9 | async function sign(decode: decodeType) {
10 | return jwt.sign(decode, process.env.KEY as string, { expiresIn: "365d" });
11 | }
12 | export default sign;
13 |
--------------------------------------------------------------------------------
/server/src/common/utils/auth/sign-session.ts:
--------------------------------------------------------------------------------
1 | import { v4 } from "uuid";
2 | import redis from "@/common/utils/redis";
3 |
4 | interface decodeType {
5 | id: number;
6 | auth: number;
7 | [key: string]: any;
8 | }
9 |
10 | async function signSession(decode: decodeType) {
11 | let session_id = "auth_" + decode.id + `_${v4().replace(/-/g, "")}`;
12 | await redis.set(session_id, JSON.stringify(decode), "EX", 365 * 24 * 60 * 60);
13 |
14 | return session_id;
15 | }
16 | export default signSession;
17 |
--------------------------------------------------------------------------------
/server/src/common/utils/auth/sign.ts:
--------------------------------------------------------------------------------
1 | import signJwt from "./sign-jwt";
2 | import signSwssion from "./sign-session";
3 |
4 | export default process.env.AUTH_MODE == "jwt" ? signJwt : signSwssion;
5 |
--------------------------------------------------------------------------------
/server/src/common/utils/auth/verify-jwt.ts:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | interface decodeType {
4 | [key: string]: any;
5 | }
6 |
7 | async function verify(token: string): Promise {
8 | return new Promise((resolve, reject) => {
9 | jwt.verify(
10 | token,
11 | process.env.KEY as string,
12 | async function (err, decoded: any) {
13 | if (err) {
14 | reject();
15 | } else {
16 | resolve(decoded);
17 | }
18 | },
19 | );
20 | });
21 | }
22 | export default verify;
23 |
--------------------------------------------------------------------------------
/server/src/common/utils/auth/verify-session.ts:
--------------------------------------------------------------------------------
1 | import redis from "@/common/utils/redis";
2 |
3 | interface decodeType {
4 | [key: string]: any;
5 | }
6 |
7 | async function verifySession(session_id: string) {
8 | let decode = await redis
9 | .get(session_id)
10 | .then((res) => (res ? JSON.parse(res) : null))
11 | .catch(() => null);
12 |
13 | return new Promise((resolve, reject) => {
14 | if (decode) {
15 | resolve(decode);
16 | } else {
17 | reject();
18 | }
19 | });
20 | }
21 | export default verifySession;
22 |
--------------------------------------------------------------------------------
/server/src/common/utils/auth/verify.ts:
--------------------------------------------------------------------------------
1 | import verifyJwt from "./verify-jwt";
2 | import verifySwssion from "./verify-session";
3 |
4 | export default process.env.AUTH_MODE == "jwt" ? verifyJwt : verifySwssion;
5 |
--------------------------------------------------------------------------------
/server/src/common/utils/email/index.ts:
--------------------------------------------------------------------------------
1 | import nodemailer from "nodemailer";
2 |
3 | interface paramsType {
4 | /** 接收人*/
5 | target: string;
6 | /** 主题*/
7 | subject: string;
8 | /** 内容HTML*/
9 | content: string;
10 | }
11 |
12 | let transporter = nodemailer.createTransport({
13 | service: "qq",
14 | port: 587,
15 | secure: false,
16 | auth: {
17 | user: process.env.EMAIL_USER,
18 | pass: process.env.EMAIL_KEY,
19 | },
20 | });
21 | function sendEmail(params: paramsType) {
22 | return new Promise((resolve, reject) => {
23 | let mailOptions = {
24 | from: process.env.EMAIL_USER,
25 | to: params.target,
26 | subject: params.subject,
27 | html: params.content,
28 | };
29 | transporter.sendMail(mailOptions, (error, info) => {
30 | if (error) {
31 | reject(error);
32 | } else {
33 | resolve();
34 | }
35 | });
36 | });
37 | }
38 | export default sendEmail;
39 |
--------------------------------------------------------------------------------
/server/src/common/utils/id.ts:
--------------------------------------------------------------------------------
1 | import random from "./random";
2 |
3 | /** 机器号(最大9)*/
4 | const WorkerID = Math.min(
5 | process.env.NODE_APP_INSTANCE ? +process.env.NODE_APP_INSTANCE : random(0, 9),
6 | 9,
7 | );
8 | /** 基础时间,时间戳从这个时间开始算*/
9 | const BaseTime = +new Date("2022-06-29");
10 | /** 最后一次设置ID的时间戳*/
11 | let lastTimeTick: undefined | Number;
12 | /** 最后一次生成的随机数*/
13 | let lastRandomNumberTick = random(1, 3);
14 |
15 | function id() {
16 | /** 时间戳*/
17 | let timestamp = +new Date() - BaseTime;
18 | lastRandomNumberTick =
19 | timestamp == lastTimeTick
20 | ? ++(lastRandomNumberTick as number)
21 | : random(0, 3);
22 | lastTimeTick = timestamp;
23 |
24 | return +`${timestamp}${WorkerID}${lastRandomNumberTick}`;
25 | }
26 |
27 | export default id;
28 |
--------------------------------------------------------------------------------
/server/src/common/utils/map.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 |
3 | /** 传入参数判断文章、答案、问题是否存在*/
4 | export type type = "article" | "problem" | "answer";
5 | function map(type: type, attributes?: string[]) {
6 | return {
7 | article: {
8 | db: (value: number) =>
9 | DB.Article.findOne({
10 | where: {
11 | id: value,
12 | state: 1,
13 | },
14 | attributes: attributes || ["id"],
15 | raw: true,
16 | }),
17 | message: "未找到指定的文章",
18 | },
19 | problem: {
20 | db: (value: number) =>
21 | DB.Problem.findByPk(value, {
22 | attributes: attributes || ["id"],
23 | raw: true,
24 | }),
25 | message: "未找到指定的问题",
26 | },
27 | answer: {
28 | db: (value: number) =>
29 | DB.Answer.findByPk(value, {
30 | attributes: attributes || ["id"],
31 | raw: true,
32 | }),
33 | message: "未找到指定的答案",
34 | },
35 | }[type];
36 | }
37 | export default map;
38 |
--------------------------------------------------------------------------------
/server/src/common/utils/random.ts:
--------------------------------------------------------------------------------
1 | /** 根据指定范围生成随机数(整数)*/
2 | const random = (min: number, max: number) =>
3 | Math.round(Math.random() * (max - min)) + min;
4 | export default random;
5 |
--------------------------------------------------------------------------------
/server/src/common/utils/redis.ts:
--------------------------------------------------------------------------------
1 | import ioredis from "ioredis";
2 |
3 | const redis = new ioredis({
4 | host: process.env.DB_REDIS_HOST || "127.0.0.1",
5 | port: process.env.DB_REDIS_PORT ? +process.env.DB_REDIS_PORT : 6379,
6 | password: process.env.DB_REDIS_PASSWORD,
7 | db: 0,
8 | username: process.env.DB_REDIS_USER,
9 | retryStrategy: function (times) {
10 | return Math.min(times * 50, 5000);
11 | },
12 | });
13 | export default redis;
14 |
--------------------------------------------------------------------------------
/server/src/common/utils/sha256.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 |
3 | function sha256(plaintext: string) {
4 | const sha256Hash = crypto.createHash("sha256");
5 | sha256Hash.update(plaintext);
6 | return sha256Hash.digest("hex");
7 | }
8 | export default sha256;
9 |
--------------------------------------------------------------------------------
/server/src/common/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | /** 使用Promise实现,需要传递等待时间(默认1秒)*/
2 | const sleep = (time: number = 1000) => {
3 | return new Promise((resolve) => {
4 | setTimeout(() => {
5 | resolve(null);
6 | }, time);
7 | });
8 | };
9 | export default sleep;
10 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/ali/deleteFile.ts:
--------------------------------------------------------------------------------
1 | import aliOSS from "./utils/oss";
2 |
3 | function deleteFile(images: string[]) {
4 | images = images.slice(0, 1000);
5 |
6 | return new Promise(async (success, error) => {
7 | aliOSS
8 | .deleteMulti(images, {
9 | quiet: true,
10 | })
11 | .then((res) => {
12 | success(res);
13 | })
14 | .catch((err) => {
15 | error(err);
16 | });
17 | });
18 | }
19 | export default deleteFile;
20 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/ali/imageInfo.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | function imageInfo(
4 | fileName: string,
5 | ): Promise<{ width: number; height: number; size: number }> {
6 | let url = `${process.env.CDN}${fileName}@info`;
7 | return new Promise(async (success, error) => {
8 | axios
9 | .get(url)
10 | .then((res) => {
11 | let { width, height, size } = res.data;
12 | if (width && height && size) {
13 | success({ width, height, size });
14 | } else {
15 | error("获取推广图片宽高错误:" + url);
16 | }
17 | })
18 | .catch((err) => {
19 | error(err);
20 | });
21 | });
22 | }
23 |
24 | export default imageInfo;
25 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/ali/listPrefix.ts:
--------------------------------------------------------------------------------
1 | import aliOSS from "./utils/oss";
2 |
3 | let listPrefix = (
4 | prefix: string,
5 | marker?: string,
6 | ): Promise<{ items: any[]; marker?: string }> =>
7 | new Promise(async (resolve, reject) => {
8 | await aliOSS
9 | .list({ prefix: `${prefix}/`, marker: marker, "max-keys": 1000 }, {})
10 | .then((res) => {
11 | resolve({
12 | items: res.objects.map((item) => ({
13 | key: item.name.replace(`${prefix}/`, ""),
14 | create_time: new Date(item.lastModified),
15 | })),
16 | marker: res.nextMarker,
17 | });
18 | })
19 | .catch((err) => {
20 | reject();
21 | });
22 | });
23 | export default listPrefix;
24 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/ali/refreshUrls.ts:
--------------------------------------------------------------------------------
1 | import Cdn, * as $Cdn from "@alicloud/cdn20180510";
2 | import OpenApi, * as $OpenApi from "@alicloud/openapi-client";
3 |
4 | export default function refreshUrls(url: string[]) {
5 | let config = new $OpenApi.Config({});
6 | // 您的AccessKey ID
7 | config.accessKeyId = process.env.CLOUD_SERVER_ACCESS_KEY_ID;
8 | // 您的AccessKey Secret
9 | config.accessKeySecret = process.env.CLOUD_SERVER_ACCESS_KEY_SECRET;
10 | // 访问的域名
11 | config.endpoint = "cdn.aliyuncs.com";
12 | let req = new $Cdn.RefreshObjectCachesRequest({});
13 | req.objectPath = url.join("\n");
14 | return new Cdn(config)
15 | .refreshObjectCaches(req)
16 | .then((res) => res)
17 | .catch((err) => err);
18 | }
19 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/ali/utils/oss.ts:
--------------------------------------------------------------------------------
1 | import OSS from "ali-oss";
2 |
3 | const aliOSS = new OSS({
4 | region: process.env.OSS_REGION,
5 | accessKeyId: process.env.CLOUD_SERVER_ACCESS_KEY_ID,
6 | accessKeySecret: process.env.CLOUD_SERVER_ACCESS_KEY_SECRET,
7 | bucket: process.env.OSS_BUCKET,
8 | });
9 | export default aliOSS;
10 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/folderList.ts:
--------------------------------------------------------------------------------
1 | const folderList = [
2 | { folder: "article", quality: 80, animated: true },
3 | { folder: "problem", quality: 80, animated: true },
4 | { folder: "answer", quality: 80, animated: true },
5 | { folder: "comment", quality: 80, animated: true },
6 | { folder: "advertisement", quality: 80, animated: true },
7 | { folder: "avatar", quality: 90, width: 80, height: 80 },
8 | { folder: "cover", quality: 100, width: 195, height: 130 },
9 | { folder: "tag", quality: 70, width: 80, height: 80 },
10 | { folder: "friendly-link", quality: 80, width: 80, height: 80 },
11 | ];
12 |
13 | export default folderList;
14 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/qiniu/deleteFile.ts:
--------------------------------------------------------------------------------
1 | import qiniu from "qiniu";
2 | import bucketManager from "./utils/bucketManager";
3 |
4 | function deleteFile(images: string[]) {
5 | images = images.slice(0, 100);
6 |
7 | const deleteOperations = images.map((item) => {
8 | return qiniu.rs.deleteOp(process.env.OSS_BUCKET, item);
9 | });
10 | return bucketManager
11 | .batch(deleteOperations)
12 | .then(({ data, resp }) => {
13 | if (Math.floor(resp.statusCode! / 100) === 2) {
14 | data.forEach(function (item) {
15 | if (item.code === 200) {
16 | return "全部成功";
17 | } else {
18 | return `删除文件错误:${data.filter((item) => item.code == 200).length}/${
19 | images.length
20 | }`;
21 | }
22 | });
23 | } else {
24 | return `删除错误`;
25 | }
26 | })
27 | .catch((err) => `删除错误: ${err}`);
28 | }
29 | export default deleteFile;
30 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/qiniu/imageInfo.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | function imageInfo(
4 | fileName: string,
5 | ): Promise<{ width: number; height: number; size: number }> {
6 | let url = `${process.env.CDN}${fileName}?imageInfo`;
7 | return new Promise(async (success, error) => {
8 | axios
9 | .get(url)
10 | .then((res) => {
11 | let { width, height, size } = res.data;
12 | if (width && height && size) {
13 | success({ width, height, size });
14 | } else {
15 | error("获取推广图片宽高错误:" + url);
16 | }
17 | })
18 | .catch((err) => {
19 | error(err);
20 | });
21 | });
22 | }
23 |
24 | export default imageInfo;
25 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/qiniu/listPrefix.ts:
--------------------------------------------------------------------------------
1 | import bucketManager from "./utils/bucketManager";
2 |
3 | let listPrefix = (
4 | prefix: string,
5 | marker?: string,
6 | ): Promise<{ items: any[]; marker?: string }> =>
7 | bucketManager
8 | .listPrefix(process.env.OSS_BUCKET, {
9 | limit: 1000,
10 | prefix: `${prefix}/`,
11 | marker: marker,
12 | })
13 | .then(({ data, resp }) => {
14 | return {
15 | items: data.items.map((item: any) => ({
16 | key: item.key.replace(`${prefix}/`, ""),
17 | create_time: new Date(item.putTime / 10000),
18 | })),
19 | marker: data.marker,
20 | };
21 | })
22 | .catch((err) => err);
23 |
24 | export default listPrefix;
25 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/qiniu/refreshUrls.ts:
--------------------------------------------------------------------------------
1 | import qiniu from "qiniu";
2 | import Mac from "./utils/Mac";
3 |
4 | function refreshUrls(list: string[]): Promise {
5 | let cdnManager = new qiniu.cdn.CdnManager(Mac);
6 | return new Promise((resolve, reject) => {
7 | cdnManager.refreshUrls(list, function (err, respBody, respInfo) {
8 | if (err) {
9 | reject(err);
10 | return;
11 | }
12 | if (respInfo.statusCode == 200) {
13 | resolve(undefined);
14 | } else {
15 | reject(`错误的响应码:${respInfo.statusCode}`);
16 | }
17 | });
18 | });
19 | }
20 | export default refreshUrls;
21 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/qiniu/utils/Mac.ts:
--------------------------------------------------------------------------------
1 | import qiniu from "qiniu";
2 |
3 | /** 七牛云Mac生成*/
4 | const Mac = new qiniu.auth.digest.Mac(
5 | process.env.CLOUD_SERVER_ACCESS_KEY_ID,
6 | process.env.CLOUD_SERVER_ACCESS_KEY_SECRET,
7 | );
8 | export default Mac;
9 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/qiniu/utils/bucketManager.ts:
--------------------------------------------------------------------------------
1 | import qiniu from "qiniu";
2 | import mac from "./Mac";
3 |
4 | let config = new qiniu.conf.Config({});
5 | /** 七牛云OSS空间管理器*/
6 | const bucketManager = new qiniu.rs.BucketManager(mac, config);
7 |
8 | export default bucketManager;
9 |
--------------------------------------------------------------------------------
/server/src/common/utils/static/qiniu/utils/zone.ts:
--------------------------------------------------------------------------------
1 | import qiniu from "qiniu";
2 |
3 | let zone = {
4 | huadong: qiniu.zone.Zone_z0,
5 | huabei: qiniu.zone.Zone_z1,
6 | huanan: qiniu.zone.Zone_z2,
7 | };
8 |
9 | export default zone[process.env.OSS_REGION as unknown as keyof typeof zone];
10 |
--------------------------------------------------------------------------------
/server/src/common/utils/xss/comment.ts:
--------------------------------------------------------------------------------
1 | import sanitizeHtml from "sanitize-html";
2 |
3 | // https://github.com/apostrophecms/sanitize-html
4 |
5 | /**
6 | * XSS
7 | * 用户端向服务端发送数据,对评论内容在进入数据前进行加工
8 | */
9 | function xss(content: string) {
10 | return sanitizeHtml(content, {
11 | enforceHtmlBoundary: true,
12 | allowedTags: [
13 | "div",
14 | "span",
15 | "a",
16 | "b",
17 | "blockquote",
18 | "del",
19 | "em",
20 | "i",
21 | "li",
22 | "ol",
23 | "p",
24 | "pre",
25 | "code",
26 | "s",
27 | "strong",
28 | "ul",
29 | ],
30 | allowedAttributes: {
31 | a: ["href"],
32 | img: ["src"],
33 | },
34 | allowProtocolRelative: false,
35 | allowedClasses: {
36 | pre: ["language-*"],
37 | code: ["language-*"],
38 | },
39 | });
40 | }
41 | export default xss;
42 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/advertisement/create-update.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 | import verify from "@/common/middleware/verify/validator";
3 | import { fileName } from "@/common/verify/modules/file-name";
4 | import { urlAllowNull } from "@/common/verify/modules/url";
5 |
6 | const schema = Joi.object({
7 | poster_file_name: fileName,
8 | url: urlAllowNull,
9 | indexes: Joi.number().min(0).required(),
10 | position: Joi.string().valid("index", "article", "creator").required(),
11 | });
12 |
13 | export default verify(schema);
14 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/answer/delete.ts:
--------------------------------------------------------------------------------
1 | import { Context, Next } from "koa";
2 | import DB from "@/db";
3 | import Joi from "joi";
4 | import compose from "koa-compose";
5 | import auth from "@/common/middleware/auth";
6 | import validator from "@/common/middleware/verify/validatorAsync";
7 |
8 | let schema = Joi.object({
9 | id: Joi.number().min(0).required().error(new Error("答案ID格式不正确")),
10 | });
11 |
12 | async function verify(ctx: Context, next: Next) {
13 | await DB.Problem.findOne({
14 | where: { answer_id: ctx.params.id },
15 | raw: true,
16 | attributes: ["id"],
17 | })
18 | .then(async (row) => {
19 | if (row) {
20 | ctx.status = 400;
21 | ctx.body = { success: false, message: "被采纳的答案无法删除" };
22 | } else {
23 | await next();
24 | }
25 | })
26 | .catch(() => {
27 | ctx.status = 400;
28 | ctx.body = { success: false, message: "服务器查询错误请稍后重试" };
29 | });
30 | }
31 |
32 | export default compose([auth(0), validator(schema, true), verify]);
33 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/answer/problem-md.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 | import compose from "koa-compose";
3 | import auth from "@/common/middleware/auth";
4 | import validator from "@/common/middleware/verify/validatorAsync";
5 |
6 | let schema = Joi.object({
7 | problem_id: Joi.number()
8 | .min(0)
9 | .required()
10 | .error(new Error("问题ID格式不正确")),
11 | });
12 |
13 | export default compose([auth(0), validator(schema)]);
14 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/article/list.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Next } from "koa";
2 | import Joi from "joi";
3 | import compose from "koa-compose";
4 | import auth from "@/common/middleware/auth";
5 | import verify from "@/common/middleware/verify/validator";
6 | import interger from "../../integer";
7 |
8 | const schema = Joi.object({
9 | sort: Joi.string()
10 | .valid("recommend", "newest", "hottest")
11 | .error(new Error("排序方式参数错误")),
12 | // 暂时 以下三个参数只能通知存在一个
13 | tag: Joi.string().min(5).max(18).error(new Error("标签错误")),
14 | type: Joi.string().min(5).max(18).error(new Error("类型错误")),
15 | follow: Joi.boolean().error(new Error("follow参数错误")),
16 | });
17 |
18 | const _auth = async (ctx: Context, next: Next) => {
19 | if (ctx.query.follow) {
20 | return auth(0)(ctx, next);
21 | } else {
22 | await next();
23 | }
24 | };
25 |
26 | export default compose([verify(schema), _auth, interger([], ["page"])]);
27 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/article/search.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Next } from "koa";
2 | import Joi from "joi";
3 | import compose from "koa-compose";
4 | import verify from "@/common/middleware/verify/validator";
5 | import interger from "../../integer";
6 |
7 | const schema = Joi.object({
8 | author: Joi.number().min(1).error(new Error("ID格式错误")),
9 | state: Joi.number().valid(0, 1).error(new Error("文章状态错误")),
10 | keyword: Joi.string().min(0).max(30).error(new Error("文章关键词错误")),
11 | tag: Joi.string().min(1).max(30).error(new Error("文章标签错误")),
12 | });
13 |
14 | const verify1 = async (ctx: Context, next: Next) =>
15 | interger([], ["page"])(ctx, next);
16 | const verify2 = async (ctx: Context, next: Next) => verify(schema)(ctx, next);
17 |
18 | export default compose([verify1, verify2]);
19 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/collection/update.ts:
--------------------------------------------------------------------------------
1 | import { Context, Next } from "koa";
2 | import DB from "@/db";
3 | import Joi from "joi";
4 | import compose from "koa-compose";
5 | import auth from "@/common/middleware/auth";
6 | import validator from "@/common/middleware/verify/validator";
7 | import interger from "@/common/verify/integer";
8 |
9 | const schema = Joi.object({
10 | favorites_id: Joi.array().items(Joi.number()).required().min(1),
11 | });
12 |
13 | async function verify(ctx: Context, next: Next) {
14 | /** 判断是否有favorites_id不在列表内*/
15 | let hasFavoritesLength = await DB.Favorites.count({
16 | where: {
17 | id: ctx.request.body.favorites_id,
18 | user_id: ctx.id,
19 | },
20 | attributes: ["id"],
21 | });
22 |
23 | if (hasFavoritesLength != ctx.request.body.favorites_id.length) {
24 | ctx.body = { success: false, message: "收藏夹选择错误" };
25 | ctx.status = 400;
26 | return;
27 | }
28 | await next();
29 | }
30 |
31 | export default compose([
32 | auth(0),
33 | interger([], ["belong_id"]),
34 | validator(schema),
35 | verify,
36 | ]);
37 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/external-link/create.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import Joi from "joi";
3 | import validator from "@/common/middleware/verify/validatorAsync";
4 |
5 | const schema = Joi.object({
6 | href: Joi.string()
7 | .min(3)
8 | .max(150)
9 | .pattern(/^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$/)
10 | .error(new Error("链接格式错误"))
11 | .external(async (value: string) => {
12 | let count = await DB.ExternalLink.count({ where: { href: value } });
13 | if (count) {
14 | throw new Error("链接已存在");
15 | }
16 | }),
17 | });
18 | export default validator(schema);
19 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/favorites/create.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 | import compose from "koa-compose";
3 | import auth from "@/common/middleware/auth";
4 | import validator from "@/common/middleware/verify/validatorAsync";
5 |
6 | const schema = Joi.object({
7 | name: Joi.string().required().max(15).error(new Error("name错误")),
8 | description: Joi.string()
9 | .max(100)
10 | .allow(null)
11 | .error(new Error("description错误")),
12 | is_private: Joi.boolean().required().error(new Error("is_private错误")),
13 | });
14 |
15 | export default compose([auth(0), validator(schema)]);
16 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/follow/create/map.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import {
3 | AnswerAttributes,
4 | ArticleAttributes,
5 | ProblemAttributes,
6 | UserAttributes,
7 | } from "@/db/models/init-models";
8 |
9 | /** 传入参数判断文章、答案、问题是否存在*/
10 | export type type = "problem" | "user";
11 | function map(type: type, attributes?: string[]) {
12 | return {
13 | problem: {
14 | db: (value: number) =>
15 | DB.Problem.findByPk(value, {
16 | attributes: attributes || ["id"],
17 | raw: true,
18 | }),
19 | message: "未找到指定的问题",
20 | },
21 | user: {
22 | db: (value: number) =>
23 | DB.User.findByPk(value, {
24 | attributes: attributes || ["id"],
25 | raw: true,
26 | }),
27 | message: "未找到指定的用户",
28 | },
29 | }[type];
30 | }
31 | export default map;
32 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/friendly-link/create.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 | import validator from "@/common/middleware/verify/validatorAsync";
3 | import { exist } from "@/common/utils/static";
4 | import { fileName } from "../../modules/file-name";
5 | import { url } from "../../modules/url";
6 |
7 | const schema = Joi.object({
8 | name: Joi.string()
9 | .required()
10 | .min(2)
11 | .max(30)
12 | .error(new Error("网站名称填写错误")),
13 | url: url.error(new Error("网址填写错误")),
14 | logo_file_name: fileName.external(async (value: string) => {
15 | let result = await exist([`friendly-link/${value}`])
16 | .then((res) => res)
17 | .catch((err) => err);
18 | if (!result.success) throw new Error(result.message);
19 | }),
20 | });
21 | export default validator(schema);
22 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/problem/cancel.ts:
--------------------------------------------------------------------------------
1 | import { Context, Next } from "koa";
2 | import Joi from "joi";
3 | import compose from "koa-compose";
4 | import authMiddleware from "@/common/middleware/auth";
5 | import validator from "@/common/middleware/verify/validatorAsync";
6 |
7 | /** 验证是否存在该采纳*/
8 | async function verifyParams(ctx: Context, next: Next) {
9 | const schema = Joi.object({
10 | id: Joi.number().min(0).required().error(new Error("问题ID错误")),
11 | });
12 | return validator(schema, true)(ctx, next);
13 | }
14 | export default compose([authMiddleware(0), verifyParams]);
15 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/problem/list.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 | import compose from "koa-compose";
3 | import validator from "@/common/middleware/verify/validator";
4 | import interger from "../../integer";
5 |
6 | const schema = Joi.object({
7 | type: Joi.string()
8 | .valid("newest", "noanswer")
9 | .required()
10 | .error(new Error("Type参数错误")),
11 | });
12 | export default compose([validator(schema), interger([], ["page"])]);
13 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/problem/questions.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 | import compose from "koa-compose";
3 | import getUserId from "@/common/middleware/auth/getUserId";
4 | import validator from "@/common/middleware/verify/validator";
5 |
6 | const schema = Joi.object({
7 | id: Joi.number().min(0).required().error(new Error("问题ID错误")),
8 | });
9 | export default compose([validator(schema, true), getUserId]);
10 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/theme/create.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import Joi from "joi";
3 | import compose from "koa-compose";
4 | import auth from "@/common/middleware/auth";
5 | import validator from "@/common/middleware/verify/validatorAsync";
6 |
7 | let schema = Joi.object({
8 | name: Joi.string()
9 | .min(1)
10 | .max(40)
11 | .required()
12 | .external(async (value: string) => {
13 | let result = await DB.Theme.findOne({ where: { name: value } });
14 | if (result) {
15 | if (result.state == 0) {
16 | throw new Error("已有相同名称的主题等待审核");
17 | } else {
18 | throw new Error("不能重复添加主题");
19 | }
20 | }
21 | })
22 | .error(new Error("主题名称错误")),
23 | content: Joi.string().min(1).required().error(new Error("样式内容错误")),
24 | });
25 |
26 | export default compose([auth(0), validator(schema)]);
27 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/theme/remove.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 | import compose from "koa-compose";
3 | import auth from "@/common/middleware/auth";
4 | import validator from "@/common/middleware/verify/validatorAsync";
5 |
6 | let schema = Joi.object({
7 | id: Joi.number().min(1).not(0).error(new Error("ID参数错误")),
8 | });
9 |
10 | export default compose([auth(), validator(schema)]);
11 |
--------------------------------------------------------------------------------
/server/src/common/verify/api-verify/theme/update.ts:
--------------------------------------------------------------------------------
1 | import DB from "@/db";
2 | import Joi from "joi";
3 | import compose from "koa-compose";
4 | import auth from "@/common/middleware/auth";
5 | import validator from "@/common/middleware/verify/validatorAsync";
6 |
7 | let schema = Joi.object({
8 | name: Joi.string()
9 | .min(1)
10 | .max(40)
11 | .external(async (value: string) => {
12 | if (value) {
13 | let result = await DB.Theme.findOne({ where: { name: value } });
14 | if (result) {
15 | if (result.state == 0) {
16 | throw new Error("已有相同名称的主题等待审核");
17 | } else {
18 | throw new Error("不能重复添加主题");
19 | }
20 | }
21 | }
22 | })
23 | .error(new Error("主题名称错误"))
24 | .optional(),
25 | content: Joi.string().min(1).error(new Error("样式内容错误")).optional(),
26 | indexes: Joi.number().min(1).error(new Error("索引值错误")).optional(),
27 | state: Joi.number().valid(0, 1).error(new Error("状态值错误")).optional(),
28 | });
29 |
30 | export default compose([auth(), validator(schema)]);
31 |
--------------------------------------------------------------------------------
/server/src/common/verify/integer.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Next } from "koa";
2 | import Joi from "joi";
3 |
4 | const schema = Joi.array().items(Joi.number().min(0)).required();
5 | /** 验证整数,第一个数组是query的key第二个是params的key*/
6 | const interger =
7 | (query: string[] = [], params: string[] = []) =>
8 | async (ctx: Context, next: Next) => {
9 | let validate = schema.validate(
10 | query
11 | .map((item) => ctx.query[item])
12 | .concat(params.map((item) => ctx.params[item])),
13 | );
14 | if (validate.error) {
15 | ctx.status = 400;
16 | ctx.body = { success: false, message: `请求参数错误(整数)` };
17 | } else {
18 | await next();
19 | }
20 | };
21 | export default interger;
22 |
--------------------------------------------------------------------------------
/server/src/common/verify/modules/file-name.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 |
3 | /** 上传图片验证配置*/
4 | const fileName = Joi.string()
5 | .min(10)
6 | .max(50)
7 | .required()
8 | .lowercase()
9 | .pattern(/^((?!http).)*$/)
10 | .pattern(/^((?!,).)*$/)
11 | .pattern(/^((?!\/).)*$/)
12 | .error(new Error("图片名称错误"));
13 |
14 | /** 上传图片验证配置(允许null)*/
15 | const fileNameAllowNull = fileName.allow("").allow(null).lowercase();
16 |
17 | export { fileNameAllowNull, fileName };
18 |
--------------------------------------------------------------------------------
/server/src/common/verify/modules/tag.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 | import { TagAttributes } from "@/db/models/init-models";
3 | import { cache } from "@/common/modules/cache/type";
4 |
5 | /** 上传图片验证配置(允许null)*/
6 | const tag = Joi.array()
7 | .items(Joi.number().required())
8 | .min(1)
9 | .max(6)
10 | .required()
11 | .error(new Error("网站标签为1-6个"))
12 | .custom((value: number[], helper) => {
13 | if (new Set(value).size != value.length) {
14 | return helper.message(new Error("禁止重复的tag_id") as any);
15 | }
16 |
17 | let tag = (cache.get("tag") as Array).map((item) => item.id);
18 |
19 | if (value.every((item) => tag.includes(item))) {
20 | return true;
21 | } else {
22 | return helper.message(new Error("tag_id不在数据表内") as any);
23 | }
24 | });
25 |
26 | export default tag;
27 |
--------------------------------------------------------------------------------
/server/src/common/verify/modules/type.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 |
3 | /** 类型:文章、问题*/
4 | export const typeCollection = Joi.valid("article", "problem")
5 | .required()
6 | .error(new Error("类型错误"));
7 | /** 类型:文章、问题、答案*/
8 | export const typeLikeComment = Joi.valid("article", "problem", "answer")
9 | .required()
10 | .error(new Error("类型错误"));
11 | /** 用于对用户和问题进行关注*/
12 | export const typeFollwoProblem = Joi.valid("user", "problem")
13 | .required()
14 | .error(new Error("类型错误"));
15 |
--------------------------------------------------------------------------------
/server/src/common/verify/modules/url.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 |
3 | const url = Joi.string()
4 | .min(8)
5 | .max(100)
6 | .required()
7 | .lowercase()
8 | .pattern(/^https:\/\/.*/)
9 | .error(new Error("URL为1-100的字符串,要求为https网址"));
10 |
11 | /** 链接验证(允许null)*/
12 | const urlAllowNull = url.allow("").allow(null);
13 | export { url, urlAllowNull };
14 |
--------------------------------------------------------------------------------
/server/src/db/config/index.ts:
--------------------------------------------------------------------------------
1 | import { Sequelize } from "sequelize";
2 |
3 | const sequelize = new Sequelize(
4 | "blog",
5 | process.env.DB_MYSQL_USER + "",
6 | process.env.DB_MYSQL_PASSWORD || "",
7 | {
8 | host: process.env.DB_MYSQL_HOST + "",
9 | dialect: "mysql",
10 | port: process.env.DB_MYSQL_PORT
11 | ? +(process.env.DB_MYSQL_PORT as string)
12 | : 3306,
13 | timezone: "+08:00",
14 | // logging: process.env.ENV == "development",
15 | logging: false, //不打印日志
16 | pool: {
17 | max: 5,
18 | min: 0,
19 | idle: 1000,
20 | },
21 | dialectOptions: {
22 | dateStrings: true,
23 | typeCast: true,
24 | },
25 | define: {},
26 | },
27 | );
28 | export default sequelize;
29 |
--------------------------------------------------------------------------------
/server/src/db/hooks/advertisement.ts:
--------------------------------------------------------------------------------
1 | import { setData } from "@/common/modules/cache/advertisement";
2 | import type {
3 | Advertisement,
4 | AdvertisementAttributes,
5 | } from "../models/init-models";
6 | import init from "./utils/init";
7 |
8 | export default init(
9 | async (model, type) => {
10 | setData();
11 | },
12 | );
13 |
--------------------------------------------------------------------------------
/server/src/db/hooks/external-link.ts:
--------------------------------------------------------------------------------
1 | import { setData } from "@/common/modules/cache/external-link";
2 | import type {
3 | ExternalLink,
4 | ExternalLinkAttributes,
5 | } from "../models/init-models";
6 | import init from "./utils/init";
7 |
8 | export default init(
9 | async (model, type) => {
10 | setData();
11 | },
12 | );
13 |
--------------------------------------------------------------------------------
/server/src/db/hooks/type.ts:
--------------------------------------------------------------------------------
1 | import { setData } from "@/common/modules/cache/type";
2 | import type { Tag, TagAttributes } from "../models/init-models";
3 | import init from "./utils/init";
4 |
5 | // 只要有变化就刷新缓存
6 | export default init(async (model, type) => {
7 | setData();
8 | });
9 |
--------------------------------------------------------------------------------
/server/src/routes/advertisement/create.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import id from "@/common/utils/id";
5 | import verify from "@/common/verify/api-verify/advertisement/create-update";
6 |
7 | let router = new Router();
8 |
9 | router.post("/advertisement", verify, auth(), async (ctx) => {
10 | let { poster_file_name, url, indexes, position } = ctx.request.body;
11 |
12 | await DB.Advertisement.create({
13 | id: id(),
14 | poster_file_name: poster_file_name,
15 | url: url,
16 | indexes: indexes,
17 | position: position,
18 | create_time: new Date(),
19 | })
20 | .then((res) => {
21 | ctx.body = { success: true, message: "添加成功" };
22 | })
23 | .catch((err) => {
24 | ctx.body = { success: false, message: "添加失败" };
25 | console.log(err);
26 | });
27 | });
28 | export default router;
29 |
--------------------------------------------------------------------------------
/server/src/routes/advertisement/delete.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import interger from "@/common/verify/integer";
5 |
6 | let router = new Router();
7 |
8 | router.delete(
9 | "/advertisement/:id",
10 | interger([], ["id"]),
11 | auth(),
12 | async (ctx) => {
13 | await DB.Advertisement.destroy({ where: { id: ctx.params.id } })
14 | .then((rows) => {
15 | if (rows) {
16 | ctx.body = { success: true, message: "删除成功" };
17 | } else {
18 | ctx.body = { success: false, message: "删除失败" };
19 | }
20 | })
21 | .catch((err) => {
22 | ctx.body = { success: false, message: "删除失败" };
23 | });
24 | },
25 | );
26 | export default router;
27 |
--------------------------------------------------------------------------------
/server/src/routes/advertisement/get-all.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import Joi from "joi";
3 | import validator from "@/common/middleware/verify/validator";
4 | import { getData } from "@/common/modules/cache/advertisement";
5 |
6 | let router = new Router();
7 | const schema = Joi.object({
8 | position: Joi.string().valid("index", "article", "creator"),
9 | });
10 | router.get("/advertisement", validator(schema), async (ctx) => {
11 | try {
12 | let rows = ctx.header.isadmin
13 | ? await getData("all", undefined)
14 | : await getData("list", ctx.query.position as any);
15 |
16 | ctx.body = { success: true, message: "查询推广内容", data: rows };
17 | } catch (error) {
18 | console.log(error);
19 | ctx.status = 500;
20 | ctx.body = { success: false, message: "查询失败" };
21 | }
22 | });
23 | export default router;
24 |
--------------------------------------------------------------------------------
/server/src/routes/advertisement/id.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import interger from "@/common/verify/integer";
5 |
6 | let router = new Router();
7 |
8 | router.get("/advertisement/:id", interger([], ["id"]), auth(), async (ctx) => {
9 | let id = ctx.params.id;
10 | await DB.Advertisement.findByPk(id)
11 | .then((row) => {
12 | if (row) {
13 | ctx.body = { success: true, message: "查询指定推广内容", data: row };
14 | } else {
15 | ctx.status = 404;
16 | ctx.body = { success: false, message: "查询失败" };
17 | }
18 | })
19 | .catch((err) => {
20 | ctx.body = { success: false, message: "查询失败" };
21 | console.log(err);
22 | });
23 | });
24 | export default router;
25 |
--------------------------------------------------------------------------------
/server/src/routes/answer/delete.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import sequelize from "@/db/config";
4 | import transaction from "@/common/transaction/answer/delete";
5 | import verify from "@/common/verify/api-verify/answer/delete";
6 |
7 | let router = new Router();
8 | router.delete("/answer/:id", verify, async (ctx) => {
9 | let t = await sequelize.transaction();
10 | //删除答案
11 | let result = await DB.Answer.destroy({
12 | where: {
13 | id: ctx.params.id,
14 | author: ctx.id,
15 | },
16 | transaction: t,
17 | })
18 | .then((row) => row)
19 | .catch((err) => {
20 | ctx.status = 500;
21 | console.log(err);
22 | return false;
23 | });
24 | let _t = await transaction(ctx.params.id, t);
25 |
26 | let isSuccess = _t && result;
27 | if (isSuccess) {
28 | ctx.body = { success: true, message: "删除成功" };
29 | t.commit();
30 | } else {
31 | ctx.body = { success: false, message: "删除失败" };
32 | ctx.status = 500;
33 | t.rollback();
34 | }
35 | });
36 | export default router;
37 |
--------------------------------------------------------------------------------
/server/src/routes/answer/problem-md.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import setImageTag from "@/common/modules/article/get/img-add-prefix";
4 | import verify from "@/common/verify/api-verify/answer/problem-md";
5 |
6 | let router = new Router();
7 |
8 | /** 查询用户在某个问题下的回答内容(返回MarkDown)*/
9 | router.get("/answer", verify, async (ctx) => {
10 | await DB.Answer.findOne({
11 | where: {
12 | problem_id: ctx.query.problem_id,
13 | author: ctx.id,
14 | },
15 | attributes: ["id", "content"],
16 | })
17 | .then((row) => {
18 | if (row) {
19 | ctx.body = {
20 | success: true,
21 | message: "查询答案并且以MarkDown形式返回内容",
22 | data: Object.assign(row, {
23 | content: setImageTag(row.content, "answer"),
24 | }),
25 | };
26 | } else {
27 | ctx.status = 404;
28 | }
29 | })
30 | .catch((err) => {
31 | console.log(err);
32 | ctx.status = 500;
33 | });
34 | });
35 | export default router;
36 |
--------------------------------------------------------------------------------
/server/src/routes/answer/update.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import verify from "@/common/verify/api-verify/answer/update";
4 |
5 | let router = new Router();
6 | router.put("/answer/:id", verify, async (ctx) => {
7 | const { content } = ctx.request.body;
8 | const { id } = ctx.params;
9 | await DB.Answer.update({ content }, { where: { id, author: ctx.id } })
10 | .then((result) => {
11 | if (result) {
12 | ctx.body = { success: true, message: "修改成功" };
13 | } else {
14 | ctx.status = 500;
15 | }
16 | })
17 | .catch((err) => {
18 | ctx.status = 500;
19 | console.log(err);
20 | });
21 | });
22 | export default router;
23 |
--------------------------------------------------------------------------------
/server/src/routes/article/delete-article.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import sequelize from "@/db/config";
4 | import auth from "@/common/middleware/auth";
5 | import transaction from "@/common/transaction/article/delete-article";
6 | import interger from "@/common/verify/integer";
7 |
8 | let router = new Router();
9 |
10 | router.delete("/article/:id", interger([], ["id"]), auth(0), async (ctx) => {
11 | let id = +ctx.params.id;
12 | let where: { id: number | string; author?: number } = {
13 | id: id,
14 | };
15 | if (ctx.auth != 1) {
16 | where.author = ctx.id;
17 | }
18 |
19 | let t = await sequelize.transaction();
20 | let deleteArticleCount = await DB.Article.destroy({
21 | where: where,
22 | transaction: t,
23 | });
24 |
25 | let _t = await transaction(id, t);
26 | ctx.body = {
27 | success: _t && deleteArticleCount,
28 | message: _t && deleteArticleCount ? `删除成功` : "删除失败",
29 | };
30 |
31 | if (_t && deleteArticleCount) {
32 | t.commit();
33 | } else {
34 | t.rollback();
35 | }
36 | });
37 | export default router;
38 |
--------------------------------------------------------------------------------
/server/src/routes/collection/collection-state.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import interger from "@/common/verify/integer";
5 |
6 | let router = new Router();
7 | router.get(
8 | "/collection/state/:belong_id",
9 | auth(0),
10 | interger([], ["belong_id"]),
11 | async (ctx) => {
12 | await DB.Collection.findOne({
13 | where: {
14 | user_id: ctx.id,
15 | belong_id: ctx.params.belong_id,
16 | },
17 | })
18 | .then((res) => {
19 | if (res) {
20 | ctx.body = { success: true, message: "收藏了" };
21 | } else {
22 | ctx.body = { success: false, message: "没收藏" };
23 | }
24 | })
25 | .catch((err) => {
26 | ctx.body = { success: false, message: "没收藏" };
27 | console.log(err);
28 | });
29 | },
30 | );
31 | export default router;
32 |
--------------------------------------------------------------------------------
/server/src/routes/collection/create.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import id from "@/common/utils/id";
4 | import verify from "@/common/verify/api-verify/collection/create";
5 |
6 | let router = new Router();
7 | router.post("/collection/:belong_id", verify, async (ctx) => {
8 | let belong_id = +ctx.params.belong_id;
9 | let favorites: any[] = ctx.request.body.favorites_id;
10 |
11 | await DB.Collection.bulkCreate(
12 | favorites.map((item) => ({
13 | id: id(),
14 | belong_id: belong_id,
15 | type: ctx.request.body.type as string,
16 | user_id: ctx.id as number,
17 | create_time: new Date(),
18 | favorites_id: item,
19 | })),
20 | )
21 | .then(() => {
22 | ctx.body = { success: true, message: "收藏成功" };
23 | })
24 | .catch((err) => {
25 | ctx.status = 500;
26 | console.log(err);
27 | });
28 | });
29 | export default router;
30 |
--------------------------------------------------------------------------------
/server/src/routes/collection/delete-favorites.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import sequelize from "@/db/config";
4 | import auth from "@/common/middleware/auth";
5 | import interger from "@/common/verify/integer";
6 |
7 | let router = new Router();
8 |
9 | // 删除收藏中的某个收藏集
10 | router.delete(
11 | "/collection/favorites/:belong_id",
12 | auth(0),
13 | interger(["favorites_id"], ["belong_id"]),
14 | async (ctx) => {
15 | let favorites_id = ctx.query.favorites_id as string;
16 | let belong_id = ctx.params.belong_id as string;
17 | let t = await sequelize.transaction();
18 |
19 | await DB.Collection.destroy({
20 | where: { favorites_id: favorites_id, belong_id: belong_id },
21 | })
22 | .then((res) => {
23 | ctx.body = { success: true, message: "删除成功" };
24 | })
25 | .catch((err) => {
26 | ctx.status = 500;
27 | ctx.body = { success: false, message: "删除失败" };
28 | });
29 | },
30 | );
31 | export default router;
32 |
--------------------------------------------------------------------------------
/server/src/routes/collection/delete.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import interger from "@/common/verify/integer";
5 |
6 | let router = new Router();
7 |
8 | // 直接删除整条收藏信息
9 | router.delete(
10 | "/collection/:belong_id",
11 | interger([], ["belong_id"]),
12 | auth(0),
13 | async (ctx) => {
14 | let belong_id = +ctx.params.belong_id;
15 |
16 | await DB.Collection.destroy({
17 | where: {
18 | belong_id: belong_id,
19 | user_id: ctx.id,
20 | },
21 | })
22 | .then((res) => {
23 | let isSuccess = !!res;
24 | ctx.body = {
25 | success: isSuccess,
26 | message: isSuccess ? "删除成功" : "删除失败",
27 | };
28 | })
29 | .catch((err) => {
30 | ctx.status = 500;
31 | ctx.body = {
32 | success: false,
33 | message: "删除失败",
34 | };
35 | console.log(err);
36 | });
37 | },
38 | );
39 | export default router;
40 |
--------------------------------------------------------------------------------
/server/src/routes/comment/review.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 |
5 | let router = new Router();
6 |
7 | // 修改评论的审阅属性
8 | router.put("/comment", auth(), async (ctx) => {
9 | let { id }: { id: number[] } = ctx.request.body;
10 |
11 | DB.Comment.update(
12 | { is_review: 1 },
13 | {
14 | where: {
15 | id: id,
16 | },
17 | },
18 | );
19 | ctx.body = { success: true };
20 | });
21 | export default router;
22 |
--------------------------------------------------------------------------------
/server/src/routes/external-link/create.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import id from "@/common/utils/id";
5 | import verify from "@/common/verify/api-verify/external-link/create";
6 |
7 | let router = new Router();
8 |
9 | router.post("/external-link", verify, auth(), async (ctx) => {
10 | let href = ctx.request.body.href;
11 | await DB.ExternalLink.create({
12 | id: id(),
13 | href: href,
14 | create_time: new Date(),
15 | })
16 | .then((res) => {
17 | ctx.body = { success: true, message: "添加成功" };
18 | })
19 | .catch((err) => {
20 | console.log(err);
21 | ctx.status = 500;
22 | ctx.body = { success: false, message: "添加失败" };
23 | });
24 | });
25 | export default router;
26 |
--------------------------------------------------------------------------------
/server/src/routes/external-link/delete.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 |
5 | let router = new Router();
6 |
7 | router.delete("/external-link/:id", auth(), async (ctx) => {
8 | await DB.ExternalLink.destroy({ where: { id: ctx.params.id } })
9 | .then((rows) => {
10 | if (rows) {
11 | ctx.body = { success: true, message: "删除成功" };
12 | } else {
13 | ctx.status = 500;
14 | ctx.body = { success: false, message: "删除失败" };
15 | }
16 | })
17 | .catch((err) => {
18 | console.log(err);
19 | ctx.status = 500;
20 | ctx.body = { success: false, message: "删除失败" };
21 | });
22 | });
23 | export default router;
24 |
--------------------------------------------------------------------------------
/server/src/routes/external-link/find.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import url from "url";
4 | import { getData } from "@/common/modules/cache/external-link";
5 |
6 | let router = new Router();
7 |
8 | router.get("/external-link/find", async (ctx) => {
9 | if (
10 | getData()!.some((item) =>
11 | url.parse(ctx.request.query.href as string).hostname!.endsWith(item),
12 | )
13 | ) {
14 | ctx.body = { success: true, message: "查询外链是否白名单内" };
15 | } else {
16 | ctx.body = { success: false, message: "查找错误" };
17 | }
18 | });
19 | export default router;
20 |
--------------------------------------------------------------------------------
/server/src/routes/external-link/list.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 |
5 | let router = new Router();
6 |
7 | router.get("/external-link", auth(), async (ctx) => {
8 | await DB.ExternalLink.findAll({ order: [["create_time", "desc"]] })
9 | .then((rows) => {
10 | ctx.body = { success: true, message: "查询外联列表", data: rows };
11 | })
12 | .catch((err) => {
13 | console.log(err);
14 | ctx.status = 500;
15 | ctx.body = { success: false, message: "查询外联列表失败" };
16 | });
17 | });
18 | export default router;
19 |
--------------------------------------------------------------------------------
/server/src/routes/favorites/create.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import id from "@/common/utils/id";
4 | import verify from "@/common/verify/api-verify/favorites/create";
5 |
6 | let router = new Router();
7 |
8 | router.post("/favorites", verify, async (ctx) => {
9 | await DB.Favorites.create({
10 | id: id(),
11 | user_id: ctx.id as number,
12 | name: ctx.request.body.name,
13 | description: ctx.request.body.description,
14 | is_private: ctx.request.body.is_private,
15 | create_time: new Date(),
16 | })
17 | .then((row) => {
18 | ctx.body = { success: true, message: "创建成功" };
19 | })
20 | .catch((err) => {
21 | ctx.status = 500;
22 | ctx.body = { success: false, message: "创建失败" };
23 | });
24 | });
25 | export default router;
26 |
--------------------------------------------------------------------------------
/server/src/routes/favorites/update.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import verify from "@/common/verify/api-verify/favorites/create";
4 |
5 | let router = new Router();
6 |
7 | router.put("/favorites/:id", verify, async (ctx) => {
8 | await DB.Favorites.update(
9 | {
10 | user_id: ctx.id as number,
11 | name: ctx.request.body.name,
12 | description: ctx.request.body.description,
13 | is_private: ctx.request.body.is_private,
14 | },
15 | {
16 | where: { id: ctx.params.id, user_id: ctx.id },
17 | },
18 | )
19 | .then((row) => {
20 | ctx.body = { success: true, message: "修改成功" };
21 | })
22 | .catch((err) => {
23 | ctx.status = 500;
24 | ctx.body = { success: false, message: "修改失败" };
25 | });
26 | });
27 | export default router;
28 |
--------------------------------------------------------------------------------
/server/src/routes/follow/follow.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import id from "@/common/utils/id";
4 | import verify from "@/common/verify/api-verify/follow/create";
5 |
6 | let router = new Router();
7 |
8 | router.post("/follow/:belong_id", verify, async (ctx) => {
9 | let boggerID = +ctx.params.belong_id;
10 | let type = ctx.request.body.type;
11 |
12 | await DB.Follow.create({
13 | id: id(),
14 | belong_id: boggerID,
15 | type: type,
16 | user_id: ctx.id as number,
17 | create_time: new Date(),
18 | })
19 | .then((res) => {
20 | ctx.body = { success: true, message: "关注成功" };
21 | })
22 | .catch((err) => {
23 | ctx.status = 500;
24 | ctx.body = { success: false, message: "关注失败" };
25 | console.log(err);
26 | });
27 | });
28 | export default router;
29 |
--------------------------------------------------------------------------------
/server/src/routes/follow/user-follow-state.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import interger from "@/common/verify/integer";
5 |
6 | let router = new Router();
7 |
8 | /** 判断用户是否关注了某个博主*/
9 | router.get("/follow/state/:id", interger([], ["id"]), auth(0), async (ctx) => {
10 | await DB.Follow.findAndCountAll({
11 | where: {
12 | belong_id: ctx.params.id,
13 | user_id: ctx.id,
14 | },
15 | })
16 | .then(({ count }) => {
17 | let isSuccess = !!count;
18 | ctx.body = {
19 | success: isSuccess,
20 | message: isSuccess ? "已关注" : "未关注",
21 | };
22 | })
23 | .catch((err) => {
24 | ctx.body = { success: false, message: "未关注" };
25 | console.log(err);
26 | });
27 | });
28 | export default router;
29 |
--------------------------------------------------------------------------------
/server/src/routes/friendly-link/apply.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import id from "@/common/utils/id";
5 | import verify from "@/common/verify/api-verify/friendly-link/create";
6 |
7 | let router = new Router();
8 |
9 | /** 在网站注册的用户可以发起申请*/
10 | router.post("/friendly-link/apply", verify, auth(0), async (ctx) => {
11 | let { name, url, logo_file_name } = ctx.request.body;
12 | await DB.FriendlyLink.create({
13 | id: id(),
14 | user_id: ctx.id as number,
15 | name,
16 | url,
17 | logo_file_name,
18 | create_time: new Date(),
19 | state: 0,
20 | })
21 | .then(() => {
22 | ctx.body = { success: true, message: "申请成功,请等待邮箱回复结果" };
23 | })
24 | .catch((err) => {
25 | ctx.status = 500;
26 | ctx.body = { success: false, message: "申请失败" };
27 | console.log(err);
28 | });
29 | });
30 | export default router;
31 |
--------------------------------------------------------------------------------
/server/src/routes/friendly-link/create.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import authMiddleware from "@/common/middleware/auth";
4 | import id from "@/common/utils/id";
5 | import verify from "@/common/verify/api-verify/friendly-link/create";
6 |
7 | let router = new Router();
8 |
9 | /** 管理员添加友链用于没在网站注册的用户*/
10 | router.post("/friendly-link", authMiddleware(), verify, async (ctx) => {
11 | let { name, url, logo_file_name } = ctx.request.body;
12 |
13 | await DB.FriendlyLink.create({
14 | id: id(),
15 | name,
16 | url,
17 | logo_file_name,
18 | create_time: new Date(),
19 | state: 1,
20 | })
21 | .then(() => {
22 | ctx.body = { success: true, message: "创建成功" };
23 | })
24 | .catch((err) => {
25 | ctx.status = 500;
26 | ctx.body = { success: false, message: "创建失败" };
27 | console.log(err);
28 | });
29 | });
30 | export default router;
31 |
--------------------------------------------------------------------------------
/server/src/routes/high-light/language-list.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import StreamZip from "node-stream-zip";
3 | import auth from "@/common/middleware/auth";
4 |
5 | let router = new Router();
6 |
7 | let list: any[] = [];
8 | const zip = new StreamZip({
9 | file: "public/prism.zip",
10 | storeEntries: true,
11 | });
12 | zip.on("error", (err) => {
13 | throw new Error("文件解压错误");
14 | });
15 | zip.on("ready", () => {
16 | const data = JSON.parse(
17 | zip.entryDataSync("prism/components.json").toString(),
18 | ).languages;
19 | delete data.meta;
20 | Object.keys(data).forEach((item) => {
21 | list.push({
22 | title: data[item].title,
23 | language: item,
24 | });
25 | });
26 | zip.close();
27 | });
28 | router.get("/language-list", auth(0), async (ctx) => {
29 | ctx.body = {
30 | success: true,
31 | message: "查询支持代码高亮的语言列表",
32 | data: list,
33 | };
34 | });
35 | export default router;
36 |
--------------------------------------------------------------------------------
/server/src/routes/like/create.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import id from "@/common/utils/id";
4 | import verify from "@/common/verify/api-verify/like/create";
5 |
6 | let router = new Router();
7 |
8 | router.post("/like/:belong_id", verify, async (ctx) => {
9 | let belong_id = +ctx.params.belong_id;
10 | let type = ctx.request.body.type as string;
11 |
12 | await DB.Likes.create({
13 | id: id(),
14 | belong_id: belong_id,
15 | type,
16 | user_id: ctx.id as number,
17 | create_time: new Date(),
18 | })
19 | .then(() => {
20 | ctx.body = { success: true, message: "点赞成功" };
21 | })
22 | .catch((err) => {
23 | console.log(err);
24 | ctx.status = 500;
25 | });
26 | });
27 | export default router;
28 |
--------------------------------------------------------------------------------
/server/src/routes/like/delete.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import interger from "@/common/verify/integer";
5 |
6 | let router = new Router();
7 | router.delete(
8 | "/like/:belong_id",
9 | interger([], ["belong_id"]),
10 | auth(0),
11 | async (ctx) => {
12 | let belong_id = +ctx.params.belong_id;
13 |
14 | await DB.Likes.destroy({
15 | where: {
16 | belong_id: belong_id,
17 | user_id: ctx.id,
18 | },
19 | })
20 | .then((res) => {
21 | let isSuccess = !!res;
22 | ctx.body = {
23 | success: isSuccess,
24 | message: isSuccess ? "取消成功" : "取消失败",
25 | };
26 | })
27 | .catch((err) => {
28 | console.log(err);
29 | ctx.status = 500;
30 | ctx.body = {
31 | success: false,
32 | message: "取消失败",
33 | };
34 | });
35 | },
36 | );
37 | export default router;
38 |
--------------------------------------------------------------------------------
/server/src/routes/like/state.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import interger from "@/common/verify/integer";
5 |
6 | let router = new Router();
7 | router.get(
8 | "/like/state/:belong_id",
9 | auth(0),
10 | interger([], ["belong_id"]),
11 | async (ctx) => {
12 | await DB.Likes.findOne({
13 | where: {
14 | user_id: ctx.id,
15 | belong_id: ctx.params.belong_id,
16 | },
17 | })
18 | .then((res) => {
19 | if (res) {
20 | ctx.body = { success: true, message: "点赞了" };
21 | } else {
22 | ctx.body = { success: false, message: "没点赞" };
23 | }
24 | })
25 | .catch((err) => {
26 | ctx.body = { success: false, message: "没点赞" };
27 | console.log(err);
28 | });
29 | },
30 | );
31 | export default router;
32 |
--------------------------------------------------------------------------------
/server/src/routes/notice/notice-count.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import authMiddleware from "@/common/middleware/auth";
4 |
5 | let router = new Router();
6 | router.get("/notice/count", authMiddleware(0), async (ctx) => {
7 | await DB.Notice.findAndCountAll({
8 | where: { user_id: ctx.id as number, is_read: 0 },
9 | })
10 | .then(({ count }) => {
11 | ctx.body = {
12 | success: true,
13 | message: "查询用户的通知数量",
14 | data: { count: count },
15 | };
16 | })
17 | .catch(() => {
18 | ctx.body = { success: false, message: "查询用户的通知数量失败", data: 0 };
19 | });
20 | });
21 | export default router;
22 |
--------------------------------------------------------------------------------
/server/src/routes/notice/update-notice.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import Joi from "joi";
4 | import auth from "@/common/middleware/auth";
5 | import verify from "@/common/middleware/verify/validator";
6 |
7 | let router = new Router();
8 |
9 | let schema = Joi.object({
10 | notice_list: Joi.array()
11 | .items(Joi.number())
12 | .min(0)
13 | .error(new Error("通知ID错误")),
14 | });
15 |
16 | router.put("/notice/read", auth(0), verify(schema), async (ctx) => {
17 | let list = ctx.request.body.notice_list;
18 | await DB.Notice.update(
19 | { is_read: 1 },
20 | { where: { id: list, user_id: ctx.id } },
21 | )
22 | .then((res) => {
23 | ctx.body = {
24 | success: true,
25 | message: "用户阅读通知,修改状态",
26 | data: {
27 | affected: res[0],
28 | },
29 | };
30 | })
31 | .catch(() => {});
32 | });
33 | export default router;
34 |
--------------------------------------------------------------------------------
/server/src/routes/problem/adopt.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import sequelize from "@/db/config";
4 | import transaction from "@/common/transaction/problem/adopt";
5 | import verify from "@/common/verify/api-verify/problem/adopt";
6 |
7 | let router = new Router();
8 |
9 | /** 问题采纳答案*/
10 | router.put("/problem/adopt/:id", verify, async (ctx) => {
11 | let t = await sequelize.transaction();
12 | let _t = await transaction(+ctx.params.id, t);
13 | let result = await DB.Problem.update(
14 | { answer_id: ctx.request.body.answer_id },
15 | { where: { id: ctx.params.id }, transaction: t },
16 | )
17 | .then(([res]) => !!res)
18 | .catch((err) => {
19 | console.log(err);
20 | return false;
21 | });
22 |
23 | if (_t && result) {
24 | ctx.body = { success: true, message: "修改成功" };
25 | t.commit();
26 | } else {
27 | ctx.status = 500;
28 | t.rollback();
29 | }
30 | });
31 | export default router;
32 |
--------------------------------------------------------------------------------
/server/src/routes/problem/cancel.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import sequelize from "@/db/config";
4 | import transaction from "@/common/transaction/problem/adopt";
5 | import verify from "@/common/verify/api-verify/problem/cancel";
6 |
7 | let router = new Router();
8 |
9 | /** 取消采纳答案*/
10 | router.put("/problem/cancel/:id", verify, async (ctx) => {
11 | let t = await sequelize.transaction();
12 | let _t = await transaction(+ctx.params.id, t);
13 |
14 | let result = await DB.Problem.update(
15 | { answer_id: null as any },
16 | { where: { id: ctx.params.id, author: ctx.id }, transaction: t },
17 | )
18 | .then(([res]) => !!res)
19 | .catch((err) => {
20 | console.log(err);
21 | return false;
22 | });
23 |
24 | if (_t && result) {
25 | ctx.body = { success: true, message: "修改成功" };
26 | t.commit();
27 | } else {
28 | ctx.status = 500;
29 | t.rollback();
30 | }
31 | });
32 | export default router;
33 |
--------------------------------------------------------------------------------
/server/src/routes/problem/delete.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import sequelize from "@/db/config";
4 | import transaction from "@/common/transaction/problem/delete";
5 | import integer from "@/common/verify/integer";
6 |
7 | let router = new Router();
8 |
9 | router.delete("/problem/:id", integer([], ["id"]), async (ctx) => {
10 | const id = ctx.params.id;
11 | let t = await sequelize.transaction();
12 | let result = await DB.Problem.destroy({
13 | where: {
14 | id,
15 | },
16 | transaction: t,
17 | })
18 | .then((res) => !!res)
19 | .catch((err) => {
20 | console.log(err);
21 | return false;
22 | });
23 | let _t = await transaction(+id, t);
24 | if (result && _t) {
25 | ctx.body = { success: true, message: "发布成功" };
26 | t.commit();
27 | } else {
28 | ctx.body = { success: false, message: "发布失败" };
29 | t.rollback();
30 | }
31 | });
32 | export default router;
33 |
--------------------------------------------------------------------------------
/server/src/routes/ranking/author.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import redis from "@/common/utils/redis";
3 |
4 | let router = new Router();
5 |
6 | router.get("/ranking/author", async (ctx) => {
7 | let data = await redis.get("ranking-author");
8 |
9 | ctx.body = {
10 | success: true,
11 | message: "查询作者榜",
12 | data: data ? JSON.parse(data) : [],
13 | };
14 | });
15 | export default router;
16 |
--------------------------------------------------------------------------------
/server/src/routes/ranking/funs.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import redis from "@/common/utils/redis";
3 |
4 | let router = new Router();
5 |
6 | // 粉丝排行榜
7 | router.get("/ranking/funs", async (ctx) => {
8 | let data = await redis.get("ranking-funs");
9 |
10 | ctx.body = {
11 | success: true,
12 | message: "查询粉丝榜",
13 | data: data ? JSON.parse(data) : [],
14 | };
15 | });
16 | export default router;
17 |
--------------------------------------------------------------------------------
/server/src/routes/sitemap/index.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 |
4 | let router = new Router();
5 |
6 | router.get("/sitemap/:type", async (ctx) => {
7 | let type = ctx.params.type as "article" | "problem";
8 | if (!["article", "problem"].includes(type)) {
9 | ctx.status = 401;
10 | return;
11 | }
12 |
13 | let count =
14 | type == "article"
15 | ? await DB.Article.count({
16 | where: { state: 1 },
17 | attributes: ["id"],
18 | })
19 | : await DB.Problem.count({
20 | attributes: ["id"],
21 | });
22 |
23 | ctx.body = {
24 | success: true,
25 | message: "获取sitemap列表",
26 | data: new Array(Math.ceil(count / 1000)).fill(null).map((_, index) => ({
27 | href: `${process.env.CLIENT_HOST}/sitemap/${type}/index${index + 1}.xml`,
28 | })),
29 | };
30 | });
31 | export default router;
32 |
--------------------------------------------------------------------------------
/server/src/routes/tag/create-tag.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import authMiddleware from "@/common/middleware/auth";
4 | import id from "@/common/utils/id";
5 |
6 | let router = new Router();
7 |
8 | router.post("/tag", authMiddleware(), async (ctx) => {
9 | let { name, belong_id, icon_file_name, description } = ctx.request.body;
10 | let indexes = await DB.Tag.findAndCountAll({
11 | where: {
12 | belong_id: belong_id,
13 | },
14 | });
15 |
16 | await DB.Tag.create({
17 | id: id(),
18 | name: name,
19 | belong_id: belong_id,
20 | icon_file_name: icon_file_name,
21 | indexes: indexes.count + 1,
22 | description,
23 | })
24 | .then((res) => {
25 | ctx.body = { success: true, message: `成功添加类型:${name}` };
26 | })
27 | .catch((err) => {
28 | ctx.body = { success: false, message: "添加失败" };
29 | console.log(err);
30 | });
31 | });
32 | export default router;
33 |
--------------------------------------------------------------------------------
/server/src/routes/tag/get-data-id.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import interger from "@/common/verify/integer";
4 |
5 | let router = new Router();
6 | router.get("/tag/:id", interger([], ["id"]), async (ctx) => {
7 | let { id } = ctx.params;
8 |
9 | await DB.Tag.findByPk(id)
10 | .then((row: any) => {
11 | let isSuccess = !!row;
12 | if (!isSuccess) {
13 | ctx.status = 404;
14 | }
15 | ctx.body = {
16 | success: isSuccess,
17 | message: isSuccess ? `查询成功` : "查询失败",
18 | data: row,
19 | };
20 | })
21 | .catch((err: any) => {
22 | ctx.status = 500;
23 | ctx.body = { success: false, message: `错误` };
24 | console.log(err);
25 | });
26 | });
27 | export default router;
28 |
--------------------------------------------------------------------------------
/server/src/routes/tag/list.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import { cache } from "@/common/modules/cache/type";
3 |
4 | let router = new Router();
5 |
6 | /**
7 | * 将type和tag合并生成树形数组返回
8 | */
9 | router.get("/tag", async (ctx) => {
10 | ctx.body = {
11 | success: true,
12 | message: "管理员查询文章类型",
13 | data: cache.get(ctx.query.type!),
14 | };
15 | });
16 | export default router;
17 |
--------------------------------------------------------------------------------
/server/src/routes/tag/update.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import authMiddleware from "@/common/middleware/auth";
4 | import interger from "@/common/verify/integer";
5 |
6 | let router = new Router();
7 |
8 | router.put("/tag/:id", interger([], ["id"]), authMiddleware(), async (ctx) => {
9 | let { name, indexes, icon_file_name, belong_id, description } =
10 | ctx.request.body;
11 | let { id } = ctx.params;
12 |
13 | await DB.Tag.update(
14 | {
15 | name,
16 | indexes,
17 | icon_file_name,
18 | belong_id,
19 | description,
20 | },
21 | { where: { id: id } },
22 | )
23 | .then((rows) => {
24 | ctx.body = { success: !!rows[0], message: `${rows[0]}行数据受到影响` };
25 | })
26 | .catch((err) => {
27 | ctx.status = 500;
28 | ctx.body = { success: false, message: `修改错误` };
29 | console.log(err);
30 | });
31 | });
32 | export default router;
33 |
--------------------------------------------------------------------------------
/server/src/routes/theme/item.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 |
4 | let router = new Router();
5 |
6 | router.get("/theme/:id", async (ctx) => {
7 | await DB.Theme.findByPk(ctx.params.id)
8 | .then((row) => {
9 | if (row) {
10 | ctx.body = { success: true, message: "查询成功", data: row };
11 | } else {
12 | ctx.status = 404;
13 | ctx.body = { success: false, message: "未找到对应主题" };
14 | }
15 | })
16 | .catch((err) => {
17 | ctx.status = 500;
18 | ctx.body = { success: false, message: "服务器查询错误" };
19 | });
20 | });
21 | export default router;
22 |
--------------------------------------------------------------------------------
/server/src/routes/theme/list.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 |
5 | let router = new Router();
6 |
7 | router.get("/theme", auth(0), async (ctx) => {
8 | let isAll = ctx.request.query.all;
9 |
10 | await DB.Theme.findAll({
11 | attributes: { exclude: ["content"] },
12 | order: [["indexes", "asc"]],
13 | where: isAll ? undefined : { state: 1 },
14 | })
15 | .then((rows) => {
16 | ctx.body = { success: true, message: "查询成功", data: rows };
17 | })
18 | .catch((err) => {
19 | ctx.status = 500;
20 | ctx.body = { success: false, message: "服务器查询错误" };
21 | });
22 | });
23 | export default router;
24 |
--------------------------------------------------------------------------------
/server/src/routes/theme/remove.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import sequelize from "@/db/config";
4 | import verify from "@/common/verify/api-verify/theme/remove";
5 |
6 | let router = new Router();
7 |
8 | router.delete("/theme/:id", verify, async (ctx) => {
9 | let t = await sequelize.transaction();
10 |
11 | // 需要将使用到主题的文章的主题ID换成默认ID
12 | try {
13 | await DB.Article.update({ theme_id: 0 }!, {
14 | where: { theme_id: +ctx.params.id },
15 | transaction: t,
16 | });
17 |
18 | await DB.Theme.destroy({
19 | where: { id: ctx.params.id },
20 | transaction: t,
21 | });
22 | ctx.body = { success: true, message: "删除成功" };
23 | } catch (error) {
24 | ctx.status = 500;
25 | ctx.body = { success: false, message: "删除失败" };
26 | console.log(error);
27 | }
28 | });
29 | export default router;
30 |
--------------------------------------------------------------------------------
/server/src/routes/theme/update.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import { refreshUrls } from "@/common/utils/static";
4 | import verify from "@/common/verify/api-verify/theme/update";
5 |
6 | let router = new Router();
7 |
8 | router.put("/theme/:id", verify, async (ctx) => {
9 | await DB.Theme.update(ctx.request.body, { where: { id: ctx.params.id } })
10 | .then((res) => {
11 | ctx.body = { success: true, message: "修改成功" };
12 | // 清除掉缓存
13 | if (ctx.request.body.content && process.env.ENV == "production") {
14 | refreshUrls([`${process.env.CLIENT_CDN}/${ctx.params.id}.css`]).catch(
15 | (err) => {
16 | console.log(err);
17 | },
18 | );
19 | }
20 | })
21 | .catch((err) => {
22 | console.log(err);
23 | ctx.body = { success: false, message: "修改失败" };
24 | ctx.status = 500;
25 | });
26 | });
27 | export default router;
28 |
--------------------------------------------------------------------------------
/server/src/routes/user/destroy/destroy.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import auth from "@/common/middleware/auth";
3 | import destroy from "@/common/modules/user/destroy";
4 |
5 | let router = new Router();
6 | router.post("/user/destroy", auth(0), async (ctx) => {
7 | if (process.env.AUTH_MODE != "session") {
8 | ctx.body = {
9 | success: false,
10 | message: "当前鉴权模式不为Session,不支持注销",
11 | };
12 | ctx.status = 500;
13 | return;
14 | }
15 |
16 | if (ctx.auth != 0) {
17 | ctx.body = { success: false, message: "只有普通用户才能注销账号" };
18 | ctx.status = 500;
19 | }
20 |
21 | await destroy(ctx.id!)
22 | .then((params) => {
23 | ctx.body = params;
24 | if (!params.success) {
25 | ctx.status = 500;
26 | }
27 | })
28 | .catch((err) => {
29 | ctx.body = { success: false, message: "注销失败" };
30 | ctx.status = 500;
31 | console.log(err);
32 | });
33 | });
34 | export default router;
35 |
--------------------------------------------------------------------------------
/server/src/routes/user/update/update.ts:
--------------------------------------------------------------------------------
1 | import Router from "@koa/router";
2 | import DB from "@/db";
3 | import auth from "@/common/middleware/auth";
4 | import validator from "@/common/verify/api-verify/user/update";
5 |
6 | let router = new Router();
7 | router.put("/user", validator, auth(0), async (ctx) => {
8 | await DB.User.update(ctx.request.body, { where: { id: ctx.id } })
9 | .then(() => {
10 | ctx.body = { success: true, message: "修改成功" };
11 | })
12 | .catch((err) => {
13 | ctx.status = 500;
14 | ctx.body = { success: false, message: "更新错误" };
15 | console.log(err);
16 | });
17 | });
18 | export default router;
19 |
--------------------------------------------------------------------------------
/server/src/socket/index.ts:
--------------------------------------------------------------------------------
1 | import { globSync } from "glob";
2 | import getFilePath from "../common/modules/getFilePath";
3 |
4 | function start() {
5 | getFilePath("getSocket", [__dirname], () =>
6 | globSync([`**/*.js`, `**/*.ts`], {
7 | ignore: ["index.js", "index.ts"],
8 | cwd: __dirname,
9 | }),
10 | ).forEach((item) => {
11 | import(item);
12 | });
13 | }
14 | export default start;
15 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "target": "ES2017",
5 | "module": "commonjs",
6 | "outDir": "./dist",
7 | "rootDir": "./",
8 | "moduleResolution": "node",
9 | //变量和函数参数未使用警告
10 | // "noUnusedLocals": true,
11 | // "noUnusedParameters": true,
12 | "removeComments": true, //取消注释
13 | "strict": true,
14 | "baseUrl": "./",
15 | "paths": {
16 | "@/*": ["src/*"],
17 | "@type/*": ["types/*"]
18 | },
19 | "skipLibCheck": true, //跳过声明文件的类型检查。
20 | "esModuleInterop": true, // 允许export=导出,由import from 导入
21 | "noImplicitAny": true, // 不允许隐式的 any 类型
22 | "typeRoots": ["types"],
23 | },
24 | "include": ["src", "scripts"],
25 | "exclude": ["**/*.d.ts"]
26 | }
27 |
--------------------------------------------------------------------------------
/server/types/identicon.js/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "identicon.js" {
2 | let a: any;
3 | export default a;
4 | }
5 |
--------------------------------------------------------------------------------
/server/types/koa/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { Context } from "koa";
2 | import type { Request } from "koa";
3 |
4 | declare module "koa" {
5 | interface Context {
6 | /** 用户ID*/
7 | id?: number;
8 | /** 用户身份*/
9 | auth?: number;
10 | /** 用户访问token*/
11 | token?: string;
12 | request: Request & {
13 | /** 设置请求体默认类型*/
14 | body: any;
15 | };
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/server/types/turndown-plugin-gfm/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "turndown-plugin-gfm" {
2 | let a:any;
3 | export default a;
4 | }
5 |
--------------------------------------------------------------------------------