├── doc
├── tech.png
├── preview.png
└── lighthouse.png
├── web
├── .env
├── src
│ ├── pages
│ │ ├── index
│ │ │ ├── assets
│ │ │ │ ├── logo.webp
│ │ │ │ └── video.jpg
│ │ │ ├── App.vue
│ │ │ ├── scss
│ │ │ │ ├── core
│ │ │ │ │ ├── _all.scss
│ │ │ │ │ ├── content.scss
│ │ │ │ │ ├── _mixin.scss
│ │ │ │ │ ├── _layout.scss
│ │ │ │ │ ├── _vars.scss
│ │ │ │ │ └── _init.scss
│ │ │ │ ├── main.scss
│ │ │ │ ├── _mixins.scss
│ │ │ │ ├── _page.scss
│ │ │ │ ├── _cards.scss
│ │ │ │ └── _theme_dark.scss
│ │ │ ├── components
│ │ │ │ ├── cards
│ │ │ │ │ ├── MText.vue
│ │ │ │ │ ├── MRichText.vue
│ │ │ │ │ ├── MVideo.vue
│ │ │ │ │ └── Opt.vue
│ │ │ │ ├── Main.vue
│ │ │ │ ├── Footer.vue
│ │ │ │ ├── HoTab.vue
│ │ │ │ └── Content.vue
│ │ │ ├── registerSW.js
│ │ │ ├── store
│ │ │ │ └── main.js
│ │ │ ├── main.js
│ │ │ └── router
│ │ │ │ └── router.js
│ │ └── admin
│ │ │ ├── App.vue
│ │ │ ├── styles
│ │ │ ├── _mixins.scss
│ │ │ ├── main.scss
│ │ │ ├── libs
│ │ │ │ └── _all.scss
│ │ │ ├── _animate.scss
│ │ │ ├── _card.scss
│ │ │ ├── _element.scss
│ │ │ ├── _section.scss
│ │ │ ├── _custom.scss
│ │ │ └── _theme-default.scss
│ │ │ ├── stores
│ │ │ ├── style.js
│ │ │ └── main.js
│ │ │ ├── components
│ │ │ ├── toast
│ │ │ │ ├── Toast.vue
│ │ │ │ └── index.js
│ │ │ ├── FormControl.vue
│ │ │ ├── MenuLink.vue
│ │ │ ├── BoxMain.vue
│ │ │ ├── FormField.vue
│ │ │ ├── BasicIcon.vue
│ │ │ ├── Tile.vue
│ │ │ └── MenuItem.vue
│ │ │ ├── main.js
│ │ │ ├── config.js
│ │ │ ├── views
│ │ │ ├── Home.vue
│ │ │ ├── Login.vue
│ │ │ └── User.vue
│ │ │ ├── layouts
│ │ │ └── Admin.vue
│ │ │ └── router
│ │ │ └── index.js
│ └── lib
│ │ └── http.js
├── .env.development
├── .gitignore
├── README.md
├── admin.html
├── index.html
├── package.json
└── vite.config.js
├── internal
├── application
│ ├── dto
│ │ ├── query.go
│ │ ├── oauth.go
│ │ ├── user.go
│ │ ├── node.go
│ │ ├── favor.go
│ │ └── site.go
│ ├── service
│ │ ├── user_test.go
│ │ ├── craw_test.go
│ │ ├── oauth.go
│ │ ├── favor.go
│ │ ├── user.go
│ │ ├── craw.go
│ │ └── node.go
│ └── store
│ │ ├── base_repo.go
│ │ ├── node_repo.go
│ │ ├── favor_repo.go
│ │ ├── site_repo.go
│ │ └── user_repo.go
├── constant
│ ├── resp.go
│ └── common.go
├── domain
│ ├── model
│ │ ├── base.go
│ │ ├── favor.go
│ │ ├── user.go
│ │ ├── node.go
│ │ └── site.go
│ └── repo
│ │ ├── favor.go
│ │ ├── node.go
│ │ ├── user.go
│ │ └── site.go
├── pb
│ ├── commander.proto
│ └── agent.proto
├── util
│ └── token.go
├── api
│ ├── middleware
│ │ ├── cache.go
│ │ ├── online.go
│ │ └── auth.go
│ ├── handler
│ │ ├── base_test.go
│ │ ├── user.go
│ │ ├── base.go
│ │ ├── user_test.go
│ │ ├── index.go
│ │ ├── stat.go
│ │ ├── admin
│ │ │ ├── node.go
│ │ │ └── site.go
│ │ ├── auth.go
│ │ └── favor.go
│ └── route.go
├── config
│ ├── config_test.go
│ └── config.go
├── infra
│ ├── cache
│ │ └── redis.go
│ └── db
│ │ ├── mysql_test.go
│ │ └── mysql.go
├── commander
│ ├── commander.go
│ └── job.go
├── core
│ ├── rpc
│ │ ├── client.go
│ │ └── pool.go
│ └── site
│ │ ├── github_test.go
│ │ ├── hacker_test.go
│ │ ├── zhihu.go
│ │ ├── weibo.go
│ │ ├── hacker.go
│ │ ├── tieba.go
│ │ ├── v2ex.go
│ │ ├── guanggu.go
│ │ ├── zaobao.go
│ │ ├── chouti.go
│ │ ├── github.go
│ │ ├── reddit.go
│ │ └── wbvideo.go
├── agent
│ ├── agent_test.go
│ └── agent.go
├── agent.go
└── api.go
├── .gitignore
├── pkg
├── helper
│ ├── hash.go
│ ├── time.go
│ └── ip.go
├── oauth
│ ├── oauth.go
│ └── github.go
├── flow
│ └── flow.go
├── http
│ └── client.go
└── logger
│ └── log.go
├── scripts
├── dockerfiles
│ ├── front.Dockerfile
│ ├── agent.Dockerfile
│ ├── commander.Dockerfile
│ └── api.Dockerfile
├── k8s
│ ├── mu-agent.yaml
│ ├── mu-api.yaml
│ └── mu-commander.yaml
└── configs
│ └── nginx.conf
├── conf
└── demo.yml
├── README.md
├── cmd
├── api
│ └── main.go
├── agent
│ └── main.go
└── commander
│ └── main.go
├── test
├── setup.go
└── web.go
├── LICENSE
├── mage.go
└── go.mod
/doc/tech.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronzjc/mu/HEAD/doc/tech.png
--------------------------------------------------------------------------------
/doc/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronzjc/mu/HEAD/doc/preview.png
--------------------------------------------------------------------------------
/doc/lighthouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronzjc/mu/HEAD/doc/lighthouse.png
--------------------------------------------------------------------------------
/web/.env:
--------------------------------------------------------------------------------
1 | VITE_APP_URL=https://mu.memosa.cn
2 | VITE_APP_VERSION=$VERSION
3 |
4 | VITE_TOKEN_KEY=mu_cache_access_token
--------------------------------------------------------------------------------
/web/src/pages/index/assets/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronzjc/mu/HEAD/web/src/pages/index/assets/logo.webp
--------------------------------------------------------------------------------
/web/src/pages/index/assets/video.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronzjc/mu/HEAD/web/src/pages/index/assets/video.jpg
--------------------------------------------------------------------------------
/web/src/pages/index/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/web/.env.development:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | VITE_APP_URL=https://mu.memosa.cn
3 | VITE_APP_VERSION=1.0
4 |
5 | VITE_TOKEN_KEY=mu_cache_access_token
--------------------------------------------------------------------------------
/internal/application/dto/query.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | type Query struct {
4 | Query string
5 | Args []interface{}
6 | Order string
7 | Limit int
8 | }
9 |
--------------------------------------------------------------------------------
/web/src/pages/admin/App.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/web/src/pages/index/scss/core/_all.scss:
--------------------------------------------------------------------------------
1 | @import "vars";
2 | @import "mixin";
3 | @import "init";
4 | @import "layout";
5 | @import "elements";
6 | @import "content";
--------------------------------------------------------------------------------
/internal/application/dto/oauth.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | type OAuthPlatform struct {
4 | Name string `json:"name"`
5 | Type string `json:"type"`
6 | Url string `json:"url"`
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin/
2 | coverage.out
3 | dagger/
4 | public/
5 | **/dev.yml
6 | **/prod.yml
7 | **/kubeconf.yaml
8 |
9 | # Editor directories and files
10 | .idea
11 | .vscode
12 | *.suo
13 | *.ntvs*
14 | *.njsproj
15 | *.sln
16 | *.sw?
--------------------------------------------------------------------------------
/internal/constant/resp.go:
--------------------------------------------------------------------------------
1 | package constant
2 |
3 | const (
4 | CodeSuccess = 10000
5 | CodeError = 10001
6 | CodeForbidden = 10002
7 | CodeAuthFailed = 10003
8 |
9 | // 错误消息定义
10 | ERR_MSG_USERLIST = "获取用户列表失败"
11 | )
12 |
--------------------------------------------------------------------------------
/pkg/helper/hash.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "crypto/md5"
5 | "fmt"
6 | )
7 |
8 | func Md5(input string) string {
9 | has := md5.Sum([]byte(input))
10 | md5Str := fmt.Sprintf("%x", has)
11 |
12 | return md5Str
13 | }
14 |
--------------------------------------------------------------------------------
/scripts/dockerfiles/front.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:stable-alpine
2 | COPY ./dagger/frontend /usr/share/nginx/html
3 | COPY ./scripts/nginx.conf /etc/nginx/conf.d/default.conf
4 | EXPOSE 80
5 | VOLUME /usr/share/nginx/html
6 | CMD ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/web/src/pages/index/scss/main.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * just to reduce dst css size
3 | */
4 |
5 | @import "core/_all.scss";
6 |
7 | @import "theme_dark";
8 |
9 | @import "mixins";
10 | @import "cards";
11 |
12 | @import "page";
13 |
14 | @import "nprogress/nprogress.css";
--------------------------------------------------------------------------------
/internal/domain/model/base.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "time"
4 |
5 | const DB_MU = "mu"
6 |
7 | type BaseModel struct {
8 | ID uint `gorm:"column:id" json:"id"`
9 | CreatedAt time.Time `json:"created_at"`
10 | UpdatedAt time.Time `json:"updated_at"`
11 | }
12 |
--------------------------------------------------------------------------------
/scripts/dockerfiles/agent.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.7
2 | ENV APP_ENV prod
3 | RUN apk add --no-cache ca-certificates tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
4 | RUN mkdir -p /app/bin
5 | COPY ./dagger/backend/agent /app/bin/
6 | WORKDIR /app
7 | EXPOSE 7990
8 | CMD ["./bin/agent"]
--------------------------------------------------------------------------------
/pkg/oauth/oauth.go:
--------------------------------------------------------------------------------
1 | package oauth
2 |
3 | type OAuth interface {
4 | Type() string
5 | RedirectAuth() string
6 | RequestAccessToken(string) (string, error)
7 | RequestUser(string) (User, error)
8 | }
9 |
10 | type User struct {
11 | ID int64
12 | Username string
13 | Nickname string
14 | Avatar string
15 | }
16 |
--------------------------------------------------------------------------------
/internal/pb/commander.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | package pb;
3 |
4 | option go_package = "./internal/pb";
5 |
6 | service Commander {
7 | rpc UpdateCron (Cron) returns (CronRes) {}
8 | }
9 |
10 | message Empty {}
11 |
12 | message Cron {
13 | string site = 1;
14 | }
15 | message CronRes {
16 | bool success = 1;
17 | }
--------------------------------------------------------------------------------
/scripts/dockerfiles/commander.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.7
2 | ENV APP_ENV prod
3 | RUN apk add --no-cache ca-certificates tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
4 | RUN mkdir -p /app/bin /app/conf
5 | COPY ./dagger/backend/commander /app/bin
6 | EXPOSE 7970
7 | VOLUME /app/conf
8 | WORKDIR /app
9 | CMD ["./bin/commander", "-c", "conf/prod.yml"]
--------------------------------------------------------------------------------
/internal/util/token.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/md5"
5 | "fmt"
6 | "time"
7 | )
8 |
9 | func GenerateToken(input string, salt string) string {
10 | data := []byte(fmt.Sprintf("%s%s%s", input, salt, time.Now().Format("2006_01_02_15_04_05")))
11 | has := md5.Sum(data)
12 | md5Str := fmt.Sprintf("%x", has)
13 |
14 | return md5Str
15 | }
16 |
--------------------------------------------------------------------------------
/web/src/pages/admin/styles/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin transition($t) {
2 | transition: $t 100ms ease-in-out 50ms;
3 | }
4 |
5 | @mixin icon-with-update-mark($icon-base-width) {
6 | .icon {
7 | width: $icon-base-width;
8 |
9 | &.has-update-mark:after {
10 | right: ($icon-base-width * 0.5) - 0.85;
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/internal/constant/common.go:
--------------------------------------------------------------------------------
1 | package constant
2 |
3 | type SvcName string
4 |
5 | const (
6 | // 认证通过后用户缓存Key
7 | LoginKey = "login_user"
8 |
9 | // Commander用于选举的状态key
10 | IdMachine = "id_machine"
11 | JobVisor = "job_visor"
12 | Election = "election"
13 |
14 | // svc定义
15 | SvcCommander SvcName = "commander"
16 | SvcOnline SvcName = "online"
17 | )
18 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/internal/api/middleware/cache.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // 给静态资源添加一个客户端缓存时间
10 | func AddCacheControlHeader() gin.HandlerFunc {
11 | return func(c *gin.Context) {
12 | if strings.HasPrefix(c.Request.RequestURI, "/static/") {
13 | c.Header("Cache-Control", "max-age=31536000")
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/web/src/pages/admin/styles/main.scss:
--------------------------------------------------------------------------------
1 | /* Theme style (colors & sizes) */
2 | @import 'theme-default';
3 |
4 | /* Core Libs & Lib configs */
5 | @import 'libs/all';
6 |
7 | /* Mixins */
8 | @import 'mixins';
9 |
10 | /* Theme components */
11 | @import 'element';
12 | @import 'menu';
13 | @import 'card';
14 | @import 'form';
15 | @import 'section';
16 | @import 'custom';
17 | @import 'animate';
--------------------------------------------------------------------------------
/scripts/dockerfiles/api.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.7
2 | ENV APP_ENV prod
3 | RUN apk add --no-cache ca-certificates tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
4 | RUN mkdir -p /app/bin /app/conf /app/public
5 | COPY ./dagger/backend/api /app/bin
6 | COPY ./dagger/frontend /app/public
7 | EXPOSE 7980
8 | VOLUME /app/conf
9 | WORKDIR /app
10 | CMD ["./bin/api", "-c", "conf/prod.yml"]
--------------------------------------------------------------------------------
/web/src/pages/index/scss/core/content.scss:
--------------------------------------------------------------------------------
1 | .has-text-grey {
2 | color: $grey-dark;
3 | }
4 |
5 | .has-text-centered {
6 | text-align: center;
7 | }
8 |
9 | body {
10 | color: $grey-dark;
11 | }
12 |
13 | h4 {
14 | font-size: $font-size-large;
15 | }
16 |
17 | a {
18 | color: $link;
19 | cursor: pointer;
20 | text-decoration: none;
21 | &:hover {
22 | color: $grey-dark;
23 | }
24 | }
--------------------------------------------------------------------------------
/web/src/pages/admin/styles/libs/_all.scss:
--------------------------------------------------------------------------------
1 | @import 'node_modules/bulma-radio/bulma-radio';
2 | @import 'node_modules/bulma-responsive-tables/bulma-responsive-tables';
3 | @import 'node_modules/bulma-checkbox/bulma-checkbox';
4 | @import 'node_modules/bulma-switch-control/bulma-switch-control';
5 | @import 'node_modules/bulma-upload-control/bulma-upload-control';
6 |
7 | /* Bulma */
8 | @import 'node_modules/bulma/bulma';
9 |
--------------------------------------------------------------------------------
/web/src/pages/index/scss/core/_mixin.scss:
--------------------------------------------------------------------------------
1 | $tablet: 768px;
2 | $desktop: 1024px;
3 |
4 | @mixin is($device) {
5 | @media screen and (min-width: $device) {
6 | @content;
7 | }
8 | }
9 |
10 | @mixin until($device) {
11 | @media screen and (max-width: $device - 1) {
12 | @content
13 | }
14 | }
15 |
16 | @mixin mobile() {
17 | @media screen and (max-width: $tablet - 1) {
18 | @content
19 | }
20 | }
--------------------------------------------------------------------------------
/internal/domain/repo/favor.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/aaronzjc/mu/internal/application/dto"
7 | "github.com/aaronzjc/mu/internal/domain/model"
8 | )
9 |
10 | type FavorRepo interface {
11 | Get(context.Context, *dto.Query) ([]model.Favor, error)
12 | Create(context.Context, model.Favor) error
13 | Del(context.Context, model.Favor) error
14 | Sites(context.Context, *dto.Query) []string
15 | }
16 |
--------------------------------------------------------------------------------
/internal/domain/repo/node.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/aaronzjc/mu/internal/application/dto"
7 | "github.com/aaronzjc/mu/internal/domain/model"
8 | )
9 |
10 | type NodeRepo interface {
11 | Get(context.Context, *dto.Query) ([]model.Node, error)
12 | Create(context.Context, model.Node) error
13 | Update(context.Context, model.Node, map[string]interface{}) error
14 | Del(context.Context, model.Node) error
15 | }
16 |
--------------------------------------------------------------------------------
/web/src/pages/index/components/cards/MText.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/internal/domain/model/favor.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type Favor struct {
8 | ID int `gorm:"id"`
9 | UserId int `gorm:"user_id"`
10 | Site string `gorm:"site"`
11 | Key string `gorm:"key"`
12 | OriginUrl string `gorm:"origin_url"`
13 | Title string `gorm:"title"`
14 | CreateAt time.Time `gorm:"create_at"`
15 | }
16 |
17 | func (f *Favor) TableName() string {
18 | return "favor"
19 | }
20 |
--------------------------------------------------------------------------------
/web/src/pages/admin/stores/style.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useStyleStore = defineStore('style', {
4 | state: () => ({
5 | isAsideMobileOpen: false,
6 | isNavMobileOpen: false
7 | }),
8 | actions: {
9 | toggleAside() {
10 | this.isAsideMobileOpen = !this.isAsideMobileOpen
11 | },
12 | toggleNav() {
13 | this.isNavMobileOpen = !this.isNavMobileOpen
14 | }
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/internal/domain/repo/user.go:
--------------------------------------------------------------------------------
1 | package repo
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/aaronzjc/mu/internal/application/dto"
7 | "github.com/aaronzjc/mu/internal/domain/model"
8 | )
9 |
10 | // User 用户相关行为
11 | type UserRepo interface {
12 | GetUsers(context.Context, *dto.Query) ([]model.User, error)
13 | GetUser(context.Context, *dto.Query) (model.User, error)
14 | CreateUser(context.Context, model.User) error
15 | Update(context.Context, model.User, map[string]interface{}) error
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/helper/time.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import "time"
4 |
5 | const (
6 | LocalTimezone = "Asia/Shanghai"
7 | LayoutISO = "2006-01-02 15:04:05"
8 | )
9 |
10 | var localTimezone *time.Location
11 |
12 | func init() {
13 | localTimezone, _ = time.LoadLocation(LocalTimezone)
14 | }
15 |
16 | func TimeToLocalStr(t time.Time) string {
17 | return t.In(localTimezone).Format(LayoutISO)
18 | }
19 |
20 | func CurrentTimeStr() string {
21 | return time.Now().In(localTimezone).Format(LayoutISO)
22 | }
23 |
--------------------------------------------------------------------------------
/internal/application/service/user_test.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | // func TestUserGetAll(t *testing.T) {
4 | // assert := assert.New(t)
5 |
6 | // userRepo := mocks.NewUserRepo(t)
7 | // userRepo.EXPECT().GetAll(mock.Anything).Return([]model.User{{BaseModel: model.BaseModel{ID: 1}, Username: "aaron"}}, nil)
8 |
9 | // userService := NewUserService(userRepo)
10 | // users, _ := userService.GetUserList(context.Background())
11 | // assert.NotEmpty(users)
12 | // userRepo.AssertExpectations(t)
13 | // }
14 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | # Vue 3 + Vite
2 |
3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `
18 |