├── 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 | Logo 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 | Logo 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 | Logo 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 | ![推荐](https://qiu-blog.oss-cn-hangzhou.aliyuncs.com/Q/douyin/shot_2023-08-17_01-23-10.png) 115 | ![评论](https://qiu-blog.oss-cn-hangzhou.aliyuncs.com/Q/douyin/shot_2023-08-17_01-23-29.png) 116 | ![主页](https://qiu-blog.oss-cn-hangzhou.aliyuncs.com/Q/douyin/shot_2023-08-17_01-23-47.png) 117 | ![关注](https://qiu-blog.oss-cn-hangzhou.aliyuncs.com/Q/douyin/shot_2023-08-17_01-24-09.png) 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 | --------------------------------------------------------------------------------