├── public
└── test.mp4
├── utils
├── struct_test.go
├── README.md
├── rand_test.go
├── file.go
├── checks
│ ├── user.go
│ └── user_test.go
├── upload
│ └── aliyun.go
├── rand.go
├── tokens
│ └── jwt.go
└── struct.go
├── web
├── static
│ ├── photo.png
│ ├── team.png
│ ├── emoji
│ │ ├── dq.webp
│ │ ├── dx.webp
│ │ ├── han.webp
│ │ ├── hx.webp
│ │ ├── oh.webp
│ │ ├── ok.webp
│ │ ├── sl.webp
│ │ ├── tu.webp
│ │ ├── wy.webp
│ │ ├── xia.webp
│ │ ├── xq.webp
│ │ ├── yh.webp
│ │ ├── aixin.webp
│ │ ├── cold.webp
│ │ ├── daku.webp
│ │ ├── doge.webp
│ │ ├── eyes.webp
│ │ ├── ganga.webp
│ │ ├── guai.webp
│ │ ├── heart.webp
│ │ ├── hejiu.webp
│ │ ├── huaji.webp
│ │ ├── like.webp
│ │ ├── miaoa.webp
│ │ ├── sikao.webp
│ │ ├── smile.webp
│ │ ├── star.webp
│ │ ├── teeth.webp
│ │ ├── teng.webp
│ │ ├── tushe.webp
│ │ ├── tv
│ │ │ ├── se.webp
│ │ │ ├── tv.webp
│ │ │ ├── bishi.webp
│ │ │ ├── bizui.webp
│ │ │ ├── chan.webp
│ │ │ ├── dai.webp
│ │ │ ├── daku.webp
│ │ │ ├── dalao.webp
│ │ │ ├── doge.webp
│ │ │ ├── facai.webp
│ │ │ ├── fanu.webp
│ │ │ ├── ganga.webp
│ │ │ ├── keai.webp
│ │ │ ├── koubi.webp
│ │ │ ├── kun.webp
│ │ │ ├── lenmo.webp
│ │ │ ├── outu.webp
│ │ │ ├── sikao.webp
│ │ │ ├── tuxue.webp
│ │ │ ├── weiqu.webp
│ │ │ ├── wunai.webp
│ │ │ ├── yiwen.webp
│ │ │ ├── yun.webp
│ │ │ ├── dalian.webp
│ │ │ ├── dianzan.webp
│ │ │ ├── guilian.webp
│ │ │ ├── guzhang.webp
│ │ │ ├── haixiu.webp
│ │ │ ├── jingxia.webp
│ │ │ ├── liuhan.webp
│ │ │ ├── liulei.webp
│ │ │ ├── nanguo.webp
│ │ │ ├── qinqin.webp
│ │ │ ├── shengqi.webp
│ │ │ ├── shuizhe.webp
│ │ │ ├── tiaokan.webp
│ │ │ ├── tiaopi.webp
│ │ │ ├── touxiao.webp
│ │ │ ├── weixiao.webp
│ │ │ ├── wenhao.webp
│ │ │ ├── xiaoku.webp
│ │ │ ├── zaijian.webp
│ │ │ ├── zhoumei.webp
│ │ │ ├── huaixiao.webp
│ │ │ ├── liubixue.webp
│ │ │ ├── miantian.webp
│ │ │ ├── shengbing.webp
│ │ │ ├── xieyanxiao.webp
│ │ │ ├── zhuakuang.webp
│ │ │ └── mudengkoudai.webp
│ │ ├── weiqu.webp
│ │ ├── wuyan.webp
│ │ ├── aojiao.webp
│ │ ├── baoquan.webp
│ │ ├── baoyou.webp
│ │ ├── chaozan.webp
│ │ ├── chigua.webp
│ │ ├── dacall.webp
│ │ ├── daxiao.webp
│ │ ├── daxiao2.png
│ │ ├── fengdou.webp
│ │ ├── goutou.webp
│ │ ├── guzhang.webp
│ │ ├── haqian.webp
│ │ ├── jianxiao.webp
│ │ ├── jingxi.webp
│ │ ├── jingya.webp
│ │ ├── kouzhao.webp
│ │ ├── kungou.webp
│ │ ├── lianhong.webp
│ │ ├── mojing.webp
│ │ ├── nanguo.webp
│ │ ├── piezui.webp
│ │ ├── shengli.webp
│ │ ├── shengqi.webp
│ │ ├── tiaopi.webp
│ │ ├── touxiao.webp
│ │ ├── wulian.webp
│ │ ├── wulianku.webp
│ │ ├── xiaoku.webp
│ │ ├── xusheng.webp
│ │ ├── yinxian.webp
│ │ ├── zaijian.webp
│ │ ├── zanghu.webp
│ │ ├── zhichi.webp
│ │ ├── chaokaixin.webp
│ │ ├── fanbaiyan.webp
│ │ ├── layanjing.webp
│ │ ├── shengbing.webp
│ │ ├── zhuakuang.webp
│ │ └── linghunchuqiao.webp
│ ├── logo-font.ttf
│ ├── logo.svg
│ └── logo-font.svg
├── assets
│ ├── index-a20a1c5f.css
│ ├── 404-a33a6ad7.css
│ ├── index-b58b7239.js
│ ├── index-8d9d392b.js
│ ├── index-49849839.js
│ ├── index-87d063d9.js
│ ├── index-cfb193b2.js
│ ├── index-d5fcdf76.js
│ ├── index-f3ff56ac.js
│ ├── index-7c0478df.js
│ ├── 404-eda881f3.js
│ ├── index-a45c59c0.js
│ ├── index-0b7c28d3.js
│ ├── _commonjsHelpers-de833af9.js
│ ├── leftSidebar-69677b96.css
│ ├── index-cef44d9a.js
│ ├── index-cac82435.css
│ ├── leftSidebar-a2720e0a.js
│ ├── el-message-f448e6ff.css
│ ├── videoList-ec29bb1e.css
│ └── catalog-1808318e.css
├── README.md
├── docs
│ ├── team.md
│ ├── ocyss.md
│ ├── project.md
│ └── model.md
└── index.html
├── README.md
├── cmd
├── flags
│ └── flags.go
├── system.go
├── update.go
├── README.md
├── migrate.go
├── config.go
├── init.go
├── stop.go
├── common.go
├── root.go
├── start.go
└── server.go
├── server
├── middleware
│ ├── test.go
│ └── logger.go
├── common
│ ├── binder.go
│ └── common.go
├── handlers
│ ├── handlers.go
│ ├── favorite.go
│ ├── comment.go
│ ├── message.go
│ ├── relation.go
│ ├── user.go
│ └── video.go
├── README.md
├── myrouter.go
├── router.go
├── handlers_test.go
└── server_test.go
├── internal
├── README.md
├── db
│ ├── message.go
│ ├── comment.go
│ ├── user.go
│ ├── db.go
│ ├── relation.go
│ └── video.go
├── model
│ ├── model.go
│ ├── redis.go
│ ├── comment.go
│ ├── message.go
│ ├── video.go
│ └── user.go
├── conf
│ ├── conf.go
│ └── type.go
└── bootstrap
│ ├── log.go
│ ├── conf.go
│ └── db.go
├── file_parms.md
├── main.go
├── test
├── key.go
└── key_test.go
├── go.mod
└── go.sum
/public/test.mp4:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/utils/struct_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
--------------------------------------------------------------------------------
/web/static/photo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/photo.png
--------------------------------------------------------------------------------
/web/static/team.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/team.png
--------------------------------------------------------------------------------
/web/assets/index-a20a1c5f.css:
--------------------------------------------------------------------------------
1 | .videoList[data-v-85d3710b]{--el-color-primary: #cd3521}
2 |
--------------------------------------------------------------------------------
/web/static/emoji/dq.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/dq.webp
--------------------------------------------------------------------------------
/web/static/emoji/dx.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/dx.webp
--------------------------------------------------------------------------------
/web/static/emoji/han.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/han.webp
--------------------------------------------------------------------------------
/web/static/emoji/hx.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/hx.webp
--------------------------------------------------------------------------------
/web/static/emoji/oh.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/oh.webp
--------------------------------------------------------------------------------
/web/static/emoji/ok.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/ok.webp
--------------------------------------------------------------------------------
/web/static/emoji/sl.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/sl.webp
--------------------------------------------------------------------------------
/web/static/emoji/tu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tu.webp
--------------------------------------------------------------------------------
/web/static/emoji/wy.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/wy.webp
--------------------------------------------------------------------------------
/web/static/emoji/xia.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/xia.webp
--------------------------------------------------------------------------------
/web/static/emoji/xq.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/xq.webp
--------------------------------------------------------------------------------
/web/static/emoji/yh.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/yh.webp
--------------------------------------------------------------------------------
/web/static/logo-font.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/logo-font.ttf
--------------------------------------------------------------------------------
/web/static/emoji/aixin.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/aixin.webp
--------------------------------------------------------------------------------
/web/static/emoji/cold.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/cold.webp
--------------------------------------------------------------------------------
/web/static/emoji/daku.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/daku.webp
--------------------------------------------------------------------------------
/web/static/emoji/doge.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/doge.webp
--------------------------------------------------------------------------------
/web/static/emoji/eyes.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/eyes.webp
--------------------------------------------------------------------------------
/web/static/emoji/ganga.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/ganga.webp
--------------------------------------------------------------------------------
/web/static/emoji/guai.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/guai.webp
--------------------------------------------------------------------------------
/web/static/emoji/heart.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/heart.webp
--------------------------------------------------------------------------------
/web/static/emoji/hejiu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/hejiu.webp
--------------------------------------------------------------------------------
/web/static/emoji/huaji.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/huaji.webp
--------------------------------------------------------------------------------
/web/static/emoji/like.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/like.webp
--------------------------------------------------------------------------------
/web/static/emoji/miaoa.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/miaoa.webp
--------------------------------------------------------------------------------
/web/static/emoji/sikao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/sikao.webp
--------------------------------------------------------------------------------
/web/static/emoji/smile.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/smile.webp
--------------------------------------------------------------------------------
/web/static/emoji/star.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/star.webp
--------------------------------------------------------------------------------
/web/static/emoji/teeth.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/teeth.webp
--------------------------------------------------------------------------------
/web/static/emoji/teng.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/teng.webp
--------------------------------------------------------------------------------
/web/static/emoji/tushe.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tushe.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/se.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/se.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/tv.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/tv.webp
--------------------------------------------------------------------------------
/web/static/emoji/weiqu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/weiqu.webp
--------------------------------------------------------------------------------
/web/static/emoji/wuyan.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/wuyan.webp
--------------------------------------------------------------------------------
/web/static/emoji/aojiao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/aojiao.webp
--------------------------------------------------------------------------------
/web/static/emoji/baoquan.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/baoquan.webp
--------------------------------------------------------------------------------
/web/static/emoji/baoyou.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/baoyou.webp
--------------------------------------------------------------------------------
/web/static/emoji/chaozan.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/chaozan.webp
--------------------------------------------------------------------------------
/web/static/emoji/chigua.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/chigua.webp
--------------------------------------------------------------------------------
/web/static/emoji/dacall.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/dacall.webp
--------------------------------------------------------------------------------
/web/static/emoji/daxiao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/daxiao.webp
--------------------------------------------------------------------------------
/web/static/emoji/daxiao2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/daxiao2.png
--------------------------------------------------------------------------------
/web/static/emoji/fengdou.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/fengdou.webp
--------------------------------------------------------------------------------
/web/static/emoji/goutou.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/goutou.webp
--------------------------------------------------------------------------------
/web/static/emoji/guzhang.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/guzhang.webp
--------------------------------------------------------------------------------
/web/static/emoji/haqian.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/haqian.webp
--------------------------------------------------------------------------------
/web/static/emoji/jianxiao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/jianxiao.webp
--------------------------------------------------------------------------------
/web/static/emoji/jingxi.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/jingxi.webp
--------------------------------------------------------------------------------
/web/static/emoji/jingya.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/jingya.webp
--------------------------------------------------------------------------------
/web/static/emoji/kouzhao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/kouzhao.webp
--------------------------------------------------------------------------------
/web/static/emoji/kungou.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/kungou.webp
--------------------------------------------------------------------------------
/web/static/emoji/lianhong.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/lianhong.webp
--------------------------------------------------------------------------------
/web/static/emoji/mojing.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/mojing.webp
--------------------------------------------------------------------------------
/web/static/emoji/nanguo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/nanguo.webp
--------------------------------------------------------------------------------
/web/static/emoji/piezui.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/piezui.webp
--------------------------------------------------------------------------------
/web/static/emoji/shengli.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/shengli.webp
--------------------------------------------------------------------------------
/web/static/emoji/shengqi.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/shengqi.webp
--------------------------------------------------------------------------------
/web/static/emoji/tiaopi.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tiaopi.webp
--------------------------------------------------------------------------------
/web/static/emoji/touxiao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/touxiao.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/bishi.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/bishi.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/bizui.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/bizui.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/chan.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/chan.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/dai.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/dai.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/daku.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/daku.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/dalao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/dalao.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/doge.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/doge.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/facai.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/facai.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/fanu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/fanu.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/ganga.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/ganga.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/keai.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/keai.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/koubi.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/koubi.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/kun.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/kun.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/lenmo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/lenmo.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/outu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/outu.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/sikao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/sikao.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/tuxue.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/tuxue.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/weiqu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/weiqu.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/wunai.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/wunai.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/yiwen.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/yiwen.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/yun.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/yun.webp
--------------------------------------------------------------------------------
/web/static/emoji/wulian.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/wulian.webp
--------------------------------------------------------------------------------
/web/static/emoji/wulianku.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/wulianku.webp
--------------------------------------------------------------------------------
/web/static/emoji/xiaoku.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/xiaoku.webp
--------------------------------------------------------------------------------
/web/static/emoji/xusheng.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/xusheng.webp
--------------------------------------------------------------------------------
/web/static/emoji/yinxian.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/yinxian.webp
--------------------------------------------------------------------------------
/web/static/emoji/zaijian.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/zaijian.webp
--------------------------------------------------------------------------------
/web/static/emoji/zanghu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/zanghu.webp
--------------------------------------------------------------------------------
/web/static/emoji/zhichi.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/zhichi.webp
--------------------------------------------------------------------------------
/web/static/emoji/chaokaixin.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/chaokaixin.webp
--------------------------------------------------------------------------------
/web/static/emoji/fanbaiyan.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/fanbaiyan.webp
--------------------------------------------------------------------------------
/web/static/emoji/layanjing.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/layanjing.webp
--------------------------------------------------------------------------------
/web/static/emoji/shengbing.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/shengbing.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/dalian.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/dalian.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/dianzan.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/dianzan.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/guilian.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/guilian.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/guzhang.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/guzhang.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/haixiu.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/haixiu.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/jingxia.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/jingxia.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/liuhan.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/liuhan.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/liulei.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/liulei.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/nanguo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/nanguo.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/qinqin.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/qinqin.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/shengqi.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/shengqi.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/shuizhe.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/shuizhe.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/tiaokan.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/tiaokan.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/tiaopi.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/tiaopi.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/touxiao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/touxiao.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/weixiao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/weixiao.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/wenhao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/wenhao.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/xiaoku.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/xiaoku.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/zaijian.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/zaijian.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/zhoumei.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/zhoumei.webp
--------------------------------------------------------------------------------
/web/static/emoji/zhuakuang.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/zhuakuang.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/huaixiao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/huaixiao.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/liubixue.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/liubixue.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/miantian.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/miantian.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/shengbing.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/shengbing.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/xieyanxiao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/xieyanxiao.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/zhuakuang.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/zhuakuang.webp
--------------------------------------------------------------------------------
/web/static/emoji/linghunchuqiao.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/linghunchuqiao.webp
--------------------------------------------------------------------------------
/web/static/emoji/tv/mudengkoudai.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/8xmx8/douyin/HEAD/web/static/emoji/tv/mudengkoudai.webp
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 对字节简易版抖音后端项目,个人开发,搭建了一个简单的单机架构,作为上手golang的第一个练手项目。
2 | 字节青训营后端项目:GuTikTok
3 |
--------------------------------------------------------------------------------
/utils/README.md:
--------------------------------------------------------------------------------
1 | ## utils 文件夹介绍
2 |
3 | 工具包,存放通用方法
4 |
5 | ### 文件夹结构
6 | 2023-07-26 15:02:09
7 | ```
8 | utils
9 | ├── file.go // 文件处理相关
10 | └── README.md
11 | ```
12 |
13 |
--------------------------------------------------------------------------------
/web/assets/404-a33a6ad7.css:
--------------------------------------------------------------------------------
1 | .errmain[data-v-e1b2a26a]{display:flex;justify-content:center;align-items:center;flex-direction:column}.errmain .lord[data-v-e1b2a26a]{width:100%;height:50vh}.errmain div[data-v-e1b2a26a]{display:inline-block}
2 |
--------------------------------------------------------------------------------
/cmd/flags/flags.go:
--------------------------------------------------------------------------------
1 | package flags
2 |
3 | var (
4 | DataDir string // 数据目录
5 | Debug bool // Debug 模式
6 | Dev bool // 开发环境
7 | Pro bool // 生产环境
8 | Tst bool // 测试环境
9 | LogStd bool // 是否开始控制台输出
10 | ExPath string // 可执行文件路径
11 | Memory bool // 是否为内存数据库
12 | )
13 |
--------------------------------------------------------------------------------
/cmd/system.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | var systemCmd = &cobra.Command{
8 | Use: "system",
9 | Short: "注册系统服务",
10 | Run: func(cmd *cobra.Command, args []string) {
11 | },
12 | }
13 |
14 | func init() {
15 | rootCmd.AddCommand(systemCmd)
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/update.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | var updateCmd = &cobra.Command{
8 | Use: "update",
9 | Short: "检查版本更新",
10 | Run: func(cmd *cobra.Command, args []string) {
11 | },
12 | }
13 |
14 | func init() {
15 | rootCmd.AddCommand(updateCmd)
16 | }
17 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | ## web 文件夹介绍
2 |
3 | 存放前端编译文件
4 |
5 | ### 文件夹结构
6 | 2023-07-26 15:05:04
7 | ```
8 | web
9 | ├── assets
10 | │ ├── index-12345678.js
11 | │ └── index-12345678.css
12 | ├── index.html
13 | ├── README.md
14 | └── static
15 | ├── logo-font.svg
16 | └── logo.svg
17 | ```
18 |
19 |
--------------------------------------------------------------------------------
/cmd/README.md:
--------------------------------------------------------------------------------
1 | ## cmd 文件夹介绍
2 |
3 | 存放全局 flag,和启动选项
4 |
5 | ### 文件夹结构
6 | 2023-07-25 22:10:01
7 | ```
8 | cmd
9 | ├── common.go // 公共方法
10 | ├── flags // 全局 flag 使用
11 | │ └── flags.go
12 | ├── README.md
13 | ├── root.go // 主文件
14 | ├── server.go // 启动选项
15 | ├── start.go
16 | ├── stop.go
17 | └── ....
18 | ```
19 |
20 |
--------------------------------------------------------------------------------
/web/assets/index-b58b7239.js:
--------------------------------------------------------------------------------
1 | import{_ as e}from"./videoList.vue_vue_type_style_index_0_lang-157dca39.js";import{E as o,H as t,I as r}from"./index-483b83c5.js";import"./positionBox-37075afa.js";import"./_commonjsHelpers-de833af9.js";/* empty css */const f=o({__name:"index",setup(a){return(p,m)=>(t(),r(e,{type:"all"}))}});export{f as default};
2 |
--------------------------------------------------------------------------------
/server/middleware/test.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "github.com/Godvictory/douyin/cmd/flags"
5 | "github.com/gin-gonic/gin"
6 | )
7 |
8 | // Test 测试中间件,仅开发模式下使用
9 | func Test() gin.HandlerFunc {
10 | return func(c *gin.Context) {
11 | if flags.Dev || flags.Tst {
12 | c.Next()
13 | } else {
14 | c.Abort()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/web/docs/team.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
ByteHunters
4 |
5 | “字节猎人”
6 |
7 | 暗示着团队成员们在数字世界中探寻、发现和利用各种有用的字节(byte),通过不断地学习和探索,来提升自己的技能和能力,从而更好地应对各种挑战。(GPT)
8 |
9 |
10 | #
11 |
--------------------------------------------------------------------------------
/web/assets/index-8d9d392b.js:
--------------------------------------------------------------------------------
1 | const u=(o,i=200)=>{let e=0;return(...n)=>new Promise(t=>{e&&(clearTimeout(e),t("cancel")),e=window.setTimeout(()=>{o.apply(void 0,n),e=0,t("done")},i)})},a=(o,i=200)=>{let e=0,n=null;return(...t)=>{const l=r=>{e===0&&(e=r),r-e>=i?(o.apply(void 0,n),n=null,e=0):window.requestAnimationFrame(l)};n===null&&window.requestAnimationFrame(l),n=t}};export{a as L,u as x};
2 |
--------------------------------------------------------------------------------
/server/common/binder.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | // Bind Query/Json 双绑定,为后续网页端提供支持
10 | func Bind(c *gin.Context, data any) error {
11 | if err := c.ShouldBindQuery(data); err != nil {
12 | if err2 := c.ShouldBindJSON(data); err2 != nil {
13 | return errors.Join(err, err2)
14 | }
15 | }
16 | return nil
17 | }
18 |
--------------------------------------------------------------------------------
/web/assets/index-49849839.js:
--------------------------------------------------------------------------------
1 | import{E as a,x as o,al as s,a_ as n,H as r,I as c,M as i}from"./index-483b83c5.js";import{M as m}from"./mdPreview-6631e0bb.js";import"./index-8d9d392b.js";import"./_commonjsHelpers-de833af9.js";const l=a({__name:"index",setup(p){const e=o("");return s(async()=>{const t=await n.get("/docs/ocyss.md");e.value=t.data}),(t,_)=>(r(),c(m,{text:i(e)},null,8,["text"]))}});export{l as default};
2 |
--------------------------------------------------------------------------------
/web/assets/index-87d063d9.js:
--------------------------------------------------------------------------------
1 | import{E as a,x as o,al as s,a_ as r,H as n,I as c,M as p}from"./index-483b83c5.js";import{M as i}from"./mdPreview-6631e0bb.js";import"./index-8d9d392b.js";import"./_commonjsHelpers-de833af9.js";const l=a({__name:"index",setup(m){const e=o("");return s(async()=>{const t=await r.get("/docs/project.md");e.value=t.data}),(t,_)=>(n(),c(i,{text:p(e)},null,8,["text"]))}});export{l as default};
2 |
--------------------------------------------------------------------------------
/web/assets/index-cfb193b2.js:
--------------------------------------------------------------------------------
1 | import{E as a,x as o,al as s,a_ as n,H as r,I as c,M as m}from"./index-483b83c5.js";import{M as i}from"./mdPreview-6631e0bb.js";import"./index-8d9d392b.js";import"./_commonjsHelpers-de833af9.js";const f=a({__name:"index",setup(p){const e=o("");return s(async()=>{const t=await n.get("/docs/model.md");e.value=t.data}),(t,d)=>(r(),c(i,{text:m(e)},null,8,["text"]))}});export{f as default};
2 |
--------------------------------------------------------------------------------
/web/assets/index-d5fcdf76.js:
--------------------------------------------------------------------------------
1 | import{E as a,x as o,al as s,a_ as n,H as r,I as c,M as m}from"./index-483b83c5.js";import{M as i}from"./mdPreview-6631e0bb.js";import"./index-8d9d392b.js";import"./_commonjsHelpers-de833af9.js";const l=a({__name:"index",setup(p){const e=o("");return s(async()=>{const t=await n.get("/docs/team.md");e.value=t.data}),(t,_)=>(r(),c(i,{text:m(e)},null,8,["text"]))}});export{l as default};
2 |
--------------------------------------------------------------------------------
/web/assets/index-f3ff56ac.js:
--------------------------------------------------------------------------------
1 | import{E as a,x as o,al as s,a_ as n,H as r,I as c,M as i}from"./index-483b83c5.js";import{M as p}from"./mdPreview-6631e0bb.js";import"./index-8d9d392b.js";import"./_commonjsHelpers-de833af9.js";const l=a({__name:"index",setup(m){const e=o("");return s(async()=>{const t=await n.get("/docs/api.md");e.value=t.data}),(t,_)=>(r(),c(p,{text:i(e)},null,8,["text"]))}});export{l as default};
2 |
--------------------------------------------------------------------------------
/cmd/migrate.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/Godvictory/douyin/internal/bootstrap"
5 | "github.com/spf13/cobra"
6 | )
7 |
8 | var migrateCmd = &cobra.Command{
9 | Use: "migrate",
10 | Short: "迁移数据库",
11 | Run: func(cmd *cobra.Command, args []string) {
12 | bootstrap.InitConf()
13 | bootstrap.InitDb()
14 | },
15 | }
16 |
17 | func init() {
18 | rootCmd.AddCommand(migrateCmd)
19 | }
20 |
--------------------------------------------------------------------------------
/server/handlers/handlers.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | type H map[string]any
4 |
5 | type MyErr struct {
6 | Msg string
7 | Errs []error
8 | }
9 |
10 | func Ok(data any) (int, any) {
11 | return 0, data
12 | }
13 |
14 | func Err(msg string, errs ...error) (int, MyErr) {
15 | return 1, MyErr{msg, errs}
16 | }
17 |
18 | func ErrParam(errs ...error) (int, MyErr) {
19 | return 1, MyErr{"参数不正确", errs}
20 | }
21 |
--------------------------------------------------------------------------------
/utils/rand_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | func TestGetId(t *testing.T) {
9 | for i := 0; i < 10; i++ {
10 | go func() {
11 | t.Log("ID获取测试: ", GetId(2, 20230724))
12 | }()
13 | }
14 | time.Sleep(3 * time.Second)
15 | }
16 |
17 | func TestRandString(t *testing.T) {
18 | for i := 1; i < 6; i++ {
19 | t.Logf("长度: %d,随机字符串测试: %s", i*11, RandString(i*11))
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/internal/README.md:
--------------------------------------------------------------------------------
1 | ## internal 文件夹介绍
2 | > `internal` : 体内的;内部的;国内的
3 |
4 | **内部服务文件夹**
5 |
6 | 存放内部服务,数据模型
7 |
8 | ### 文件夹结构
9 | 2023-07-25 22:14:16
10 | ```
11 | internal
12 | ├── bootstrap // 项目启动初始化
13 | │ ├── conf.go
14 | │ ├── db.go
15 | │ └── log.go
16 | ├── conf // 配置模块
17 | │ ├── conf.go
18 | │ └── type.go
19 | ├── db // 数据库模块
20 | │ └── db.go
21 | ├── model // 数据库模型
22 | │ └── model.go
23 | └── README.md
24 | ```
25 |
--------------------------------------------------------------------------------
/file_parms.md:
--------------------------------------------------------------------------------
1 |
2 | - O_RDONLY 打开只读文件
3 | - O_WRONLY 打开只写文件
4 | - O_RDWR 打开既可以读取又可以写入文件
5 | - O_APPEND 写入文件时将数据追加到文件尾部
6 | - O_CREATE 如果文件不存在,则创建一个新的文件
7 | - O_TRUNC 表示如果文件存在,则截断文件到零长度
8 | - 0o666:表示文件权限的八进制数。0o666 表示文件所有者、所属组和其他用户都具有读写权限。
9 |
10 | - 在八进制表示法中,0o 前缀表示八进制数。数字 766 对应了文件权限 rw-rw-rw-。
11 | - 7 表示所有者(owner)具有读取、写入和执行权限。
12 | - 6 表示所属组(group)具有读取和写入权限。
13 | - 6 表示其他用户(others)具有读取和写入权限。
14 |
15 |
--------------------------------------------------------------------------------
/web/docs/ocyss.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Ocyss
7 |
8 | 一个04年普通且业余的 编程爱好者
9 |
10 | 没有牛逼的学历,没有厉害的背景,没有过硬的技术~也许n年后只会剩下一丝丝的热爱
11 |
12 | Blog
13 | GitHub
14 |
15 |
16 | #
17 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | ## server 文件夹介绍
2 |
3 | **服务器相关内容**
4 |
5 | 路由,中间件等
6 |
7 | ### 文件夹结构
8 | 2023-07-26 14:58:27
9 | ```
10 | server
11 | ├── common // 统一返回结构
12 | │ └── common.go
13 | ├── handlers // 路由处理/接口
14 | │ ├── comment.go
15 | │ ├── favorite.go
16 | │ ├── message.go
17 | │ ├── publish.go
18 | │ ├── relation.go
19 | │ └── user.go
20 | ├── middleware // 中间件
21 | │ └── logger.go
22 | ├── README.md
23 | └── router.go // 路由初始化
24 | ```
25 |
26 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/Godvictory/douyin/cmd"
5 | )
6 |
7 | func main() {
8 | //f, err := os.Create("trace.out")
9 | //if err != nil {
10 | // log.Fatalf("failed to create trace output file: %v", err)
11 | //}
12 | //defer func() {
13 | // f.Close()
14 | //}()
15 | //// 2. trace绑定文件句柄
16 | //if err := trace.Start(f); err != nil {
17 | // log.Fatalf("failed to start trace: %v", err)
18 | //}
19 | //defer trace.Stop()
20 | cmd.Execute()
21 | }
22 |
--------------------------------------------------------------------------------
/utils/file.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "os"
4 |
5 | // Exists 确定指定的文件是否存在
6 | func Exists(name string) bool {
7 | stat, err := os.Stat(name)
8 | if err != nil {
9 | if os.IsNotExist(err) {
10 | return false
11 | }
12 | }
13 | return !stat.IsDir()
14 | }
15 |
16 | // ExistsDir 确定指定的目录是否存在
17 | func ExistsDir(name string) bool {
18 | stat, err := os.Stat(name)
19 | if err != nil {
20 | if os.IsNotExist(err) {
21 | return false
22 | }
23 | }
24 | return stat.IsDir()
25 | }
26 |
--------------------------------------------------------------------------------
/web/assets/index-7c0478df.js:
--------------------------------------------------------------------------------
1 | import{_ as s}from"./videoList.vue_vue_type_style_index_0_lang-157dca39.js";import{E as n,aw as m,x as p,al as c,w as u,M as t,H as _,I as i,X as f}from"./index-483b83c5.js";import"./positionBox-37075afa.js";import"./_commonjsHelpers-de833af9.js";/* empty css */const h=n({__name:"index",setup(l){const o=m(),e=p("");return c(()=>{u(()=>o.params,(a,r)=>{e.value=a.name},{immediate:!0})}),(a,r)=>t(e)?(_(),i(s,{type:t(e),key:t(e),repeat:""},null,8,["type"])):f("",!0)}});export{h as default};
2 |
--------------------------------------------------------------------------------
/web/assets/404-eda881f3.js:
--------------------------------------------------------------------------------
1 | import{E as o,H as a,O as t,ah as c,b1 as _,aA as r,aB as d,K as s,av as n}from"./index-483b83c5.js";const p=e=>(r("data-v-e1b2a26a"),e=e(),d(),e),i={class:"errmain"},l=p(()=>s("div",{class:"lord"},[s("lord-icon",{src:"https://cdn.lordicon.com/msdnoxun.json",trigger:"loop",colors:"primary:#4be1ec,secondary:#cb5eee",style:{width:"100%",height:"100%"}})],-1)),m=o({__name:"404",setup(e){return(h,u)=>(a(),t("div",i,[l,c(_,{size:"150px",msg:"404"})]))}});const v=n(m,[["__scopeId","data-v-e1b2a26a"]]);export{v as default};
2 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 极简版抖音
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/cmd/config.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/Godvictory/douyin/internal/bootstrap"
7 | "github.com/Godvictory/douyin/internal/conf"
8 | "log"
9 |
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | var configCmd = &cobra.Command{
14 | Use: "config",
15 | Short: "查看配置",
16 | Run: func(cmd *cobra.Command, args []string) {
17 | bootstrap.InitConf()
18 | s, err := json.MarshalIndent(conf.Conf, "", " ")
19 | if err != nil {
20 | log.Fatal("未知错误: ", err)
21 | }
22 | fmt.Println(string(s))
23 | },
24 | }
25 |
26 | func init() {
27 | rootCmd.AddCommand(configCmd)
28 | }
29 |
--------------------------------------------------------------------------------
/web/assets/index-a45c59c0.js:
--------------------------------------------------------------------------------
1 | import{E as l,m as n,x as _,al as d,aC as c,aD as u,H as s,O as i,T as p,ai as v,I as f,M as m,av as x}from"./index-483b83c5.js";import{v as g}from"./positionBox-37075afa.js";/* empty css */import"./_commonjsHelpers-de833af9.js";const h={class:"videoList"},I=l({__name:"index",setup(b){const o=n("loginDialog"),t=_(),e=n("userInfo");return d(()=>{e!=null&&e.value?c.video.publishFollow().then(a=>{a.status_code==0&&(t.value=a.video_list)}):(u.error("请先登录"),o&&(o.value=!0))}),(a,k)=>(s(),i("div",h,[(s(!0),i(p,null,v(m(t),r=>(s(),f(g,{data:r},null,8,["data"]))),256))]))}});const y=x(I,[["__scopeId","data-v-85d3710b"]]);export{y as default};
2 |
--------------------------------------------------------------------------------
/utils/checks/user.go:
--------------------------------------------------------------------------------
1 | package checks
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | )
7 |
8 | // ValidateInput 输入内容验证
9 | func ValidateInput(minLen, maxLen int, s ...string) string {
10 | for i := range s {
11 | n := len(s[i])
12 | switch {
13 | case n == 0:
14 | return "不可以为空值!"
15 | case n < minLen:
16 | return fmt.Sprintf("长度太短,最短为%d个字符!", minLen)
17 | case n > maxLen:
18 | return fmt.Sprintf("长度太长,最长为%d个字符!", maxLen)
19 | case !isValidString(s[i]):
20 | return "不合法,只能使用大小写字母,数字与.!@$%#特殊符号!"
21 | }
22 | }
23 | return ""
24 | }
25 |
26 | func isValidString(s string) bool {
27 | ok, _ := regexp.MatchString("^[a-zA-Z0-9\\.!@$%#]+$", s)
28 | return ok
29 | }
30 |
--------------------------------------------------------------------------------
/internal/db/message.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import "github.com/Godvictory/douyin/internal/model"
4 |
5 | // MessagePush 发送消息
6 | func MessagePush(fid, tid int64, content string) error {
7 | data := model.Message{
8 | ToUserID: tid,
9 | FromUserID: fid,
10 | Content: content,
11 | }
12 | return db.Create(&data).Error
13 | }
14 |
15 | // MessageGet 获取消息列表
16 | func MessageGet(fid, tid, preTime int64) ([]*model.Message, error) {
17 | var data []*model.Message
18 | err := db.Where("created_at > ? AND (to_user_id = ? AND from_user_id = ? OR to_user_id = ? AND from_user_id = ?)", preTime, fid, tid, tid, fid).
19 | Order("created_at ASC").Find(&data).Error
20 | return data, err
21 | }
22 |
--------------------------------------------------------------------------------
/web/assets/index-0b7c28d3.js:
--------------------------------------------------------------------------------
1 | import{E as u,m as n,x as l,al as f,aC as r,aD as c,M as o,H as p,I as m,X as v}from"./index-483b83c5.js";import{u as _}from"./positionBox-37075afa.js";/* empty css */import"./_commonjsHelpers-de833af9.js";const C=u({__name:"index",setup(d){const t=n("userInfo"),i=l([]),s=l([]),a=n("loginDialog");return f(()=>{t!=null&&t.value?(r.video.publish().then(e=>{e.status_code==0&&(s.value=e.video_list)}),r.video.favorite().then(e=>{e.status_code==0&&(i.value=e.video_list)})):(c.error("请先登录"),a&&(a.value=!0))}),(e,h)=>o(t)?(p(),m(_,{key:0,info:o(t),"favorite-list":o(i),"publish-list":o(s)},null,8,["info","favorite-list","publish-list"])):v("",!0)}});export{C as default};
2 |
--------------------------------------------------------------------------------
/web/assets/_commonjsHelpers-de833af9.js:
--------------------------------------------------------------------------------
1 | var u=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function f(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function l(e){if(e.__esModule)return e;var r=e.default;if(typeof r=="function"){var t=function o(){return this instanceof o?Reflect.construct(r,arguments,this.constructor):r.apply(this,arguments)};t.prototype=r.prototype}else t={};return Object.defineProperty(t,"__esModule",{value:!0}),Object.keys(e).forEach(function(o){var n=Object.getOwnPropertyDescriptor(e,o);Object.defineProperty(t,o,n.get?n:{enumerable:!0,get:function(){return e[o]}})}),t}export{l as a,u as c,f as g};
2 |
--------------------------------------------------------------------------------
/internal/model/model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "time"
5 |
6 | "gorm.io/gorm"
7 | )
8 |
9 | // 总结:如果需要查出所有关联的数据就用Preload,查一条关联数据用Related
10 |
11 | var migrate = make([]any, 0, 10)
12 |
13 | type Model struct {
14 | ID int64 `json:"id" gorm:"primarykey;comment:主键"`
15 | CreatedAt time.Time `json:"-" gorm:"comment:创建时间"`
16 | UpdatedAt time.Time `json:"-" gorm:"comment:修改时间"`
17 | DeletedAt gorm.DeletedAt `json:"-" gorm:"comment:删除时间"`
18 | }
19 |
20 | func (m *Model) BeforeCreate(tx *gorm.DB) (err error) {
21 | return
22 | }
23 |
24 | func GetMigrate() []any {
25 | return migrate
26 | }
27 |
28 | // addMigrate 加入自动迁移列表中
29 | func addMigrate(model ...any) {
30 | migrate = append(migrate, model...)
31 | }
32 |
--------------------------------------------------------------------------------
/internal/conf/conf.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | var Conf Config
4 |
5 | func DefaultConfig() Config {
6 | return Config{
7 | Address: "0.0.0.0",
8 | Port: 23724, // 2023-07-24
9 | Database: confDatabase{
10 | Type: "mysql",
11 | Host: "localhost",
12 | Port: 3306,
13 | User: "root",
14 | Password: "root",
15 | Name: "douyin",
16 | DbFile: "data/data.db",
17 | },
18 | Redis: confRedis{
19 | Host: "127.0.0.1",
20 | Port: 6379,
21 | Password: "123456",
22 | Db: 3,
23 | },
24 | Log: confLog{
25 | Enable: true,
26 | Level: "info",
27 | Name: "data/log/log.log",
28 | MaxSize: 10,
29 | MaxBackups: 5,
30 | MaxAge: 28,
31 | Compress: false,
32 | },
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/cmd/init.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/Godvictory/douyin/cmd/flags"
5 | "github.com/Godvictory/douyin/internal/bootstrap"
6 | "os"
7 | "path/filepath"
8 |
9 | log "github.com/sirupsen/logrus"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var initCmd = &cobra.Command{
15 | Use: "init",
16 | Short: "初始化配置",
17 | Run: func(cmd *cobra.Command, args []string) {
18 | log.Info("开始初始化配置文件")
19 | if bootstrap.InitConf() == 0 {
20 | log.Info("已有配置文件,开始进行备份")
21 | err := os.Rename(filepath.Join(flags.DataDir, "config.json"), filepath.Join(flags.DataDir, "config.old.json"))
22 | if err != nil {
23 | log.Fatal("改名失败...")
24 | }
25 | bootstrap.InitConf()
26 | }
27 | log.Info("Ok!")
28 | },
29 | }
30 |
31 | func init() {
32 | rootCmd.AddCommand(initCmd)
33 | }
34 |
--------------------------------------------------------------------------------
/utils/checks/user_test.go:
--------------------------------------------------------------------------------
1 | package checks
2 |
3 | import "testing"
4 |
5 | func TestIsValidString(t *testing.T) {
6 | tests := []struct {
7 | args string
8 | want bool
9 | }{
10 | {"qwertyuuio", true},
11 | {"123456789", true},
12 | {"asdqwe5451232", true},
13 | {"1", true},
14 | {"", false},
15 | {"1564165....", true},
16 | {"!!!!....", true},
17 | {"@@@@@@aaa...", true},
18 | {"###%%%%...", true},
19 | {"+-()==", false},
20 | {"]][[", false},
21 | {"}}{{", false},
22 | {"....", true},
23 | {"++++", false},
24 | {"----", false},
25 | {"~~~~", false},
26 | {"~~~", false},
27 | {":::'''\"\"|||\\\\///", false},
28 | }
29 | for _, tt := range tests {
30 | t.Run(tt.args, func(t *testing.T) {
31 | if got := isValidString(tt.args); got != tt.want {
32 | t.Errorf("f() = %v, want %v", got, tt.want)
33 | }
34 | })
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/cmd/stop.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var stopCmd = &cobra.Command{
11 | Use: "stop",
12 | Short: "停止守护进程",
13 | Run: func(cmd *cobra.Command, args []string) {
14 | stop()
15 | },
16 | }
17 |
18 | func stop() {
19 | initDaemon()
20 | if pid == -1 {
21 | fmt.Println("似乎还没有启动。尝试使用 `./douyin start` 启动服务器.")
22 | return
23 | }
24 | process, err := os.FindProcess(pid)
25 | if err != nil {
26 | fmt.Println("无法按pid找到进程:%d,原因: %v", pid, process)
27 | return
28 | }
29 | err = process.Kill()
30 | if err != nil {
31 | fmt.Println("无法终止进程 %d: %v", pid, err)
32 | } else {
33 | fmt.Println("杀死进程: ", pid)
34 | }
35 | err = os.Remove(pidFile)
36 | if err != nil {
37 | fmt.Println("pid 文件未能正常删除")
38 | }
39 | pid = -1
40 | }
41 |
42 | func init() {
43 | rootCmd.AddCommand(stopCmd)
44 | }
45 |
--------------------------------------------------------------------------------
/internal/model/redis.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "github.com/Godvictory/douyin/cmd/flags"
6 | "strconv"
7 | "time"
8 |
9 | log "github.com/sirupsen/logrus"
10 |
11 | "github.com/redis/go-redis/v9"
12 | )
13 |
14 | var rdb *redis.Client
15 |
16 | // InitRdb 初始化 Redis
17 | func InitRdb(r *redis.Client) {
18 | rdb = r
19 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
20 | defer cancel()
21 | _, err := rdb.Ping(ctx).Result()
22 | if err != nil {
23 | log.Fatalf("连接redis出错,错误信息:%v", err)
24 | }
25 | // 内存模式下清空 Redis
26 | if flags.Memory {
27 | rdb.FlushAll(ctx)
28 | }
29 | }
30 |
31 | func GetRdb() *redis.Client {
32 | return rdb
33 | }
34 |
35 | // getKey 字符串快速拼接
36 | func getKey(id int64, prefix []byte) string {
37 | s := make([]byte, len(prefix), 50)
38 | copy(s, prefix)
39 | s = append(s, strconv.FormatInt(id, 10)...)
40 | return string(s)
41 | }
42 |
--------------------------------------------------------------------------------
/utils/upload/aliyun.go:
--------------------------------------------------------------------------------
1 | package upload
2 |
3 | import (
4 | "fmt"
5 | "github.com/Godvictory/douyin/internal/conf"
6 | "io"
7 |
8 | "github.com/aliyun/aliyun-oss-go-sdk/oss"
9 | )
10 |
11 | func Aliyun(uploadName string, file io.Reader) (string, error) {
12 | AliyunAccessKeyId := conf.Conf.Oss.AccessKeyID
13 | AliyunAccessKeySecret := conf.Conf.Oss.AccessKeySecret
14 | AliyunEndpoint := conf.Conf.Oss.Endpoint
15 | AliyunBucketName := conf.Conf.Oss.BucketName
16 |
17 | client, err := oss.New(AliyunEndpoint, AliyunAccessKeyId, AliyunAccessKeySecret)
18 | if err != nil {
19 | return "", err
20 | }
21 | // 获取存储空间。
22 | bucket, err := client.Bucket(AliyunBucketName)
23 | if err != nil {
24 | return "", err
25 | }
26 | err = bucket.PutObject(uploadName, file)
27 | if err != nil {
28 | return "", err
29 | }
30 | // 拼接链接,默认使用https
31 | return fmt.Sprintf("https://%s.%s/", AliyunBucketName, AliyunEndpoint), nil
32 | }
33 |
--------------------------------------------------------------------------------
/internal/model/comment.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/Godvictory/douyin/utils"
5 | "gorm.io/gorm"
6 | "time"
7 | )
8 |
9 | type (
10 | // Comment 评论表
11 | Comment struct {
12 | Model
13 | UserID int64 `json:"-" gorm:"index:idx_uvid;comment:评论用户信息"`
14 | VideoID int64 `json:"-" gorm:"index:idx_uvid;comment:评论视频信息"`
15 | User User `json:"user" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
16 | Video Video `json:"video" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
17 | Content string `json:"content" gorm:"comment:评论内容"`
18 | CreateDate string `json:"create_date" gorm:"comment:评论发布日期"` // 格式 mm-dd
19 | // 自建字段
20 | ReplyID int64 `json:"reply_id" gorm:"index;comment:回复ID"`
21 | }
22 | )
23 |
24 | func (c *Comment) BeforeCreate(tx *gorm.DB) (err error) {
25 | if c.ID == 0 {
26 | c.ID = utils.GetId(2, 20230724)
27 | }
28 | c.CreateDate = time.Now().Format("01-02")
29 | return
30 | }
31 |
32 | func init() {
33 | addMigrate(&Comment{})
34 | }
35 |
--------------------------------------------------------------------------------
/internal/model/message.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/Godvictory/douyin/utils"
5 | "gorm.io/gorm"
6 | )
7 |
8 | type (
9 | // Message 消息表
10 | Message struct {
11 | ID int64 `json:"id" gorm:"primarykey;comment:主键"`
12 | CreatedAt int64 `json:"create_time" gorm:"autoUpdateTime:milli"`
13 | ToUserID int64 `json:"to_user_id" gorm:"primaryKey;comment:该消息接收者的id"`
14 | FromUserID int64 `json:"from_user_id" gorm:"primaryKey;comment:该消息发送者的id"`
15 | ToUser User `json:"-" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
16 | FromUser User `json:"-" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
17 | Content string `json:"content" gorm:"comment:消息内容"`
18 | }
19 | )
20 |
21 | func (m *Message) BeforeCreate(tx *gorm.DB) (err error) {
22 | if m.ID == 0 {
23 | m.ID = utils.GetId(3, 114514)
24 | }
25 | // 来自一个天坑
26 | // m.CreateTime = time.Now().Format("2006-01-02 15:04:05")
27 | return
28 | }
29 |
30 | func init() {
31 | addMigrate(&Message{})
32 | }
33 |
--------------------------------------------------------------------------------
/utils/rand.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "math/rand"
5 | "time"
6 | )
7 |
8 | var r *rand.Rand
9 |
10 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
11 |
12 | // GetId 获取ID
13 | func GetId(a, b int64) int64 {
14 | // 使用纳秒时间戳,保证递增
15 | now := time.Now()
16 | return now.UnixNano()*a - b
17 | }
18 |
19 | // RandString 获取随机字符串
20 | func RandString(n int) string {
21 | b := make([]byte, n)
22 | for i := range b {
23 | b[i] = letterBytes[r.Int63()%int64(len(letterBytes))]
24 | }
25 | return string(b)
26 | }
27 |
28 | func RandVid(all []int64, n int) (res []int64) {
29 | if len(all) <= n {
30 | return all
31 | }
32 | set := make(map[int64]struct{}, n)
33 | for len(set) < n {
34 | if index := r.Intn(len(all)); index >= 0 {
35 | set[all[index]] = struct{}{}
36 | }
37 | }
38 | for k := range set {
39 | res = append(res, k)
40 | }
41 | return
42 | }
43 | func RandShuffle(n int, f func(int, int)) {
44 | r.Shuffle(n, f)
45 | }
46 |
47 | func init() {
48 | r = rand.New(rand.NewSource(time.Now().UnixNano()))
49 | }
50 |
--------------------------------------------------------------------------------
/internal/db/comment.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "github.com/Godvictory/douyin/internal/model"
5 | )
6 |
7 | // CommentPush 发送评论
8 | func CommentPush(uid, vid int64, content string) (*model.Comment, error) {
9 | data := model.Comment{UserID: uid, VideoID: vid, Content: content}
10 | err := db.Create(&data).Error
11 | if err != nil {
12 | return nil, err
13 | }
14 | data.User.ID = uid
15 | db.Find(&data.User)
16 | video := model.Video{Model: id(vid)}
17 | video.HIncrByCommentCount(1)
18 | return &data, nil
19 | }
20 |
21 | // CommentDel 删除评论
22 | func CommentDel(cid int64) error {
23 | var data model.Comment
24 | data.ID = cid
25 | err := db.Find(&data).Error
26 | if err != nil {
27 | return err
28 | }
29 | video := model.Video{Model: id(data.VideoID)}
30 | video.HIncrByCommentCount(-1)
31 | return db.Delete(&data).Error
32 | }
33 |
34 | // CommentGet 获取评论
35 | func CommentGet(vid int64) ([]*model.Comment, error) {
36 | var data []*model.Comment
37 | err := db.Preload("User").Where("video_id = ?", vid).Find(&data).Error
38 | if err != nil {
39 | return nil, err
40 | }
41 | return data, nil
42 | }
43 |
--------------------------------------------------------------------------------
/internal/bootstrap/log.go:
--------------------------------------------------------------------------------
1 | package bootstrap
2 |
3 | import (
4 | "fmt"
5 | "github.com/Godvictory/douyin/cmd/flags"
6 | "github.com/Godvictory/douyin/internal/conf"
7 | "io"
8 | "os"
9 |
10 | "github.com/natefinch/lumberjack"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | func InitLog() {
15 | logConf := conf.Conf.Log
16 | if flags.Dev || flags.Debug {
17 | log.SetLevel(log.DebugLevel)
18 | log.SetReportCaller(flags.Debug)
19 | } else {
20 | level, err := log.ParseLevel(logConf.Level)
21 | if err != nil {
22 | panic(fmt.Sprintf("日志级别不正确,可用: [panic,fatal,error,warn,info,debug,trace],%v", err))
23 | }
24 | log.SetLevel(level)
25 | }
26 | if logConf.Enable {
27 | var w io.Writer = &lumberjack.Logger{
28 | Filename: logConf.Name,
29 | MaxSize: logConf.MaxSize,
30 | MaxBackups: logConf.MaxBackups,
31 | MaxAge: logConf.MaxAge,
32 | Compress: logConf.Compress,
33 | }
34 | if flags.Dev || flags.Debug || flags.LogStd {
35 | w = io.MultiWriter(os.Stdout, w)
36 | }
37 | log.SetOutput(w)
38 | }
39 | if flags.Dev || flags.Debug {
40 | log.Info("当前程序运行路径: ", flags.ExPath)
41 | }
42 | log.Info("初始化 logrus 成功!")
43 | }
44 |
--------------------------------------------------------------------------------
/server/common/common.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/Godvictory/douyin/cmd/flags"
5 | "net/http"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | // 2023-08-02 20:28:15 决定废弃此方案,重新封装路由
11 | const (
12 | statusOk = 0
13 | statusErr = 1
14 | )
15 |
16 | // OK 成功的请求
17 | func OK(c *gin.Context, data ...map[string]any) {
18 | res := gin.H{
19 | "status_code": statusOk,
20 | "status_msg": "Success",
21 | }
22 | for d := range data {
23 | for k, v := range data[d] {
24 | res[k] = v
25 | }
26 | }
27 | c.JSON(http.StatusOK, res)
28 | }
29 |
30 | // Err 失败的请求
31 | // 推荐所有错误返回都带上err,包括参数错误,在debug和dev模式下能快速定位bug
32 | func Err(c *gin.Context, msg string, err ...error) {
33 | res := gin.H{
34 | "status_code": statusErr,
35 | "status_msg": msg,
36 | }
37 | // 调试与开发模式,返回错误消息。
38 | if (flags.Dev || flags.Debug) && len(err) > 0 {
39 | errs := make([]string, len(err))
40 | for i, e := range err {
41 | if e != nil {
42 | errs[i] = e.Error()
43 | }
44 | }
45 | res["errmsg"] = errs
46 | }
47 | c.JSON(http.StatusOK, res)
48 | }
49 |
50 | // ErrParam 参数错误封装
51 | func ErrParam(c *gin.Context, err ...error) {
52 | Err(c, "参数不正确", err...)
53 | }
54 |
--------------------------------------------------------------------------------
/utils/tokens/jwt.go:
--------------------------------------------------------------------------------
1 | package tokens
2 |
3 | import (
4 | "errors"
5 | "github.com/Godvictory/douyin/internal/conf"
6 | "time"
7 |
8 | "github.com/golang-jwt/jwt/v5"
9 | )
10 |
11 | type MyClaims struct {
12 | ID int64 `json:"id"`
13 | Username string `json:"username"`
14 | jwt.RegisteredClaims
15 | }
16 |
17 | // GetToken 生成token
18 | func GetToken(id int64, username string) (string, error) {
19 | expireTime := time.Now().Add(time.Hour * 24 * 90) // 三个月过期
20 | SetClaims := MyClaims{
21 | id,
22 | username,
23 | jwt.RegisteredClaims{
24 | ExpiresAt: jwt.NewNumericDate(expireTime),
25 | Issuer: "ByteHunters",
26 | },
27 | }
28 | reqClaim := jwt.NewWithClaims(jwt.SigningMethodHS256, SetClaims)
29 | return reqClaim.SignedString([]byte(conf.Conf.JwtSecret))
30 | }
31 |
32 | // CheckToken 验证token
33 | func CheckToken(token string) (*MyClaims, error) {
34 | key, err := jwt.ParseWithClaims(token, &MyClaims{}, func(*jwt.Token) (any, error) {
35 | return []byte(conf.Conf.JwtSecret), nil
36 | })
37 | if err != nil {
38 | return nil, err
39 | }
40 | if claims, ok := key.Claims.(*MyClaims); ok && key.Valid {
41 | return claims, nil
42 | } else {
43 | return nil, errors.New("你的token已过期")
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/internal/db/user.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "github.com/Godvictory/douyin/internal/model"
5 | "gopkg.in/hlandau/passlib.v1"
6 | )
7 |
8 | // Register 注册
9 | func Register(data *model.User) (msg string, err error) {
10 | hash, err := passlib.Hash(data.Pawd)
11 | if err != nil {
12 | msg = "请更换您的密码再试一次"
13 | return
14 | }
15 | data.Pawd = hash
16 | err = db.Create(&data).Error
17 | if err != nil {
18 | msg = "抱歉,请稍后再试..."
19 | return
20 | }
21 | return "", nil
22 | }
23 |
24 | // Login 登录
25 | func Login(user, pawd string) (data *model.User, msg string, err error) {
26 | data = new(model.User)
27 | // 根据用户名获取对应的全部数据
28 | err = db.Where("name = ?", user).Find(&data).Error
29 | if err != nil {
30 | msg = "没有此用户名~"
31 | return
32 | }
33 | // 进行哈希值效验密码是否正确
34 | newHash, err := passlib.Verify(pawd, data.Pawd)
35 | if err != nil {
36 | msg = "用户名或者密码不正确!"
37 | return
38 | }
39 | if newHash != "" {
40 | // 登陆成功,判断是否需要更换哈希值
41 | db.Where(data).Update("pawd", newHash)
42 | }
43 | return
44 | }
45 |
46 | // UserInfo 获取用户信息
47 | func UserInfo(id int64) (data model.User, msg string, err error) {
48 | data.ID = id
49 | err = db.Set("user_id", id).Find(&data).Error
50 | if err != nil {
51 | msg = "抱歉,请稍后再试"
52 | }
53 | return
54 | }
55 |
--------------------------------------------------------------------------------
/cmd/common.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/Godvictory/douyin/cmd/flags"
5 | "github.com/Godvictory/douyin/internal/bootstrap"
6 | "github.com/Godvictory/douyin/utils"
7 | "github.com/sirupsen/logrus"
8 | "math/rand"
9 | "os"
10 | "path/filepath"
11 | "strconv"
12 | "time"
13 | )
14 |
15 | var (
16 | pid = -1
17 | pidFile string
18 | )
19 |
20 | // initServer 初始化服务
21 | func initServer() {
22 | // 配置日志格式
23 | formatter := logrus.TextFormatter{
24 | ForceColors: true,
25 | EnvironmentOverrideColors: true,
26 | TimestampFormat: "2006-01-02 15:04:05",
27 | FullTimestamp: true,
28 | DisableQuote: true,
29 | }
30 | logrus.SetFormatter(&formatter)
31 | // 服务初始化
32 | bootstrap.InitConf()
33 | bootstrap.InitLog()
34 | bootstrap.InitDb()
35 | bootstrap.InitRdb()
36 | rand.Seed(time.Now().Unix())
37 | }
38 |
39 | // initDaemon 守护进程初始化
40 | func initDaemon() {
41 | pidFile = filepath.Join(flags.DataDir, "pid")
42 | if utils.Exists(pidFile) {
43 | bytes, err := os.ReadFile(pidFile)
44 | if err != nil {
45 | logrus.Fatal("无法读取pid文件,", err)
46 | }
47 | id, err := strconv.Atoi(string(bytes))
48 | if err != nil {
49 | logrus.Fatal("无法转换pid,", err)
50 | }
51 | pid = id
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/web/assets/leftSidebar-69677b96.css:
--------------------------------------------------------------------------------
1 | .el-divider{position:relative}.el-divider--horizontal{display:block;height:1px;width:100%;margin:24px 0;border-top:1px var(--el-border-color) var(--el-border-style)}.el-divider--vertical{display:inline-block;width:1px;height:1em;margin:0 8px;vertical-align:middle;position:relative;border-left:1px var(--el-border-color) var(--el-border-style)}.el-divider__text{position:absolute;background-color:var(--el-bg-color);padding:0 20px;font-weight:500;color:var(--el-text-color-primary);font-size:14px}.el-divider__text.is-left{left:20px;transform:translateY(-50%)}.el-divider__text.is-center{left:50%;transform:translate(-50%) translateY(-50%)}.el-divider__text.is-right{right:20px;transform:translateY(-50%)}.el-menu-vertical[data-v-d2bf7a98]{--el-menu-item-font-size: 20px;display:flex;justify-content:center;align-items:center;border-right:0;flex-direction:column}.el-menu-vertical .el-menu-item[data-v-d2bf7a98]{--el-menu-hover-bg-color: "" !important;--el-menu-active-color: rgba(255, 255, 255, 1);--el-menu-text-color: rgba(255, 255, 255, .5)}.el-menu-vertical .el-menu-item lord-icon[data-v-d2bf7a98]{width:30px;height:30px}.el-menu-vertical .el-menu-item[data-v-d2bf7a98]:hover{--el-menu-text-color: rgba(255, 255, 255, 1)}.el-menu-vertical .el-divider[data-v-d2bf7a98]{width:40%}
2 |
--------------------------------------------------------------------------------
/test/key.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | var byteAdata = make([]byte, 0, 50)
11 |
12 | func init() {
13 | byteAdata = append(byteAdata, "video:favorite_count/"...)
14 | }
15 |
16 | func ByteA(vid int64) string {
17 | t := make([]byte, 0, 50)
18 | copy(t, byteAdata)
19 | t = append(t, strconv.FormatInt(vid, 10)...)
20 | return string(t)
21 | }
22 |
23 | func ByteB(vid int64) string {
24 | t := make([]byte, 0, 50)
25 | copy(t, byteAdata)
26 | t = append(t, strconv.FormatInt(vid, 16)...)
27 | return string(t)
28 | }
29 |
30 | func ByteC(vid int64) string {
31 | t := make([]byte, 0, 50)
32 | copy(t, byteAdata)
33 | t = append(t, strconv.FormatInt(vid, 36)...)
34 | return string(t)
35 | }
36 |
37 | func Builder(vid int64) string {
38 | var builder strings.Builder
39 |
40 | builder.Grow(50)
41 | builder.WriteString("video:favorite_count/")
42 | builder.WriteString(strconv.FormatInt(vid, 10))
43 | return builder.String()
44 | }
45 |
46 | func Buffer(vid int64) string {
47 | var buffer bytes.Buffer
48 |
49 | buffer.Grow(50)
50 | buffer.WriteString("video:favorite_count/")
51 | buffer.WriteString(strconv.FormatInt(vid, 10))
52 | return buffer.String()
53 | }
54 |
55 | func Printf(vid int64) string {
56 | return fmt.Sprintf("video:favorite_count/%d", vid)
57 | }
58 |
--------------------------------------------------------------------------------
/utils/struct.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "errors"
5 | "reflect"
6 | )
7 |
8 | // verify 简单的类型判断
9 | func verify(dst, src any) (srcT, dstT reflect.Type, srcV, dstV reflect.Value, err error) {
10 | srcT, srcV = reflect.TypeOf(src), reflect.ValueOf(src)
11 | if srcT.Kind() == reflect.Ptr {
12 | srcT, srcV = srcT.Elem(), srcV.Elem()
13 | }
14 | if srcT.Kind() != reflect.Struct {
15 | err = errors.New("仅支持 Struct 进行合并")
16 | return
17 | }
18 | dstT, dstV = reflect.TypeOf(dst), reflect.ValueOf(dst)
19 | if dstT.Kind() != reflect.Ptr || dstT.Elem().Kind() != reflect.Struct {
20 | err = errors.New("dst 必须为 Struct指针")
21 | } else {
22 | dstT, dstV = dstT.Elem(), dstV.Elem()
23 | }
24 | return
25 | }
26 |
27 | // Merge 合并两个结构体
28 | // 危危危,反射很危险,多测试
29 | func Merge(dst, src any) error {
30 | srcT, dstT, srcV, dstV, err := verify(dst, src)
31 | if err != nil {
32 | return err
33 | }
34 | if srcV.NumField() < dstV.NumField() {
35 | for i := 0; i < srcV.NumField(); i++ {
36 | curT, curV := srcT.Field(i), srcV.Field(i)
37 | f := dstV.FieldByName(curT.Name)
38 | if f.IsValid() && curV.Type() == f.Type() {
39 | f.Set(curV)
40 | }
41 | }
42 | } else {
43 | for i := 0; i < dstV.NumField(); i++ {
44 | curT, curV := dstT.Field(i), dstV.Field(i)
45 | f := srcV.FieldByName(curT.Name)
46 | if f.IsValid() && curV.Type() == f.Type() {
47 | curV.Set(f)
48 | }
49 | }
50 | }
51 | return nil
52 | }
53 |
--------------------------------------------------------------------------------
/internal/db/db.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "github.com/Godvictory/douyin/internal/model"
5 | "reflect"
6 |
7 | log "github.com/sirupsen/logrus"
8 | "gorm.io/gorm"
9 | )
10 |
11 | var (
12 | db *gorm.DB
13 | videoAll map[string][]int64
14 | )
15 |
16 | // InitDb 初始化数据库服务
17 | func InitDb(d *gorm.DB) {
18 | db = d
19 | for _, m := range model.GetMigrate() {
20 | err := db.AutoMigrate(m)
21 | if err != nil {
22 | log.Fatalf("%s 模型自动迁移失败: %s", reflect.TypeOf(m), err.Error())
23 | }
24 | }
25 | err := db.SetupJoinTable(&model.Video{}, "CoAuthor", &model.UserCreation{})
26 | if err != nil {
27 | log.Fatalf("自定义连接表设置失败,Video: %s", err)
28 | }
29 | err = db.SetupJoinTable(&model.User{}, "Videos", &model.UserCreation{})
30 | if err != nil {
31 | log.Fatalf("自定义连接表设置失败,User: %s", err)
32 | }
33 | {
34 | var data []map[string]any
35 | db.Model(&model.Video{}).Select("id", "`type_of`").Find(&data)
36 | videoAll = make(map[string][]int64, 10)
37 | videoAll["all"] = make([]int64, 0, len(data))
38 | for i := range data {
39 | id := data[i]["id"].(int64)
40 | videoAll["all"] = append(videoAll["all"], id)
41 | if data[i]["type_of"] != nil {
42 | ty := data[i]["type_of"].(string)
43 | videoAll[ty] = append(videoAll[ty], id)
44 | }
45 | }
46 | }
47 | }
48 |
49 | // id 快捷用法返回一个Model{id:val}
50 | func id(val int64) model.Model {
51 | return model.Model{ID: val}
52 | }
53 |
54 | func GetDb() *gorm.DB {
55 | return db
56 | }
57 |
--------------------------------------------------------------------------------
/internal/bootstrap/conf.go:
--------------------------------------------------------------------------------
1 | package bootstrap
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/Godvictory/douyin/cmd/flags"
6 | "github.com/Godvictory/douyin/internal/conf"
7 | "github.com/Godvictory/douyin/utils"
8 | "os"
9 | "path/filepath"
10 |
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | var configPath string
15 |
16 | func InitConf() int {
17 | configPath = filepath.Join(flags.DataDir, "config.json")
18 | if !utils.Exists(configPath) {
19 | // 配置文件不存在,创建默认配置
20 | log.Info("没检测到配置文件,将进行初始化 config.json.")
21 | basePath := filepath.Dir(configPath)
22 | err := os.MkdirAll(basePath, 0o766)
23 | if err != nil {
24 | log.Fatalf("无法创建文件夹, %s", err)
25 | }
26 | conf.Conf = conf.DefaultConfig()
27 | conf.Conf.JwtSecret = utils.RandString(17)
28 | defaultData, _ := json.MarshalIndent(conf.Conf, "", " ")
29 | err = os.WriteFile(configPath, defaultData, 0o666)
30 | if err != nil {
31 | log.Fatalf("配置文件写入错误,请检查,{%s}", err)
32 | }
33 | return 1
34 | }
35 | file, err := os.ReadFile(configPath)
36 | if err != nil {
37 | log.Fatalf("配置读取错误,请检查,{%s}", err)
38 | }
39 | data := os.ExpandEnv(string(file))
40 | err = json.Unmarshal([]byte(data), &conf.Conf)
41 | if err != nil {
42 | log.Fatalf("配置文件解析错误,请检查,{%s}", err)
43 | }
44 | // 解析完在回写一次,保证配置文件格式最新
45 | //fileData, _ := json.MarshalIndent(conf.Conf, "", " ")
46 | //err = os.WriteFile(configPath, fileData, 0o666)
47 | //if err != nil {
48 | // log.Error("配置文件更新错误,请检查,{%s}", err)
49 | //}
50 | return 0
51 | }
52 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "github.com/Godvictory/douyin/cmd/flags"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/sirupsen/logrus"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | var rootCmd = &cobra.Command{
14 | Use: "douyin",
15 | Short: "你所热爱的,就是你的生活。",
16 | Long: `抖音让每一个人看见并连接更大的世界,鼓励表达、沟通和记录,激发创造,丰富人们的精神世界,让现实生活更美好。`,
17 | Version: "v0.7.26",
18 | }
19 |
20 | func Execute() {
21 | if err := rootCmd.Execute(); err != nil {
22 | _, _ = fmt.Fprintln(os.Stderr, err)
23 | os.Exit(1)
24 | }
25 | }
26 |
27 | func init() {
28 | var baseDir, dataDir string
29 | var err error
30 | rootCmd.PersistentFlags().StringVar(&dataDir, "data", "data", "修改配置文件路径")
31 | rootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "Debug 模式(更多的日志输出)")
32 | rootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "开发环境")
33 | rootCmd.PersistentFlags().BoolVar(&flags.Tst, "Tst", false, "测试环境")
34 | rootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "日志强制打印到控制台")
35 | rootCmd.PersistentFlags().BoolVar(&flags.Memory, "memory", false, "使用内存数据库")
36 |
37 | cobra.OnInitialize(func() {
38 | flags.Pro = !flags.Dev
39 | // 获取可执行文件路径
40 | if baseDir, err = os.Executable(); err != nil {
41 | logrus.Fatal("无法获取可执行文件路径", err)
42 | }
43 | flags.ExPath = filepath.Dir(baseDir)
44 | flags.DataDir = filepath.Join(flags.ExPath, dataDir)
45 | })
46 |
47 | // no-completion
48 | rootCmd.AddCommand(&cobra.Command{
49 | Use: "completion",
50 | Hidden: true,
51 | })
52 | rootCmd.SetHelpCommand(&cobra.Command{
53 | Use: "no-help",
54 | Hidden: true,
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/server/handlers/favorite.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "github.com/Godvictory/douyin/internal/db"
5 | "github.com/Godvictory/douyin/internal/model"
6 | "github.com/Godvictory/douyin/server/common"
7 | "github.com/Godvictory/douyin/utils/tokens"
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | type actionReqs struct {
12 | Token string `form:"token" json:"token" binding:"required"` // 用户鉴权token
13 | VideoId int64 `form:"video_id" json:"video_id" binding:"required"` // 视频id
14 | ActionType int `form:"action_type" json:"action_type" binding:"required"` // 1-点赞,2-取消点赞
15 | }
16 |
17 | // FavoriteAction 点赞
18 | func FavoriteAction(c *gin.Context) (int, any) {
19 | var reqs actionReqs
20 | // 参数绑定
21 | if err := common.Bind(c, &reqs); err != nil {
22 | return ErrParam(err)
23 | }
24 | claims, err := tokens.CheckToken(reqs.Token)
25 | if err != nil {
26 | return Err("Token 错误", err)
27 | }
28 | err = db.VideoLike(claims.ID, reqs.VideoId, reqs.ActionType)
29 | if err != nil {
30 | return Err("网卡了,再试一次吧", err)
31 | }
32 | return Ok(nil)
33 | }
34 |
35 | // FavoriteList 点赞列表
36 | func FavoriteList(c *gin.Context) (int, any) {
37 | var (
38 | data []*model.Video
39 | reqs userReqs
40 | )
41 | // 参数绑定
42 | if err := c.ShouldBindQuery(&reqs); err != nil {
43 | return ErrParam(err)
44 | }
45 | claims, err := tokens.CheckToken(reqs.Token)
46 | if err != nil {
47 | return Err("Token 错误", err)
48 | }
49 | if reqs.ID == 0 {
50 | reqs.ID = claims.ID
51 | }
52 | data, err = db.VideoLikeList(reqs.ID)
53 | if err != nil {
54 | return Err("网卡了,再试一次吧", err)
55 | }
56 | return Ok(H{"video_list": data})
57 | }
58 |
--------------------------------------------------------------------------------
/cmd/start.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/Godvictory/douyin/cmd/flags"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "strconv"
9 |
10 | log "github.com/sirupsen/logrus"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var startCmd = &cobra.Command{
15 | Use: "start",
16 | Short: "启动守护进程",
17 | Run: func(cmd *cobra.Command, args []string) {
18 | start()
19 | },
20 | }
21 |
22 | func start() {
23 | initServer()
24 | initDaemon()
25 | if pid != -1 {
26 | _, err := os.FindProcess(pid)
27 | if err == nil {
28 | log.Info("抖音已经启动了, pid ", pid)
29 | return
30 | }
31 | }
32 | args := os.Args
33 | args[1] = "server"
34 | cmd := &exec.Cmd{
35 | Path: args[0],
36 | Args: args,
37 | Env: os.Environ(),
38 | }
39 | /*
40 | O_RDONLY 打开只读文件
41 | O_WRONLY 打开只写文件
42 | O_RDWR 打开既可以读取又可以写入文件
43 | O_APPEND 写入文件时将数据追加到文件尾部
44 | O_CREATE 如果文件不存在,则创建一个新的文件
45 | O_TRUNC 表示如果文件存在,则截断文件到零长度
46 | 0o666:表示文件权限的八进制数。0o666 表示文件所有者、所属组和其他用户都具有读写权限。
47 | */
48 | /*
49 | 在八进制表示法中,0o 前缀表示八进制数。数字 766 对应了文件权限 rw-rw-rw-。
50 | 7 表示所有者(owner)具有读取、写入和执行权限。
51 | 6 表示所属组(group)具有读取和写入权限。
52 | 6 表示其他用户(others)具有读取和写入权限。
53 | */
54 | stdout, err := os.OpenFile(filepath.Join(flags.ExPath, "data", "start.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666)
55 | if err != nil {
56 | log.Fatal(os.Getpid(), ": 无法打开启动日志文件:", err)
57 | }
58 | cmd.Stderr = stdout
59 | cmd.Stdout = stdout
60 | err = cmd.Start()
61 | if err != nil {
62 | log.Fatal("未能启动子进程: ", err)
63 | }
64 | log.Infof("成功启动 pid: %d", cmd.Process.Pid)
65 | err = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0o666)
66 | if err != nil {
67 | log.Warn("pid 未能正常写入文件,您可能无法使用 `./douyin stop` 停止程序.")
68 | }
69 | }
70 |
71 | func init() {
72 | rootCmd.AddCommand(startCmd)
73 | }
74 |
--------------------------------------------------------------------------------
/cmd/server.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "github.com/Godvictory/douyin/cmd/flags"
8 | "github.com/Godvictory/douyin/internal/conf"
9 | "github.com/Godvictory/douyin/server"
10 | "net/http"
11 | "os"
12 | "os/signal"
13 | "syscall"
14 | "time"
15 |
16 | "github.com/gin-gonic/gin"
17 | log "github.com/sirupsen/logrus"
18 | "github.com/spf13/cobra"
19 | )
20 |
21 | var serverCmd = &cobra.Command{
22 | Use: "server",
23 | Short: "前台启动服务",
24 | Long: `Start the douyin server`,
25 | Run: func(cmd *cobra.Command, args []string) {
26 | initServer()
27 | if flags.Debug || flags.Dev {
28 | gin.SetMode(gin.DebugMode)
29 | } else {
30 | gin.SetMode(gin.ReleaseMode)
31 | }
32 | r := gin.New()
33 | server.Init(r)
34 | base := fmt.Sprintf("%s:%d", conf.Conf.Address, conf.Conf.Port)
35 | log.Infof("启动服务器 @ %s", base)
36 | srv := &http.Server{Addr: base, Handler: r}
37 | go func() {
38 | var err error
39 | if conf.Conf.Scheme.Https {
40 | err = srv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
41 | } else {
42 | err = srv.ListenAndServe()
43 | }
44 | if err != nil && !errors.Is(err, http.ErrServerClosed) {
45 | log.Fatalf("无法启动: %s", err.Error())
46 | }
47 | }()
48 |
49 | // 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
50 | quit := make(chan os.Signal)
51 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
52 | <-quit
53 | log.Println("Shutdown Server ...")
54 |
55 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
56 | defer cancel()
57 | if err := srv.Shutdown(ctx); err != nil {
58 | log.Fatal("Server Shutdown:", err)
59 | }
60 | log.Println("Server exiting")
61 | },
62 | }
63 |
64 | func init() {
65 | rootCmd.AddCommand(serverCmd)
66 | }
67 |
--------------------------------------------------------------------------------
/server/handlers/comment.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "errors"
5 | "github.com/Godvictory/douyin/internal/db"
6 | "github.com/Godvictory/douyin/internal/model"
7 | "github.com/Godvictory/douyin/server/common"
8 | "github.com/Godvictory/douyin/utils/tokens"
9 |
10 | "github.com/gin-gonic/gin"
11 | )
12 |
13 | type commentReqs struct {
14 | Token string `form:"token" json:"token"` // 用户鉴权token
15 | VideoId int64 `form:"video_id" json:"video_id" binding:"required"` // 视频id
16 | ActionType int `form:"action_type" json:"action_type"` // 1-发布评论,2-删除评论
17 | CommentText string `form:"comment_text" json:"comment_text"` // 用户填写的评论内容
18 | CommentId int64 `form:"comment_id" json:"comment_id"` // 要删除的评论id
19 | }
20 |
21 | // CommentAction 评论操作
22 | func CommentAction(c *gin.Context) (int, any) {
23 | var (
24 | reqs commentReqs
25 | resp *model.Comment
26 | err error
27 | )
28 | // 参数绑定
29 | if err := common.Bind(c, &reqs); err != nil {
30 | return ErrParam(err)
31 | }
32 | claims, err := tokens.CheckToken(reqs.Token)
33 | if err != nil {
34 | return Err("Token 错误", err)
35 | }
36 | switch reqs.ActionType {
37 | case 1:
38 | resp, err = db.CommentPush(claims.ID, reqs.VideoId, reqs.CommentText)
39 | case 2:
40 | err = db.CommentDel(reqs.CommentId)
41 | default:
42 | return ErrParam(errors.New("不合法的 ActionType"))
43 | }
44 | if err != nil {
45 | return Err("请再试一次吧", err)
46 | }
47 | return Ok(H{"comment": resp})
48 | }
49 |
50 | // CommentList 评论列表
51 | func CommentList(c *gin.Context) (int, any) {
52 | var reqs commentReqs
53 | // 参数绑定
54 | if err := common.Bind(c, &reqs); err != nil {
55 | return ErrParam(err)
56 | }
57 | data, err := db.CommentGet(reqs.VideoId)
58 | if err != nil {
59 | return Err("稍后试试.", err)
60 | }
61 | return Ok(H{"comment_list": data})
62 | }
63 |
--------------------------------------------------------------------------------
/server/handlers/message.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "errors"
5 | "github.com/Godvictory/douyin/internal/db"
6 | "github.com/Godvictory/douyin/server/common"
7 | "github.com/Godvictory/douyin/utils/tokens"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | type messageReqs struct {
13 | Token string `json:"token" form:"token" binding:"required"` // 用户鉴权token
14 | ToUserId int64 `json:"to_user_id" form:"to_user_id" binding:"required"` // 对方用户id
15 | ActionType int32 `json:"action_type" form:"action_type"` // 1-发送消息
16 | Content string `json:"content" form:"content"` // 消息内容
17 | PreMsgTime int64 `json:"pre_msg_time" form:"pre_msg_time"` // 上次最新消息的时间
18 | }
19 |
20 | // MessageChat 聊天记录
21 | func MessageChat(c *gin.Context) (int, any) {
22 | var reqs messageReqs
23 | // 参数绑定
24 | if err := common.Bind(c, &reqs); err != nil {
25 | return ErrParam(err)
26 | }
27 | claims, err := tokens.CheckToken(reqs.Token)
28 | if err != nil {
29 | return Err("Token 错误", err)
30 | }
31 |
32 | data, err := db.MessageGet(claims.ID, reqs.ToUserId, reqs.PreMsgTime)
33 | if err != nil {
34 | return Err("聊天记录获取失败", err)
35 | }
36 | return Ok(H{"message_list": data})
37 | }
38 |
39 | // MessageAction 消息操作
40 | func MessageAction(c *gin.Context) (int, any) {
41 | var (
42 | reqs messageReqs
43 | err error
44 | )
45 | // 参数绑定
46 | if err := common.Bind(c, &reqs); err != nil {
47 | return ErrParam(err)
48 | }
49 | claims, err := tokens.CheckToken(reqs.Token)
50 | if err != nil {
51 | return Err("Token 错误", err)
52 | }
53 | switch reqs.ActionType {
54 | case 1:
55 | err = db.MessagePush(claims.ID, reqs.ToUserId, reqs.Content)
56 | default:
57 | return ErrParam(errors.New("不合法的 ActionType"))
58 | }
59 | if err != nil {
60 | return Err("发送失败.", err)
61 | }
62 | return Ok(H{})
63 | }
64 |
--------------------------------------------------------------------------------
/server/myrouter.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "github.com/Godvictory/douyin/cmd/flags"
5 | "github.com/Godvictory/douyin/server/handlers"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | type MyHandler func(*gin.Context) (int, any)
12 |
13 | // decorator 装饰器
14 | func decorator() func(h MyHandler) gin.HandlerFunc {
15 | return func(h MyHandler) gin.HandlerFunc {
16 | return func(c *gin.Context) {
17 | code, data := h(c)
18 | req := gin.H{
19 | "status_code": code,
20 | "status_msg": "",
21 | }
22 | c.Set("code", code)
23 | if code == 0 {
24 | // 判断数据类型
25 | if val, ok := data.(handlers.H); ok {
26 | for k, v := range val {
27 | req[k] = v
28 | }
29 | }
30 |
31 | req["status_msg"] = "ok!"
32 | c.JSON(200, req)
33 | } else {
34 | switch data.(type) {
35 | case string:
36 | c.Set("msg", data)
37 | req["status_msg"] = data
38 | case error:
39 | // 判断是否debug模式,是的话返回错误信息
40 | if flags.Dev || flags.Debug || flags.Tst {
41 | req["errmsg"] = data.(error).Error()
42 | }
43 | case handlers.MyErr:
44 | e := data.(handlers.MyErr)
45 | req["status_msg"] = e.Msg
46 | c.Set("msg", e.Msg)
47 | // 判断是否debug模式,是的话返回错误信息
48 | if flags.Dev || flags.Debug || flags.Tst {
49 | errs := make([]string, 0, 10)
50 | for i := range e.Errs {
51 | if e.Errs[i] == nil {
52 | continue
53 | }
54 | errs = append(errs, e.Errs[i].Error())
55 | }
56 | req["errmsg"] = errs
57 | }
58 | }
59 |
60 | c.JSON(http.StatusOK, req)
61 | }
62 | }
63 | }
64 | }
65 |
66 | func newRouter(group *gin.RouterGroup, method string, path string, handler MyHandler, handlers ...gin.HandlerFunc) {
67 | if handler != nil {
68 | // 未开发的路由传nil,不挂载
69 | group.Handle(method, path, append(handlers, decorator()(handler))...)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/web/static/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/internal/conf/type.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | type Config struct {
4 | Address string `json:"address"` // 监听地址
5 | Port int `json:"port"` // 监听端口
6 | JwtSecret string `json:"jwt_secret"` // Jwt密钥,随机生成
7 | Scheme confScheme `json:"scheme"` // HTTPS配置
8 | Database confDatabase `json:"database"` // 数据库配置
9 | Redis confRedis `json:"redis"` // Redis 缓存配置
10 | Oss confOss `json:"oss"` // Oss 配置(阿里云)
11 | Log confLog `json:"log"` // Log配置
12 | }
13 | type confScheme struct {
14 | Https bool `json:"https"` // 启用HTTPS
15 | CertFile string `json:"cert_file"` // 证书路径
16 | KeyFile string `json:"key_file"` // 证书路径
17 | }
18 |
19 | type confDatabase struct {
20 | Type string `json:"type"` // 数据库类型,支持 sqlite3,mysql,postgresSql
21 | Host string `json:"host"` // 数据库地址
22 | Port int `json:"port"` // 数据库端口
23 | User string `json:"user"` // 用户名
24 | Password string `json:"password"` // 密码
25 | Name string `json:"name"` // 数据库名
26 | DbFile string `json:"db_file"` // sqlite3的数据库文件,当为空时使用内存数据库
27 | }
28 | type confLog struct {
29 | Enable bool `json:"enable"` // 是否启用日志
30 | Level string `json:"level"` // 日志等级,可用 panic,fatal,error,warn,info,debug,trace
31 | Name string `json:"name"` // 日志文件名
32 | MaxSize int `json:"max_size"` // 日志最大大小
33 | MaxBackups int `json:"max_backups"` // 日志最大备份数
34 | MaxAge int `json:"max_age"` // 日志最长时间
35 | Compress bool `json:"compress"` // 日志是否压缩
36 | }
37 |
38 | type confRedis struct {
39 | Host string `json:"host"` // 数据库地址
40 | Port int `json:"port"` // 数据库端口
41 | Password string `json:"password"` // 密码
42 | Db int `json:"db"` // 数据库编号
43 | }
44 |
45 | type confOss struct {
46 | // 阿里云配置
47 | AccessKeyID string `json:"AccessKeyId"`
48 | AccessKeySecret string `json:"AccessKeySecret"`
49 | Endpoint string `json:"Endpoint"`
50 | BucketName string `json:"BucketName"`
51 | }
52 |
--------------------------------------------------------------------------------
/web/assets/index-cef44d9a.js:
--------------------------------------------------------------------------------
1 | import{E as w,x as c,m as k,al as F,aC as g,aD as M,aE as N,H as _,O as r,ah as d,J as x,T as b,ai as z,M as u,L as B,K as v,W as C,P as T,V as A,aF as H,aG as R,aH as U,aI as j,av as G}from"./index-483b83c5.js";/* empty css */const J={class:"main"},K=["onClick"],O={style:{width:"70%"}},P={style:{"font-size":"18px"}},W={class:"message"},q={class:"chat"},Q=w({__name:"index",setup(X){const I=c(-1),E=c([]),p=c([]),l=c(""),y=c();let f;const m=k("userInfo"),V=k("loginDialog");let h=0;function D(e,n){y.value=e,I.value=n,g.user.MessageChat(e.id).then(t=>{var o;t.status_code==0&&(p.value=t.message_list,t.message_list.length>0&&(h=(o=t.message_list[t.message_list.length-1])==null?void 0:o.create_time),clearInterval(f),f=setInterval(()=>{g.user.MessageChat(e.id,h).then(a=>{a.status_code==0&&(a.message_list.length>0&&(h=a.message_list[a.message_list.length-1].create_time),p.value.push(...a.message_list))})},1e3))})}function L(){var e;g.user.MessageAction((e=y.value)==null?void 0:e.id,l.value).then(n=>{n.status_code==0&&(l.value="",M.success("发送成功"))})}return F(()=>{m!=null&&m.value?g.user.RelationFriendList().then(e=>{e.status_code==0&&(E.value=e.user_list)}):(M.error("请先登录"),V&&(V.value=!0))}),N(()=>{clearInterval(f)}),(e,n)=>{const t=H,o=R,a=U,S=j;return _(),r("div",J,[d(o,{class:"friend"},{default:x(()=>[(_(!0),r(b,null,z(u(E),(s,i)=>(_(),r("div",{class:B(["item",{click:u(I)==i}]),onClick:()=>{D(s,i)}},[d(t,{src:s.avatar,size:32},null,8,["src"]),v("div",O,[v("div",P,C(s.name),1),v("div",W,C(s.message),1)])],10,K))),256))]),_:1}),v("div",q,[d(o,{class:"message"},{default:x(()=>[(_(!0),r(b,null,z(u(p),s=>{var i;return _(),r("div",{class:B(["const",s.from_user_id==((i=u(m))==null?void 0:i.id)?"right":"left"])},C(s.content),3)}),256))]),_:1}),d(a,{modelValue:u(l),"onUpdate:modelValue":n[0]||(n[0]=s=>T(l)?l.value=s:null),type:"textarea",placeholder:"我来讲两句.",maxlength:100,resize:"none"},null,8,["modelValue"]),d(S,{type:"success",plain:"",class:"send",onClick:L},{default:x(()=>[A(" Send ")]),_:1})])])}}});const $=G(Q,[["__scopeId","data-v-d534a8ed"]]);export{$ as default};
2 |
--------------------------------------------------------------------------------
/web/assets/index-cac82435.css:
--------------------------------------------------------------------------------
1 | .el-scrollbar{--el-scrollbar-opacity:.3;--el-scrollbar-bg-color:var(--el-text-color-secondary);--el-scrollbar-hover-opacity:.5;--el-scrollbar-hover-bg-color:var(--el-text-color-secondary)}.el-scrollbar{overflow:hidden;position:relative;height:100%}.el-scrollbar__wrap{overflow:auto;height:100%}.el-scrollbar__wrap--hidden-default{scrollbar-width:none}.el-scrollbar__wrap--hidden-default::-webkit-scrollbar{display:none}.el-scrollbar__thumb{position:relative;display:block;width:0;height:0;cursor:pointer;border-radius:inherit;background-color:var(--el-scrollbar-bg-color,var(--el-text-color-secondary));transition:var(--el-transition-duration) background-color;opacity:var(--el-scrollbar-opacity,.3)}.el-scrollbar__thumb:hover{background-color:var(--el-scrollbar-hover-bg-color,var(--el-text-color-secondary));opacity:var(--el-scrollbar-hover-opacity,.5)}.el-scrollbar__bar{position:absolute;right:2px;bottom:2px;z-index:1;border-radius:4px}.el-scrollbar__bar.is-vertical{width:6px;top:2px}.el-scrollbar__bar.is-vertical>div{width:100%}.el-scrollbar__bar.is-horizontal{height:6px;left:2px}.el-scrollbar__bar.is-horizontal>div{height:100%}.el-scrollbar-fade-enter-active{transition:opacity .34s ease-out}.el-scrollbar-fade-leave-active{transition:opacity .12s ease-out}.el-scrollbar-fade-enter-from,.el-scrollbar-fade-leave-active{opacity:0}.item[data-v-d534a8ed]{display:flex;justify-content:center;padding:5px;font-size:13px;border:2px solid saddlebrown;margin:8px 0;width:100%;box-sizing:border-box;justify-content:space-around;align-items:center;cursor:pointer}.item.click[data-v-d534a8ed]{background-color:var(--el-color-info-light-3)}.item .message[data-v-d534a8ed]{display:inline-block;white-space:nowrap;width:100%;overflow:hidden;text-overflow:ellipsis}.chat[data-v-d534a8ed]{height:100%;position:relative;flex:1}.chat .message[data-v-d534a8ed]{height:79%;box-sizing:border-box}.chat .message .const[data-v-d534a8ed]{width:80%;margin:10px 0;padding:20px;min-height:30px}.chat .message .const.left[data-v-d534a8ed]{background-color:#09034e;border-radius:0 25px 25px 0}.chat .message .const.right[data-v-d534a8ed]{background-color:#034e12;margin-left:20%;border-radius:25px 0 0 25px}.chat .el-textarea[data-v-d534a8ed]{height:20%;box-sizing:border-box}.chat .el-textarea[data-v-d534a8ed] textarea{height:100%}.chat .send[data-v-d534a8ed]{position:absolute;bottom:10px;right:10px}.friend[data-v-d534a8ed]{width:30%;max-width:400px;height:100%;background:var(--el-color-primary-light-7)}.main[data-v-d534a8ed]{width:100%;height:94vh;display:flex;justify-content:center;align-items:center}
2 |
--------------------------------------------------------------------------------
/server/middleware/logger.go:
--------------------------------------------------------------------------------
1 | package middleware
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "net/http"
7 | "os"
8 | "time"
9 |
10 | "github.com/gin-gonic/gin"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | // Logger is the logrus logger handler
15 | // https://github.com/toorop/gin-logrus
16 | func Logger(logger log.FieldLogger, notLogged ...string) gin.HandlerFunc {
17 | hostname, err := os.Hostname()
18 | if err != nil {
19 | hostname = "unknow"
20 | }
21 |
22 | var skip map[string]struct{}
23 |
24 | if length := len(notLogged); length > 0 {
25 | skip = make(map[string]struct{}, length)
26 |
27 | for _, p := range notLogged {
28 | skip[p] = struct{}{}
29 | }
30 | }
31 |
32 | return func(c *gin.Context) {
33 | // other handler can change c.Path so:
34 | path := c.Request.URL.Path
35 | start := time.Now()
36 | c.Next()
37 | stop := time.Since(start)
38 | latency := int(math.Ceil(float64(stop.Nanoseconds()) / 1000000.0))
39 | statusCode := c.Writer.Status()
40 | clientIP := c.ClientIP()
41 | clientUserAgent := c.Request.UserAgent()
42 | referer := c.Request.Referer()
43 | dataLength := c.Writer.Size()
44 | if dataLength < 0 {
45 | dataLength = 0
46 | }
47 |
48 | if _, ok := skip[path]; ok {
49 | return
50 | }
51 |
52 | code, _ := c.Get("code")
53 | msg, _ := c.Get("msg")
54 | entry := logger.WithFields(log.Fields{
55 | "msg": msg,
56 | "hostname": hostname,
57 | "statusCode": statusCode,
58 | //"latency": latency, // time to process
59 | "clientIP": clientIP,
60 | //"method": c.Request.Method,
61 | //"path": path,
62 | "referer": referer,
63 | "dataLength": dataLength,
64 | //"userAgent": clientUserAgent,
65 | })
66 |
67 | if len(c.Errors) > 0 {
68 | entry.Error(c.Errors.ByType(gin.ErrorTypePrivate).String())
69 | } else {
70 | msg := fmt.Sprintf("\"%s %s Code:%d (%dms)\"", c.Request.Method, path, code, latency)
71 | if statusCode >= http.StatusInternalServerError {
72 | entry.Error(msg, clientUserAgent)
73 | } else if statusCode >= http.StatusBadRequest {
74 | entry.Warn(msg, clientUserAgent)
75 | } else {
76 | if code == 0 {
77 | entry.Info(msg)
78 | } else {
79 | entry.Warn(msg)
80 | }
81 | }
82 | }
83 | }
84 | }
85 |
86 | func LoggerDebug(httpMethod, absolutePath, handlerName string, nuHandlers int) {
87 | entry := log.WithFields(log.Fields{
88 | //"httpMethod": httpMethod,
89 | //"absolutePath": absolutePath,
90 | "handlerName": handlerName,
91 | "nuHandlers": nuHandlers,
92 | })
93 | msg := fmt.Sprintf("%-6s %-25s", httpMethod, absolutePath)
94 | entry.Debug(msg)
95 | }
96 |
--------------------------------------------------------------------------------
/internal/bootstrap/db.go:
--------------------------------------------------------------------------------
1 | package bootstrap
2 |
3 | import (
4 | "fmt"
5 | "github.com/Godvictory/douyin/cmd/flags"
6 | "github.com/Godvictory/douyin/internal/conf"
7 | "github.com/Godvictory/douyin/internal/db"
8 | "github.com/Godvictory/douyin/internal/model"
9 | "github.com/redis/go-redis/v9"
10 | log "github.com/sirupsen/logrus"
11 | "gorm.io/driver/mysql"
12 | "gorm.io/driver/postgres"
13 | "gorm.io/driver/sqlite"
14 | "gorm.io/gorm"
15 | "gorm.io/gorm/logger"
16 | "gorm.io/gorm/schema"
17 | stdlog "log"
18 | "strings"
19 | "time"
20 | )
21 |
22 | func InitDb() {
23 | var dialector gorm.Dialector
24 | var dB *gorm.DB
25 | var err error
26 | log.Info("开始初始化 Database 服务!")
27 | if flags.Memory {
28 | log.Info("当前处于 Memory模式,将使用内存数据库,并清空 Redis!")
29 | dialector = sqlite.Open("file::memory:?cache=shared")
30 | } else {
31 | database := conf.Conf.Database
32 | switch strings.ToUpper(database.Type) {
33 | case "SQLITE3":
34 | sqliteUrl := fmt.Sprintf("%s?_journal=WAL&_vacuum=incremental", database.DbFile)
35 | if database.DbFile == "" {
36 | sqliteUrl = "file::memory:?cache=shared"
37 | }
38 | dialector = sqlite.Open(sqliteUrl)
39 | case "MYSQL":
40 | dialector = mysql.Open(fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
41 | database.User, database.Password, database.Host, database.Port, database.Name))
42 | case "POSTGRES":
43 | dialector = postgres.Open(fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
44 | database.Host, database.User, database.Password, database.Name, database.Port))
45 | default:
46 | log.Fatalf("not supported database type: %s,supported:[sqlite3,mysql,postgres]", database.Type)
47 | }
48 | }
49 | logLevel := logger.Silent
50 | if flags.Debug || flags.Dev {
51 | logLevel = logger.Info
52 | }
53 | dB, err = gorm.Open(dialector, &gorm.Config{
54 | Logger: logger.New(
55 | stdlog.New(log.StandardLogger().Out, "\r\n", stdlog.LstdFlags),
56 | logger.Config{
57 | SlowThreshold: time.Second, // 设置慢查询阈值
58 | LogLevel: logLevel, // 设置日志级别
59 | IgnoreRecordNotFoundError: true, // 忽略记录未找到的错误
60 | Colorful: true, // 启用彩色日志输出
61 | },
62 | ),
63 | NamingStrategy: schema.NamingStrategy{
64 | SingularTable: true, //表名以单数形式命名
65 | },
66 | TranslateError: true, // 启用错误翻译功能
67 | })
68 | if err != nil {
69 | log.Fatalf("无法连接到数据库:%s", err.Error())
70 | }
71 | db.InitDb(dB)
72 | log.Info("初始化 Database 成功!")
73 | }
74 |
75 | func InitRdb() {
76 | log.Info("开始初始化 Redis 服务!")
77 | rconf := conf.Conf.Redis
78 | rdb := redis.NewClient(&redis.Options{
79 | Addr: fmt.Sprintf("%s:%d", rconf.Host, rconf.Port),
80 | Password: rconf.Password,
81 | DB: rconf.Db,
82 | })
83 | model.InitRdb(rdb)
84 | log.Info("初始化 Redis 成功!")
85 | }
86 |
--------------------------------------------------------------------------------
/internal/db/relation.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "errors"
5 | "github.com/Godvictory/douyin/internal/model"
6 |
7 | "gorm.io/gorm"
8 | )
9 |
10 | // RelationAction 关注/取关
11 | func RelationAction(fid, tid int64, ActionType int) error {
12 | var (
13 | association *gorm.Association
14 | err error
15 | )
16 | tx := db.Begin()
17 | fval := &model.User{Model: id(fid)}
18 | tval := &model.User{Model: id(tid)}
19 | association = tx.Model(fval).Association("Follow")
20 | switch ActionType {
21 | case 1:
22 | err = association.Append(tval)
23 | fval.HIncrByFollowCount(1)
24 | tval.HIncrByFollowerCount(1)
25 | case 2:
26 | err = association.Delete(tval)
27 | fval.HIncrByFollowCount(-1)
28 | tval.HIncrByFollowerCount(-1)
29 | default:
30 | return errors.New("不合法的 ActionType")
31 | }
32 | if err != nil {
33 | tx.Rollback()
34 | return err
35 | }
36 | tx.Commit()
37 | return nil
38 | }
39 |
40 | // RelationFollowGet 获取关注列表 uid:本人id tid:待查id
41 | func RelationFollowGet(uid, tid int64) ([]*model.User, error) {
42 | var data []*model.User
43 | err := db.Set("user_id", uid).Model(&model.User{Model: id(tid)}).Association("Follow").Find(&data)
44 | if err != nil {
45 | return nil, err
46 | }
47 | return data, nil
48 | }
49 |
50 | // RelationFollowerGet 获取粉丝列表 uid:本人id tid:待查id
51 | func RelationFollowerGet(uid, tid int64) ([]*model.User, error) {
52 | var data []*model.User
53 | err := db.Set("user_id", uid).Table("user").
54 | Joins("JOIN user_follow ON `user`.`id` = `user_follow`.`user_id` AND `user_follow`.`follow_id` = ?", tid).
55 | Select("`user`.*").Find(&data).Error
56 | if err != nil {
57 | return nil, err
58 | }
59 | return data, nil
60 | }
61 |
62 | // RelationFriendGet 获取好友列表 uid:本人id tid:待查id
63 | func RelationFriendGet(uid int64) ([]*model.FriendUser, error) {
64 | var (
65 | data []*model.User
66 | res []*model.FriendUser
67 | )
68 | err := db.Set("user_id", uid).
69 | Table("(SELECT `user`.* FROM `user` JOIN `user_follow` ON `user`.`id` = `user_follow`.`follow_id` AND `user_follow`.`user_id` = ?) as t", uid).
70 | Joins("JOIN `user_follow` ON `t`.`id` = `user_follow`.`user_id`").
71 | Where(" `user_follow`.`follow_id` = ?", uid).
72 | Select("`t`.*").Find(&data).Error
73 | if err != nil {
74 | return nil, err
75 | }
76 | for _, d := range data {
77 | val := model.FriendUser{User: *d}
78 | tmsg := model.Message{ToUserID: d.ID, FromUserID: uid}
79 | fmsg := model.Message{ToUserID: uid, FromUserID: d.ID}
80 |
81 | db.Order("created_at DESC").Take(&tmsg)
82 | db.Order("created_at DESC").Take(&fmsg)
83 |
84 | if tmsg.CreatedAt > fmsg.CreatedAt {
85 | val.Message = tmsg.Content
86 | val.MsgType = 0
87 | } else if tmsg.CreatedAt < fmsg.CreatedAt {
88 | val.Message = fmsg.Content
89 | val.MsgType = 1
90 | } else {
91 | val.Message = "快来和你的新朋友聊天吧"
92 | }
93 | res = append(res, &val)
94 | }
95 | return res, nil
96 | }
97 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Godvictory/douyin
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/aliyun/aliyun-oss-go-sdk v2.2.8+incompatible
7 | github.com/gin-contrib/cors v1.4.0
8 | github.com/gin-contrib/static v0.0.1
9 | github.com/gin-gonic/gin v1.9.1
10 | github.com/golang-jwt/jwt/v5 v5.0.0
11 | github.com/natefinch/lumberjack v2.0.0+incompatible
12 | github.com/redis/go-redis/v9 v9.2.1
13 | github.com/sirupsen/logrus v1.9.3
14 | github.com/spf13/cobra v1.7.0
15 | github.com/stretchr/testify v1.8.4
16 | gopkg.in/hlandau/passlib.v1 v1.0.11
17 | gorm.io/driver/mysql v1.5.1
18 | gorm.io/driver/postgres v1.5.2
19 | gorm.io/driver/sqlite v1.5.2
20 | gorm.io/gorm v1.25.2
21 | )
22 |
23 | require (
24 | github.com/BurntSushi/toml v1.3.2 // indirect
25 | github.com/bytedance/sonic v1.9.1 // indirect
26 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
27 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
28 | github.com/davecgh/go-spew v1.1.1 // indirect
29 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
30 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
31 | github.com/gin-contrib/sse v0.1.0 // indirect
32 | github.com/go-playground/locales v0.14.1 // indirect
33 | github.com/go-playground/universal-translator v0.18.1 // indirect
34 | github.com/go-playground/validator/v10 v10.14.0 // indirect
35 | github.com/go-sql-driver/mysql v1.7.0 // indirect
36 | github.com/goccy/go-json v0.10.2 // indirect
37 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
38 | github.com/jackc/pgpassfile v1.0.0 // indirect
39 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
40 | github.com/jackc/pgx/v5 v5.3.1 // indirect
41 | github.com/jinzhu/inflection v1.0.0 // indirect
42 | github.com/jinzhu/now v1.1.5 // indirect
43 | github.com/json-iterator/go v1.1.12 // indirect
44 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect
45 | github.com/leodido/go-urn v1.2.4 // indirect
46 | github.com/mattn/go-isatty v0.0.19 // indirect
47 | github.com/mattn/go-sqlite3 v1.14.17 // indirect
48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
49 | github.com/modern-go/reflect2 v1.0.2 // indirect
50 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
51 | github.com/pmezard/go-difflib v1.0.0 // indirect
52 | github.com/spf13/pflag v1.0.5 // indirect
53 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
54 | github.com/ugorji/go/codec v1.2.11 // indirect
55 | golang.org/x/arch v0.3.0 // indirect
56 | golang.org/x/crypto v0.9.0 // indirect
57 | golang.org/x/net v0.10.0 // indirect
58 | golang.org/x/sys v0.8.0 // indirect
59 | golang.org/x/text v0.9.0 // indirect
60 | golang.org/x/time v0.3.0 // indirect
61 | google.golang.org/protobuf v1.30.0 // indirect
62 | gopkg.in/hlandau/easymetric.v1 v1.0.0 // indirect
63 | gopkg.in/hlandau/measurable.v1 v1.0.1 // indirect
64 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
65 | gopkg.in/yaml.v3 v3.0.1 // indirect
66 | )
67 |
--------------------------------------------------------------------------------
/web/assets/leftSidebar-a2720e0a.js:
--------------------------------------------------------------------------------
1 | import{$ as b,a0 as g,E as _,G as j,n as h,H as p,O as m,L as f,M as i,R as S,X as z,Z as w,_ as k,a3 as I,aw as E,I as P,J as o,ah as t,K as e,ax as l,ay as B,az as $,aA as C,aB as D,av as N}from"./index-483b83c5.js";const V=b({direction:{type:String,values:["horizontal","vertical"],default:"horizontal"},contentPosition:{type:String,values:["left","center","right"],default:"center"},borderStyle:{type:g(String),default:"solid"}}),M=_({name:"ElDivider"}),q=_({...M,props:V,setup(r){const u=r,d=j("divider"),n=h(()=>d.cssVar({"border-style":u.borderStyle}));return(a,y)=>(p(),m("div",{class:f([i(d).b(),i(d).m(a.direction)]),style:w(i(n)),role:"separator"},[a.$slots.default&&a.direction!=="vertical"?(p(),m("div",{key:0,class:f([i(d).e("text"),i(d).is(a.contentPosition)])},[S(a.$slots,"default")],2)):z("v-if",!0)],6))}});var R=k(q,[["__file","/home/runner/work/element-plus/element-plus/packages/components/divider/src/divider.vue"]]);const A=I(R);const c=r=>(C("data-v-d2bf7a98"),r=r(),D(),r),G=c(()=>e("span",null,"推荐",-1)),H=c(()=>e("span",null,"关注",-1)),J=c(()=>e("span",null,"朋友",-1)),K=c(()=>e("span",null,"我的",-1)),L=c(()=>e("span",null,"投稿",-1)),O=c(()=>e("span",null,"音乐",-1)),T=c(()=>e("span",null,"爱情",-1)),X=c(()=>e("span",null,"宠物",-1)),Z=c(()=>e("span",null,"美食",-1)),F=c(()=>e("span",null,"默剧",-1)),Q=c(()=>e("span",null,"煽情",-1)),U=c(()=>e("span",null,"妙招",-1)),W=_({__name:"leftSidebar",setup(r){const u=E(),d=h(()=>u.path),n={trigger:"loop-on-hover",colors:"primary:#4be1ec,secondary:#cb5eee",target:".el-menu-item"};return(a,y)=>{const s=B,v=A,x=$;return p(),P(x,{"default-active":i(d),class:"el-menu-vertical",router:""},{default:o(()=>[t(s,{index:"/"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/ihyatngg.json"},n),null,16),G]),_:1}),t(s,{index:"/follow"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/yrxnwkni.json"},n),null,16),H]),_:1}),t(s,{index:"/friend"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/mjmrmyzg.json"},n),null,16),J]),_:1}),t(s,{index:"/my"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/ljvjsnvh.json"},n),null,16),K]),_:1}),t(v),t(s,{index:"/type/t"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/lxotnbfa.json"},n),null,16),L]),_:1}),t(s,{index:"/type/y"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/pgbyoxin.json"},n),null,16),O]),_:1}),t(s,{index:"/type/a"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/wgydzbzz.json"},n),null,16),T]),_:1}),t(s,{index:"/type/c"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/whxfxpyt.json"},n),null,16),X]),_:1}),t(s,{index:"/type/s"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/usapctuq.json"},n),null,16),Z]),_:1}),t(s,{index:"/type/m"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/btogdxxi.json"},n),null,16),F]),_:1}),t(s,{index:"/type/q"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/zywndszx.json"},n),null,16),Q]),_:1}),t(s,{index:"/type/x"},{default:o(()=>[e("lord-icon",l({src:"https://cdn.lordicon.com/uckdrslf.json"},n),null,16),U]),_:1})]),_:1},8,["default-active"])}}});const ee=N(W,[["__scopeId","data-v-d2bf7a98"]]);export{ee as default};
2 |
--------------------------------------------------------------------------------
/server/router.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "github.com/Godvictory/douyin/cmd/flags"
5 | "github.com/Godvictory/douyin/server/handlers"
6 | "github.com/Godvictory/douyin/server/middleware"
7 | "github.com/gin-contrib/cors"
8 | "github.com/gin-contrib/static"
9 | "github.com/gin-gonic/gin"
10 | log "github.com/sirupsen/logrus"
11 | "net/http"
12 | "os"
13 | "strings"
14 | )
15 |
16 | func Init(r *gin.Engine) {
17 | r.MaxMultipartMemory = 16 << 20 // 16 MiB
18 | if !flags.Tst {
19 | r.Use(middleware.Logger(log.StandardLogger())) // 使用logrus记录日志
20 | r.Use(gin.Recovery()) // 恐慌恢复
21 | r.Use(cors.Default()) // 跨域处理
22 | }
23 | r.GET("ping", func(c *gin.Context) {
24 | c.String(http.StatusOK, "pong")
25 | })
26 |
27 | router := r.Group("douyin")
28 | tester := r.Group("douyin", middleware.Test())
29 | // 视频类接口
30 | {
31 | newRouter(router, "GET", "feed", handlers.VideoGet) // 获取视频流
32 | newRouter(router, "POST", "publish/action/", handlers.VideoAction) // 视频投稿
33 | newRouter(tester, "POST", "publish/actionUrl/", handlers.VideoActionUrl) // 视频投稿(测试接口)
34 | newRouter(router, "GET", "publish/list/", handlers.VideoList) // 获取发布列表
35 | newRouter(router, "GET", "publish/follow/", handlers.VideoFollowList) // 获取关注视频列表
36 | }
37 | // 用户类接口
38 | {
39 | newRouter(router, "POST", "user/register/", handlers.UserRegister) // 用户注册
40 | newRouter(router, "POST", "user/login/", handlers.UserLogin) // 用户登录
41 | newRouter(router, "GET", "user/", handlers.UserInfo) // 获取用户信息
42 | }
43 | // 互动类接口,
44 | {
45 | newRouter(router, "POST", "favorite/action/", handlers.FavoriteAction) // 点赞操作
46 | newRouter(router, "GET", "favorite/list/", handlers.FavoriteList) // 获取喜欢列表
47 | newRouter(router, "POST", "comment/action/", handlers.CommentAction) // 评论操作
48 | newRouter(router, "GET", "comment/list/", handlers.CommentList) // 获取评论列表
49 | }
50 | // 社交类接口
51 | {
52 | newRouter(router, "POST", "relation/action/", handlers.RelationAction) // 关注/取关 操作
53 | newRouter(router, "GET", "relation/follow/list/", handlers.RelationFollowList) // 获取用户关注列表
54 | newRouter(router, "GET", "relation/follower/list/", handlers.RelationFollowerList) // 获取用户粉丝列表
55 | newRouter(router, "GET", "relation/friend/list/", handlers.RelationFriendList) // 获取用户好友列表
56 | // 消息类接口
57 | {
58 | newRouter(router, "GET", "message/chat/", handlers.MessageChat) // 获取消息
59 | newRouter(router, "POST", "message/action/", handlers.MessageAction) // 发送消息
60 | }
61 | }
62 | // 挂载 web 服务
63 | r.Use(static.Serve("/", static.LocalFile("web", true)))
64 | r.NoRoute(func(c *gin.Context) {
65 | accept := c.Request.Header.Get("Accept")
66 | flag := strings.Contains(accept, "text/html")
67 | if flag {
68 | content, err := os.ReadFile("web/index.html")
69 | if (err) != nil {
70 | c.Writer.WriteHeader(404)
71 | c.Writer.WriteString("Not Found")
72 | return
73 | }
74 | c.Writer.WriteHeader(200)
75 | c.Writer.Header().Add("Accept", "text/html")
76 | c.Writer.Write(content)
77 | c.Writer.Flush()
78 | }
79 | })
80 | }
81 |
--------------------------------------------------------------------------------
/server/handlers/relation.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "errors"
5 | "github.com/Godvictory/douyin/internal/db"
6 | "github.com/Godvictory/douyin/server/common"
7 | "github.com/Godvictory/douyin/utils/tokens"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | type relationReqs struct {
13 | Token string `json:"token" form:"token" binding:"required"` // 用户鉴权token
14 | ToUserId int64 `json:"to_user_id" form:"to_user_id"` // 对方用户id
15 | ActionType int `json:"action_type" form:"action_type"` // 1-关注,2-取消关注
16 | UserId int64 `json:"user_id" form:"user_id"` // 用户id
17 | }
18 |
19 | // RelationAction 关系操作
20 | func RelationAction(c *gin.Context) (int, any) {
21 | var (
22 | reqs relationReqs
23 | err error
24 | )
25 | // 参数绑定
26 | if err := common.Bind(c, &reqs); err != nil {
27 | return ErrParam(err)
28 | }
29 | claims, err := tokens.CheckToken(reqs.Token)
30 | if err != nil {
31 | return Err("Token 错误", err)
32 | }
33 | if claims.ID == reqs.ToUserId {
34 | return Err("调皮~不可以关注自己哦.", err)
35 | }
36 | switch reqs.ActionType {
37 | case 1, 2:
38 | err = db.RelationAction(claims.ID, reqs.ToUserId, reqs.ActionType)
39 | default:
40 | return ErrParam(errors.New("不合法的 ActionType"))
41 | }
42 | if err != nil {
43 | return Err("失败了.", err)
44 | }
45 | return Ok(H{})
46 | }
47 |
48 | // RelationFollowList 用户关注列表
49 | func RelationFollowList(c *gin.Context) (int, any) {
50 | var (
51 | reqs relationReqs
52 | err error
53 | )
54 | // 参数绑定
55 | if err := common.Bind(c, &reqs); err != nil {
56 | return ErrParam(err)
57 | }
58 | claims, err := tokens.CheckToken(reqs.Token)
59 | if err != nil {
60 | return Err("Token 错误", err)
61 | }
62 | if reqs.UserId == 0 {
63 | reqs.UserId = claims.ID
64 | }
65 | data, err := db.RelationFollowGet(claims.ID, reqs.UserId)
66 | if err != nil {
67 | return Err("再试试吧", err)
68 | }
69 | return Ok(H{"user_list": data})
70 | }
71 |
72 | // RelationFollowerList 用户粉丝列表
73 | func RelationFollowerList(c *gin.Context) (int, any) {
74 | var (
75 | reqs relationReqs
76 | err error
77 | )
78 | // 参数绑定
79 | if err := common.Bind(c, &reqs); err != nil {
80 | return ErrParam(err)
81 | }
82 | claims, err := tokens.CheckToken(reqs.Token)
83 | if err != nil {
84 | return Err("Token 错误", err)
85 | }
86 | if reqs.UserId == 0 {
87 | reqs.UserId = claims.ID
88 | }
89 | data, err := db.RelationFollowerGet(claims.ID, reqs.UserId)
90 | if err != nil {
91 | return Err("再试试吧", err)
92 | }
93 | return Ok(H{"user_list": data})
94 | }
95 |
96 | // RelationFriendList 用户好友列表
97 | func RelationFriendList(c *gin.Context) (int, any) {
98 | var (
99 | reqs relationReqs
100 | err error
101 | )
102 | // 参数绑定
103 | if err := common.Bind(c, &reqs); err != nil {
104 | return ErrParam(err)
105 | }
106 | claims, err := tokens.CheckToken(reqs.Token)
107 | if err != nil {
108 | return Err("Token 错误", err)
109 | }
110 | if reqs.UserId != 0 && claims.ID != reqs.UserId {
111 | return Err("只能查看自己的朋友哦")
112 | }
113 | data, err := db.RelationFriendGet(claims.ID)
114 | if err != nil {
115 | return Err("再试试吧", err)
116 | }
117 | return Ok(H{"user_list": data})
118 | }
119 |
--------------------------------------------------------------------------------
/test/key_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "testing"
5 | "time"
6 | )
7 |
8 | /*
9 | go test -benchmem -benchtime=3s -bench=.
10 |
11 | cpu: AMD Ryzen 5 5600X 6-Core Processor
12 | BenchmarkByteA-12 46484457 98.90 ns/op 24 B/op 1 allocs/op
13 | BenchmarkByteB-12 44854060 83.33 ns/op 16 B/op 1 allocs/op
14 | BenchmarkByteC-12 38626191 97.36 ns/op 16 B/op 1 allocs/op
15 | BenchmarkBuilder-12 28206279 128.5 ns/op 88 B/op 2 allocs/op
16 | BenchmarkPrintf-12 26774776 166.6 ns/op 56 B/op 2 allocs/op
17 | BenchmarkBuffer-12 24778485 156.5 ns/op 136 B/op 3 allocs/op
18 | BenchmarkByteAParallel-12 298355262 12.21 ns/op 24 B/op 1 allocs/op
19 | BenchmarkByteBParallel-12 328606353 11.07 ns/op 16 B/op 1 allocs/op
20 | BenchmarkByteCParallel-12 313040088 11.64 ns/op 16 B/op 1 allocs/op
21 | BenchmarkBuilderParallel-12 180520376 19.96 ns/op 88 B/op 2 allocs/op
22 | BenchmarkPrintfParallel-12 148170528 24.34 ns/op 56 B/op 2 allocs/op
23 | BenchmarkBufferParallel-12 124574508 28.74 ns/op 136 B/op 3 allocs/op
24 | PASS
25 | */
26 | func BenchmarkByteA(b *testing.B) {
27 | for i := 0; i < b.N; i++ {
28 | ByteA(time.Now().UnixNano())
29 | }
30 | }
31 |
32 | func BenchmarkByteB(b *testing.B) {
33 | for i := 0; i < b.N; i++ {
34 | ByteB(time.Now().UnixNano())
35 | }
36 | }
37 |
38 | func BenchmarkByteC(b *testing.B) {
39 | for i := 0; i < b.N; i++ {
40 | ByteC(time.Now().UnixNano())
41 | }
42 | }
43 |
44 | func BenchmarkBuilder(b *testing.B) {
45 | for i := 0; i < b.N; i++ {
46 | Builder(time.Now().UnixNano())
47 | }
48 | }
49 |
50 | func BenchmarkPrintf(b *testing.B) {
51 | for i := 0; i < b.N; i++ {
52 | Printf(time.Now().UnixNano())
53 | }
54 | }
55 |
56 | func BenchmarkBuffer(b *testing.B) {
57 | for i := 0; i < b.N; i++ {
58 | Buffer(time.Now().UnixNano())
59 | }
60 | }
61 |
62 | func BenchmarkByteAParallel(b *testing.B) {
63 | b.RunParallel(func(pb *testing.PB) {
64 | for pb.Next() {
65 | ByteA(time.Now().UnixNano())
66 | }
67 | })
68 | }
69 |
70 | func BenchmarkByteBParallel(b *testing.B) {
71 | b.RunParallel(func(pb *testing.PB) {
72 | for pb.Next() {
73 | ByteB(time.Now().UnixNano())
74 | }
75 | })
76 | }
77 |
78 | func BenchmarkByteCParallel(b *testing.B) {
79 | b.RunParallel(func(pb *testing.PB) {
80 | for pb.Next() {
81 | ByteC(time.Now().UnixNano())
82 | }
83 | })
84 | }
85 |
86 | func BenchmarkBuilderParallel(b *testing.B) {
87 | b.RunParallel(func(pb *testing.PB) {
88 | for pb.Next() {
89 | Builder(time.Now().UnixNano())
90 | }
91 | })
92 | }
93 |
94 | func BenchmarkPrintfParallel(b *testing.B) {
95 | b.RunParallel(func(pb *testing.PB) {
96 | for pb.Next() {
97 | Printf(time.Now().UnixNano())
98 | }
99 | })
100 | }
101 |
102 | func BenchmarkBufferParallel(b *testing.B) {
103 | b.RunParallel(func(pb *testing.PB) {
104 | for pb.Next() {
105 | Buffer(time.Now().UnixNano())
106 | }
107 | })
108 | }
109 |
--------------------------------------------------------------------------------
/web/docs/project.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
极简版抖音
7 |
8 | 一个字节青训营的实战项目
9 |
10 | 开始于2023.7.24 结束于2023.8.20
11 |
12 |
13 | 报告Bug
14 | 提出新特性
15 |
16 |
17 | ## 前言
18 |
19 | 虽然是一个青训营的项目,开发的时间也不算长,但还是会尽力去写好一个项目,做好各个文档和注释,即使结束了,也能帮助后来者学习.
20 |
21 | 一开始想着收集些 17,18 年的热门视频来当资料,耗费了很多时间,确实不好收集,就直接 pxx 用了营销号的"无版权资源"~
22 |
23 | 项目结构借鉴 [Alist](https://github.com/alist-org/alist) 项目
24 |
25 | ## 技术栈
26 |
27 | #### 后端 Golang 1.20
28 |
29 | - Gin [(Web 框架)](https://gin-gonic.com/zh-cn/)
30 | - GORM [(ORM)](https://gorm.io/zh_CN/)
31 | - Cobra [(CLI 框架)](https://github.com/spf13/cobra)
32 | - MySQL,SQLite,PostgreSQL [(数据库)]()
33 | - Redis [(缓存)]()
34 |
35 | #### 前端 Vue.js 3
36 |
37 | - Vite [(构建工具)](https://cn.vitejs.dev/)
38 | - element-plus [(UI 库)](https://element-plus.org/zh-CN/)
39 | - xgplayer [(西瓜播放器)](https://v2.h5player.bytedance.com/gettingStarted/)
40 | - md-editor-v3 [(Markdown 编辑器)](https://www.wangeditor.com/)
41 |
42 | ## 部署方法
43 |
44 | #### clone 项目
45 |
46 | ```sh
47 | git clone https://github.com/Ocyss/douyin.git && cd douyin
48 | ```
49 |
50 | #### 编译/运行
51 |
52 | ```sh
53 | go build && ./douyin
54 | ```
55 |
56 | > 项目端口默认`:23724`
57 |
58 | #### Web 端配置
59 |
60 | ##### 1.直接下载 releases
61 |
62 | https://github.com/Ocyss/douyin-web/releases
63 |
64 | 解压到 web 文件夹中,结构如下
65 |
66 | ```
67 | douyin
68 | ├── data
69 | │ ├── config.json
70 | │ └── log
71 | ├── web
72 | │ ├── assets
73 | │ ├── docs
74 | │ ├── static
75 | │ └── index.html
76 | └── douyin
77 | ```
78 |
79 | 运行`douyin`,打开`http://localhost:23724`,即可看到 Web 端界面
80 |
81 | ##### 2.自行编译
82 |
83 | ```sh
84 | git clone https://github.com/Ocyss/douyin-web.git
85 | ```
86 |
87 | ### 文件目录说明
88 |
89 | ```
90 | douyin
91 | ├── cmd # 启动项/参数配置
92 | │ └── flags
93 | ├── data # 数据目录
94 | │ └── log
95 | ├── internal # 内部服务
96 | │ ├── bootstrap
97 | │ ├── conf
98 | │ ├── db
99 | │ └── model
100 | ├── server # 路由服务
101 | │ ├── common
102 | │ ├── handlers
103 | │ └── middleware
104 | ├── test
105 | ├── utils # 通用工具
106 | │ ├── checks
107 | │ ├── tokens
108 | │ └── upload
109 | └── web # Web 服务
110 | ```
111 |
112 | ## 预览
113 |
114 | 
115 | 
116 | 
117 | 
118 |
119 | ### 版本控制
120 |
121 | 该项目使用 Git 进行版本管理。您可以在 repository 参看当前可用版本。
122 |
123 | ### 联系方式
124 |
125 | [me@ocyss.icu](mailto:me@ocyss.icu)
126 |
127 | ### 团队成员
128 |
129 | - [daydayw](https://github.com/daydayw)
130 | - [Godvictory](https://github.com/Godvictory)
131 | - [haoer](https://github.com/haoaer)
132 | - [hblovo](https://github.com/hblovo)
133 | - [koutaManaka](https://github.com/koutaManaka)
134 | - [leaveYoung](https://github.com/leaveYoung)
135 | - [lyhlyh03](https://github.com/lyhlyh03)
136 | - [Ocyss_04](https://github.com/ocyss)
137 |
--------------------------------------------------------------------------------
/server/handlers/user.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "errors"
5 | "github.com/Godvictory/douyin/internal/db"
6 | "github.com/Godvictory/douyin/internal/model"
7 | "github.com/Godvictory/douyin/server/common"
8 | "github.com/Godvictory/douyin/utils"
9 | "github.com/Godvictory/douyin/utils/checks"
10 | "github.com/Godvictory/douyin/utils/tokens"
11 |
12 | "github.com/gin-gonic/gin"
13 | "gorm.io/gorm"
14 | )
15 |
16 | type (
17 | userLogRegReqs struct {
18 | Name string `json:"username" form:"username" binding:"required"` // 用户名称
19 | Pawd string `json:"password" form:"password" binding:"required"` // 用户密码
20 | Avatar string `json:"avatar" form:"avatar"` // 用户头像
21 | BackgroundImage string `json:"background_image" form:"background_image"` // 用户个人页顶部大图
22 | Signature string `json:"signature" form:"signature"` // 个人简介
23 | }
24 | userReqs struct {
25 | ID int64 `json:"user_id" form:"user_id"` // 用户id
26 | Token string `json:"token" form:"token" binding:"required"` // 用户鉴权token
27 | }
28 | UserInfoResp struct {
29 | ID int64 `json:"user_id"` // 用户id
30 | Name string `json:"name"` // 用户名称
31 | FollowCount int64 `json:"follow_count"` // 关注总数
32 | FollowerCount int64 `json:"follower_count"` // 粉丝总数
33 | IsFollow bool `json:"is_follow"` // 是否关注
34 | Avatar string `json:"avatar"` // 用户头像
35 | BackgroundImage string `json:"background_image"` // 用户个人页顶部大图
36 | Signature string `json:"signature"` // 个人简介
37 | WorkCount int64 `json:"work_count"` // 作品数量
38 | TotalFavorited int64 `json:"total_favorited"` // 获赞数量
39 | FavoriteCount int64 `json:"favorite_count"` // 点赞数量
40 | }
41 | )
42 |
43 | // UserLogin 用户登陆
44 | func UserLogin(c *gin.Context) (int, any) {
45 | var reqs userLogRegReqs
46 | // 参数绑定
47 | if err := common.Bind(c, &reqs); err != nil {
48 | return ErrParam(err)
49 | }
50 | if msg := checks.ValidateInput(5, 32, reqs.Name, reqs.Pawd); len(msg) > 0 {
51 | return Err("账户或者密码" + msg)
52 | }
53 |
54 | data, msg, err := db.Login(reqs.Name, reqs.Pawd)
55 | if err != nil {
56 | return Err(msg, err)
57 | }
58 | token, err := tokens.GetToken(data.ID, data.Name)
59 | if err != nil {
60 | return Err("抱歉,麻烦再试一次吧...", err)
61 | }
62 | return Ok(H{"user_id": data.ID, "token": token})
63 | }
64 |
65 | // UserRegister 用户注册
66 | func UserRegister(c *gin.Context) (int, any) {
67 | var (
68 | reqs userLogRegReqs
69 | data model.User
70 | )
71 | // 参数绑定
72 | if err := common.Bind(c, &reqs); err != nil {
73 | return ErrParam(err)
74 | }
75 | if msg := checks.ValidateInput(5, 32, reqs.Name, reqs.Pawd); len(msg) > 0 {
76 | return Err("账户或者密码" + msg)
77 | }
78 | _ = utils.Merge(&data, reqs)
79 |
80 | msg, err := db.Register(&data)
81 | if err != nil {
82 | return Err(msg, err)
83 | }
84 |
85 | token, err := tokens.GetToken(data.ID, data.Name)
86 | if err != nil {
87 | switch {
88 | case errors.Is(err, gorm.ErrDuplicatedKey):
89 | return Err("该用户名已被使用!", err)
90 | default:
91 | return Err("抱歉,麻烦再试一次吧...", err)
92 | }
93 | }
94 | return Ok(H{"user_id": data.ID, "token": token})
95 | }
96 |
97 | // UserInfo 用户信息
98 | func UserInfo(c *gin.Context) (int, any) {
99 | var (
100 | reqs userReqs
101 | resp UserInfoResp
102 | )
103 | // 参数绑定
104 | if err := c.ShouldBindQuery(&reqs); err != nil {
105 | return ErrParam(err)
106 | }
107 |
108 | claims, err := tokens.CheckToken(reqs.Token)
109 | if err != nil {
110 | return Err("Token 错误", err)
111 | }
112 | if reqs.ID == 0 {
113 | reqs.ID = claims.ID
114 | }
115 | data, msg, err := db.UserInfo(reqs.ID)
116 | if err != nil {
117 | return Err(msg, err)
118 | }
119 | _ = utils.Merge(&resp, data)
120 | return Ok(H{"user": resp})
121 | }
122 |
--------------------------------------------------------------------------------
/web/assets/el-message-f448e6ff.css:
--------------------------------------------------------------------------------
1 | .el-badge{--el-badge-bg-color:var(--el-color-danger);--el-badge-radius:10px;--el-badge-font-size:12px;--el-badge-padding:6px;--el-badge-size:18px;position:relative;vertical-align:middle;display:inline-block;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.el-badge__content{background-color:var(--el-badge-bg-color);border-radius:var(--el-badge-radius);color:var(--el-color-white);display:inline-flex;justify-content:center;align-items:center;font-size:var(--el-badge-font-size);height:var(--el-badge-size);padding:0 var(--el-badge-padding);white-space:nowrap;border:1px solid var(--el-bg-color)}.el-badge__content.is-fixed{position:absolute;top:0;right:calc(1px + var(--el-badge-size)/ 2);transform:translateY(-50%) translate(100%);z-index:var(--el-index-normal)}.el-badge__content.is-fixed.is-dot{right:5px}.el-badge__content.is-dot{height:8px;width:8px;padding:0;right:0;border-radius:50%}.el-badge__content--primary{background-color:var(--el-color-primary)}.el-badge__content--success{background-color:var(--el-color-success)}.el-badge__content--warning{background-color:var(--el-color-warning)}.el-badge__content--info{background-color:var(--el-color-info)}.el-badge__content--danger{background-color:var(--el-color-danger)}.el-message{--el-message-bg-color:var(--el-color-info-light-9);--el-message-border-color:var(--el-border-color-lighter);--el-message-padding:15px 19px;--el-message-close-size:16px;--el-message-close-icon-color:var(--el-text-color-placeholder);--el-message-close-hover-color:var(--el-text-color-secondary)}.el-message{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;max-width:calc(100% - 32px);box-sizing:border-box;border-radius:var(--el-border-radius-base);border-width:var(--el-border-width);border-style:var(--el-border-style);border-color:var(--el-message-border-color);position:fixed;left:50%;top:20px;transform:translate(-50%);background-color:var(--el-message-bg-color);transition:opacity var(--el-transition-duration),transform .4s,top .4s;padding:var(--el-message-padding);display:flex;align-items:center}.el-message.is-center{justify-content:center}.el-message.is-closable .el-message__content{padding-right:31px}.el-message p{margin:0}.el-message--success{--el-message-bg-color:var(--el-color-success-light-9);--el-message-border-color:var(--el-color-success-light-8);--el-message-text-color:var(--el-color-success)}.el-message--success .el-message__content{color:var(--el-message-text-color);overflow-wrap:anywhere}.el-message .el-message-icon--success{color:var(--el-message-text-color)}.el-message--info{--el-message-bg-color:var(--el-color-info-light-9);--el-message-border-color:var(--el-color-info-light-8);--el-message-text-color:var(--el-color-info)}.el-message--info .el-message__content{color:var(--el-message-text-color);overflow-wrap:anywhere}.el-message .el-message-icon--info{color:var(--el-message-text-color)}.el-message--warning{--el-message-bg-color:var(--el-color-warning-light-9);--el-message-border-color:var(--el-color-warning-light-8);--el-message-text-color:var(--el-color-warning)}.el-message--warning .el-message__content{color:var(--el-message-text-color);overflow-wrap:anywhere}.el-message .el-message-icon--warning{color:var(--el-message-text-color)}.el-message--error{--el-message-bg-color:var(--el-color-error-light-9);--el-message-border-color:var(--el-color-error-light-8);--el-message-text-color:var(--el-color-error)}.el-message--error .el-message__content{color:var(--el-message-text-color);overflow-wrap:anywhere}.el-message .el-message-icon--error{color:var(--el-message-text-color)}.el-message__icon{margin-right:10px}.el-message .el-message__badge{position:absolute;top:-8px;right:-8px}.el-message__content{padding:0;font-size:14px;line-height:1}.el-message__content:focus{outline-width:0}.el-message .el-message__closeBtn{position:absolute;top:50%;right:19px;transform:translateY(-50%);cursor:pointer;color:var(--el-message-close-icon-color);font-size:var(--el-message-close-size)}.el-message .el-message__closeBtn:focus{outline-width:0}.el-message .el-message__closeBtn:hover{color:var(--el-message-close-hover-color)}.el-message-fade-enter-from,.el-message-fade-leave-to{opacity:0;transform:translate(-50%,-100%)}
2 |
--------------------------------------------------------------------------------
/server/handlers/video.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "github.com/Godvictory/douyin/internal/db"
5 | "github.com/Godvictory/douyin/internal/model"
6 | "github.com/Godvictory/douyin/utils/tokens"
7 | "mime/multipart"
8 | "strconv"
9 |
10 | "github.com/gin-gonic/gin"
11 | )
12 |
13 | type (
14 | videoActionData struct {
15 | Data multipart.File `json:"data" form:"data"` // 视频数据
16 | Token string `json:"token" form:"token"` // 用户鉴权token
17 | Title string `json:"title" form:"title"` // 视频标题
18 | TypeOf string `json:"type" form:"type"` // 视频类型
19 | Url string `json:"url" form:"url"` // 视频URL(测试环境)
20 | CoverUrl string `json:"cover_url" form:"cover_url"` // 视频封面URL(测试环境)
21 | UserID int64 `json:"id,string" form:"id"` // 用户ID(测试环境)
22 | UserCreations []*model.UserCreation `json:"user_creations"` // 联合投稿作者(半成品)
23 | }
24 | )
25 |
26 | // VideoGet 视频流获取
27 | func VideoGet(c *gin.Context) (int, any) {
28 | var err error
29 | claims := new(tokens.MyClaims)
30 | token := c.Query("token")
31 | ty := c.Query("type")
32 | repeat, _ := strconv.ParseBool(c.Query("repeat"))
33 | if token != "" {
34 | claims, err = tokens.CheckToken(token)
35 | if err != nil {
36 | return Err("Token 错误,请重新登录", err)
37 | } // 没办法控制客户端退出登录,就这样好了(反正token 3个月才过期)
38 | }
39 | if ty == "" {
40 | ty = "all"
41 | }
42 | data, err := db.Feed(claims.ID, c.ClientIP(), ty, repeat)
43 | if err != nil {
44 | return Err("数据获取出错,请稍后再试.", err)
45 | }
46 |
47 | res := H{
48 | "video_list": data,
49 | }
50 | return Ok(res)
51 | }
52 |
53 | // VideoAction 视频投稿
54 | func VideoAction(c *gin.Context) (int, any) {
55 | var data videoActionData
56 | file, _, err := c.Request.FormFile("data")
57 | data.Data = file
58 | data.Token = c.PostForm("token")
59 | data.Title = c.PostForm("title")
60 | data.TypeOf = c.PostForm("type")
61 | if err != nil || data.Token == "" {
62 | return ErrParam(err)
63 | }
64 | token, err := tokens.CheckToken(data.Token)
65 | if err != nil {
66 | return Err("Token 错误", err)
67 | }
68 | id, msg, err := db.VideoUpload(token.ID, data.Data, "", "", data.Title, data.TypeOf, data.UserCreations)
69 | if err != nil {
70 | return Err(msg, err)
71 | }
72 | return Ok(H{"vid": id})
73 | }
74 |
75 | // VideoActionUrl 视频投稿
76 | // 测试接口可直接指定URL,或使用ID进行投稿
77 | func VideoActionUrl(c *gin.Context) (int, any) {
78 | var data videoActionData
79 |
80 | err := c.ShouldBindJSON(&data)
81 | if err != nil || (data.UserID == 0 && data.Token == "") || (data.Data == nil && data.Url == "") {
82 | return ErrParam(err)
83 | }
84 | if data.Token != "" {
85 | token, err := tokens.CheckToken(data.Token)
86 | if err != nil {
87 | return Err("Token 错误", err)
88 | }
89 | data.UserID = token.ID
90 | }
91 |
92 | id, msg, err := db.VideoUpload(data.UserID, data.Data, data.Url, data.CoverUrl, data.Title, data.TypeOf, data.UserCreations)
93 | if err != nil {
94 | return Err(msg, err)
95 | }
96 |
97 | return Ok(H{"vid": id})
98 | }
99 |
100 | // VideoList 发布列表
101 | func VideoList(c *gin.Context) (int, any) {
102 | var reqs userReqs
103 | // 参数绑定
104 | if err := c.ShouldBindQuery(&reqs); err != nil {
105 | return ErrParam(err)
106 | }
107 | if reqs.Token != "" {
108 | claims, err := tokens.CheckToken(reqs.Token)
109 | if err != nil {
110 | return Err("Token 错误", err)
111 | }
112 | if reqs.ID == 0 {
113 | reqs.ID = claims.ID
114 | }
115 | } else if reqs.ID == 0 {
116 | return Err("无参数!!!")
117 | }
118 | data, err := db.VideoList(reqs.ID)
119 | if err != nil {
120 | return Err("网卡了,再试一次吧", err)
121 | }
122 |
123 | return Ok(H{"video_list": data})
124 | }
125 |
126 | func VideoFollowList(c *gin.Context) (int, any) {
127 | var reqs userReqs
128 | // 参数绑定
129 | if err := c.ShouldBindQuery(&reqs); err != nil {
130 | return ErrParam(err)
131 | }
132 | claims, err := tokens.CheckToken(reqs.Token)
133 | if err != nil {
134 | return Err("Token 错误", err)
135 | }
136 | data, err := db.VideoFollowList(claims.ID)
137 | if err != nil {
138 | return Err("网卡了,再试一次吧", err)
139 | }
140 | return Ok(H{"video_list": data})
141 | }
142 |
--------------------------------------------------------------------------------
/server/handlers_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "mime/multipart"
8 | "net/http"
9 | "net/http/httptest"
10 | "testing"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func videoGet(t *testing.T, data *Data) map[string]any {
16 | return newReq(t, "GET", "/douyin/feed?repeat=true", data)
17 | }
18 |
19 | func videoAction(t *testing.T, data *Data) map[string]any {
20 | return newReq(t, "POST", "/douyin/publish/action/", data)
21 | }
22 |
23 | func videoActionurl(t *testing.T, data *Data) map[string]any {
24 | return newReq(t, "POST", "/douyin/publish/actionUrl/", data)
25 | }
26 |
27 | func videoList(t *testing.T, data *Data) map[string]any {
28 | return newReq(t, "GET", "/douyin/publish/list/", data)
29 | }
30 |
31 | func videoFollowList(t *testing.T, data *Data) map[string]any {
32 | return newReq(t, "GET", "/douyin/publish/follow/", data)
33 | }
34 |
35 | func userRegister(t *testing.T, data *Data) map[string]any {
36 | return newReq(t, "POST", "/douyin/user/register/", data)
37 | }
38 |
39 | func userLogin(t *testing.T, data *Data) map[string]any {
40 | return newReq(t, "POST", "/douyin/user/login/", data)
41 | }
42 |
43 | func userInfo(t *testing.T, data *Data) map[string]any {
44 | return newReq(t, "GET", "/douyin/user/", data)
45 | }
46 |
47 | func favoriteAction(t *testing.T, data *Data) map[string]any {
48 | return newReq(t, "POST", "/douyin/favorite/action/", data)
49 | }
50 |
51 | func favoriteList(t *testing.T, data *Data) map[string]any {
52 | return newReq(t, "GET", "/douyin/favorite/list/", data)
53 | }
54 |
55 | func commentAction(t *testing.T, data *Data) map[string]any {
56 | return newReq(t, "POST", "/douyin/comment/action/", data)
57 | }
58 |
59 | func commentList(t *testing.T, data *Data) map[string]any {
60 | return newReq(t, "GET", "/douyin/comment/list/", data)
61 | }
62 |
63 | func relationAction(t *testing.T, data *Data) map[string]any {
64 | return newReq(t, "POST", "/douyin/relation/action/", data)
65 | }
66 |
67 | func relationFollowList(t *testing.T, data *Data) map[string]any {
68 | return newReq(t, "GET", "/douyin/relation/follow/list/", data)
69 | }
70 |
71 | func relationFollowerList(t *testing.T, data *Data) map[string]any {
72 | return newReq(t, "GET", "/douyin/relation/follower/list/", data)
73 | }
74 |
75 | func relationFriendList(t *testing.T, data *Data) map[string]any {
76 | return newReq(t, "GET", "/douyin/relation/friend/list/", data)
77 | }
78 |
79 | func messageChat(t *testing.T, data *Data) map[string]any {
80 | return newReq(t, "GET", "/douyin/message/chat/", data)
81 | }
82 |
83 | func messageAction(t *testing.T, data *Data) map[string]any {
84 | return newReq(t, "POST", "/douyin/message/action/", data)
85 | }
86 |
87 | type Data struct {
88 | Json H
89 | Form *F
90 | Query S
91 | }
92 | type (
93 | H map[string]any
94 | S map[string]string
95 | F struct {
96 | r io.Reader
97 | w *multipart.Writer
98 | }
99 | )
100 |
101 | func newReq(t *testing.T, mod, url string, data *Data) map[string]any {
102 | w := httptest.NewRecorder()
103 | var req *http.Request
104 | if data != nil && data.Json != nil {
105 | jsonData, _ := json.Marshal(data.Json)
106 | req, _ = http.NewRequest(mod, url, bytes.NewBuffer(jsonData))
107 | } else if data != nil && data.Form != nil {
108 | req, _ = http.NewRequest(mod, url, data.Form.r)
109 | req.Header.Set("Content-Type", data.Form.w.FormDataContentType())
110 | } else {
111 | req, _ = http.NewRequest(mod, url, nil)
112 | }
113 | if data != nil && data.Query != nil {
114 | q := req.URL.Query()
115 | for k, v := range data.Query {
116 | q.Add(k, v)
117 | }
118 | req.URL.RawQuery = q.Encode()
119 | }
120 |
121 | router.ServeHTTP(w, req)
122 | assert.Equal(t, 200, w.Code)
123 | code, msg, _data := unmarshal(w.Body.Bytes())
124 | assert.Equal(t, float64(0), code, msg, _data)
125 | return _data
126 | }
127 |
128 | func unmarshal(body []byte) (float64, string, map[string]any) {
129 | m := make(map[string]any)
130 | if err := json.Unmarshal(body, &m); err != nil {
131 | return 1, "json 解析失败", m
132 | }
133 | if v, ok := m["status_code"]; ok {
134 | if v != "0" && v != float64(0) && v != 0 {
135 | return 1, "错误的status_code", m
136 | }
137 | } else {
138 | return 1, "无 status_code", m
139 | }
140 | return m["status_code"].(float64), "", m
141 | }
142 |
--------------------------------------------------------------------------------
/internal/db/video.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/Godvictory/douyin/internal/model"
7 | "github.com/Godvictory/douyin/utils"
8 | "github.com/Godvictory/douyin/utils/upload"
9 | "gorm.io/gorm"
10 | "mime/multipart"
11 | )
12 |
13 | // Feed 获取视频流
14 | func Feed(uid int64, ip, ty string, repeat bool) ([]model.Video, error) {
15 | var data []model.Video
16 | res := make([]int64, 0, 10)
17 | // 循环20次,随机生成20个主键id,通过IP来减少重复推送
18 | for batch := 0; len(res) < 3 && batch < 6; batch++ {
19 | var rv []int64
20 | if v, ok := videoAll[ty]; ok {
21 | rv = utils.RandVid(v, 20)
22 | }
23 | for i := 0; i < len(rv) && len(res) < 3; i++ {
24 | if repeat || model.ViewedFilter(rv[i], ip) {
25 | res = append(res, rv[i])
26 | }
27 | }
28 | }
29 | if len(res) > 0 {
30 | db.Set("user_id", uid).Where(res).Find(&data)
31 | utils.RandShuffle(len(data), func(i, j int) {
32 | data[i], data[j] = data[j], data[i]
33 | })
34 | }
35 | return data, nil
36 | }
37 |
38 | // VideoUpload 视频投稿
39 | func VideoUpload(uid int64, file multipart.File, PlayUrl, CoverUrl, title, typeOf string, UserCreations []*model.UserCreation) (int64, string, error) {
40 | if typeOf == "" {
41 | typeOf = "t"
42 | }
43 | data := model.Video{
44 | AuthorID: uid,
45 | PlayUrl: PlayUrl,
46 | CoverUrl: CoverUrl,
47 | Title: title,
48 | TypeOf: typeOf,
49 | }
50 | // 开启事务,上传失败不添加数据
51 | tx := db.Begin()
52 | err := tx.Create(&data).Error
53 | if err != nil {
54 | tx.Rollback()
55 | return 0, "", err
56 | }
57 | if file != nil {
58 | // reader := bytes.NewReader(file)
59 | fname := fmt.Sprintf("t/%d.mp4", data.ID)
60 | url, err := upload.Aliyun(fname, file)
61 | if err != nil {
62 | tx.Rollback()
63 | return 0, "上传出错...", err
64 | }
65 | data.PlayUrl = url + fname
66 | data.CoverUrl = url + fmt.Sprintf("t/%d.jpg", data.ID)
67 | err = tx.Save(&data).Error
68 | if err != nil {
69 | tx.Rollback()
70 | return 0, "更新出错...", err
71 | }
72 | }
73 | UserCreations = append([]*model.UserCreation{{VideoID: data.ID, UserID: uid, Type: "Up主"}}, UserCreations...)
74 | for _, uc := range UserCreations {
75 | uc.VideoID = data.ID
76 | err := tx.Create(uc).Error
77 | if err != nil {
78 | tx.Rollback()
79 | return 0, "创建出错...", err
80 | }
81 | tx.Model(&model.User{Model: id(uc.UserID)}).UpdateColumn("work_count", gorm.Expr("work_count + ?", 1))
82 | }
83 | tx.Commit()
84 | videoAll["all"] = append(videoAll["all"], data.ID)
85 | videoAll[typeOf] = append(videoAll[typeOf], data.ID)
86 | return data.ID, "", nil
87 | }
88 |
89 | // VideoLike 视频点赞操作
90 | func VideoLike(uid, vid int64, _type int) error {
91 | var err error
92 | // association := db.Model(&model.User{Model: id(uid)}).Omit("Favorite").Association("Favorite")
93 | val := &model.Video{Model: id(vid)}
94 | switch _type {
95 | case 1:
96 | row := db.Exec("INSERT INTO `user_favorite` (`user_id`,`video_id`) VALUES (?,?)", uid, vid)
97 | if row.Error == nil && row.RowsAffected == 1 {
98 | val.HIncrByFavoriteCount(1)
99 | } else {
100 | err = errors.Join(row.Error, errors.New("err:可能已有数据"))
101 | }
102 | case 2:
103 | row := db.Exec("DELETE FROM user_favorite Where user_id = ? AND video_id = ?", uid, vid)
104 | if row.Error == nil && row.RowsAffected == 1 {
105 | val.HIncrByFavoriteCount(-1)
106 | } else {
107 | err = errors.Join(row.Error, errors.New("err:可能无该数据"))
108 | }
109 | default:
110 | err = errors.New("你看看你传的什么东西吧")
111 | }
112 | if err != nil {
113 | return err
114 | }
115 | return nil
116 | }
117 |
118 | // VideoLikeList 获取喜欢列表
119 | func VideoLikeList(uid int64) ([]*model.Video, error) {
120 | var data []*model.Video
121 | err := db.Set("user_id", uid).Model(&model.User{Model: id(uid)}).Association("Favorite").Find(&data)
122 | if err != nil {
123 | return nil, err
124 | }
125 | return data, nil
126 | }
127 |
128 | // VideoList 获取作品列表
129 | func VideoList(uid int64) ([]*model.Video, error) {
130 | var data []*model.Video
131 | err := db.Set("user_id", uid).Model(&model.User{Model: id(uid)}).Association("Videos").Find(&data)
132 | if err != nil {
133 | return nil, err
134 | }
135 | return data, nil
136 | }
137 |
138 | // VideoFollowList 获取关注作品列表
139 | func VideoFollowList(uid int64) ([]*model.Video, error) {
140 | var user []*model.User
141 | var data []*model.Video
142 | err := db.Set("user_id", uid).Model(&model.User{Model: id(uid)}).Association("Follow").Find(&user)
143 | if err != nil {
144 | return nil, err
145 | }
146 | for _, u := range user {
147 | var videos []*model.Video
148 | err := db.Set("user_id", uid).Model(&model.User{Model: id(u.ID)}).Association("Videos").Find(&videos)
149 | if err != nil {
150 | return nil, err
151 | }
152 | data = append(data, videos...)
153 | }
154 | return data, nil
155 | }
156 |
--------------------------------------------------------------------------------
/server/server_test.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "github.com/Godvictory/douyin/cmd/flags"
7 | "github.com/Godvictory/douyin/internal/bootstrap"
8 | "github.com/Godvictory/douyin/internal/model"
9 | "mime/multipart"
10 | "os"
11 | "testing"
12 |
13 | "github.com/stretchr/testify/assert"
14 |
15 | "github.com/gin-gonic/gin"
16 | )
17 |
18 | var router *gin.Engine
19 |
20 | func init() {
21 | flags.DataDir = "../data"
22 | bootstrap.InitConf()
23 | bootstrap.InitDb()
24 | bootstrap.InitRdb()
25 | flags.Tst = true
26 | gin.SetMode(gin.TestMode)
27 | router = gin.New()
28 | Init(router)
29 | }
30 |
31 | func TestServer(t *testing.T) {
32 | t.Run("游客视频流获取", func(t *testing.T) {
33 | v := videoGet(t, nil)
34 | assert.NotNil(t, v["video_list"], v)
35 | })
36 | var token1, token2 string
37 | var uid1, uid2 string
38 | var video1 map[string]any
39 | t.Run("注册,登录,用户信息", func(t *testing.T) {
40 | user := &Data{Json: H{
41 | "username": "test111",
42 | "password": "230724",
43 | }}
44 | v1 := userRegister(t, user)
45 | assert.IsType(t, "", v1["user_id"])
46 | v2 := userLogin(t, user)
47 | assert.IsType(t, "", v2["token"])
48 | assert.IsType(t, "", v2["user_id"])
49 | token1 = v2["token"].(string)
50 | uid1 = v2["user_id"].(string)
51 | v3 := userInfo(t, &Data{Query: S{"user_id": uid1, "token": token1}})
52 | assert.NotNil(t, v3["user"], uid1, token1)
53 | })
54 | t.Run("登录视频流获取", func(t *testing.T) {
55 | v := videoGet(t, &Data{Query: S{"token": token1}})
56 | assert.NotNil(t, []model.Video{}, v["video_list"])
57 | video1 = v["video_list"].([]any)[0].(map[string]any)
58 | })
59 | t.Run("点赞,评论,关注", func(t *testing.T) {
60 | favoriteAction(t, &Data{Json: H{
61 | "token": token1,
62 | "video_id": video1["id"],
63 | "action_type": 1,
64 | }})
65 | commentAction(t, &Data{Json: H{
66 | "token": token1,
67 | "video_id": video1["id"],
68 | "action_type": 1,
69 | "comment_text": "testtest111",
70 | }})
71 | relationAction(t, &Data{Json: H{
72 | "token": token1,
73 | "to_user_id": video1["author"].(map[string]any)["id"],
74 | "action_type": 1,
75 | }})
76 | })
77 | t.Run("喜欢/评论/关注列表", func(t *testing.T) {
78 | v := favoriteList(t, &Data{Query: S{"user_id": uid1, "token": token1}})
79 | assert.NotNil(t, v["video_list"])
80 | v = commentList(t, &Data{Query: S{"video_id": uid1, "token": token1}})
81 | assert.NotNil(t, v["comment_list"])
82 | v = relationFollowList(t, &Data{Query: S{"user_id": uid1, "token": token1}})
83 | assert.NotNil(t, v["user_list"])
84 | })
85 | t.Run("投稿,发布列表", func(t *testing.T) {
86 | bodyBuf := &bytes.Buffer{}
87 | bodyWriter := multipart.NewWriter(bodyBuf)
88 | filePath := "../public/test1.mp4"
89 | file, err := os.Open(filePath)
90 | if err != nil {
91 | t.Error(err)
92 | }
93 |
94 | part, _ := bodyWriter.CreateFormFile("data", filePath)
95 | _ = bodyWriter.WriteField("token", token1)
96 | _ = bodyWriter.WriteField("title", "video_test111")
97 | var fileData []byte
98 | file.Read(fileData)
99 | part.Write(fileData)
100 | file.Close()
101 | bodyWriter.Close()
102 | videoAction(t, &Data{Form: &F{
103 | r: bodyBuf,
104 | w: bodyWriter,
105 | }})
106 | v := videoList(t, &Data{Query: S{"user_id": uid1, "token": token1}})
107 | assert.NotNil(t, v["video_list"])
108 | })
109 | t.Run("注册2,关注,粉丝/好友列表", func(t *testing.T) {
110 | user := &Data{Json: H{
111 | "username": "test222",
112 | "password": "230724",
113 | }}
114 | v := userRegister(t, user)
115 | assert.IsType(t, "", v["user_id"])
116 | assert.IsType(t, "", v["token"])
117 | token2 = v["token"].(string)
118 | uid2 = v["user_id"].(string)
119 | relationAction(t, &Data{Json: H{
120 | "token": token2,
121 | "to_user_id": uid1,
122 | "action_type": 1,
123 | }})
124 | relationAction(t, &Data{Json: H{
125 | "token": token1,
126 | "to_user_id": uid2,
127 | "action_type": 1,
128 | }})
129 | relationFollowerList(t, &Data{Json: H{"user_id": uid1, "token": token1}})
130 | relationFriendList(t, &Data{Json: H{"user_id": uid1, "token": token1}})
131 | })
132 | t.Run("轮询问候", func(t *testing.T) {
133 | for i := 0; i < 10; i++ {
134 | if i%2 == 0 {
135 | // 1 To 2
136 | messageAction(t, &Data{Json: H{
137 | "token": token1,
138 | "to_user_id": uid2,
139 | "action_type": 1,
140 | "content": fmt.Sprint("message Test 1To2", i),
141 | }})
142 | } else {
143 | // 2 To 1
144 | messageAction(t, &Data{Json: H{
145 | "token": token2,
146 | "to_user_id": uid1,
147 | "action_type": 1,
148 | "content": fmt.Sprint("message Test 2To1", i),
149 | }})
150 | }
151 | }
152 | })
153 | t.Run("聊天记录", func(t *testing.T) {
154 | messageChat(t, &Data{Query: S{
155 | "token": token2,
156 | "to_user_id": uid1,
157 | "pre_msg_time": "0",
158 | }})
159 | messageChat(t, &Data{Query: S{
160 | "token": token1,
161 | "to_user_id": uid2,
162 | "pre_msg_time": "0",
163 | }})
164 | })
165 | }
166 |
--------------------------------------------------------------------------------
/web/docs/model.md:
--------------------------------------------------------------------------------
1 | # 公共
2 |
3 | ## model
4 |
5 | > 不使用自增 id,改为使用雪花自增 19 位 id (雪花个毛线,就是纳秒时间戳简单乘加法运算<-🤡)
6 |
7 | ```go
8 | type Model struct {
9 | ID int64 `json:"id" gorm:"primarykey;comment:主键"`
10 | CreatedAt time.Time `json:"-" gorm:"comment:创建时间"`
11 | UpdatedAt time.Time `json:"-" gorm:"comment:修改时间"`
12 | DeletedAt gorm.DeletedAt `json:"-" gorm:"comment:删除时间"`
13 | }
14 |
15 | func (u *Model) BeforeCreate(tx *gorm.DB) (err error) {
16 | u.ID = utils.GetId()
17 | return
18 | }
19 | ```
20 |
21 | # User 类
22 |
23 | ## User
24 |
25 | > 关注总数,粉丝总数 走 Redis,获赞,点赞走 db
26 | >
27 | > 粉丝,好友列表使用原生 SQL 走 UserFollow 连接表
28 |
29 | ```go
30 | User struct {
31 | Model
32 | Name string `json:"name" gorm:"index:,unique;size:32;comment:用户名称"`
33 | Pawd string `json:"-" gorm:"size:128;comment:用户密码"`
34 | Avatar string `json:"avatar" gorm:"comment:用户头像"`
35 | BackgroundImage string `json:"background_image" gorm:"comment:用户个人页顶部大图"`
36 | Signature string `json:"signature" gorm:"default:此人巨懒;comment:个人简介"`
37 | WorkCount int64 `json:"work_count" gorm:"default:0;comment:作品数量"`
38 | Follow []*User `json:"follow,omitempty" gorm:"many2many:UserFollow;comment:关注列表"`
39 | Favorite []*Video `json:"like_list,omitempty" gorm:"many2many:UserFavorite;comment:喜欢列表"`
40 | Videos []*Video `json:"video_list,omitempty" gorm:"many2many:UserCreation;comment:作品列表"`
41 | Comment []*Comment `json:"comment_list,omitempty" gorm:"comment:评论列表"`
42 | FollowCount int64 `json:"follow_count" gorm:"-"` // 关注总数
43 | FollowerCount int64 `json:"follower_count" gorm:"-"` // 粉丝总数
44 | TotalFavorited int64 `json:"total_favorited" gorm:"-"` // 获赞数量
45 | FavoriteCount int64 `json:"favorite_count" gorm:"-"` // 点赞数量
46 | IsFollow bool `json:"is_follow" gorm:"-"` // 是否关注
47 | Follower []*User `json:"follower,omitempty" gorm:"-"` // 粉丝列表
48 | Friend []*User `json:"friend,omitempty" gorm:"-"` // 好友列表
49 | }
50 | ```
51 |
52 | ## FriendUser
53 |
54 | > 这好像也不是模型,怎么在这?
55 |
56 | ```go
57 | FriendUser struct {
58 | User
59 | Message string `json:"message"` // 和该好友的最新聊天消息
60 | MsgType bool `json:"msg_type,number"` // 0 => 当前请求用户接收的消息, 1 => 当前请求用户发送的消息
61 | }
62 | ```
63 |
64 | ## UserCreation
65 |
66 | > 联合投稿~ 好像没时间写了....
67 |
68 | ```go
69 | UserCreation struct {
70 | VideoID int64 `json:"video_id,omitempty" gorm:"primaryKey"`
71 | UserID int64 `json:"author_id" gorm:"primaryKey"`
72 | Type string `json:"type" gorm:"comment:创作者类型"` //Up主,参演,剪辑,录像,道具,编剧,打酱油
73 | CreatedAt time.Time
74 | DeletedAt gorm.DeletedAt `json:"-"`
75 | }
76 | ```
77 |
78 | # Video 类
79 |
80 | ## Video
81 |
82 | > 点赞,点赞数量,评论数量,播放量都走 Redis
83 | > 播放量是根据 ip 然后 HyperLogLog,并简单实现不重复推送
84 | > 封面走阿里云 OSS 视频裁剪便宜好用,不像隔壁某山
85 | > 联合投稿?不知道设计的时候咋写出来的捏
86 |
87 | ```go
88 | Video struct {
89 | Model
90 | AuthorID int64 `json:"-" gorm:"index;notNull;comment:视频作者信"`
91 | Author User `json:"author"`
92 | PlayUrl string `json:"play_url" gorm:"comment:视频播放地址"`
93 | CoverUrl string `json:"cover_url" gorm:"comment:视频封面地址"`
94 | Title string `json:"title" gorm:"comment:视频标题"`
95 | Desc string `json:"desc" gorm:"comment:简介"`
96 | Comment []*Comment `json:"comment,omitempty" gorm:"comment:评论列表"`
97 | FavoriteUser []*User `json:"-" gorm:"many2many:UserFavorite;comment:欢用户列表"`
98 | IsFavorite bool `json:"is_favorite" gorm:"-"` // 是否点赞
99 | PlayCount int64 `json:"play_count" gorm:"-"` // 视频播放量
100 | FavoriteCount int64 `json:"favorite_count" gorm:"-"` // 视频的点赞总数
101 | CommentCount int64 `json:"comment_count" gorm:"-"` // 视频的评论总数
102 | // 自建字段
103 | CoAuthor []*User `json:"authors,omitempty" gorm:"many2many:UserCreation;"` // 联合投稿
104 | }
105 | ```
106 |
107 | # Message 类
108 |
109 | ## Message
110 |
111 | > **大坑!!!**
112 | >
113 | > api 文档 create_time 解释是`消息发送时间 yyyy-MM-dd HH:MM:ss`,项目文档也是 string 类型,最后才发现是毫秒级时间戳 🙃
114 |
115 | ```go
116 | Message struct {
117 | ID int64 `json:"id" gorm:"primarykey;comment:主键"`
118 | CreatedAt int64 `json:"create_time" gorm:"autoUpdateTime:milli"`
119 | ToUserID int64 `json:"to_user_id" gorm:"primaryKey;comment:该消息接收者的id"`
120 | FromUserID int64 `json:"from_user_id" gorm:"primaryKey;comment:该消息发送者的id"`
121 | ToUser User `json:"-" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
122 | FromUser User `json:"-" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
123 | Content string `json:"content" gorm:"comment:消息内容"`
124 | }
125 | ```
126 |
127 | # Comment 类
128 |
129 | ## Comment
130 |
131 | > CreateDate 的生成使用`BeforeCreate`钩子实现
132 |
133 | ```go
134 | Comment struct {
135 | Model
136 | UserID int64 `json:"-" gorm:"index:idx_uvid;comment:评论用户信息"`
137 | VideoID int64 `json:"-" gorm:"index:idx_uvid;comment:评论视频信息"`
138 | User User `json:"user" gorm:"constraint:OnUpdate:CASCADEOnDelete:CASCADE;"`
139 | Video *Video `json:"video,omitempty"gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
140 | Content string `json:"content" gorm:"comment:评论内容"`
141 | CreateDate string `json:"create_date" gorm:"comment:评论发布日期"` // 格式mm-dd
142 | // 自建字段
143 | ReplyID int64 `json:"reply_id" gorm:"index;comment:回复ID"`
144 | }
145 |
146 | c.CreateDate = time.Now().Format("01-02")
147 | ```
148 |
--------------------------------------------------------------------------------
/web/static/logo-font.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/internal/model/video.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "github.com/Godvictory/douyin/utils"
6 | "gorm.io/gorm"
7 | "time"
8 | )
9 |
10 | type (
11 | // Video 视频表
12 | Video struct {
13 | Model
14 | AuthorID int64 `json:"-" gorm:"index;notNull;comment:视频作者信息"`
15 | Author User `json:"author"`
16 | PlayUrl string `json:"play_url" gorm:"comment:视频播放地址"`
17 | CoverUrl string `json:"cover_url" gorm:"comment:视频封面地址"`
18 | Title string `json:"title" gorm:"comment:视频标题"`
19 | Desc string `json:"desc" gorm:"comment:简介"`
20 | Comment []*Comment `json:"comment,omitempty" gorm:"comment:评论列表"`
21 | FavoriteUser []*User `json:"-" gorm:"many2many:UserFavorite;comment:喜欢用户列表"`
22 | IsFavorite bool `json:"is_favorite" gorm:"-"` // 是否点赞
23 | PlayCount int64 `json:"play_count" gorm:"-"` // 视频播放量
24 | FavoriteCount int64 `json:"favorite_count" gorm:"-"` // 视频的点赞总数
25 | CommentCount int64 `json:"comment_count" gorm:"-"` // 视频的评论总数
26 | // 自建字段
27 | TypeOf string `json:"typeOf" gorm:"comment:视频类型"`
28 | CoAuthor []*User `json:"authors,omitempty" gorm:"many2many:UserCreation;"` // 联合投稿
29 | }
30 | )
31 |
32 | var (
33 | videoCountKey = make([]byte, 0, 50) // video_count:
34 | videoPlayCountKey = make([]byte, 0, 50) // video_play_count:
35 | )
36 |
37 | func (v *Video) AfterFind(tx *gorm.DB) (err error) {
38 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
39 | defer cancel()
40 | key := getKey(v.ID, videoCountKey)
41 | playKey := getKey(v.ID, videoPlayCountKey)
42 | if uidA, ok := tx.Get("user_id"); ok {
43 | if uid, ok := uidA.(int64); ok {
44 | v.IsFavorite = getVideoIsFavorite(tx, uid, v.ID)
45 | }
46 | }
47 | // v.PlayCount, _ = rdb.HIncrBy(ctx, key, "play_count", 1).Result()
48 | //使用 Redis 的 HyperLogLog 数据结构,获取指定 HyperLogLog 集合 playKey 的基数(cardinality)并将结果赋值给变量 v.PlayCount。
49 | v.PlayCount, _ = rdb.PFCount(ctx, playKey).Result()
50 | //使用 Redis 的哈希(Hash)数据结构,获取指定键 key 中字段 "favorite_count" 的值,并将其转换为 int64 类型。
51 | v.FavoriteCount, _ = rdb.HGet(ctx, key, "favorite_count").Int64()
52 | v.CommentCount, _ = rdb.HGet(ctx, key, "comment_count").Int64()
53 | tx.Find(&v.Author, v.AuthorID)
54 | return
55 | }
56 |
57 | func ViewedFilter(id int64, ip string) bool {
58 | playKey := getKey(id, videoPlayCountKey)
59 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
60 | defer cancel()
61 | //使用 Redis 的 HyperLogLog 数据结构将 ip 添加到名为 playKey 的集合中。返回集合基数
62 | val, _ := rdb.PFAdd(ctx, playKey, ip).Result()
63 | // 1:未看 0:已看
64 | return val == 1
65 | }
66 |
67 | func (v *Video) BeforeCreate(tx *gorm.DB) (err error) {
68 | if v.ID == 0 {
69 | v.ID = utils.GetId(3, 20230724)
70 | }
71 | return
72 | }
73 |
74 | func (v *Video) HIncrByFavoriteCount(incr int64) int64 {
75 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
76 | defer cancel()
77 | key := getKey(v.ID, videoCountKey)
78 | //对指定的哈希表中的favorite_count字段增加incr的值。获取新的操作数,即增量后的操作数
79 | v.FavoriteCount, _ = rdb.HIncrBy(ctx, key, "favorite_count", incr).Result()
80 | return v.FavoriteCount
81 | }
82 |
83 | func (v *Video) HIncrByCommentCount(incr int64) int64 {
84 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
85 | defer cancel()
86 | key := getKey(v.ID, videoCountKey)
87 | //对指定的哈希表中的comment_count字段增加incr的值。获取新的操作数,即增量后的操作数
88 | v.FavoriteCount, _ = rdb.HIncrBy(ctx, key, "comment_count", incr).Result()
89 | return v.FavoriteCount
90 | }
91 |
92 | //// getVideoFavoriteCount 视频的点赞总数
93 | //func getVideoFavoriteCount(tx *gorm.DB, vid int64) int64 {
94 | // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
95 | // defer cancel()
96 | // key := getKey(vid, videoFavoriteCountKey)
97 | // favoriteCount, err := rdb.Get(ctx, key).Int64()
98 | // if err == redis.Nil {
99 | // tx.Table("user_favorite").Where("video_id = ?", vid).Count(&favoriteCount)
100 | // _ = rdb.Set(ctx, key, favoriteCount, 3*time.Second)
101 | // }
102 | // return favoriteCount
103 | //}
104 |
105 | //// getVideoCommentCount 视频的评论总数
106 | //func getVideoCommentCount(tx *gorm.DB, vid int64) int64 {
107 | // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
108 | // defer cancel()
109 | // key := getKey(vid, videoCommentCountKey)
110 | // CommentCount, err := rdb.Get(ctx, key).Int64()
111 | // if err == redis.Nil {
112 | // tx.Model(&Comment{}).Where("video_id = ?", vid).Count(&CommentCount)
113 | // _ = rdb.Set(ctx, key, CommentCount, 3*time.Second)
114 | // }
115 | // return CommentCount
116 | //}
117 |
118 | //// getVideoPlayCount 视频的播放量
119 | //func getVideoPlayCount(ip string, vid int64) int64 {
120 | // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
121 | // defer cancel()
122 | // key := getKey(vid, videoPlayCountKey)
123 | // rdb.PFAdd(ctx, key, ip)
124 | // val, _ := rdb.PFCount(ctx, key).Result()
125 | // return val
126 | //}
127 |
128 | // getVideoIsFavorite 视频是否点赞
129 | func getVideoIsFavorite(tx *gorm.DB, uid, vid int64) bool {
130 | result := map[string]any{}
131 | return tx.Table("user_favorite").Where("user_id = ? AND video_id = ?", uid, vid).Scan(&result).RowsAffected == 1
132 | // data[i].IsFavorite = db.Raw("SELECT * FROM user_favorite WHERE user_id = ? AND video_id = ?", uid, data[i].ID).Scan(&result).RowsAffected == 1
133 | }
134 |
135 | func init() {
136 | addMigrate(&Video{})
137 | videoCountKey = append(videoCountKey, "video_count:"...)
138 | videoPlayCountKey = append(videoPlayCountKey, "video_play_count:"...)
139 | }
140 |
--------------------------------------------------------------------------------
/internal/model/user.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "context"
5 | "github.com/Godvictory/douyin/utils"
6 | "time"
7 |
8 | "gorm.io/gorm"
9 | )
10 |
11 | type (
12 | // User 用户信息表
13 | User struct {
14 | Model
15 | Name string `json:"name" gorm:"index:,unique;size:32;comment:用户名称"`
16 | Pawd string `json:"-" gorm:"size:128;comment:用户密码"`
17 | Avatar string `json:"avatar" gorm:"comment:用户头像"`
18 | BackgroundImage string `json:"background_image" gorm:"comment:用户个人页顶部大图"`
19 | Signature string `json:"signature" gorm:"default:此人巨懒;comment:个人简介"`
20 | WorkCount int64 `json:"work_count" gorm:"default:0;comment:作品数量"`
21 | Follow []*User `json:"follow,omitempty" gorm:"many2many:UserFollow;comment:关注列表"`
22 | Favorite []*Video `json:"like_list,omitempty" gorm:"many2many:UserFavorite;comment:喜欢列表"`
23 | Videos []*Video `json:"video_list,omitempty" gorm:"many2many:UserCreation;comment:作品列表"`
24 | Comment []*Comment `json:"comment_list,omitempty" gorm:"comment:评论列表"`
25 | FollowCount int64 `json:"follow_count" gorm:"-"` // 关注总数
26 | FollowerCount int64 `json:"follower_count" gorm:"-"` // 粉丝总数
27 | TotalFavorited int64 `json:"total_favorited" gorm:"-"` // 获赞数量
28 | FavoriteCount int64 `json:"favorite_count" gorm:"-"` // 点赞数量
29 | IsFollow bool `json:"is_follow" gorm:"-"` // 是否关注
30 | Follower []*User `json:"follower,omitempty" gorm:"-"` // 粉丝列表
31 | Friend []*User `json:"friend,omitempty" gorm:"-"` // 好友列表
32 | }
33 | // FriendUser 好友结构体
34 | FriendUser struct {
35 | User
36 | Message string `json:"message"` // 和该好友的最新聊天消息
37 | MsgType int `json:"msg_type"` // 0 => 当前请求用户接收的消息, 1 => 当前请求用户发送的消息
38 | }
39 | // UserCreation 联合作者
40 | UserCreation struct {
41 | VideoID int64 `json:"video_id,omitempty" gorm:"primaryKey"`
42 | UserID int64 `json:"author_id" gorm:"primaryKey"`
43 | Type string `json:"type" gorm:"comment:创作者类型"` // Up主, 参演,剪辑,录像,道具,编剧,打酱油
44 | CreatedAt time.Time `json:"created_at"`
45 | DeletedAt gorm.DeletedAt `json:"-"`
46 | }
47 | )
48 |
49 | var userCountKey = make([]byte, 0, 50)
50 |
51 | func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
52 | if u.ID == 0 {
53 | u.ID = utils.GetId(2, 114514)
54 | }
55 | if u.Avatar == "" {
56 | url := make([]byte, 0, 88)
57 | url = append(url, "https://api.multiavatar.com/"...)
58 | url = append(url, u.Name...)
59 | url = append(url, ".png"...)
60 | u.Avatar = string(url)
61 | }
62 | if u.BackgroundImage == "" {
63 | u.BackgroundImage = "https://api.paugram.com/wallpaper/"
64 | }
65 | return
66 | }
67 |
68 | func (u *User) AfterFind(tx *gorm.DB) (err error) {
69 | if uid, ok := tx.Get("user_id"); ok || u.ID != 0 {
70 | result := map[string]any{}
71 | u.IsFollow = tx.Table("user_follow").Where("follow_id = ? AND user_id = ?", u.ID, uid).Take(&result).RowsAffected == 1
72 | }
73 | // tx.Table("user_follow").Where("user_id = ?", u.ID).Count(&u.FollowCount)
74 | // tx.Table("user_follow").Where("follow_id = ?", u.ID).Count(&u.FollowerCount)
75 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
76 | defer cancel()
77 | key := getKey(u.ID, userCountKey)
78 | u.FollowCount, _ = rdb.HGet(ctx, key, "follow_count").Int64()
79 | u.FollowerCount, _ = rdb.HGet(ctx, key, "follower_count").Int64()
80 | u.TotalFavorited = getUserTotalFavorited(tx, u.ID)
81 | u.FavoriteCount = getUserFavoriteCount(tx, u.ID)
82 | return
83 | }
84 |
85 | func (u *User) HIncrByFollowCount(incr int64) int64 {
86 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
87 | defer cancel()
88 | key := getKey(u.ID, videoCountKey)
89 | u.FollowCount, _ = rdb.HIncrBy(ctx, key, "follow_count", incr).Result()
90 | return u.FollowCount
91 | }
92 |
93 | func (u *User) HIncrByFollowerCount(incr int64) int64 {
94 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
95 | defer cancel()
96 | key := getKey(u.ID, videoCountKey)
97 | u.FollowerCount, _ = rdb.HIncrBy(ctx, key, "follower_count", incr).Result()
98 | return u.FollowerCount
99 | }
100 |
101 | //// getUserFollowCount 获取关注数
102 | //func getUserFollowCount(tx *gorm.DB, uid int64) int64 {
103 | // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
104 | // defer cancel()
105 | // key := getKey(uid, userFollowCountKey)
106 | // FollowCount, err := rdb.Get(ctx, key).Int64()
107 | // if err == redis.Nil {
108 | // tx.Table("user_follow").Where("user_id = ?", uid).Count(&FollowCount)
109 | // _ = rdb.Set(ctx, key, FollowCount, 3*time.Second)
110 | // }
111 | // return FollowCount
112 | //}
113 | //
114 | //// getUserFollowerCount 获取粉丝数
115 | //func getUserFollowerCount(tx *gorm.DB, uid int64) int64 {
116 | // ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
117 | // defer cancel()
118 | // key := getKey(uid, userFollowerCountKey)
119 | // FollowerCount, err := rdb.Get(ctx, key).Int64()
120 | // if err == redis.Nil {
121 | // tx.Table("user_follow").Where("follow_id = ?", uid).Count(&FollowerCount)
122 | // _ = rdb.Set(ctx, key, FollowerCount, 3*time.Second)
123 | // }
124 | // return FollowerCount
125 | //}
126 |
127 | // getUserTotalFavorited 获取获赞数量
128 | func getUserTotalFavorited(tx *gorm.DB, uid int64) (totalFavorited int64) {
129 | tx.Table("`user_creation`").
130 | Joins("JOIN `user_favorite` ON `user_creation`.`video_id` = `user_favorite`.`video_id`").
131 | Where("`user_creation`.`user_id` = ?", uid).Count(&totalFavorited)
132 | return
133 | }
134 |
135 | // getUserFavoriteCount 获取点赞数量
136 | func getUserFavoriteCount(tx *gorm.DB, uid int64) (favoriteCount int64) {
137 | tx.Table("user_favorite").Where("user_id = ?", uid).Count(&favoriteCount)
138 | return
139 | }
140 |
141 | func init() {
142 | addMigrate(&User{}, &UserCreation{})
143 | userCountKey = append(userCountKey, "user_count:"...)
144 | }
145 |
--------------------------------------------------------------------------------
/web/assets/videoList-ec29bb1e.css:
--------------------------------------------------------------------------------
1 | .el-empty{--el-empty-padding:40px 0;--el-empty-image-width:160px;--el-empty-description-margin-top:20px;--el-empty-bottom-margin-top:20px;--el-empty-fill-color-0:var(--el-color-white);--el-empty-fill-color-1:#fcfcfd;--el-empty-fill-color-2:#f8f9fb;--el-empty-fill-color-3:#f7f8fc;--el-empty-fill-color-4:#eeeff3;--el-empty-fill-color-5:#edeef2;--el-empty-fill-color-6:#e9ebef;--el-empty-fill-color-7:#e5e7e9;--el-empty-fill-color-8:#e0e3e9;--el-empty-fill-color-9:#d5d7de;display:flex;justify-content:center;align-items:center;flex-direction:column;text-align:center;box-sizing:border-box;padding:var(--el-empty-padding)}.el-empty__image{width:var(--el-empty-image-width)}.el-empty__image img{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:100%;height:100%;vertical-align:top;-o-object-fit:contain;object-fit:contain}.el-empty__image svg{color:var(--el-svg-monochrome-grey);fill:currentColor;width:100%;height:100%;vertical-align:top}.el-empty__description{margin-top:var(--el-empty-description-margin-top)}.el-empty__description p{margin:0;font-size:var(--el-font-size-base);color:var(--el-text-color-secondary)}.el-empty__bottom{margin-top:var(--el-empty-bottom-margin-top)}.video[data-v-325bfb55] .xgplayer-dynamic-bg{z-index:0}.video[data-v-325bfb55] .controls-autohide{opacity:1;visibility:visible}.contentBox[data-v-325bfb55]{position:absolute;padding:10px;z-index:10;bottom:68px;font-size:16px}.contentBox .info[data-v-325bfb55]{margin-bottom:5px}.contentBox .info .name[data-v-325bfb55]{font-size:23px;font-weight:600}.contentBox .info .name[data-v-325bfb55]:hover{text-decoration:underline}@font-face{font-family:swiper-icons;src:url(data:application/font-woff;charset=utf-8;base64,\ d09GRgABAAAAAAZgABAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAGRAAAABoAAAAci6qHkUdERUYAAAWgAAAAIwAAACQAYABXR1BPUwAABhQAAAAuAAAANuAY7+xHU1VCAAAFxAAAAFAAAABm2fPczU9TLzIAAAHcAAAASgAAAGBP9V5RY21hcAAAAkQAAACIAAABYt6F0cBjdnQgAAACzAAAAAQAAAAEABEBRGdhc3AAAAWYAAAACAAAAAj//wADZ2x5ZgAAAywAAADMAAAD2MHtryVoZWFkAAABbAAAADAAAAA2E2+eoWhoZWEAAAGcAAAAHwAAACQC9gDzaG10eAAAAigAAAAZAAAArgJkABFsb2NhAAAC0AAAAFoAAABaFQAUGG1heHAAAAG8AAAAHwAAACAAcABAbmFtZQAAA/gAAAE5AAACXvFdBwlwb3N0AAAFNAAAAGIAAACE5s74hXjaY2BkYGAAYpf5Hu/j+W2+MnAzMYDAzaX6QjD6/4//Bxj5GA8AuRwMYGkAPywL13jaY2BkYGA88P8Agx4j+/8fQDYfA1AEBWgDAIB2BOoAeNpjYGRgYNBh4GdgYgABEMnIABJzYNADCQAACWgAsQB42mNgYfzCOIGBlYGB0YcxjYGBwR1Kf2WQZGhhYGBiYGVmgAFGBiQQkOaawtDAoMBQxXjg/wEGPcYDDA4wNUA2CCgwsAAAO4EL6gAAeNpj2M0gyAACqxgGNWBkZ2D4/wMA+xkDdgAAAHjaY2BgYGaAYBkGRgYQiAHyGMF8FgYHIM3DwMHABGQrMOgyWDLEM1T9/w8UBfEMgLzE////P/5//f/V/xv+r4eaAAeMbAxwIUYmIMHEgKYAYjUcsDAwsLKxc3BycfPw8jEQA/gZBASFhEVExcQlJKWkZWTl5BUUlZRVVNXUNTQZBgMAAMR+E+gAEQFEAAAAKgAqACoANAA+AEgAUgBcAGYAcAB6AIQAjgCYAKIArAC2AMAAygDUAN4A6ADyAPwBBgEQARoBJAEuATgBQgFMAVYBYAFqAXQBfgGIAZIBnAGmAbIBzgHsAAB42u2NMQ6CUAyGW568x9AneYYgm4MJbhKFaExIOAVX8ApewSt4Bic4AfeAid3VOBixDxfPYEza5O+Xfi04YADggiUIULCuEJK8VhO4bSvpdnktHI5QCYtdi2sl8ZnXaHlqUrNKzdKcT8cjlq+rwZSvIVczNiezsfnP/uznmfPFBNODM2K7MTQ45YEAZqGP81AmGGcF3iPqOop0r1SPTaTbVkfUe4HXj97wYE+yNwWYxwWu4v1ugWHgo3S1XdZEVqWM7ET0cfnLGxWfkgR42o2PvWrDMBSFj/IHLaF0zKjRgdiVMwScNRAoWUoH78Y2icB/yIY09An6AH2Bdu/UB+yxopYshQiEvnvu0dURgDt8QeC8PDw7Fpji3fEA4z/PEJ6YOB5hKh4dj3EvXhxPqH/SKUY3rJ7srZ4FZnh1PMAtPhwP6fl2PMJMPDgeQ4rY8YT6Gzao0eAEA409DuggmTnFnOcSCiEiLMgxCiTI6Cq5DZUd3Qmp10vO0LaLTd2cjN4fOumlc7lUYbSQcZFkutRG7g6JKZKy0RmdLY680CDnEJ+UMkpFFe1RN7nxdVpXrC4aTtnaurOnYercZg2YVmLN/d/gczfEimrE/fs/bOuq29Zmn8tloORaXgZgGa78yO9/cnXm2BpaGvq25Dv9S4E9+5SIc9PqupJKhYFSSl47+Qcr1mYNAAAAeNptw0cKwkAAAMDZJA8Q7OUJvkLsPfZ6zFVERPy8qHh2YER+3i/BP83vIBLLySsoKimrqKqpa2hp6+jq6RsYGhmbmJqZSy0sraxtbO3sHRydnEMU4uR6yx7JJXveP7WrDycAAAAAAAH//wACeNpjYGRgYOABYhkgZgJCZgZNBkYGLQZtIJsFLMYAAAw3ALgAeNolizEKgDAQBCchRbC2sFER0YD6qVQiBCv/H9ezGI6Z5XBAw8CBK/m5iQQVauVbXLnOrMZv2oLdKFa8Pjuru2hJzGabmOSLzNMzvutpB3N42mNgZGBg4GKQYzBhYMxJLMlj4GBgAYow/P/PAJJhLM6sSoWKfWCAAwDAjgbRAAB42mNgYGBkAIIbCZo5IPrmUn0hGA0AO8EFTQAA);font-weight:400;font-style:normal}:root{--swiper-theme-color: #007aff}:host{position:relative;display:block;margin-left:auto;margin-right:auto;z-index:1}.swiper{margin-left:auto;margin-right:auto;position:relative;overflow:hidden;overflow:clip;list-style:none;padding:0;z-index:1;display:block}.swiper-vertical>.swiper-wrapper{flex-direction:column}.swiper-wrapper{position:relative;width:100%;height:100%;z-index:1;display:flex;transition-property:transform;transition-timing-function:var(--swiper-wrapper-transition-timing-function, initial);box-sizing:content-box}.swiper-android .swiper-slide,.swiper-ios .swiper-slide,.swiper-wrapper{transform:translateZ(0)}.swiper-horizontal{touch-action:pan-y}.swiper-vertical{touch-action:pan-x}.swiper-slide{flex-shrink:0;width:100%;height:100%;position:relative;transition-property:transform;display:block}.swiper-slide-invisible-blank{visibility:hidden}.swiper-autoheight,.swiper-autoheight .swiper-slide{height:auto}.swiper-autoheight .swiper-wrapper{align-items:flex-start;transition-property:transform,height}.swiper-backface-hidden .swiper-slide{transform:translateZ(0);-webkit-backface-visibility:hidden;backface-visibility:hidden}.swiper-3d.swiper-css-mode .swiper-wrapper{perspective:1200px}.swiper-3d .swiper-wrapper{transform-style:preserve-3d}.swiper-3d{perspective:1200px}.swiper-3d .swiper-slide,.swiper-3d .swiper-cube-shadow{transform-style:preserve-3d}.swiper-css-mode>.swiper-wrapper{overflow:auto;scrollbar-width:none;-ms-overflow-style:none}.swiper-css-mode>.swiper-wrapper::-webkit-scrollbar{display:none}.swiper-css-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:start start}.swiper-css-mode.swiper-horizontal>.swiper-wrapper{scroll-snap-type:x mandatory}.swiper-css-mode.swiper-vertical>.swiper-wrapper{scroll-snap-type:y mandatory}.swiper-css-mode.swiper-free-mode>.swiper-wrapper{scroll-snap-type:none}.swiper-css-mode.swiper-free-mode>.swiper-wrapper>.swiper-slide{scroll-snap-align:none}.swiper-css-mode.swiper-centered>.swiper-wrapper:before{content:"";flex-shrink:0;order:9999}.swiper-css-mode.swiper-centered>.swiper-wrapper>.swiper-slide{scroll-snap-align:center center;scroll-snap-stop:always}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper>.swiper-slide:first-child{margin-inline-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-horizontal>.swiper-wrapper:before{height:100%;min-height:1px;width:var(--swiper-centered-offset-after)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper>.swiper-slide:first-child{margin-block-start:var(--swiper-centered-offset-before)}.swiper-css-mode.swiper-centered.swiper-vertical>.swiper-wrapper:before{width:100%;min-width:1px;height:var(--swiper-centered-offset-after)}.swiper-3d .swiper-slide-shadow,.swiper-3d .swiper-slide-shadow-left,.swiper-3d .swiper-slide-shadow-right,.swiper-3d .swiper-slide-shadow-top,.swiper-3d .swiper-slide-shadow-bottom{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:10}.swiper-3d .swiper-slide-shadow{background:rgba(0,0,0,.15)}.swiper-3d .swiper-slide-shadow-left{background-image:linear-gradient(to left,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-right{background-image:linear-gradient(to right,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-top{background-image:linear-gradient(to top,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-3d .swiper-slide-shadow-bottom{background-image:linear-gradient(to bottom,rgba(0,0,0,.5),rgba(0,0,0,0))}.swiper-lazy-preloader{width:42px;height:42px;position:absolute;left:50%;top:50%;margin-left:-21px;margin-top:-21px;z-index:10;transform-origin:50%;box-sizing:border-box;border:4px solid var(--swiper-preloader-color, var(--swiper-theme-color));border-radius:50%;border-top-color:transparent}.swiper:not(.swiper-watch-progress) .swiper-lazy-preloader,.swiper-watch-progress .swiper-slide-visible .swiper-lazy-preloader{animation:swiper-preloader-spin 1s infinite linear}.swiper-lazy-preloader-white{--swiper-preloader-color: #fff}.swiper-lazy-preloader-black{--swiper-preloader-color: #000}@keyframes swiper-preloader-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.swiper{width:100%;height:94vh}
2 |
--------------------------------------------------------------------------------
/web/assets/catalog-1808318e.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";.fade-in-linear-enter-active,.fade-in-linear-leave-active{transition:var(--el-transition-fade-linear)}.fade-in-linear-enter-from,.fade-in-linear-leave-to{opacity:0}.el-fade-in-linear-enter-active,.el-fade-in-linear-leave-active{transition:var(--el-transition-fade-linear)}.el-fade-in-linear-enter-from,.el-fade-in-linear-leave-to{opacity:0}.el-fade-in-enter-active,.el-fade-in-leave-active{transition:all var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-fade-in-enter-from,.el-fade-in-leave-active{opacity:0}.el-zoom-in-center-enter-active,.el-zoom-in-center-leave-active{transition:all var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-zoom-in-center-enter-from,.el-zoom-in-center-leave-active{opacity:0;transform:scaleX(0)}.el-zoom-in-top-enter-active,.el-zoom-in-top-leave-active{opacity:1;transform:scaleY(1);transition:var(--el-transition-md-fade);transform-origin:center top}.el-zoom-in-top-enter-active[data-popper-placement^=top],.el-zoom-in-top-leave-active[data-popper-placement^=top]{transform-origin:center bottom}.el-zoom-in-top-enter-from,.el-zoom-in-top-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-bottom-enter-active,.el-zoom-in-bottom-leave-active{opacity:1;transform:scaleY(1);transition:var(--el-transition-md-fade);transform-origin:center bottom}.el-zoom-in-bottom-enter-from,.el-zoom-in-bottom-leave-active{opacity:0;transform:scaleY(0)}.el-zoom-in-left-enter-active,.el-zoom-in-left-leave-active{opacity:1;transform:scale(1);transition:var(--el-transition-md-fade);transform-origin:top left}.el-zoom-in-left-enter-from,.el-zoom-in-left-leave-active{opacity:0;transform:scale(.45)}.collapse-transition{transition:var(--el-transition-duration) height ease-in-out,var(--el-transition-duration) padding-top ease-in-out,var(--el-transition-duration) padding-bottom ease-in-out}.el-collapse-transition-leave-active,.el-collapse-transition-enter-active{transition:var(--el-transition-duration) max-height ease-in-out,var(--el-transition-duration) padding-top ease-in-out,var(--el-transition-duration) padding-bottom ease-in-out}.horizontal-collapse-transition{transition:var(--el-transition-duration) width ease-in-out,var(--el-transition-duration) padding-left ease-in-out,var(--el-transition-duration) padding-right ease-in-out}.el-list-enter-active,.el-list-leave-active{transition:all 1s}.el-list-enter-from,.el-list-leave-to{opacity:0;transform:translateY(-30px)}.el-list-leave-active{position:absolute!important}.el-opacity-transition{transition:opacity var(--el-transition-duration) cubic-bezier(.55,0,.1,1)}.el-tree{--el-tree-node-content-height: 26px;--el-tree-node-hover-bg-color: var(--el-fill-color-light);--el-tree-text-color: var(--el-text-color-regular);--el-tree-expand-icon-color: var(--el-text-color-placeholder)}.el-tree{position:relative;cursor:default;background:var(--el-fill-color-blank);color:var(--el-tree-text-color);font-size:var(--el-font-size-base)}.el-tree__empty-block{position:relative;min-height:60px;text-align:center;width:100%;height:100%}.el-tree__empty-text{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);color:var(--el-text-color-secondary);font-size:var(--el-font-size-base)}.el-tree__drop-indicator{position:absolute;left:0;right:0;height:1px;background-color:var(--el-color-primary)}.el-tree-node{white-space:nowrap;outline:none}.el-tree-node:focus>.el-tree-node__content{background-color:var(--el-tree-node-hover-bg-color)}.el-tree-node.is-drop-inner>.el-tree-node__content .el-tree-node__label{background-color:var(--el-color-primary);color:#fff}.el-tree-node__content{--el-checkbox-height: var(--el-tree-node-content-height);display:flex;align-items:center;height:var(--el-tree-node-content-height);cursor:pointer}.el-tree-node__content>.el-tree-node__expand-icon{padding:6px;box-sizing:content-box}.el-tree-node__content>label.el-checkbox{margin-right:8px}.el-tree-node__content:hover{background-color:var(--el-tree-node-hover-bg-color)}.el-tree.is-dragging .el-tree-node__content{cursor:move}.el-tree.is-dragging .el-tree-node__content *{pointer-events:none}.el-tree.is-dragging.is-drop-not-allow .el-tree-node__content{cursor:not-allowed}.el-tree-node__expand-icon{cursor:pointer;color:var(--el-tree-expand-icon-color);font-size:12px;transform:rotate(0);transition:transform var(--el-transition-duration) ease-in-out}.el-tree-node__expand-icon.expanded{transform:rotate(90deg)}.el-tree-node__expand-icon.is-leaf{color:transparent;cursor:default}.el-tree-node__expand-icon.is-hidden{visibility:hidden}.el-tree-node__loading-icon{margin-right:8px;font-size:var(--el-font-size-base);color:var(--el-tree-expand-icon-color)}.el-tree-node>.el-tree-node__children{overflow:hidden;background-color:transparent}.el-tree-node.is-expanded>.el-tree-node__children{display:block}.el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content{background-color:var(--el-color-primary-light-9)}.el-checkbox{--el-checkbox-font-size: 14px;--el-checkbox-font-weight: var(--el-font-weight-primary);--el-checkbox-text-color: var(--el-text-color-regular);--el-checkbox-input-height: 14px;--el-checkbox-input-width: 14px;--el-checkbox-border-radius: var(--el-border-radius-small);--el-checkbox-bg-color: var(--el-fill-color-blank);--el-checkbox-input-border: var(--el-border);--el-checkbox-disabled-border-color: var(--el-border-color);--el-checkbox-disabled-input-fill: var(--el-fill-color-light);--el-checkbox-disabled-icon-color: var(--el-text-color-placeholder);--el-checkbox-disabled-checked-input-fill: var(--el-border-color-extra-light);--el-checkbox-disabled-checked-input-border-color: var(--el-border-color);--el-checkbox-disabled-checked-icon-color: var(--el-text-color-placeholder);--el-checkbox-checked-text-color: var(--el-color-primary);--el-checkbox-checked-input-border-color: var(--el-color-primary);--el-checkbox-checked-bg-color: var(--el-color-primary);--el-checkbox-checked-icon-color: var(--el-color-white);--el-checkbox-input-border-color-hover: var(--el-color-primary)}.el-checkbox{color:var(--el-checkbox-text-color);font-weight:var(--el-checkbox-font-weight);font-size:var(--el-font-size-base);position:relative;cursor:pointer;display:inline-flex;align-items:center;white-space:nowrap;-webkit-user-select:none;user-select:none;margin-right:30px;height:var(--el-checkbox-height, 32px)}.el-checkbox.is-disabled{cursor:not-allowed}.el-checkbox.is-bordered{padding:0 15px 0 9px;border-radius:var(--el-border-radius-base);border:var(--el-border);box-sizing:border-box}.el-checkbox.is-bordered.is-checked{border-color:var(--el-color-primary)}.el-checkbox.is-bordered.is-disabled{border-color:var(--el-border-color-lighter)}.el-checkbox.is-bordered.el-checkbox--large{padding:0 19px 0 11px;border-radius:var(--el-border-radius-base)}.el-checkbox.is-bordered.el-checkbox--large .el-checkbox__label{font-size:var(--el-font-size-base)}.el-checkbox.is-bordered.el-checkbox--large .el-checkbox__inner{height:14px;width:14px}.el-checkbox.is-bordered.el-checkbox--small{padding:0 11px 0 7px;border-radius:calc(var(--el-border-radius-base) - 1px)}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__label{font-size:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner{height:12px;width:12px}.el-checkbox.is-bordered.el-checkbox--small .el-checkbox__inner:after{height:6px;width:2px}.el-checkbox input:focus-visible+.el-checkbox__inner{outline:2px solid var(--el-checkbox-input-border-color-hover);outline-offset:1px;border-radius:var(--el-checkbox-border-radius)}.el-checkbox__input{white-space:nowrap;cursor:pointer;outline:none;display:inline-flex;position:relative}.el-checkbox__input.is-disabled .el-checkbox__inner{background-color:var(--el-checkbox-disabled-input-fill);border-color:var(--el-checkbox-disabled-border-color);cursor:not-allowed}.el-checkbox__input.is-disabled .el-checkbox__inner:after{cursor:not-allowed;border-color:var(--el-checkbox-disabled-icon-color)}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner{background-color:var(--el-checkbox-disabled-checked-input-fill);border-color:var(--el-checkbox-disabled-checked-input-border-color)}.el-checkbox__input.is-disabled.is-checked .el-checkbox__inner:after{border-color:var(--el-checkbox-disabled-checked-icon-color)}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner{background-color:var(--el-checkbox-disabled-checked-input-fill);border-color:var(--el-checkbox-disabled-checked-input-border-color)}.el-checkbox__input.is-disabled.is-indeterminate .el-checkbox__inner:before{background-color:var(--el-checkbox-disabled-checked-icon-color);border-color:var(--el-checkbox-disabled-checked-icon-color)}.el-checkbox__input.is-disabled+span.el-checkbox__label{color:var(--el-disabled-text-color);cursor:not-allowed}.el-checkbox__input.is-checked .el-checkbox__inner{background-color:var(--el-checkbox-checked-bg-color);border-color:var(--el-checkbox-checked-input-border-color)}.el-checkbox__input.is-checked .el-checkbox__inner:after{transform:rotate(45deg) scaleY(1);border-color:var(--el-checkbox-checked-icon-color)}.el-checkbox__input.is-checked+.el-checkbox__label{color:var(--el-checkbox-checked-text-color)}.el-checkbox__input.is-focus:not(.is-checked) .el-checkbox__original:not(:focus-visible){border-color:var(--el-checkbox-input-border-color-hover)}.el-checkbox__input.is-indeterminate .el-checkbox__inner{background-color:var(--el-checkbox-checked-bg-color);border-color:var(--el-checkbox-checked-input-border-color)}.el-checkbox__input.is-indeterminate .el-checkbox__inner:before{content:"";position:absolute;display:block;background-color:var(--el-checkbox-checked-icon-color);height:2px;transform:scale(.5);left:0;right:0;top:5px}.el-checkbox__input.is-indeterminate .el-checkbox__inner:after{display:none}.el-checkbox__inner{display:inline-block;position:relative;border:var(--el-checkbox-input-border);border-radius:var(--el-checkbox-border-radius);box-sizing:border-box;width:var(--el-checkbox-input-width);height:var(--el-checkbox-input-height);background-color:var(--el-checkbox-bg-color);z-index:var(--el-index-normal);transition:border-color .25s cubic-bezier(.71,-.46,.29,1.46),background-color .25s cubic-bezier(.71,-.46,.29,1.46),outline .25s cubic-bezier(.71,-.46,.29,1.46)}.el-checkbox__inner:hover{border-color:var(--el-checkbox-input-border-color-hover)}.el-checkbox__inner:after{box-sizing:content-box;content:"";border:1px solid transparent;border-left:0;border-top:0;height:7px;left:4px;position:absolute;top:1px;transform:rotate(45deg) scaleY(0);width:3px;transition:transform .15s ease-in .05s;transform-origin:center}.el-checkbox__original{opacity:0;outline:none;position:absolute;margin:0;width:0;height:0;z-index:-1}.el-checkbox__label{display:inline-block;padding-left:8px;line-height:1;font-size:var(--el-checkbox-font-size)}.el-checkbox.el-checkbox--large{height:40px}.el-checkbox.el-checkbox--large .el-checkbox__label{font-size:14px}.el-checkbox.el-checkbox--large .el-checkbox__inner{width:14px;height:14px}.el-checkbox.el-checkbox--small{height:24px}.el-checkbox.el-checkbox--small .el-checkbox__label{font-size:12px}.el-checkbox.el-checkbox--small .el-checkbox__inner{width:12px;height:12px}.el-checkbox.el-checkbox--small .el-checkbox__input.is-indeterminate .el-checkbox__inner:before{top:4px}.el-checkbox.el-checkbox--small .el-checkbox__inner:after{width:2px;height:6px}.el-checkbox:last-of-type{margin-right:0}.el-tree[data-v-8b3b510b]{font-size:17px;--el-tree-node-hover-bg-color: "" !important;color:#fff9}.el-tree[data-v-8b3b510b] .is-current>.el-tree-node__content>.el-tree-node__label{color:#fff}.el-tree[data-v-8b3b510b] .is-current>.el-tree-node__content>.el-tree-node__label:after{content:"🤡";font-size:12px}
2 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3 | github.com/aliyun/aliyun-oss-go-sdk v2.2.8+incompatible h1:6JF1bjhT0WN2srEmijfOFtVWwV91KZ6dJY1/JbdtGrI=
4 | github.com/aliyun/aliyun-oss-go-sdk v2.2.8+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
5 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
6 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
7 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
8 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
9 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
10 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
11 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
12 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
13 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
14 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
15 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
16 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
21 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
22 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
23 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
24 | github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
25 | github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
26 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
27 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
28 | github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
29 | github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
30 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
31 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
32 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
33 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
34 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
35 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
36 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
37 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
38 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
39 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
40 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
41 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
42 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
43 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
44 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
45 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
46 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
47 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
48 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
49 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
50 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
51 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
52 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
53 | github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
54 | github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
55 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
56 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
57 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
58 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
59 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
60 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
61 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
62 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
63 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
64 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
65 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
66 | github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
67 | github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
68 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
69 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
70 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
71 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
72 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
73 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
74 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
75 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
76 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
77 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
78 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
79 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
80 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
81 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
82 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
83 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
84 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
85 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
86 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
87 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
88 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
89 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
90 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
91 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
92 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
93 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
94 | github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
95 | github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
96 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
97 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
98 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
99 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
100 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
101 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
102 | github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
103 | github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
104 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
105 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
106 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
107 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
108 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
109 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
110 | github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
111 | github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
112 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
113 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
114 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
115 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
116 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
117 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
118 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
119 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
120 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
121 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
122 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
123 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
124 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
125 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
126 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
127 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
128 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
129 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
130 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
131 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
132 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
133 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
134 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
135 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
136 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
137 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
138 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
139 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
140 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
141 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
142 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
143 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
144 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
145 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
146 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
147 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
148 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
149 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
150 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
151 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
152 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
153 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
154 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
155 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
156 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
157 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
158 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
159 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
160 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
161 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
162 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
163 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
164 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
165 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
166 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
167 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
168 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
169 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
170 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
171 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
172 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
173 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
174 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
175 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
176 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
177 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
178 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
179 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
180 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
181 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
182 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
183 | gopkg.in/hlandau/easymetric.v1 v1.0.0 h1:ZbfbH7W3giuVDjWUoFhDOjjv20hiPr5HZ2yMV5f9IeE=
184 | gopkg.in/hlandau/easymetric.v1 v1.0.0/go.mod h1:yh75hypuFzAxmvECh3ZKGCvFnIfapYJh2wv7ASaX2RE=
185 | gopkg.in/hlandau/measurable.v1 v1.0.1 h1:wH5UZKCRUnRr1iD+xIZfwhtxhmr+bprRJttqA1Rklf4=
186 | gopkg.in/hlandau/measurable.v1 v1.0.1/go.mod h1:6N+SYJGMTmetsx7wskULP+juuO+++tsHJkAgzvzsbuM=
187 | gopkg.in/hlandau/passlib.v1 v1.0.11 h1:vKeHwGRdWBD9mm4bJ56GAAdBXpFUYvg/BYYkmphjnmA=
188 | gopkg.in/hlandau/passlib.v1 v1.0.11/go.mod h1:wxGAv2CtQHlzWY8NJp+p045yl4WHyX7v2T6XbOcmqjM=
189 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
190 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
191 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
192 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
193 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
194 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
195 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
196 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
197 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
198 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
199 | gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
200 | gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
201 | gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
202 | gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
203 | gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
204 | gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
205 | gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
206 | gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
207 | gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
208 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
209 |
--------------------------------------------------------------------------------