├── .editorconfig ├── .github └── workflows │ └── docker-image.yml ├── .gitignore ├── .idea ├── .gitignore ├── modules.xml ├── toktik.iml └── vcs.xml ├── Dockerfile ├── LICENSE ├── README.md ├── README_zh-CN.md ├── add-kitex-service.sh ├── basic-build-image └── Dockerfile ├── basic-runtime-image └── Dockerfile ├── build-all.sh ├── constant ├── biz │ ├── auth.go │ ├── comment.go │ ├── favorite.go │ ├── feed.go │ ├── gwerror.go │ ├── publish.go │ ├── relation.go │ ├── response.go │ ├── statuscode.go │ ├── user.go │ └── wechat.go └── config │ ├── env.go │ └── service.go ├── docker-compose.yaml ├── docs ├── HowToAddServices.md └── images │ └── cert-cut.png ├── go.mod ├── go.sum ├── idl ├── auth.proto ├── comment.proto ├── favorite.proto ├── feed.proto ├── publish.proto ├── relation.proto ├── user.proto └── wechat.proto ├── kitex_gen └── douyin │ ├── auth │ ├── auth.pb.fast.go │ ├── auth.pb.go │ └── authservice │ │ ├── authservice.go │ │ ├── client.go │ │ ├── invoker.go │ │ └── server.go │ ├── comment │ ├── comment.pb.fast.go │ ├── comment.pb.go │ └── commentservice │ │ ├── client.go │ │ ├── commentservice.go │ │ ├── invoker.go │ │ └── server.go │ ├── favorite │ ├── favorite.pb.fast.go │ ├── favorite.pb.go │ └── favoriteservice │ │ ├── client.go │ │ ├── favoriteservice.go │ │ ├── invoker.go │ │ └── server.go │ ├── feed │ ├── feed.pb.fast.go │ ├── feed.pb.go │ └── feedservice │ │ ├── client.go │ │ ├── feedservice.go │ │ ├── invoker.go │ │ └── server.go │ ├── publish │ ├── publish.pb.fast.go │ ├── publish.pb.go │ └── publishservice │ │ ├── client.go │ │ ├── invoker.go │ │ ├── publishservice.go │ │ └── server.go │ ├── relation │ ├── relation.pb.fast.go │ ├── relation.pb.go │ └── relationservice │ │ ├── client.go │ │ ├── invoker.go │ │ ├── relationservice.go │ │ └── server.go │ ├── user │ ├── user.pb.fast.go │ ├── user.pb.go │ └── userservice │ │ ├── client.go │ │ ├── invoker.go │ │ ├── server.go │ │ └── userservice.go │ └── wechat │ ├── wechat.pb.fast.go │ ├── wechat.pb.go │ └── wechatservice │ ├── client.go │ ├── invoker.go │ ├── server.go │ └── wechatservice.go ├── logging └── init.go ├── manifests-dev ├── configmap.yaml ├── deployment-toktik-auth-api.yaml ├── deployment-toktik-comment-api.yaml ├── deployment-toktik-favorite-api.yaml ├── deployment-toktik-feed-api.yaml ├── deployment-toktik-http-api.yaml ├── deployment-toktik-publish-api.yaml ├── deployment-toktik-relation-api.yaml ├── deployment-toktik-user-api.yaml ├── deployment-toktik-wechat-api.yaml ├── service-toktik-auth-api.yaml ├── service-toktik-comment-api.yaml ├── service-toktik-favorite-api.yaml ├── service-toktik-feed-api.yaml ├── service-toktik-http-api.yaml ├── service-toktik-publish-api.yaml ├── service-toktik-relation-api.yaml ├── service-toktik-user-api.yaml └── service-toktik-wechat-api.yaml ├── otel.yaml ├── repo ├── comments.gen.go ├── favorites.gen.go ├── gen.go ├── gen │ └── gormGen.go ├── init.go ├── migrate │ └── migrate.go ├── model │ ├── comment.go │ ├── favorite.go │ ├── model.go │ ├── relation.go │ ├── token.go │ ├── user.go │ ├── user_test.go │ └── video.go ├── relations.gen.go ├── user_tokens.gen.go ├── users.gen.go └── videos.gen.go ├── rpc └── http.go ├── service ├── auth │ ├── build.sh │ ├── handler.go │ ├── handler_test.go │ ├── kitex_info.yaml │ ├── main.go │ └── script │ │ └── bootstrap.sh ├── comment │ ├── build.sh │ ├── handler.go │ ├── handler_test.go │ ├── kitex_info.yaml │ ├── main.go │ └── script │ │ └── bootstrap.sh ├── favorite │ ├── build.sh │ ├── handler.go │ ├── kitex_info.yaml │ ├── main.go │ └── script │ │ └── bootstrap.sh ├── feed │ ├── build.sh │ ├── handler.go │ ├── handler_test.go │ ├── kitex_info.yaml │ ├── main.go │ └── script │ │ └── bootstrap.sh ├── publish │ ├── build.sh │ ├── handler.go │ ├── handler_test.go │ ├── kitex_info.yaml │ ├── main.go │ ├── resources │ │ └── bear.mp4 │ └── script │ │ └── bootstrap.sh ├── relation │ ├── build.sh │ ├── handler.go │ ├── handler_test.go │ ├── kitex_info.yaml │ ├── main.go │ └── script │ │ └── bootstrap.sh ├── user │ ├── build.sh │ ├── handler.go │ ├── kitex_info.yaml │ ├── main.go │ └── script │ │ └── bootstrap.sh ├── web │ ├── auth │ │ └── handler.go │ ├── build.sh │ ├── comment │ │ └── handler.go │ ├── favorite │ │ └── handler.go │ ├── feed │ │ └── handler.go │ ├── main.go │ ├── mw │ │ ├── auth.go │ │ └── json.go │ ├── publish │ │ └── handler.go │ ├── relation │ │ └── handler.go │ ├── user │ │ └── handler.go │ └── wechat │ │ └── handler.go └── wechat │ ├── build.sh │ ├── db │ ├── gen.sh │ ├── msg.pb.go │ └── msg.proto │ ├── handler.go │ ├── handler_test.go │ ├── kitex.yaml │ ├── kitex_info.yaml │ ├── main.go │ └── script │ └── bootstrap.sh ├── start.sh ├── storage ├── fs.go ├── provider.go └── s3.go ├── test ├── e2e │ ├── base_api_test.go │ ├── common.go │ ├── interact_api_test.go │ └── social_api_test.go └── mock │ └── db.go └── unit-test.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | end_of_line = lf 7 | 8 | [*.yaml] 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "feat-github-action-unit-test" ] 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | jobs: 11 | 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - 19 | name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v2 21 | - 22 | name: Build services into one image 23 | uses: docker/build-push-action@v4 24 | with: 25 | context: . 26 | push: false 27 | tags: toktik-srv:${{ github.sha }} 28 | cache-from: type=gha 29 | cache-to: type=gha,mode=max 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project ignore 2 | service/**/output/ 3 | .env 4 | # Project ignore end 5 | 6 | # If you prefer the allow list template instead of the deny list, see community template: 7 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 8 | # 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | 25 | # Go workspace file 26 | go.work 27 | 28 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 29 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 30 | 31 | # User-specific stuff 32 | .idea/**/workspace.xml 33 | .idea/**/tasks.xml 34 | .idea/**/usage.statistics.xml 35 | .idea/**/dictionaries 36 | .idea/**/shelf 37 | 38 | # AWS User-specific 39 | .idea/**/aws.xml 40 | 41 | # Generated files 42 | .idea/**/contentModel.xml 43 | 44 | # Sensitive or high-churn files 45 | .idea/**/dataSources/ 46 | .idea/**/dataSources.ids 47 | .idea/**/dataSources.local.xml 48 | .idea/**/dataSources.xml 49 | .idea/**/sqlDataSources.xml 50 | .idea/**/dynamic.xml 51 | .idea/**/uiDesigner.xml 52 | .idea/**/dbnavigator.xml 53 | 54 | # Gradle 55 | .idea/**/gradle.xml 56 | .idea/**/libraries 57 | 58 | # Gradle and Maven with auto-import 59 | # When using Gradle or Maven with auto-import, you should exclude module files, 60 | # since they will be recreated, and may cause churn. Uncomment if using 61 | # auto-import. 62 | # .idea/artifacts 63 | # .idea/compiler.xml 64 | # .idea/jarRepositories.xml 65 | # .idea/modules.xml 66 | # .idea/*.iml 67 | # .idea/modules 68 | # *.iml 69 | # *.ipr 70 | 71 | # CMake 72 | cmake-build-*/ 73 | 74 | # Mongo Explorer plugin 75 | .idea/**/mongoSettings.xml 76 | 77 | # File-based project format 78 | *.iws 79 | 80 | # IntelliJ 81 | out/ 82 | 83 | # mpeltonen/sbt-idea plugin 84 | .idea_modules/ 85 | 86 | # JIRA plugin 87 | atlassian-ide-plugin.xml 88 | 89 | # Cursive Clojure plugin 90 | .idea/replstate.xml 91 | 92 | # SonarLint plugin 93 | .idea/sonarlint/ 94 | 95 | # Crashlytics plugin (for Android Studio and IntelliJ) 96 | com_crashlytics_export_strings.xml 97 | crashlytics.properties 98 | crashlytics-build.properties 99 | fabric.properties 100 | 101 | # Editor-based Rest Client 102 | .idea/httpRequests 103 | 104 | # Android studio 3.1+ serialized cache file 105 | .idea/caches/build_file_checksums.ser 106 | 107 | .vscode/* 108 | !.vscode/settings.json 109 | !.vscode/tasks.json 110 | !.vscode/launch.json 111 | !.vscode/extensions.json 112 | !.vscode/*.code-snippets 113 | 114 | # Local History for Visual Studio Code 115 | .history/ 116 | 117 | # Built Visual Studio Code Extensions 118 | *.vsix 119 | 120 | # Service build output 121 | **/output/ 122 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | /protoeditor.xml 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/toktik.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 编译镜像 2 | FROM docker.io/nicognaw/toktik-basic-build:v1 AS build 3 | ENV TZ=Asia/Shanghai 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | # 构建依赖 7 | RUN apt-get update && \ 8 | apt-get install -yq git ffmpeg libavcodec-dev libavutil-dev libavformat-dev libswscale-dev && \ 9 | apt-get clean && \ 10 | apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | # 获取文件 14 | RUN mkdir -p /source 15 | WORKDIR /source 16 | COPY . . 17 | 18 | # 编译 19 | RUN bash unit-test.sh && bash build-all.sh 20 | 21 | # 运行环境 22 | FROM docker.io/nicognaw/toktik-basic:v1 23 | ENV TZ=Asia/Shanghai 24 | ENV DEBIAN_FRONTEND=noninteractive 25 | 26 | # FFmpeg 及依赖 27 | RUN apt-get update && \ 28 | apt-get install -yq ca-certificates && \ 29 | apt-get clean && \ 30 | apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \ 31 | rm -rf /var/lib/apt/lists/* 32 | 33 | # RUN mkdir -p /data/apps/toktik-service-bundle 34 | WORKDIR /data/apps/toktik-service-bundle 35 | 36 | # 收集数据 37 | COPY --from=build /source/output/ . 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Toktik-Team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toktik 2 | 3 | | ![logo](https://avatars.githubusercontent.com/u/124244470?s=200&v=4) | ChatGPT integrated short video microservice application built with `Kitex` and `Hertz` , made by [Toktik-Team](https://github.com/Toktik-Team) in _The 5th Bytedance Youth Training Camp_. | 4 | | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 5 | 6 | **English** | [简体中文](README_zh-CN.md) 7 | 8 | _The 5th Bytedance Youth Training Camp_ Report Documentation: 9 | 10 | ## Recommend 11 | 12 | Now Recommend for [GuGoTik](https://github.com/GuGoOrg/GuGoTik) ! 13 | 14 | TokTik is no longer maintained and has no planned maintenance plans, please go to [GuGoTik](https://github.com/GuGoOrg/GuGoTik). GuGoTik respects and follows TokTik's workline, improving and optimizing almost every components from it and adding more features. For best expermental experience, please use [GuGoTik](https://github.com/GuGoOrg/GuGoTik) instead of TokTik. 15 | 16 | Give us your stars if you like our project. 17 | 18 | ## Awards 19 | 20 | Earned the **_Best Project_** award in _the 5th Bytedance Youth Training Camp_. 21 | 22 | ![](docs/images/cert-cut.png) 23 | 24 | ## Demo 25 | 26 | > Download Dousheng app from [here](https://bytedance.feishu.cn/docs/doccnM9KkBAdyDhg8qaeGlIz7S7#), and use this URL to test with the demo. 27 | 28 | https://toktik.xctra.cn/ 29 | 30 | ## Project Structure 31 | 32 | - [constant](constant) 33 | - [biz](constant/biz) - Business logic related constants 34 | - [config](constant/config) 35 | - [env.go](constant/config/env.go) - Environment variable configs 36 | - [service.go](constant/config/service.go) - Service names and ports 37 | - [idl](idl) 38 | - [auth.proto](idl/auth.proto) - RPC definition of auth service 39 | - [comment.proto](idl/comment.proto) - RPC definition of comment service 40 | - [favorite.proto](idl/favorite.proto) - RPC definition of favorite service 41 | - [feed.proto](idl/feed.proto) - RPC definition of feed service 42 | - [publish.proto](idl/publish.proto) - RPC definition of publish service 43 | - [relation.proto](idl/relation.proto) - RPC definition of relation service 44 | - [user.proto](idl/user.proto) - RPC definition of user service 45 | - [wechat.proto](idl/wechat.proto) - RPC definition of chat service 46 | - [kitex_gen](kitex_gen) - Generated code by Kitex 47 | - [logging](logging) - Logging middleware 48 | - [manifests-dev](manifests-dev) - Kubernetes manifests 49 | - [repo](repo) - Database schemas and generated code by Gorm Gen 50 | - [service](service) 51 | - [auth](service/auth) - Auth service impl 52 | - [comment](service/comment) - Comment service impl 53 | - [favorite](service/favorite) - Favorite service impl 54 | - [feed](service/feed) - Feed service impl 55 | - [publish](service/publish) - Publish service impl 56 | - [relation](service/relation) - Relation service impl 57 | - [user](service/user) - User service impl 58 | - [web](service/web) - Web api gateway 59 | - [mw](service/web/mw) - Hertz middlewares 60 | - [wechat](service/wechat) - Chat service impl 61 | - [storage](storage) - Storage middleware, s3, local storage & volcengine [ImageX](https://www.volcengine.com/products/imagex) supported 62 | - [test](test) 63 | - [e2e](test/e2e) - End-to-end test 64 | - [mock](test/mock) - Mock data for unit test 65 | 66 | ## Prerequisite 67 | 68 | > This project does not support Windows, as Kitex does not support either. 69 | 70 | - Linux / MacOS 71 | - Go 72 | - FFmpeg 73 | - PostgreSQL 74 | - Redis 75 | - OpenTelemetry Collector 76 | 77 | For observability infrastructures, it's recommended to use: 78 | 79 | - Jaeger All in one 80 | - Victoria Metrics 81 | - Grafana 82 | 83 | ## Build 84 | 85 | Run `./build-all.sh` in your Linux environment to compile all services. 86 | 87 | ## Configurations 88 | 89 | Check out [`constant/config/env.go`](constant/config/env.go) 90 | 91 | ## Run 92 | 93 | - Run `start.sh --service ` to start a service. 94 | - `service_name` could be any of the sub directory of `./service`. 95 | 96 | ## Test 97 | 98 | ### Unit Test 99 | 100 | Run `./unit-test.sh` 101 | 102 | ### End-to-End Test 103 | 104 | Run `go test toktik/test/e2e -tags="e2e"` 105 | 106 | ## How to Contribute 107 | 108 | 1. Please following the [HowToAddServices](docs/HowToAddServices.md) to create your own service. 109 | 2. Create a new branch and make your changes. 110 | 3. Create a pull request to the `main` branch. 111 | 4. Wait for review and merge. 112 | 113 | ## Contributors 114 | 115 | - [Nico](https://github.com/nicognaW) 116 | - [Eric_Lian](https://github.com/ExerciseBook) 117 | - [Dark Litss](https://github.com/lss233) 118 | - [HikariLan](https://github.com/shaokeyibb) 119 | - [YunShu](https://github.com/Selflocking) 120 | 121 | ## License 122 | 123 | Toktik is licensed under the [MIT License](LICENSE). 124 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # Toktik 2 | 3 | | ![logo](https://avatars.githubusercontent.com/u/124244470?s=200&v=4) | 集成 ChatGPT 的短视频微服务应用,使用 Kitex 和 Hertz 构建,由 [Toktik-Team](https://github.com/Toktik-Team) 开发,作为*第五届字节跳动青训营*大作业。 | 4 | | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | 5 | 6 | [English](README.md) | **简体中文** 7 | 8 | *第五届字节跳动青训营*大作业汇报文档: 9 | 10 | ## Recommend 11 | 现在推荐使用 [GuGoTik](https://github.com/GuGoOrg/GuGoTik)! 12 | 13 | TokTik 目前不再维护且尚未有维护计划,推荐查看 [GuGoTik](https://github.com/GuGoOrg/GuGoTik)。GuGoTik 秉承 TokTik 的核心思想并扬长避短,匠心改进了几乎所有的模块并加入了更丰富的功能。为了让您得到更好的实验体验,请移玉步到 [GuGoTik](https://github.com/GuGoOrg/GuGoTik)。 14 | 15 | 如果你喜欢我们的项目的话,请给我们 Stars ~ 16 | 17 | ## Awards 18 | 19 | 获得 *第五届字节跳动青训营* **_码如磐石奖(一等奖)_** 奖项. 20 | 21 | ![](docs/images/cert-cut.png) 22 | 23 | ## 示例 24 | 25 | > 从 [此处](https://bytedance.feishu.cn/docs/doccnM9KkBAdyDhg8qaeGlIz7S7#) 下载抖声 app 后, 填入以下地址来体验 demo。 26 | 27 | https://toktik.xctra.cn/ 28 | 29 | ## 项目结构 30 | 31 | - [constant](constant) 32 | - [biz](constant/biz) - 业务逻辑相关常量 33 | - [config](constant/config) 34 | - [env.go](constant/config/env.go) - 环境变量配置 35 | - [service.go](constant/config/service.go) - 服务名称和端口 36 | - [idl](idl) 37 | - [auth.proto](idl/auth.proto) - 认证服务 RPC 定义 38 | - [comment.proto](idl/comment.proto) - 评论服务 RPC 定义 39 | - [favorite.proto](idl/favorite.proto) - 点赞服务 RPC 定义 40 | - [feed.proto](idl/feed.proto) - 视频流服务 RPC 定义 41 | - [publish.proto](idl/publish.proto) - 视频发布服务 RPC 定义 42 | - [relation.proto](idl/relation.proto) - 关注服务 RPC 定义 43 | - [user.proto](idl/user.proto) - 用户服务 RPC 定义 44 | - [wechat.proto](idl/wechat.proto) - 聊天服务 RPC 定义 45 | - [kitex_gen](kitex_gen) - 由 Kitex 自动生成的代码 46 | - [logging](logging) - 日志中间件配置 47 | - [manifests-dev](manifests-dev) - Kubernetes 清单文件 48 | - [repo](repo) - 数据库概要和由 Gorm Gen 自动生成的代码 49 | - [service](service) 50 | - [auth](service/auth) - 鉴权服务实现 51 | - [comment](service/comment) - 评论服务实现 52 | - [favorite](service/favorite) - 点赞服务实现 53 | - [feed](service/feed) - 视频流服务实现 54 | - [publish](service/publish) - 视频发布服务实现 55 | - [relation](service/relation) - 关注服务实现 56 | - [user](service/user) - 用户服务实现 57 | - [web](service/web) - Web API 网关 58 | - [mw](service/web/mw) - Hertz 中间件 59 | - [wechat](service/wechat) - 聊天服务实现 60 | - [storage](storage) - 对象存储中间件,支持 Amazon S3 和本地存储和火山引擎 [ImageX](https://www.volcengine.com/products/imagex) 61 | - [test](test) 62 | - [e2e](test/e2e) - 端到端测试 63 | - [mock](test/mock) - 用于单元测试的 mock 数据 64 | 65 | ## 准备环境 66 | 67 | > 本项目不支持 Windows 操作系统,见 [Kitex](https://www.cloudwego.io/zh/docs/kitex/getting-started/#%E5%87%86%E5%A4%87-golang-%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83) 68 | 69 | - Linux / MacOS 70 | - Go 71 | - FFmpeg 72 | - PostgreSQL 73 | - Redis 74 | - OpenTelemetry Collector 75 | 76 | 推荐使用以下可观测性基础设施: 77 | 78 | - Jaeger All in one 79 | - Victoria Metrics 80 | - Grafana 81 | 82 | 83 | ## 构建 84 | 85 | 执行 `./build-all.sh` 一次性构建所有服务. 86 | 87 | ## 配置 88 | 89 | 见 [`constant/config/env.go`](constant/config/env.go) 90 | 91 | ## 运行 92 | 93 | - 执行 `start.sh --service ` 启动任一服务. 94 | - `service_name` 可以是 `./service` 目录下任一子目录名称. 95 | 96 | ## 测试 97 | 98 | ### 单元测试 99 | 100 | 运行 `./unit-test.sh` 101 | 102 | ### 端到端测试 103 | 104 | 执行 `go test toktik/test/e2e -tags="e2e"` 105 | 106 | ## 如何贡献 107 | 108 | 1. 请遵循 [HowToAddServices](docs/HowToAddServices.md) 文件说明以创建新的服务。 109 | 2. 创建一个新的分支并做出更改。 110 | 3. 提交一个 Pull Request 到 `main` 分支。 111 | 4. 等待 review 和合并。 112 | 113 | ## 贡献者 114 | 115 | - [Nico](https://github.com/nicognaW) 116 | - [Eric_Lian](https://github.com/ExerciseBook) 117 | - [Dark Litss](https://github.com/lss233) 118 | - [HikariLan](https://github.com/shaokeyibb) 119 | - [YunShu](https://github.com/Selflocking) 120 | 121 | ## 协议 122 | 123 | Toktik is licensed under the [MIT License](LICENSE). 124 | -------------------------------------------------------------------------------- /add-kitex-service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 检查 go 命令是否存在于 PATH 环境变量中 4 | if ! command -v go &>/dev/null; then 5 | echo "错误:go 命令未在 PATH 中找到。请安装或将其添加到 PATH 中。" 6 | exit 1 7 | fi 8 | 9 | # 检查 protoc 命令是否存在于 PATH 环境变量中 10 | if ! command -v protoc &>/dev/null; then 11 | echo "错误:protoc 命令未在 PATH 中找到。请安装或将其添加到 PATH 中。" 12 | # 检查操作系统是否为 macOS 13 | if [[ $(uname) == "Darwin" ]]; then 14 | # 如果是macOS,则检查brew是否在PATH中 15 | if ! command -v brew &>/dev/null; then 16 | echo "错误:brew 命令未在 PATH 中找到。请安装或将其添加到 PATH 中。" 17 | exit 1 18 | else 19 | echo "尝试安装 protoc......" 20 | brew install protobuf 21 | fi 22 | fi 23 | fi 24 | 25 | # 再次检查 protoc 命令是否存在于 PATH 环境变量中 26 | if ! command -v protoc &>/dev/null; then 27 | echo "错误:protoc 命令未在 PATH 中找到,看起来安装失败了。请手动安装。" 28 | exit 1 29 | fi 30 | 31 | # 检查 kitex 命令是否存在于 PATH 环境变量中 32 | if ! command -v kitex &>/dev/null; then 33 | echo "错误:kitex 命令未在 PATH 中找到,尝试安装......" 34 | go install github.com/cloudwego/kitex/tool/cmd/kitex@latest 35 | fi 36 | 37 | # 再次检查 kitex 命令是否存在于 PATH 环境变量中 38 | if ! command -v kitex &>/dev/null; then 39 | echo "错误:kitex 命令未在 PATH 中找到,看起来安装失败了。请手动安装。" 40 | exit 1 41 | fi 42 | 43 | mkdir -p kitex_gen 44 | kitex -module "toktik" -I idl/ idl/"$1".proto 45 | 46 | mkdir -p service/"$1" 47 | cd service/"$1" && kitex -module "toktik" -service "$1" -use toktik/kitex_gen/ -I ../../idl/ ../../idl/"$1".proto 48 | 49 | go mod tidy 50 | -------------------------------------------------------------------------------- /basic-build-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/golang:1.19.5-bullseye 2 | 3 | # FFmpeg 及依赖 4 | RUN apt-get update && \ 5 | apt-get install -yq ffmpeg && \ 6 | apt-get clean && \ 7 | apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \ 8 | rm -rf /var/lib/apt/lists/* 9 | -------------------------------------------------------------------------------- /basic-runtime-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/debian:bullseye-slim 2 | 3 | # FFmpeg 及依赖 4 | RUN apt-get update && \ 5 | apt-get install -yq ffmpeg && \ 6 | apt-get clean && \ 7 | apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false && \ 8 | rm -rf /var/lib/apt/lists/* 9 | -------------------------------------------------------------------------------- /build-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit if any error occured 4 | set -euxo pipefail 5 | 6 | SERVICE_DIR=$(pwd)/service 7 | 8 | # Remove the output directory 9 | echo "Removing previous output directory..." 10 | rm -rf output 11 | 12 | # Create the output directory and binaries directory 13 | echo "Creating output directory..." 14 | mkdir -p output/bin 15 | 16 | # Navigate to the service directory 17 | echo "Navigating to service directory..." 18 | cd $SERVICE_DIR || exit 19 | 20 | # Loop through all directories in the service directory 21 | for d in */ ; do 22 | echo "Building $d..." 23 | # Navigate into the current directory 24 | cd "$SERVICE_DIR/$d" 25 | 26 | # Remove the output directory 27 | echo "Removing previous output directory..." 28 | rm -rf output 29 | 30 | # Create the output directory 31 | echo "Creating output directory..." 32 | mkdir output 33 | 34 | # Check if the build.sh file exists, if not, skip the current directory 35 | if [ ! -f build.sh ]; then 36 | echo "build.sh file not found in $d, skipping..." 37 | continue 38 | fi 39 | 40 | # Execute the build.sh script 41 | echo "Executing build.sh..." 42 | if ! bash build.sh; 43 | then 44 | echo "Build failed" 45 | exit 1 46 | fi 47 | 48 | 49 | # Copy the binary to the output/binaries directory 50 | echo "Copying binary to output/bin..." 51 | cp output/bin/"$(basename "$d")" ../../output/bin/ 52 | 53 | # Check if the bootstrap.sh file exists 54 | if [ -f output/bootstrap.sh ]; then 55 | echo "bootstrap.sh file found in $d, copying to output directory..." 56 | # Rename the bootstrap.sh file and copy it to the output directory 57 | mv output/bootstrap.sh ../../output/bootstrap-"$(basename "$d")".sh 58 | fi 59 | 60 | # Navigate back to the service directory 61 | echo "Finished building $d" 62 | done 63 | -------------------------------------------------------------------------------- /constant/biz/auth.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "github.com/cloudwego/hertz/pkg/protocol/consts" 4 | 5 | var ( 6 | TokenNotFoundMessage = "会话已过期" 7 | ) 8 | 9 | var ( 10 | NoUserNameOrPassWord = GWError{HTTPStatusCode: consts.StatusBadRequest, StatusCode: 400007, StatusMsg: "no username or password"} 11 | ) 12 | -------------------------------------------------------------------------------- /constant/biz/comment.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import httpStatus "github.com/cloudwego/hertz/pkg/protocol/consts" 4 | 5 | var ( 6 | UnauthorizedError = GWError{HTTPStatusCode: httpStatus.StatusUnauthorized, StatusCode: 400003, StatusMsg: "Unauthorized"} 7 | BadRequestError = GWError{HTTPStatusCode: httpStatus.StatusBadRequest, StatusCode: 400004, StatusMsg: "Bad request"} 8 | InternalServerError = GWError{HTTPStatusCode: httpStatus.StatusInternalServerError, StatusCode: 500006, StatusMsg: "Internal server error"} 9 | ) 10 | -------------------------------------------------------------------------------- /constant/biz/favorite.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | const ( 4 | FailedToGetVideoList = 500001 5 | ) 6 | -------------------------------------------------------------------------------- /constant/biz/feed.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "github.com/cloudwego/hertz/pkg/protocol/consts" 4 | 5 | const ( 6 | VideoCount = 30 7 | ) 8 | 9 | var ( 10 | InvalidLatestTime = GWError{HTTPStatusCode: consts.StatusBadRequest, StatusCode: 400001, StatusMsg: "Invalid latest_time"} 11 | ) 12 | -------------------------------------------------------------------------------- /constant/biz/gwerror.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "fmt" 5 | "toktik/logging" 6 | 7 | "github.com/cloudwego/hertz/pkg/app" 8 | "github.com/cloudwego/hertz/pkg/protocol/consts" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // GWError is the error struct for gateway. 13 | type GWError struct { 14 | HTTPStatusCode int 15 | StatusCode uint32 16 | StatusMsg string 17 | Cause *error 18 | Fields *logrus.Fields 19 | } 20 | 21 | func (G GWError) Error() string { 22 | return fmt.Sprintf("http status code: %d, status code: %d, status msg: %s, cause: %v", G.HTTPStatusCode, G.StatusCode, G.StatusMsg, G.Cause) 23 | } 24 | 25 | // extractFieldsFromRequest extracts important fields from request context for debugging purpose. 26 | func extractFieldsFromRequest(c *app.RequestContext) logrus.Fields { 27 | return logrus.Fields{"request_context_info": map[string]any{ 28 | "request_id": c.Request.Header.Get("X-Request-Id"), 29 | "method": c.Method(), 30 | "host": c.Host(), 31 | "uri": c.URI(), 32 | "ip": c.ClientIP(), 33 | "ua": c.UserAgent(), 34 | "query_args": c.QueryArgs(), 35 | "post_args": c.PostArgs(), 36 | "content_type": c.ContentType(), 37 | "body": c.GetRawData(), 38 | "handler": c.HandlerName(), 39 | }} 40 | } 41 | 42 | // LaunchError logs the error and returns the error. 43 | func (G GWError) LaunchError(c *app.RequestContext) { 44 | logger := logging.Logger.WithFields(extractFieldsFromRequest(c)) 45 | if G.Fields != nil { 46 | logger = logger.WithFields(*G.Fields) 47 | } 48 | if G.Cause != nil { 49 | logger = logger.WithField("cause", *G.Cause) 50 | } 51 | logger.Debugf("launch error: %v", G) 52 | c.JSON(G.HTTPStatusCode, map[string]any{ 53 | "status_code": G.StatusCode, 54 | "status_msg": G.StatusMsg, 55 | }) 56 | } 57 | 58 | // WithCause adds the cause to the error. 59 | func (G GWError) WithCause(err error) GWError { 60 | G.Cause = &err 61 | return G 62 | } 63 | 64 | // WithFields adds the fields to the error. 65 | func (G GWError) WithFields(fields *logrus.Fields) GWError { 66 | G.Fields = fields 67 | return G 68 | } 69 | 70 | var ( 71 | RPCCallError = GWError{HTTPStatusCode: consts.StatusInternalServerError, StatusCode: 500001, StatusMsg: "RPC call error"} 72 | UnAuthorized = GWError{HTTPStatusCode: consts.StatusUnauthorized, StatusCode: 401001, StatusMsg: "Unauthorized"} 73 | InvalidArguments = GWError{HTTPStatusCode: consts.StatusBadRequest, StatusCode: 400002, StatusMsg: "Invalid arguments"} 74 | ) 75 | -------------------------------------------------------------------------------- /constant/biz/publish.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "github.com/cloudwego/hertz/pkg/protocol/consts" 4 | 5 | var ( 6 | PublishActionSuccess = "Uploaded successfully!" 7 | ) 8 | 9 | var ( 10 | OpenFileFailedError = GWError{HTTPStatusCode: consts.StatusInternalServerError, StatusCode: 400005, StatusMsg: "Open file failed"} 11 | SizeNotMatchError = GWError{HTTPStatusCode: consts.StatusInternalServerError, StatusCode: 400006, StatusMsg: "Size not match"} 12 | ) 13 | -------------------------------------------------------------------------------- /constant/biz/relation.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "toktik/kitex_gen/douyin/user" 4 | 5 | var openAIImage = "https://bkimg.cdn.bcebos.com/pic/8b13632762d0f703918f0d436fac463d269758ee6faf?x-bce-process=image/watermark,image_d2F0ZXIvYmFpa2U4MA==,g_7,xp_5,yp_5" 6 | var openAIIntro = "ChatGPT is a language model created by OpenAI with the ability to understand and generate human-like text responses to a wide range of topics and questions." 7 | var zero uint32 = 0 8 | 9 | var ChatGPTUser = &user.User{ 10 | Id: 0, 11 | Name: "ChatGPT", 12 | FollowCount: 0, 13 | FollowerCount: 100000000, 14 | IsFollow: true, 15 | Avatar: &openAIImage, 16 | BackgroundImage: &openAIImage, 17 | Signature: &openAIIntro, 18 | TotalFavorited: &zero, 19 | WorkCount: &zero, 20 | FavoriteCount: &zero, 21 | } 22 | -------------------------------------------------------------------------------- /constant/biz/response.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | var ( 4 | OkStatusMsg = "OK" 5 | BadRequestStatusMsg = "Unable to finish request, please check your parameters. If you think this is a bug, please contact us." 6 | ForbiddenStatusMsg = "You are not allowed to access this resource." 7 | InternalServerErrorStatusMsg = "The server had an error while processing your request. Sorry about that!" 8 | ) 9 | -------------------------------------------------------------------------------- /constant/biz/statuscode.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | // OK 4 | const ( 5 | OkStatusCode = 0 + iota 6 | ) 7 | 8 | // Service Not Available 9 | const ( 10 | ServiceNotAvailable = 503000 11 | ) 12 | 13 | // Internal Server Error 14 | const ( 15 | RedisError = 500000 + iota 16 | ProtoMarshalError 17 | UnableToCreateComment 18 | UnableToDeleteComment 19 | UnableToQueryVideo 20 | UnableToQueryComment 21 | UnableToQueryUser 22 | SQLQueryErrorStatusCode 23 | Unable2GenerateUUID 24 | Unable2CreateThumbnail 25 | Unable2UploadVideo 26 | Unable2UploadCover 27 | Unable2CreateDBEntry 28 | RequestIsNil 29 | UnableToQueryFollowList 30 | UnableToQueryFollowerList 31 | UnableToQueryIsFollow 32 | UnableToDeleteRelation 33 | UnableToLike 34 | UnableToCancelLike 35 | UnableToQueryFavorite 36 | UnableToQueryTotalFavorited 37 | ) 38 | 39 | // Bad Request 40 | const ( 41 | UserNameExist = 400000 + iota 42 | UserNotFound 43 | InvalidCommentActionType 44 | VideoNotFound 45 | Unable2ParseLatestTimeStatusCode 46 | InvalidContentType 47 | RelationNotFound 48 | RelationAlreadyExists 49 | InvalidToUserId 50 | ) 51 | 52 | // Unauthorized 53 | const ( 54 | PasswordIncorrect = 401003 55 | TokenNotFound = 401001 56 | ) 57 | 58 | // Forbidden 59 | const ( 60 | ActorIDNotMatch = 403001 61 | ) 62 | -------------------------------------------------------------------------------- /constant/biz/user.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "github.com/cloudwego/hertz/pkg/protocol/consts" 4 | 5 | var ( 6 | InvalidUserID = GWError{HTTPStatusCode: consts.StatusBadRequest, StatusCode: 400003, StatusMsg: "Invalid user_id"} 7 | ) 8 | -------------------------------------------------------------------------------- /constant/biz/wechat.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import httpStatus "github.com/cloudwego/hertz/pkg/protocol/consts" 4 | 5 | var ( 6 | InvalidActionType = GWError{HTTPStatusCode: httpStatus.StatusBadRequest, StatusCode: 400008, StatusMsg: "invalid action type"} 7 | ) 8 | -------------------------------------------------------------------------------- /constant/config/env.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "toktik/logging" 11 | 12 | "github.com/joho/godotenv" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var EnvConfig = envConfigSchema{} 17 | 18 | func (s *envConfigSchema) GetDSN() string { 19 | return dsn 20 | } 21 | 22 | var dsn string 23 | 24 | func init() { 25 | envInit() 26 | envValidate() 27 | dsn = fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s", 28 | EnvConfig.PGSQL_HOST, 29 | EnvConfig.PGSQL_USER, 30 | EnvConfig.PGSQL_PASSWORD, 31 | EnvConfig.PGSQL_DBNAME, 32 | EnvConfig.PGSQL_PORT) 33 | } 34 | 35 | var defaultConfig = envConfigSchema{ 36 | ENV: "dev", 37 | 38 | CONSUL_ADDR: "127.0.0.1:8500", 39 | 40 | EXPORT_ENDPOINT: "127.0.0.1:4317", 41 | 42 | PGSQL_HOST: "localhost", 43 | PGSQL_PORT: "5432", 44 | PGSQL_USER: "postgres", 45 | PGSQL_PASSWORD: "", 46 | PGSQL_DBNAME: "postgres", 47 | 48 | REDIS_ADDR: "localhost:6379", 49 | REDIS_PASSWORD: "", 50 | REDIS_DB: 0, 51 | 52 | STORAGE_TYPE: "s3", 53 | MAX_REQUEST_BODY_SIZE: 200 * 1024 * 1024, 54 | 55 | LOCAL_FS_LOCATION: "/tmp", 56 | LOCAL_FS_BASEURL: "http://localhost/", 57 | 58 | S3_ENDPOINT_URL: "http://localhost:9000", 59 | S3_PUBLIC_URL: "http://localhost:9000", 60 | S3_BUCKET: "bucket", 61 | S3_SECRET_ID: "minio", 62 | S3_SECRET_KEY: "12345678", 63 | S3_PATH_STYLE: "true", 64 | 65 | UNSPLASH_ACCESS_KEY: "access_key", 66 | } 67 | 68 | type envConfigSchema struct { 69 | ENV string `env:"ENV,DREAM_ENV"` 70 | 71 | CONSUL_ADDR string `env:"CONSUL_ADDR,DREAM_SERVICE_DISCOVERY_URI"` 72 | 73 | EXPORT_ENDPOINT string 74 | 75 | PGSQL_HOST string 76 | PGSQL_PORT string 77 | PGSQL_USER string 78 | PGSQL_PASSWORD string 79 | PGSQL_DBNAME string 80 | 81 | REDIS_ADDR string 82 | REDIS_PASSWORD string 83 | REDIS_DB int 84 | 85 | STORAGE_TYPE string 86 | MAX_REQUEST_BODY_SIZE int 87 | 88 | LOCAL_FS_LOCATION string 89 | LOCAL_FS_BASEURL string 90 | 91 | S3_ENDPOINT_URL string 92 | S3_PUBLIC_URL string 93 | S3_BUCKET string 94 | S3_SECRET_ID string 95 | S3_SECRET_KEY string 96 | S3_PATH_STYLE string 97 | 98 | UNSPLASH_ACCESS_KEY string 99 | } 100 | 101 | func (s *envConfigSchema) IsDev() bool { 102 | return s.ENV == "dev" || s.ENV == "TESTING" 103 | } 104 | 105 | func envValidate() { 106 | EnvConfig.CONSUL_ADDR = strings.TrimPrefix(EnvConfig.CONSUL_ADDR, "consul://") 107 | } 108 | 109 | // envInit Reads .env as environment variables and fill corresponding fields into EnvConfig. 110 | // To use a value from EnvConfig , simply call EnvConfig.FIELD like EnvConfig.ENV 111 | // Note: Please keep Env as the first field of envConfigSchema for better logging. 112 | func envInit() { 113 | err := godotenv.Load() 114 | if err != nil { 115 | log.Print("Error loading .env file, ignored") 116 | } 117 | v := reflect.ValueOf(defaultConfig) 118 | typeOfV := v.Type() 119 | 120 | for i := 0; i < v.NumField(); i++ { 121 | envNameAlt := make([]string, 0) 122 | fieldName := typeOfV.Field(i).Name 123 | fieldType := typeOfV.Field(i).Type 124 | fieldValue := v.Field(i).Interface() 125 | 126 | envNameAlt = append(envNameAlt, fieldName) 127 | if fieldTag, ok := typeOfV.Field(i).Tag.Lookup("env"); ok && len(fieldTag) > 0 { 128 | tags := strings.Split(fieldTag, ",") 129 | envNameAlt = append(envNameAlt, tags...) 130 | } 131 | 132 | switch fieldType { 133 | case reflect.TypeOf(0): 134 | { 135 | configDefaultValue, ok := fieldValue.(int) 136 | if !ok { 137 | logging.Logger.WithFields(logrus.Fields{ 138 | "field": fieldName, 139 | "type": "int", 140 | "value": fieldValue, 141 | "env": envNameAlt, 142 | }).Warningf("Failed to parse default value") 143 | continue 144 | } 145 | envValue := resolveEnv(envNameAlt, fmt.Sprintf("%d", configDefaultValue)) 146 | if EnvConfig.IsDev() { 147 | fmt.Printf("Reading field[ %s ] default: %v env: %s\n", fieldName, configDefaultValue, envValue) 148 | } 149 | if len(envValue) > 0 { 150 | envValueInteger, err := strconv.ParseInt(envValue, 10, 64) 151 | if err != nil { 152 | logging.Logger.WithFields(logrus.Fields{ 153 | "field": fieldName, 154 | "type": "int", 155 | "value": fieldValue, 156 | "env": envNameAlt, 157 | }).Warningf("Failed to parse env value, ignored") 158 | continue 159 | } 160 | reflect.ValueOf(&EnvConfig).Elem().Field(i).SetInt(envValueInteger) 161 | } 162 | continue 163 | } 164 | case reflect.TypeOf(""): 165 | { 166 | configDefaultValue, ok := fieldValue.(string) 167 | if !ok { 168 | logging.Logger.WithFields(logrus.Fields{ 169 | "field": fieldName, 170 | "type": "int", 171 | "value": fieldValue, 172 | "env": envNameAlt, 173 | }).Warningf("Failed to parse default value") 174 | continue 175 | } 176 | envValue := resolveEnv(envNameAlt, configDefaultValue) 177 | 178 | if EnvConfig.IsDev() { 179 | fmt.Printf("Reading field[ %s ] default: %v env: %s\n", fieldName, configDefaultValue, envValue) 180 | } 181 | if len(envValue) > 0 { 182 | reflect.ValueOf(&EnvConfig).Elem().Field(i).SetString(envValue) 183 | } 184 | } 185 | } 186 | 187 | } 188 | } 189 | 190 | func resolveEnv(configKeys []string, defaultValue string) string { 191 | for _, item := range configKeys { 192 | envValue := os.Getenv(item) 193 | if envValue != "" { 194 | return envValue 195 | } 196 | } 197 | return defaultValue 198 | } 199 | -------------------------------------------------------------------------------- /constant/config/service.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const WebServiceName = "toktik-api-gateway" 4 | const WebServiceAddr = ":40126" 5 | 6 | const AuthServiceName = "toktik-auth-api" 7 | const AuthServiceAddr = ":40127" 8 | 9 | const PublishServiceName = "toktik-publish-api" 10 | const PublishServiceAddr = ":40128" 11 | 12 | const FeedServiceName = "toktik-feed-api" 13 | const FeedServiceAddr = ":40129" 14 | 15 | const UserServiceName = "toktik-user-api" 16 | const UserServiceAddr = ":40130" 17 | 18 | const CommentServiceName = "toktik-comment-api" 19 | const CommentServiceAddr = ":40131" 20 | 21 | const WechatServiceName = "toktik-wechat" 22 | const WechatServiceAddr = ":40132" 23 | 24 | const RelationServiceName = "toktik-relation-api" 25 | const RelationServiceAddr = ":40133" 26 | 27 | const FavoriteServiceName = "toktik-favorite-api" 28 | const FavoriteServiceAddr = ":40134" 29 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | # PostgreSQL 4 | 5 | db: 6 | image: postgres 7 | restart: always 8 | environment: 9 | POSTGRES_PASSWORD: example 10 | 11 | adminer: 12 | image: adminer 13 | restart: always 14 | ports: 15 | - 8080:8080 16 | 17 | # Collector 18 | otel-collector: 19 | image: otel/opentelemetry-collector-contrib-dev:latest 20 | command: [ "--config=/etc/otel-collector-config.yaml", "${OTELCOL_ARGS}" ] 21 | volumes: 22 | - ./otel.yaml:/etc/otel-collector-config.yaml 23 | ports: 24 | - "1888:1888" # pprof extension 25 | - "8888" # Prometheus metrics exposed by the collector 26 | - "8889:8889" # Prometheus exporter metrics 27 | - "13133:13133" # health_check extension 28 | - "4317:4317" # OTLP gRPC receiver 29 | - "55679" # zpages extension 30 | depends_on: 31 | - jaeger-all-in-one 32 | 33 | # Jaeger 34 | jaeger-all-in-one: 35 | image: jaegertracing/all-in-one:latest 36 | environment: 37 | - COLLECTOR_OTLP_ENABLED=true 38 | ports: 39 | - "16686:16686" 40 | - "14268" 41 | - "14250:14250" 42 | - "6831:6831" 43 | 44 | # Victoriametrics 45 | victoriametrics: 46 | container_name: victoriametrics 47 | image: victoriametrics/victoria-metrics 48 | ports: 49 | - "8428:8428" 50 | - "8089:8089" 51 | - "8089:8089/udp" 52 | - "2003:2003" 53 | - "2003:2003/udp" 54 | - "4242:4242" 55 | command: 56 | - '--storageDataPath=/storage' 57 | - '--graphiteListenAddr=:2003' 58 | - '--opentsdbListenAddr=:4242' 59 | - '--httpListenAddr=:8428' 60 | - '--influxListenAddr=:8089' 61 | restart: always 62 | 63 | # Grafana 64 | grafana: 65 | image: grafana/grafana:latest 66 | environment: 67 | - GF_AUTH_ANONYMOUS_ENABLED=true 68 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 69 | - GF_AUTH_DISABLE_LOGIN_FORM=true 70 | ports: 71 | - "3000:3000" 72 | -------------------------------------------------------------------------------- /docs/HowToAddServices.md: -------------------------------------------------------------------------------- 1 | # How To Add Services - 如何添加一个服务 2 | 3 | 1. 确定要添加的服务的名称(如:`user`, `auth`, `feed` 等)可参考 `service/web/main.go` 下的 hertz group 或 API URL 4 | 2. 编写 proto IDL 文件,并将文件命名为`{服务名称}.proto` ,放入 idl 目录 5 | 3. 打开终端 cd 到项目根目录,然后调用 `./add-kitex-service.sh {服务名称}` 6 | 4. 此时 kitex 生成的代码将被放入 `kitex_gen` 目录(无需改动)和 `service/{服务名称}` 7 | 5. 完成服务业务逻辑并妥善修改 `service/{服务名称}/handler.go` 8 | 6. 在 `service/web` web api 中添加对 RPC 服务的调用 -------------------------------------------------------------------------------- /docs/images/cert-cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Toktik-Team/toktik/fd1c18ea896125d0a71b45d546a4eed5094d15d0/docs/images/cert-cut.png -------------------------------------------------------------------------------- /idl/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package douyin.auth; 3 | option go_package = "douyin/auth"; 4 | 5 | message LoginRequest { 6 | string username = 1; // 登录用户名 7 | string password = 2; // 登录密码 8 | } 9 | 10 | message LoginResponse { 11 | uint32 status_code = 1 [json_name = "status_code"]; // 状态码,0-成功,其他值-失败 12 | string status_msg = 2 [json_name = "status_msg"]; // 返回状态描述 13 | uint32 user_id = 3 [json_name = "user_id"]; // 用户id 14 | string token = 4; // 用户鉴权token 15 | } 16 | 17 | message RegisterRequest { 18 | string username = 1; // 注册用户名,最长32个字符 19 | string password = 2; // 密码,最长32个字符 20 | } 21 | 22 | message RegisterResponse { 23 | uint32 status_code = 1 [json_name = "status_code"]; // 状态码,0-成功,其他值-失败 24 | string status_msg = 2 [json_name = "status_msg"]; // 返回状态描述 25 | uint32 user_id = 3 [json_name = "user_id"]; // 用户id 26 | string token = 4; // 用户鉴权token 27 | } 28 | 29 | message AuthenticateRequest { 30 | string token = 1; // 用户鉴权token 31 | } 32 | message AuthenticateResponse { 33 | uint32 status_code = 1 [json_name = "status_code"]; // 状态码,0-成功,其他值-失败 34 | string status_msg = 2 [json_name = "status_msg"]; // 返回状态描述 35 | uint32 user_id = 3 [json_name = "user_id"]; // 用户id 36 | } 37 | 38 | service AuthService { 39 | rpc Authenticate (AuthenticateRequest) returns (AuthenticateResponse) { 40 | } 41 | 42 | rpc Register (RegisterRequest) returns (RegisterResponse) { 43 | } 44 | 45 | rpc Login (LoginRequest) returns (LoginResponse) { 46 | } 47 | } -------------------------------------------------------------------------------- /idl/comment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package douyin.comment; 3 | option go_package = "douyin/comment"; 4 | 5 | import "user.proto"; 6 | 7 | message Comment { 8 | uint32 id = 1; 9 | user.User user = 2; 10 | string content = 3; 11 | string create_date = 4; 12 | } 13 | 14 | enum ActionCommentType { 15 | ACTION_COMMENT_TYPE_UNSPECIFIED = 0; // Only for protobuf compatibility 16 | ACTION_COMMENT_TYPE_ADD = 1; 17 | ACTION_COMMENT_TYPE_DELETE = 2; 18 | } 19 | 20 | message ActionCommentRequest { 21 | uint32 actor_id = 1; 22 | uint32 video_id = 2; 23 | ActionCommentType action_type = 3; 24 | oneof action { 25 | string comment_text = 4; 26 | uint32 comment_id = 5; 27 | } 28 | } 29 | 30 | message ActionCommentResponse { 31 | uint32 status_code = 1; 32 | optional string status_msg = 2; 33 | optional Comment comment = 3; 34 | } 35 | 36 | 37 | message ListCommentRequest { 38 | uint32 actor_id = 1; 39 | uint32 video_id = 2; 40 | } 41 | 42 | message ListCommentResponse { 43 | uint32 status_code = 1; 44 | optional string status_msg = 2; 45 | repeated Comment comment_list = 3; 46 | } 47 | 48 | message CountCommentRequest { 49 | uint32 actor_id = 1; 50 | uint32 video_id = 2; 51 | } 52 | 53 | message CountCommentResponse { 54 | uint32 status_code = 1; 55 | optional string status_msg = 2; 56 | uint32 comment_count = 3; 57 | } 58 | 59 | service CommentService { 60 | rpc ActionComment(ActionCommentRequest) returns (ActionCommentResponse); 61 | rpc ListComment(ListCommentRequest) returns (ListCommentResponse); 62 | rpc CountComment(CountCommentRequest) returns(CountCommentResponse); 63 | } 64 | -------------------------------------------------------------------------------- /idl/favorite.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package douyin.favorite; 3 | option go_package = "douyin/favorite"; 4 | 5 | import "feed.proto"; 6 | 7 | message FavoriteRequest { 8 | uint32 actor_id = 1; // 用户id 9 | uint32 video_id = 2; // 视频id 10 | uint32 action_type = 3; // 1-点赞,2-取消点赞 11 | } 12 | 13 | message FavoriteResponse { 14 | uint32 status_code = 1; // 状态码,0-成功,其他值-失败 15 | optional string status_msg = 2; // 返回状态描述 16 | } 17 | 18 | message FavoriteListRequest { 19 | uint32 actor_id = 1; // 发出请求的用户的id 20 | uint32 user_id = 2; // 用户id 21 | } 22 | 23 | message FavoriteListResponse { 24 | uint32 status_code = 1; // 状态码,0-成功,其他值-失败 25 | optional string status_msg = 2; // 返回状态描述 26 | repeated feed.Video video_list = 3; // 用户点赞视频列表 27 | } 28 | 29 | message IsFavoriteRequest { 30 | uint32 user_id = 1; // 用户id 31 | uint32 video_id = 2; // 视频id 32 | } 33 | 34 | message IsFavoriteResponse { 35 | bool result = 1; // 结果 36 | } 37 | 38 | message CountFavoriteRequest { 39 | uint32 video_id = 1; // 视频id 40 | } 41 | 42 | message CountFavoriteResponse { 43 | uint32 status_code = 1; 44 | optional string status_msg = 2; 45 | uint32 count = 3; // 点赞数 46 | } 47 | 48 | message CountUserFavoriteRequest { 49 | uint32 user_id = 1; // 用户id 50 | } 51 | 52 | message CountUserFavoriteResponse { 53 | uint32 status_code = 1; 54 | optional string status_msg = 2; 55 | uint32 count = 3; // 点赞数 56 | } 57 | 58 | message CountUserTotalFavoritedRequest { 59 | uint32 actor_id = 1; 60 | uint32 user_id = 2; 61 | } 62 | 63 | message CountUserTotalFavoritedResponse { 64 | uint32 status_code = 1; 65 | optional string status_msg = 2; 66 | uint32 count = 3; // 点赞数 67 | } 68 | 69 | service FavoriteService { 70 | rpc FavoriteAction (FavoriteRequest) returns (FavoriteResponse){ 71 | } 72 | rpc FavoriteList (FavoriteListRequest) returns (FavoriteListResponse) { 73 | } 74 | rpc IsFavorite (IsFavoriteRequest) returns (IsFavoriteResponse) { 75 | } 76 | rpc CountFavorite (CountFavoriteRequest) returns (CountFavoriteResponse) { 77 | } 78 | rpc CountUserFavorite (CountUserFavoriteRequest) returns (CountUserFavoriteResponse) { 79 | } 80 | rpc CountUserTotalFavorited (CountUserTotalFavoritedRequest) returns (CountUserTotalFavoritedResponse) { 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /idl/feed.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package douyin.feed; 4 | option go_package = "douyin/feed"; 5 | 6 | import "user.proto"; 7 | 8 | message Video { 9 | uint32 id = 1; 10 | user.User author = 2; 11 | string play_url = 3; 12 | string cover_url = 4; 13 | uint32 favorite_count = 5; 14 | uint32 comment_count = 6; 15 | bool is_favorite = 7; 16 | string title = 8; 17 | } 18 | 19 | message ListFeedRequest { 20 | optional string latest_time = 1; // 限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间 21 | optional uint32 actor_id = 2; // 发送请求的用户的id 22 | } 23 | 24 | message ListFeedResponse { 25 | uint32 status_code = 1; 26 | optional string status_msg = 2; 27 | optional int64 next_time = 3; 28 | repeated Video video_list = 4; 29 | } 30 | 31 | message QueryVideosRequest { 32 | uint32 actor_id = 1; 33 | repeated uint32 video_ids = 2; 34 | } 35 | 36 | message QueryVideosResponse { 37 | uint32 status_code = 1; 38 | optional string status_msg = 2; 39 | repeated Video video_list = 3; 40 | } 41 | 42 | service FeedService { 43 | rpc ListVideos(ListFeedRequest) returns (ListFeedResponse); 44 | rpc QueryVideos(QueryVideosRequest) returns (QueryVideosResponse); 45 | } 46 | -------------------------------------------------------------------------------- /idl/publish.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package douyin.publish; 3 | option go_package = "douyin/publish"; 4 | 5 | import "feed.proto"; 6 | 7 | message CreateVideoRequest { 8 | uint32 actor_id = 1; // 用户id 9 | bytes data = 2; // 视频数据 10 | string title = 3; // 视频标题 11 | } 12 | 13 | message CreateVideoResponse { 14 | uint32 status_code = 1 [json_name = "status_code"]; // 状态码,0-成功,其他值-失败 15 | string status_msg = 2 [json_name = "status_msg"]; // 返回状态描述 16 | } 17 | 18 | message ListVideoRequest{ 19 | uint32 user_id = 1; // 用户id 20 | uint32 actor_id = 2; // 发送请求的用户的id 21 | } 22 | 23 | message ListVideoResponse{ 24 | uint32 status_code = 1; // 状态码,0-成功,其他值-失败 25 | optional string status_msg = 2; // 返回状态描述 26 | repeated feed.Video video_list = 3; // 视频列表 27 | } 28 | 29 | message CountVideoRequest{ 30 | uint32 user_id = 1; // 用户id 31 | } 32 | 33 | message CountVideoResponse{ 34 | uint32 status_code = 1; // 状态码,0-成功,其他值-失败 35 | optional string status_msg = 2; // 返回状态描述 36 | uint32 count = 3; // 视频数量 37 | } 38 | 39 | service PublishService { 40 | rpc CreateVideo(CreateVideoRequest) returns (CreateVideoResponse) {} 41 | rpc ListVideo(ListVideoRequest) returns (ListVideoResponse) {} 42 | rpc CountVideo(CountVideoRequest) returns (CountVideoResponse) {} 43 | } 44 | -------------------------------------------------------------------------------- /idl/relation.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package douyin.user; 4 | option go_package = "douyin/relation"; 5 | 6 | import "user.proto"; 7 | 8 | message RelationActionRequest { 9 | uint32 actor_id = 1; // 当前登录用户 10 | uint32 user_id = 2; // 对方用户id 11 | } 12 | 13 | message RelationActionResponse { 14 | uint32 status_code = 1; // 状态码,0-成功,其他值-失败 15 | string status_msg = 2; // 返回状态描述 16 | } 17 | 18 | 19 | message FollowListRequest { 20 | uint32 actor_id = 1; // 当前登录用户id 21 | uint32 user_id = 2; // 对方用户id 22 | } 23 | 24 | message FollowListResponse { 25 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 26 | string status_msg = 2; // 返回状态描述 27 | repeated User user_list = 3; // 用户信息列表 28 | } 29 | 30 | message CountFollowListRequest { 31 | uint32 user_id = 1; // 用户id 32 | } 33 | 34 | message CountFollowListResponse { 35 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 36 | string status_msg = 2; // 返回状态描述 37 | uint32 count = 3; // 关注数 38 | } 39 | 40 | message FollowerListRequest { 41 | uint32 actor_id = 1; // 当前登录用户id 42 | uint32 user_id = 2; // 对方用户id 43 | } 44 | 45 | message FollowerListResponse { 46 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 47 | string status_msg = 2; // 返回状态描述 48 | repeated User user_list = 3; // 用户列表 49 | } 50 | 51 | message CountFollowerListRequest { 52 | uint32 user_id = 1; // 用户id 53 | } 54 | 55 | message CountFollowerListResponse { 56 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 57 | string status_msg = 2; // 返回状态描述 58 | uint32 count = 3; // 粉丝数 59 | } 60 | 61 | 62 | message FriendListRequest { 63 | uint32 actor_id = 1; // 当前登录用户id 64 | uint32 user_id = 2; // 对方用户id 65 | } 66 | 67 | message FriendListResponse { 68 | int32 status_code = 1; // 状态码,0-成功,其他值-失败 69 | string status_msg = 2; // 返回状态描述 70 | repeated User user_list = 3; // 用户列表 71 | } 72 | 73 | message IsFollowRequest { 74 | uint32 actor_id = 1; 75 | uint32 user_id = 2; 76 | } 77 | 78 | message IsFollowResponse { 79 | bool result = 1; // 结果 80 | } 81 | 82 | service RelationService { 83 | rpc Follow (RelationActionRequest) returns (RelationActionResponse) { 84 | } 85 | rpc Unfollow (RelationActionRequest) returns (RelationActionResponse) { 86 | } 87 | 88 | rpc GetFollowList (FollowListRequest) returns (FollowListResponse) { 89 | } 90 | rpc CountFollowList (CountFollowListRequest) returns (CountFollowListResponse) { 91 | } 92 | 93 | rpc GetFollowerList (FollowerListRequest) returns (FollowerListResponse) { 94 | } 95 | rpc CountFollowerList (CountFollowerListRequest) returns (CountFollowerListResponse) { 96 | } 97 | 98 | rpc GetFriendList (FriendListRequest) returns (FriendListResponse) { 99 | } 100 | rpc IsFollow (IsFollowRequest) returns (IsFollowResponse) { 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /idl/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package douyin.user; 3 | option go_package = "douyin/user"; 4 | 5 | message UserRequest { 6 | uint32 user_id = 1; // 用户id 7 | uint32 actor_id = 2; // 发送请求的用户的id 8 | } 9 | 10 | message UserResponse { 11 | uint32 status_code = 1; // 状态码,0-成功,其他值-失败 12 | optional string status_msg = 2; // 返回状态描述 13 | User user = 3; // 用户信息 14 | } 15 | 16 | message User { 17 | uint32 id = 1; // 用户id 18 | string name = 2; // 用户名称 19 | uint32 follow_count = 3; // 关注总数 20 | uint32 follower_count = 4; // 粉丝总数 21 | bool is_follow = 5; // true-已关注,false-未关注 22 | optional string avatar = 6; // 用户头像 23 | optional string background_image = 7; // 用户个人顶部大图 24 | optional string signature = 8; // 个人简介 25 | optional uint32 total_favorited = 9; // 获赞数量 26 | optional uint32 work_count = 10; // 作品数量 27 | optional uint32 favorite_count = 11; // 点赞数量 28 | } 29 | 30 | 31 | service UserService { 32 | rpc GetUser (UserRequest) returns (UserResponse) { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /idl/wechat.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package douyin.wechat; 3 | option go_package = "douyin/wechat"; 4 | 5 | message MessageChatRequest { 6 | uint32 sender_id = 1; 7 | uint32 receiver_id = 2; 8 | int64 pre_msg_time = 6; 9 | } 10 | 11 | message Message { 12 | uint32 id = 1; 13 | string content = 2; 14 | int64 create_time = 3; 15 | optional uint32 from_user_id = 4; 16 | optional uint32 to_user_id = 5; 17 | } 18 | 19 | message MessageChatResponse { 20 | uint32 status_code = 1; 21 | string status_msg = 2; 22 | repeated Message message_list = 3; 23 | } 24 | 25 | message MessageActionRequest { 26 | uint32 sender_id = 1; 27 | uint32 receiver_id = 2; 28 | uint32 action_type = 3; 29 | string content = 4; 30 | } 31 | 32 | message MessageActionResponse { 33 | uint32 status_code = 1; 34 | string status_msg = 2; 35 | } 36 | 37 | service WechatService { 38 | rpc WechatChat(MessageChatRequest) returns (MessageChatResponse) {} 39 | rpc WechatAction(MessageActionRequest) returns (MessageActionResponse) {} 40 | } 41 | -------------------------------------------------------------------------------- /kitex_gen/douyin/auth/authservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package authservice 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | auth "toktik/kitex_gen/douyin/auth" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | Authenticate(ctx context.Context, Req *auth.AuthenticateRequest, callOptions ...callopt.Option) (r *auth.AuthenticateResponse, err error) 15 | Register(ctx context.Context, Req *auth.RegisterRequest, callOptions ...callopt.Option) (r *auth.RegisterResponse, err error) 16 | Login(ctx context.Context, Req *auth.LoginRequest, callOptions ...callopt.Option) (r *auth.LoginResponse, err error) 17 | } 18 | 19 | // NewClient creates a client for the service defined in IDL. 20 | func NewClient(destService string, opts ...client.Option) (Client, error) { 21 | var options []client.Option 22 | options = append(options, client.WithDestService(destService)) 23 | 24 | options = append(options, opts...) 25 | 26 | kc, err := client.NewClient(serviceInfo(), options...) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &kAuthServiceClient{ 31 | kClient: newServiceClient(kc), 32 | }, nil 33 | } 34 | 35 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 36 | func MustNewClient(destService string, opts ...client.Option) Client { 37 | kc, err := NewClient(destService, opts...) 38 | if err != nil { 39 | panic(err) 40 | } 41 | return kc 42 | } 43 | 44 | type kAuthServiceClient struct { 45 | *kClient 46 | } 47 | 48 | func (p *kAuthServiceClient) Authenticate(ctx context.Context, Req *auth.AuthenticateRequest, callOptions ...callopt.Option) (r *auth.AuthenticateResponse, err error) { 49 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 50 | return p.kClient.Authenticate(ctx, Req) 51 | } 52 | 53 | func (p *kAuthServiceClient) Register(ctx context.Context, Req *auth.RegisterRequest, callOptions ...callopt.Option) (r *auth.RegisterResponse, err error) { 54 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 55 | return p.kClient.Register(ctx, Req) 56 | } 57 | 58 | func (p *kAuthServiceClient) Login(ctx context.Context, Req *auth.LoginRequest, callOptions ...callopt.Option) (r *auth.LoginResponse, err error) { 59 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 60 | return p.kClient.Login(ctx, Req) 61 | } 62 | -------------------------------------------------------------------------------- /kitex_gen/douyin/auth/authservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package authservice 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | auth "toktik/kitex_gen/douyin/auth" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler auth.AuthService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/douyin/auth/authservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package authservice 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | auth "toktik/kitex_gen/douyin/auth" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler auth.AuthService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/douyin/comment/commentservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package commentservice 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | comment "toktik/kitex_gen/douyin/comment" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | ActionComment(ctx context.Context, Req *comment.ActionCommentRequest, callOptions ...callopt.Option) (r *comment.ActionCommentResponse, err error) 15 | ListComment(ctx context.Context, Req *comment.ListCommentRequest, callOptions ...callopt.Option) (r *comment.ListCommentResponse, err error) 16 | CountComment(ctx context.Context, Req *comment.CountCommentRequest, callOptions ...callopt.Option) (r *comment.CountCommentResponse, err error) 17 | } 18 | 19 | // NewClient creates a client for the service defined in IDL. 20 | func NewClient(destService string, opts ...client.Option) (Client, error) { 21 | var options []client.Option 22 | options = append(options, client.WithDestService(destService)) 23 | 24 | options = append(options, opts...) 25 | 26 | kc, err := client.NewClient(serviceInfo(), options...) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &kCommentServiceClient{ 31 | kClient: newServiceClient(kc), 32 | }, nil 33 | } 34 | 35 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 36 | func MustNewClient(destService string, opts ...client.Option) Client { 37 | kc, err := NewClient(destService, opts...) 38 | if err != nil { 39 | panic(err) 40 | } 41 | return kc 42 | } 43 | 44 | type kCommentServiceClient struct { 45 | *kClient 46 | } 47 | 48 | func (p *kCommentServiceClient) ActionComment(ctx context.Context, Req *comment.ActionCommentRequest, callOptions ...callopt.Option) (r *comment.ActionCommentResponse, err error) { 49 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 50 | return p.kClient.ActionComment(ctx, Req) 51 | } 52 | 53 | func (p *kCommentServiceClient) ListComment(ctx context.Context, Req *comment.ListCommentRequest, callOptions ...callopt.Option) (r *comment.ListCommentResponse, err error) { 54 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 55 | return p.kClient.ListComment(ctx, Req) 56 | } 57 | 58 | func (p *kCommentServiceClient) CountComment(ctx context.Context, Req *comment.CountCommentRequest, callOptions ...callopt.Option) (r *comment.CountCommentResponse, err error) { 59 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 60 | return p.kClient.CountComment(ctx, Req) 61 | } 62 | -------------------------------------------------------------------------------- /kitex_gen/douyin/comment/commentservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package commentservice 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | comment "toktik/kitex_gen/douyin/comment" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler comment.CommentService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/douyin/comment/commentservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package commentservice 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | comment "toktik/kitex_gen/douyin/comment" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler comment.CommentService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/douyin/favorite/favoriteservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package favoriteservice 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | favorite "toktik/kitex_gen/douyin/favorite" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | FavoriteAction(ctx context.Context, Req *favorite.FavoriteRequest, callOptions ...callopt.Option) (r *favorite.FavoriteResponse, err error) 15 | FavoriteList(ctx context.Context, Req *favorite.FavoriteListRequest, callOptions ...callopt.Option) (r *favorite.FavoriteListResponse, err error) 16 | IsFavorite(ctx context.Context, Req *favorite.IsFavoriteRequest, callOptions ...callopt.Option) (r *favorite.IsFavoriteResponse, err error) 17 | CountFavorite(ctx context.Context, Req *favorite.CountFavoriteRequest, callOptions ...callopt.Option) (r *favorite.CountFavoriteResponse, err error) 18 | CountUserFavorite(ctx context.Context, Req *favorite.CountUserFavoriteRequest, callOptions ...callopt.Option) (r *favorite.CountUserFavoriteResponse, err error) 19 | CountUserTotalFavorited(ctx context.Context, Req *favorite.CountUserTotalFavoritedRequest, callOptions ...callopt.Option) (r *favorite.CountUserTotalFavoritedResponse, err error) 20 | } 21 | 22 | // NewClient creates a client for the service defined in IDL. 23 | func NewClient(destService string, opts ...client.Option) (Client, error) { 24 | var options []client.Option 25 | options = append(options, client.WithDestService(destService)) 26 | 27 | options = append(options, opts...) 28 | 29 | kc, err := client.NewClient(serviceInfo(), options...) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return &kFavoriteServiceClient{ 34 | kClient: newServiceClient(kc), 35 | }, nil 36 | } 37 | 38 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 39 | func MustNewClient(destService string, opts ...client.Option) Client { 40 | kc, err := NewClient(destService, opts...) 41 | if err != nil { 42 | panic(err) 43 | } 44 | return kc 45 | } 46 | 47 | type kFavoriteServiceClient struct { 48 | *kClient 49 | } 50 | 51 | func (p *kFavoriteServiceClient) FavoriteAction(ctx context.Context, Req *favorite.FavoriteRequest, callOptions ...callopt.Option) (r *favorite.FavoriteResponse, err error) { 52 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 53 | return p.kClient.FavoriteAction(ctx, Req) 54 | } 55 | 56 | func (p *kFavoriteServiceClient) FavoriteList(ctx context.Context, Req *favorite.FavoriteListRequest, callOptions ...callopt.Option) (r *favorite.FavoriteListResponse, err error) { 57 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 58 | return p.kClient.FavoriteList(ctx, Req) 59 | } 60 | 61 | func (p *kFavoriteServiceClient) IsFavorite(ctx context.Context, Req *favorite.IsFavoriteRequest, callOptions ...callopt.Option) (r *favorite.IsFavoriteResponse, err error) { 62 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 63 | return p.kClient.IsFavorite(ctx, Req) 64 | } 65 | 66 | func (p *kFavoriteServiceClient) CountFavorite(ctx context.Context, Req *favorite.CountFavoriteRequest, callOptions ...callopt.Option) (r *favorite.CountFavoriteResponse, err error) { 67 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 68 | return p.kClient.CountFavorite(ctx, Req) 69 | } 70 | 71 | func (p *kFavoriteServiceClient) CountUserFavorite(ctx context.Context, Req *favorite.CountUserFavoriteRequest, callOptions ...callopt.Option) (r *favorite.CountUserFavoriteResponse, err error) { 72 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 73 | return p.kClient.CountUserFavorite(ctx, Req) 74 | } 75 | 76 | func (p *kFavoriteServiceClient) CountUserTotalFavorited(ctx context.Context, Req *favorite.CountUserTotalFavoritedRequest, callOptions ...callopt.Option) (r *favorite.CountUserTotalFavoritedResponse, err error) { 77 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 78 | return p.kClient.CountUserTotalFavorited(ctx, Req) 79 | } 80 | -------------------------------------------------------------------------------- /kitex_gen/douyin/favorite/favoriteservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package favoriteservice 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | favorite "toktik/kitex_gen/douyin/favorite" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler favorite.FavoriteService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/douyin/favorite/favoriteservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package favoriteservice 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | favorite "toktik/kitex_gen/douyin/favorite" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler favorite.FavoriteService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/douyin/feed/feedservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package feedservice 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | feed "toktik/kitex_gen/douyin/feed" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | ListVideos(ctx context.Context, Req *feed.ListFeedRequest, callOptions ...callopt.Option) (r *feed.ListFeedResponse, err error) 15 | QueryVideos(ctx context.Context, Req *feed.QueryVideosRequest, callOptions ...callopt.Option) (r *feed.QueryVideosResponse, err error) 16 | } 17 | 18 | // NewClient creates a client for the service defined in IDL. 19 | func NewClient(destService string, opts ...client.Option) (Client, error) { 20 | var options []client.Option 21 | options = append(options, client.WithDestService(destService)) 22 | 23 | options = append(options, opts...) 24 | 25 | kc, err := client.NewClient(serviceInfo(), options...) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &kFeedServiceClient{ 30 | kClient: newServiceClient(kc), 31 | }, nil 32 | } 33 | 34 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 35 | func MustNewClient(destService string, opts ...client.Option) Client { 36 | kc, err := NewClient(destService, opts...) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return kc 41 | } 42 | 43 | type kFeedServiceClient struct { 44 | *kClient 45 | } 46 | 47 | func (p *kFeedServiceClient) ListVideos(ctx context.Context, Req *feed.ListFeedRequest, callOptions ...callopt.Option) (r *feed.ListFeedResponse, err error) { 48 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 49 | return p.kClient.ListVideos(ctx, Req) 50 | } 51 | 52 | func (p *kFeedServiceClient) QueryVideos(ctx context.Context, Req *feed.QueryVideosRequest, callOptions ...callopt.Option) (r *feed.QueryVideosResponse, err error) { 53 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 54 | return p.kClient.QueryVideos(ctx, Req) 55 | } 56 | -------------------------------------------------------------------------------- /kitex_gen/douyin/feed/feedservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package feedservice 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | feed "toktik/kitex_gen/douyin/feed" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler feed.FeedService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/douyin/feed/feedservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package feedservice 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | feed "toktik/kitex_gen/douyin/feed" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler feed.FeedService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/douyin/publish/publishservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package publishservice 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | publish "toktik/kitex_gen/douyin/publish" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | CreateVideo(ctx context.Context, Req *publish.CreateVideoRequest, callOptions ...callopt.Option) (r *publish.CreateVideoResponse, err error) 15 | ListVideo(ctx context.Context, Req *publish.ListVideoRequest, callOptions ...callopt.Option) (r *publish.ListVideoResponse, err error) 16 | CountVideo(ctx context.Context, Req *publish.CountVideoRequest, callOptions ...callopt.Option) (r *publish.CountVideoResponse, err error) 17 | } 18 | 19 | // NewClient creates a client for the service defined in IDL. 20 | func NewClient(destService string, opts ...client.Option) (Client, error) { 21 | var options []client.Option 22 | options = append(options, client.WithDestService(destService)) 23 | 24 | options = append(options, opts...) 25 | 26 | kc, err := client.NewClient(serviceInfo(), options...) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &kPublishServiceClient{ 31 | kClient: newServiceClient(kc), 32 | }, nil 33 | } 34 | 35 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 36 | func MustNewClient(destService string, opts ...client.Option) Client { 37 | kc, err := NewClient(destService, opts...) 38 | if err != nil { 39 | panic(err) 40 | } 41 | return kc 42 | } 43 | 44 | type kPublishServiceClient struct { 45 | *kClient 46 | } 47 | 48 | func (p *kPublishServiceClient) CreateVideo(ctx context.Context, Req *publish.CreateVideoRequest, callOptions ...callopt.Option) (r *publish.CreateVideoResponse, err error) { 49 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 50 | return p.kClient.CreateVideo(ctx, Req) 51 | } 52 | 53 | func (p *kPublishServiceClient) ListVideo(ctx context.Context, Req *publish.ListVideoRequest, callOptions ...callopt.Option) (r *publish.ListVideoResponse, err error) { 54 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 55 | return p.kClient.ListVideo(ctx, Req) 56 | } 57 | 58 | func (p *kPublishServiceClient) CountVideo(ctx context.Context, Req *publish.CountVideoRequest, callOptions ...callopt.Option) (r *publish.CountVideoResponse, err error) { 59 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 60 | return p.kClient.CountVideo(ctx, Req) 61 | } 62 | -------------------------------------------------------------------------------- /kitex_gen/douyin/publish/publishservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package publishservice 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | publish "toktik/kitex_gen/douyin/publish" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler publish.PublishService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/douyin/publish/publishservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package publishservice 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | publish "toktik/kitex_gen/douyin/publish" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler publish.PublishService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/douyin/relation/relationservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package relationservice 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | relation "toktik/kitex_gen/douyin/relation" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | Follow(ctx context.Context, Req *relation.RelationActionRequest, callOptions ...callopt.Option) (r *relation.RelationActionResponse, err error) 15 | Unfollow(ctx context.Context, Req *relation.RelationActionRequest, callOptions ...callopt.Option) (r *relation.RelationActionResponse, err error) 16 | GetFollowList(ctx context.Context, Req *relation.FollowListRequest, callOptions ...callopt.Option) (r *relation.FollowListResponse, err error) 17 | CountFollowList(ctx context.Context, Req *relation.CountFollowListRequest, callOptions ...callopt.Option) (r *relation.CountFollowListResponse, err error) 18 | GetFollowerList(ctx context.Context, Req *relation.FollowerListRequest, callOptions ...callopt.Option) (r *relation.FollowerListResponse, err error) 19 | CountFollowerList(ctx context.Context, Req *relation.CountFollowerListRequest, callOptions ...callopt.Option) (r *relation.CountFollowerListResponse, err error) 20 | GetFriendList(ctx context.Context, Req *relation.FriendListRequest, callOptions ...callopt.Option) (r *relation.FriendListResponse, err error) 21 | IsFollow(ctx context.Context, Req *relation.IsFollowRequest, callOptions ...callopt.Option) (r *relation.IsFollowResponse, err error) 22 | } 23 | 24 | // NewClient creates a client for the service defined in IDL. 25 | func NewClient(destService string, opts ...client.Option) (Client, error) { 26 | var options []client.Option 27 | options = append(options, client.WithDestService(destService)) 28 | 29 | options = append(options, opts...) 30 | 31 | kc, err := client.NewClient(serviceInfo(), options...) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return &kRelationServiceClient{ 36 | kClient: newServiceClient(kc), 37 | }, nil 38 | } 39 | 40 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 41 | func MustNewClient(destService string, opts ...client.Option) Client { 42 | kc, err := NewClient(destService, opts...) 43 | if err != nil { 44 | panic(err) 45 | } 46 | return kc 47 | } 48 | 49 | type kRelationServiceClient struct { 50 | *kClient 51 | } 52 | 53 | func (p *kRelationServiceClient) Follow(ctx context.Context, Req *relation.RelationActionRequest, callOptions ...callopt.Option) (r *relation.RelationActionResponse, err error) { 54 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 55 | return p.kClient.Follow(ctx, Req) 56 | } 57 | 58 | func (p *kRelationServiceClient) Unfollow(ctx context.Context, Req *relation.RelationActionRequest, callOptions ...callopt.Option) (r *relation.RelationActionResponse, err error) { 59 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 60 | return p.kClient.Unfollow(ctx, Req) 61 | } 62 | 63 | func (p *kRelationServiceClient) GetFollowList(ctx context.Context, Req *relation.FollowListRequest, callOptions ...callopt.Option) (r *relation.FollowListResponse, err error) { 64 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 65 | return p.kClient.GetFollowList(ctx, Req) 66 | } 67 | 68 | func (p *kRelationServiceClient) CountFollowList(ctx context.Context, Req *relation.CountFollowListRequest, callOptions ...callopt.Option) (r *relation.CountFollowListResponse, err error) { 69 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 70 | return p.kClient.CountFollowList(ctx, Req) 71 | } 72 | 73 | func (p *kRelationServiceClient) GetFollowerList(ctx context.Context, Req *relation.FollowerListRequest, callOptions ...callopt.Option) (r *relation.FollowerListResponse, err error) { 74 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 75 | return p.kClient.GetFollowerList(ctx, Req) 76 | } 77 | 78 | func (p *kRelationServiceClient) CountFollowerList(ctx context.Context, Req *relation.CountFollowerListRequest, callOptions ...callopt.Option) (r *relation.CountFollowerListResponse, err error) { 79 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 80 | return p.kClient.CountFollowerList(ctx, Req) 81 | } 82 | 83 | func (p *kRelationServiceClient) GetFriendList(ctx context.Context, Req *relation.FriendListRequest, callOptions ...callopt.Option) (r *relation.FriendListResponse, err error) { 84 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 85 | return p.kClient.GetFriendList(ctx, Req) 86 | } 87 | 88 | func (p *kRelationServiceClient) IsFollow(ctx context.Context, Req *relation.IsFollowRequest, callOptions ...callopt.Option) (r *relation.IsFollowResponse, err error) { 89 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 90 | return p.kClient.IsFollow(ctx, Req) 91 | } 92 | -------------------------------------------------------------------------------- /kitex_gen/douyin/relation/relationservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package relationservice 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | relation "toktik/kitex_gen/douyin/relation" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler relation.RelationService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/douyin/relation/relationservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package relationservice 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | relation "toktik/kitex_gen/douyin/relation" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler relation.RelationService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/douyin/user/userservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package userservice 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | user "toktik/kitex_gen/douyin/user" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | GetUser(ctx context.Context, Req *user.UserRequest, callOptions ...callopt.Option) (r *user.UserResponse, err error) 15 | } 16 | 17 | // NewClient creates a client for the service defined in IDL. 18 | func NewClient(destService string, opts ...client.Option) (Client, error) { 19 | var options []client.Option 20 | options = append(options, client.WithDestService(destService)) 21 | 22 | options = append(options, opts...) 23 | 24 | kc, err := client.NewClient(serviceInfo(), options...) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &kUserServiceClient{ 29 | kClient: newServiceClient(kc), 30 | }, nil 31 | } 32 | 33 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 34 | func MustNewClient(destService string, opts ...client.Option) Client { 35 | kc, err := NewClient(destService, opts...) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return kc 40 | } 41 | 42 | type kUserServiceClient struct { 43 | *kClient 44 | } 45 | 46 | func (p *kUserServiceClient) GetUser(ctx context.Context, Req *user.UserRequest, callOptions ...callopt.Option) (r *user.UserResponse, err error) { 47 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 48 | return p.kClient.GetUser(ctx, Req) 49 | } 50 | -------------------------------------------------------------------------------- /kitex_gen/douyin/user/userservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package userservice 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | user "toktik/kitex_gen/douyin/user" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler user.UserService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/douyin/user/userservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package userservice 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | user "toktik/kitex_gen/douyin/user" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler user.UserService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /kitex_gen/douyin/user/userservice/userservice.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package userservice 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | client "github.com/cloudwego/kitex/client" 9 | kitex "github.com/cloudwego/kitex/pkg/serviceinfo" 10 | streaming "github.com/cloudwego/kitex/pkg/streaming" 11 | proto "google.golang.org/protobuf/proto" 12 | user "toktik/kitex_gen/douyin/user" 13 | ) 14 | 15 | func serviceInfo() *kitex.ServiceInfo { 16 | return userServiceServiceInfo 17 | } 18 | 19 | var userServiceServiceInfo = NewServiceInfo() 20 | 21 | func NewServiceInfo() *kitex.ServiceInfo { 22 | serviceName := "UserService" 23 | handlerType := (*user.UserService)(nil) 24 | methods := map[string]kitex.MethodInfo{ 25 | "GetUser": kitex.NewMethodInfo(getUserHandler, newGetUserArgs, newGetUserResult, false), 26 | } 27 | extra := map[string]interface{}{ 28 | "PackageName": "douyin.user", 29 | } 30 | svcInfo := &kitex.ServiceInfo{ 31 | ServiceName: serviceName, 32 | HandlerType: handlerType, 33 | Methods: methods, 34 | PayloadCodec: kitex.Protobuf, 35 | KiteXGenVersion: "v0.4.4", 36 | Extra: extra, 37 | } 38 | return svcInfo 39 | } 40 | 41 | func getUserHandler(ctx context.Context, handler interface{}, arg, result interface{}) error { 42 | switch s := arg.(type) { 43 | case *streaming.Args: 44 | st := s.Stream 45 | req := new(user.UserRequest) 46 | if err := st.RecvMsg(req); err != nil { 47 | return err 48 | } 49 | resp, err := handler.(user.UserService).GetUser(ctx, req) 50 | if err != nil { 51 | return err 52 | } 53 | if err := st.SendMsg(resp); err != nil { 54 | return err 55 | } 56 | case *GetUserArgs: 57 | success, err := handler.(user.UserService).GetUser(ctx, s.Req) 58 | if err != nil { 59 | return err 60 | } 61 | realResult := result.(*GetUserResult) 62 | realResult.Success = success 63 | } 64 | return nil 65 | } 66 | func newGetUserArgs() interface{} { 67 | return &GetUserArgs{} 68 | } 69 | 70 | func newGetUserResult() interface{} { 71 | return &GetUserResult{} 72 | } 73 | 74 | type GetUserArgs struct { 75 | Req *user.UserRequest 76 | } 77 | 78 | func (p *GetUserArgs) FastRead(buf []byte, _type int8, number int32) (n int, err error) { 79 | if !p.IsSetReq() { 80 | p.Req = new(user.UserRequest) 81 | } 82 | return p.Req.FastRead(buf, _type, number) 83 | } 84 | 85 | func (p *GetUserArgs) FastWrite(buf []byte) (n int) { 86 | if !p.IsSetReq() { 87 | return 0 88 | } 89 | return p.Req.FastWrite(buf) 90 | } 91 | 92 | func (p *GetUserArgs) Size() (n int) { 93 | if !p.IsSetReq() { 94 | return 0 95 | } 96 | return p.Req.Size() 97 | } 98 | 99 | func (p *GetUserArgs) Marshal(out []byte) ([]byte, error) { 100 | if !p.IsSetReq() { 101 | return out, fmt.Errorf("No req in GetUserArgs") 102 | } 103 | return proto.Marshal(p.Req) 104 | } 105 | 106 | func (p *GetUserArgs) Unmarshal(in []byte) error { 107 | msg := new(user.UserRequest) 108 | if err := proto.Unmarshal(in, msg); err != nil { 109 | return err 110 | } 111 | p.Req = msg 112 | return nil 113 | } 114 | 115 | var GetUserArgs_Req_DEFAULT *user.UserRequest 116 | 117 | func (p *GetUserArgs) GetReq() *user.UserRequest { 118 | if !p.IsSetReq() { 119 | return GetUserArgs_Req_DEFAULT 120 | } 121 | return p.Req 122 | } 123 | 124 | func (p *GetUserArgs) IsSetReq() bool { 125 | return p.Req != nil 126 | } 127 | 128 | type GetUserResult struct { 129 | Success *user.UserResponse 130 | } 131 | 132 | var GetUserResult_Success_DEFAULT *user.UserResponse 133 | 134 | func (p *GetUserResult) FastRead(buf []byte, _type int8, number int32) (n int, err error) { 135 | if !p.IsSetSuccess() { 136 | p.Success = new(user.UserResponse) 137 | } 138 | return p.Success.FastRead(buf, _type, number) 139 | } 140 | 141 | func (p *GetUserResult) FastWrite(buf []byte) (n int) { 142 | if !p.IsSetSuccess() { 143 | return 0 144 | } 145 | return p.Success.FastWrite(buf) 146 | } 147 | 148 | func (p *GetUserResult) Size() (n int) { 149 | if !p.IsSetSuccess() { 150 | return 0 151 | } 152 | return p.Success.Size() 153 | } 154 | 155 | func (p *GetUserResult) Marshal(out []byte) ([]byte, error) { 156 | if !p.IsSetSuccess() { 157 | return out, fmt.Errorf("No req in GetUserResult") 158 | } 159 | return proto.Marshal(p.Success) 160 | } 161 | 162 | func (p *GetUserResult) Unmarshal(in []byte) error { 163 | msg := new(user.UserResponse) 164 | if err := proto.Unmarshal(in, msg); err != nil { 165 | return err 166 | } 167 | p.Success = msg 168 | return nil 169 | } 170 | 171 | func (p *GetUserResult) GetSuccess() *user.UserResponse { 172 | if !p.IsSetSuccess() { 173 | return GetUserResult_Success_DEFAULT 174 | } 175 | return p.Success 176 | } 177 | 178 | func (p *GetUserResult) SetSuccess(x interface{}) { 179 | p.Success = x.(*user.UserResponse) 180 | } 181 | 182 | func (p *GetUserResult) IsSetSuccess() bool { 183 | return p.Success != nil 184 | } 185 | 186 | type kClient struct { 187 | c client.Client 188 | } 189 | 190 | func newServiceClient(c client.Client) *kClient { 191 | return &kClient{ 192 | c: c, 193 | } 194 | } 195 | 196 | func (p *kClient) GetUser(ctx context.Context, Req *user.UserRequest) (r *user.UserResponse, err error) { 197 | var _args GetUserArgs 198 | _args.Req = Req 199 | var _result GetUserResult 200 | if err = p.c.Call(ctx, "GetUser", &_args, &_result); err != nil { 201 | return 202 | } 203 | return _result.GetSuccess(), nil 204 | } 205 | -------------------------------------------------------------------------------- /kitex_gen/douyin/wechat/wechatservice/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package wechatservice 4 | 5 | import ( 6 | "context" 7 | client "github.com/cloudwego/kitex/client" 8 | callopt "github.com/cloudwego/kitex/client/callopt" 9 | wechat "toktik/kitex_gen/douyin/wechat" 10 | ) 11 | 12 | // Client is designed to provide IDL-compatible methods with call-option parameter for kitex framework. 13 | type Client interface { 14 | WechatChat(ctx context.Context, Req *wechat.MessageChatRequest, callOptions ...callopt.Option) (r *wechat.MessageChatResponse, err error) 15 | WechatAction(ctx context.Context, Req *wechat.MessageActionRequest, callOptions ...callopt.Option) (r *wechat.MessageActionResponse, err error) 16 | } 17 | 18 | // NewClient creates a client for the service defined in IDL. 19 | func NewClient(destService string, opts ...client.Option) (Client, error) { 20 | var options []client.Option 21 | options = append(options, client.WithDestService(destService)) 22 | 23 | options = append(options, opts...) 24 | 25 | kc, err := client.NewClient(serviceInfo(), options...) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &kWechatServiceClient{ 30 | kClient: newServiceClient(kc), 31 | }, nil 32 | } 33 | 34 | // MustNewClient creates a client for the service defined in IDL. It panics if any error occurs. 35 | func MustNewClient(destService string, opts ...client.Option) Client { 36 | kc, err := NewClient(destService, opts...) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return kc 41 | } 42 | 43 | type kWechatServiceClient struct { 44 | *kClient 45 | } 46 | 47 | func (p *kWechatServiceClient) WechatChat(ctx context.Context, Req *wechat.MessageChatRequest, callOptions ...callopt.Option) (r *wechat.MessageChatResponse, err error) { 48 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 49 | return p.kClient.WechatChat(ctx, Req) 50 | } 51 | 52 | func (p *kWechatServiceClient) WechatAction(ctx context.Context, Req *wechat.MessageActionRequest, callOptions ...callopt.Option) (r *wechat.MessageActionResponse, err error) { 53 | ctx = client.NewCtxWithCallOptions(ctx, callOptions) 54 | return p.kClient.WechatAction(ctx, Req) 55 | } 56 | -------------------------------------------------------------------------------- /kitex_gen/douyin/wechat/wechatservice/invoker.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | 3 | package wechatservice 4 | 5 | import ( 6 | server "github.com/cloudwego/kitex/server" 7 | wechat "toktik/kitex_gen/douyin/wechat" 8 | ) 9 | 10 | // NewInvoker creates a server.Invoker with the given handler and options. 11 | func NewInvoker(handler wechat.WechatService, opts ...server.Option) server.Invoker { 12 | var options []server.Option 13 | 14 | options = append(options, opts...) 15 | 16 | s := server.NewInvoker(options...) 17 | if err := s.RegisterService(serviceInfo(), handler); err != nil { 18 | panic(err) 19 | } 20 | if err := s.Init(); err != nil { 21 | panic(err) 22 | } 23 | return s 24 | } 25 | -------------------------------------------------------------------------------- /kitex_gen/douyin/wechat/wechatservice/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by Kitex v0.4.4. DO NOT EDIT. 2 | package wechatservice 3 | 4 | import ( 5 | server "github.com/cloudwego/kitex/server" 6 | wechat "toktik/kitex_gen/douyin/wechat" 7 | ) 8 | 9 | // NewServer creates a server.Server with the given handler and options. 10 | func NewServer(handler wechat.WechatService, opts ...server.Option) server.Server { 11 | var options []server.Option 12 | 13 | options = append(options, opts...) 14 | 15 | svr := server.NewServer(options...) 16 | if err := svr.RegisterService(serviceInfo(), handler); err != nil { 17 | panic(err) 18 | } 19 | return svr 20 | } 21 | -------------------------------------------------------------------------------- /logging/init.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func init() { 10 | log.SetFormatter(&log.JSONFormatter{}) 11 | 12 | log.SetOutput(os.Stdout) 13 | 14 | if os.Getenv("ENV") == "prod" { 15 | log.SetLevel(log.WarnLevel) 16 | } else { 17 | log.SetLevel(log.DebugLevel) 18 | } 19 | } 20 | 21 | var env = os.Getenv("ENV") 22 | 23 | // Logger Add fields you want to log by default. 24 | var Logger = log.WithFields(log.Fields{ 25 | "ENV": env, 26 | }) 27 | -------------------------------------------------------------------------------- /manifests-dev/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: env-config 5 | namespace: toktik-service-bundle 6 | data: 7 | DREAM_APP_NAME: toktik-service-bundle 8 | DREAM_UNIT_NAME: toktik-service-bundle 9 | DREAM_APP_VERSION: "${CI_COMMIT_ID}" 10 | DREAM_APP_ROOT: /data/apps/toktik-service-bundle 11 | DREAM_CFG_ACCESS_KEY: '' 12 | DREAM_ENV: TESTING 13 | DREAM_REGION_NAME: tencent 14 | DREAM_SEC_APP_TOKEN: '' 15 | DREAM_SERVICE_DISCOVERY_URI: 'consul://consul-server.consul.svc.cluster.local:8500' 16 | DREAM_IMAGE_TAG: ${IMAGE_TAG} 17 | -------------------------------------------------------------------------------- /manifests-dev/deployment-toktik-auth-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | sidecar.jaegertracing.io/inject: 'true' 6 | labels: 7 | app: toktik-auth-api 8 | name: toktik-auth-api 9 | namespace: toktik-service-bundle 10 | spec: 11 | selector: 12 | matchLabels: 13 | name: toktik-auth-api 14 | template: 15 | metadata: 16 | labels: 17 | app: toktik-auth-api 18 | branch: dev 19 | version: ${BUILD_NUMBER}-${CI_COMMIT_ID} 20 | name: toktik-auth-api 21 | dream-app: toktik-auth-api 22 | dream-unit: toktik-auth-api 23 | spec: 24 | imagePullSecrets: 25 | - name: regcred 26 | containers: 27 | - image: ${IMAGE} 28 | imagePullPolicy: IfNotPresent 29 | name: toktik-auth-api 30 | command: 31 | - /bin/bash 32 | args: 33 | - bootstrap-auth.sh 34 | envFrom: 35 | - configMapRef: 36 | name: env-config 37 | - configMapRef: 38 | name: config 39 | ports: 40 | - name: grpc-40127 41 | containerPort: 40127 42 | protocol: TCP 43 | resources: 44 | limits: 45 | cpu: 500m 46 | memory: 256Mi 47 | requests: 48 | cpu: 100m 49 | memory: 8Mi 50 | terminationGracePeriodSeconds: 30 51 | -------------------------------------------------------------------------------- /manifests-dev/deployment-toktik-comment-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | sidecar.jaegertracing.io/inject: 'true' 6 | labels: 7 | app: toktik-comment-api 8 | name: toktik-comment-api 9 | namespace: toktik-service-bundle 10 | spec: 11 | selector: 12 | matchLabels: 13 | name: toktik-comment-api 14 | template: 15 | metadata: 16 | labels: 17 | app: toktik-comment-api 18 | branch: dev 19 | version: ${BUILD_NUMBER}-${CI_COMMIT_ID} 20 | name: toktik-comment-api 21 | dream-app: toktik-comment-api 22 | dream-unit: toktik-comment-api 23 | spec: 24 | imagePullSecrets: 25 | - name: regcred 26 | containers: 27 | - image: ${IMAGE} 28 | imagePullPolicy: IfNotPresent 29 | name: toktik-comment-api 30 | command: 31 | - /bin/bash 32 | args: 33 | - bootstrap-comment.sh 34 | envFrom: 35 | - configMapRef: 36 | name: env-config 37 | - configMapRef: 38 | name: config 39 | ports: 40 | - name: grpc-40131 41 | containerPort: 40131 42 | protocol: TCP 43 | resources: 44 | limits: 45 | cpu: 500m 46 | memory: 256Mi 47 | requests: 48 | cpu: 100m 49 | memory: 8Mi 50 | terminationGracePeriodSeconds: 30 51 | -------------------------------------------------------------------------------- /manifests-dev/deployment-toktik-favorite-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | sidecar.jaegertracing.io/inject: 'true' 6 | labels: 7 | app: toktik-favorite-api 8 | name: toktik-favorite-api 9 | namespace: toktik-service-bundle 10 | spec: 11 | selector: 12 | matchLabels: 13 | name: toktik-favorite-api 14 | template: 15 | metadata: 16 | labels: 17 | app: toktik-favorite-api 18 | branch: dev 19 | version: ${BUILD_NUMBER}-${CI_COMMIT_ID} 20 | name: toktik-favorite-api 21 | dream-app: toktik-favorite-api 22 | dream-unit: toktik-favorite-api 23 | spec: 24 | imagePullSecrets: 25 | - name: regcred 26 | containers: 27 | - image: ${IMAGE} 28 | imagePullPolicy: IfNotPresent 29 | name: toktik-favorite-api 30 | command: 31 | - /bin/bash 32 | args: 33 | - bootstrap-favorite.sh 34 | envFrom: 35 | - configMapRef: 36 | name: env-config 37 | - configMapRef: 38 | name: config 39 | ports: 40 | - name: grpc-40134 41 | containerPort: 40134 42 | protocol: TCP 43 | resources: 44 | limits: 45 | cpu: 500m 46 | memory: 256Mi 47 | requests: 48 | cpu: 100m 49 | memory: 8Mi 50 | terminationGracePeriodSeconds: 30 51 | -------------------------------------------------------------------------------- /manifests-dev/deployment-toktik-feed-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | sidecar.jaegertracing.io/inject: 'true' 6 | labels: 7 | app: toktik-feed-api 8 | name: toktik-feed-api 9 | namespace: toktik-service-bundle 10 | spec: 11 | selector: 12 | matchLabels: 13 | name: toktik-feed-api 14 | template: 15 | metadata: 16 | labels: 17 | app: toktik-feed-api 18 | branch: dev 19 | version: ${BUILD_NUMBER}-${CI_COMMIT_ID} 20 | name: toktik-feed-api 21 | dream-app: toktik-feed-api 22 | dream-unit: toktik-feed-api 23 | spec: 24 | imagePullSecrets: 25 | - name: regcred 26 | containers: 27 | - image: ${IMAGE} 28 | imagePullPolicy: IfNotPresent 29 | name: toktik-feed-api 30 | command: 31 | - /bin/bash 32 | args: 33 | - bootstrap-feed.sh 34 | envFrom: 35 | - configMapRef: 36 | name: env-config 37 | - configMapRef: 38 | name: config 39 | ports: 40 | - name: grpc-40129 41 | containerPort: 40129 42 | protocol: TCP 43 | resources: 44 | limits: 45 | cpu: 500m 46 | memory: 256Mi 47 | requests: 48 | cpu: 100m 49 | memory: 8Mi 50 | terminationGracePeriodSeconds: 30 51 | -------------------------------------------------------------------------------- /manifests-dev/deployment-toktik-http-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | sidecar.jaegertracing.io/inject: 'true' 6 | labels: 7 | app: toktik-http-api 8 | name: toktik-http-api 9 | namespace: toktik-service-bundle 10 | spec: 11 | selector: 12 | matchLabels: 13 | name: toktik-http-api 14 | template: 15 | metadata: 16 | labels: 17 | app: toktik-http-api 18 | branch: dev 19 | version: ${BUILD_NUMBER}-${CI_COMMIT_ID} 20 | name: toktik-http-api 21 | dream-app: toktik-http-api 22 | dream-unit: toktik-http-api 23 | spec: 24 | imagePullSecrets: 25 | - name: regcred 26 | containers: 27 | - image: ${IMAGE} 28 | imagePullPolicy: IfNotPresent 29 | name: toktik-http-api 30 | command: 31 | - bin/web 32 | envFrom: 33 | - configMapRef: 34 | name: env-config 35 | - configMapRef: 36 | name: config 37 | ports: 38 | - name: http-40126 39 | containerPort: 40126 40 | protocol: TCP 41 | resources: 42 | limits: 43 | cpu: 500m 44 | memory: 256Mi 45 | requests: 46 | cpu: 100m 47 | memory: 8Mi 48 | terminationGracePeriodSeconds: 30 49 | -------------------------------------------------------------------------------- /manifests-dev/deployment-toktik-publish-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | sidecar.jaegertracing.io/inject: 'true' 6 | labels: 7 | app: toktik-publish-api 8 | name: toktik-publish-api 9 | namespace: toktik-service-bundle 10 | spec: 11 | selector: 12 | matchLabels: 13 | name: toktik-publish-api 14 | template: 15 | metadata: 16 | labels: 17 | app: toktik-publish-api 18 | branch: dev 19 | version: ${BUILD_NUMBER}-${CI_COMMIT_ID} 20 | name: toktik-publish-api 21 | dream-app: toktik-publish-api 22 | dream-unit: toktik-publish-api 23 | spec: 24 | volumes: 25 | - name: volume-storage 26 | persistentVolumeClaim: 27 | claimName: storage 28 | imagePullSecrets: 29 | - name: regcred 30 | containers: 31 | - image: ${IMAGE} 32 | imagePullPolicy: IfNotPresent 33 | name: toktik-publish-api 34 | command: 35 | - /bin/bash 36 | args: 37 | - bootstrap-publish.sh 38 | envFrom: 39 | - configMapRef: 40 | name: env-config 41 | - configMapRef: 42 | name: config 43 | ports: 44 | - name: grpc-40128 45 | containerPort: 40128 46 | protocol: TCP 47 | resources: 48 | limits: 49 | cpu: 500m 50 | memory: 256Mi 51 | requests: 52 | cpu: 100m 53 | memory: 8Mi 54 | volumeMounts: 55 | - name: volume-storage 56 | mountPath: /data/storage 57 | terminationGracePeriodSeconds: 30 58 | -------------------------------------------------------------------------------- /manifests-dev/deployment-toktik-relation-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | sidecar.jaegertracing.io/inject: 'true' 6 | labels: 7 | app: toktik-relation-api 8 | name: toktik-relation-api 9 | namespace: toktik-service-bundle 10 | spec: 11 | selector: 12 | matchLabels: 13 | name: toktik-relation-api 14 | template: 15 | metadata: 16 | labels: 17 | app: toktik-relation-api 18 | branch: dev 19 | version: ${BUILD_NUMBER}-${CI_COMMIT_ID} 20 | name: toktik-relation-api 21 | dream-app: toktik-relation-api 22 | dream-unit: toktik-relation-api 23 | spec: 24 | volumes: 25 | - name: volume-storage 26 | persistentVolumeClaim: 27 | claimName: storage 28 | imagePullSecrets: 29 | - name: regcred 30 | containers: 31 | - image: ${IMAGE} 32 | imagePullPolicy: IfNotPresent 33 | name: toktik-relation-api 34 | command: 35 | - /bin/bash 36 | args: 37 | - bootstrap-relation.sh 38 | envFrom: 39 | - configMapRef: 40 | name: env-config 41 | - configMapRef: 42 | name: config 43 | ports: 44 | - name: grpc-40133 45 | containerPort: 40133 46 | protocol: TCP 47 | resources: 48 | limits: 49 | cpu: 500m 50 | memory: 256Mi 51 | requests: 52 | cpu: 100m 53 | memory: 8Mi 54 | volumeMounts: 55 | - name: volume-storage 56 | mountPath: /data/storage 57 | terminationGracePeriodSeconds: 30 58 | -------------------------------------------------------------------------------- /manifests-dev/deployment-toktik-user-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | sidecar.jaegertracing.io/inject: 'true' 6 | labels: 7 | app: toktik-user-api 8 | name: toktik-user-api 9 | namespace: toktik-service-bundle 10 | spec: 11 | selector: 12 | matchLabels: 13 | name: toktik-user-api 14 | template: 15 | metadata: 16 | labels: 17 | app: toktik-user-api 18 | branch: dev 19 | version: ${BUILD_NUMBER}-${CI_COMMIT_ID} 20 | name: toktik-user-api 21 | dream-app: toktik-user-api 22 | dream-unit: toktik-user-api 23 | spec: 24 | imagePullSecrets: 25 | - name: regcred 26 | containers: 27 | - image: ${IMAGE} 28 | imagePullPolicy: IfNotPresent 29 | name: toktik-user-api 30 | command: 31 | - /bin/bash 32 | args: 33 | - bootstrap-user.sh 34 | envFrom: 35 | - configMapRef: 36 | name: env-config 37 | - configMapRef: 38 | name: config 39 | ports: 40 | - name: grpc-40130 41 | containerPort: 40130 42 | protocol: TCP 43 | resources: 44 | limits: 45 | cpu: 500m 46 | memory: 256Mi 47 | requests: 48 | cpu: 100m 49 | memory: 8Mi 50 | terminationGracePeriodSeconds: 30 51 | -------------------------------------------------------------------------------- /manifests-dev/deployment-toktik-wechat-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | sidecar.jaegertracing.io/inject: 'true' 6 | labels: 7 | app: toktik-wechat-api 8 | name: toktik-wechat-api 9 | namespace: toktik-service-bundle 10 | spec: 11 | selector: 12 | matchLabels: 13 | name: toktik-wechat-api 14 | template: 15 | metadata: 16 | labels: 17 | app: toktik-wechat-api 18 | branch: dev 19 | version: ${BUILD_NUMBER}-${CI_COMMIT_ID} 20 | name: toktik-wechat-api 21 | dream-app: toktik-wechat-api 22 | dream-unit: toktik-wechat-api 23 | spec: 24 | imagePullSecrets: 25 | - name: regcred 26 | containers: 27 | - image: ${IMAGE} 28 | imagePullPolicy: IfNotPresent 29 | name: toktik-wechat-api 30 | command: 31 | - /bin/bash 32 | args: 33 | - bootstrap-wechat.sh 34 | envFrom: 35 | - configMapRef: 36 | name: env-config 37 | - configMapRef: 38 | name: config 39 | ports: 40 | - name: grpc-40132 41 | containerPort: 40132 42 | protocol: TCP 43 | resources: 44 | limits: 45 | cpu: 500m 46 | memory: 256Mi 47 | requests: 48 | cpu: 100m 49 | memory: 8Mi 50 | terminationGracePeriodSeconds: 30 51 | -------------------------------------------------------------------------------- /manifests-dev/service-toktik-auth-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: toktik-auth-api 6 | name: toktik-auth-api 7 | namespace: toktik-service-bundle 8 | spec: 9 | ports: 10 | - name: grpc 11 | port: 40127 12 | protocol: TCP 13 | targetPort: 40127 14 | selector: 15 | name: toktik-auth-api 16 | branch: dev 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /manifests-dev/service-toktik-comment-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: toktik-comment-api 6 | name: toktik-comment-api 7 | namespace: toktik-service-bundle 8 | spec: 9 | ports: 10 | - name: grpc 11 | port: 40131 12 | protocol: TCP 13 | targetPort: 40131 14 | selector: 15 | name: toktik-comment-api 16 | branch: dev 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /manifests-dev/service-toktik-favorite-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: toktik-favorite-api 6 | name: toktik-favorite-api 7 | namespace: toktik-service-bundle 8 | spec: 9 | ports: 10 | - name: grpc 11 | port: 40134 12 | protocol: TCP 13 | targetPort: 40134 14 | selector: 15 | name: toktik-favorite-api 16 | branch: dev 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /manifests-dev/service-toktik-feed-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: toktik-feed-api 6 | name: toktik-feed-api 7 | namespace: toktik-service-bundle 8 | spec: 9 | ports: 10 | - name: grpc 11 | port: 40129 12 | protocol: TCP 13 | targetPort: 40129 14 | selector: 15 | name: toktik-feed-api 16 | branch: dev 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /manifests-dev/service-toktik-http-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: toktik-http-api 6 | name: toktik-http-api 7 | namespace: toktik-service-bundle 8 | spec: 9 | ports: 10 | - name: http 11 | port: 40126 12 | protocol: TCP 13 | targetPort: 40126 14 | selector: 15 | name: toktik-http-api 16 | branch: dev 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /manifests-dev/service-toktik-publish-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: toktik-publish-api 6 | name: toktik-publish-api 7 | namespace: toktik-service-bundle 8 | spec: 9 | ports: 10 | - name: grpc 11 | port: 40128 12 | protocol: TCP 13 | targetPort: 40128 14 | selector: 15 | name: toktik-publish-api 16 | branch: dev 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /manifests-dev/service-toktik-relation-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: toktik-relation-api 6 | name: toktik-relation-api 7 | namespace: toktik-service-bundle 8 | spec: 9 | ports: 10 | - name: grpc 11 | port: 40133 12 | protocol: TCP 13 | targetPort: 40133 14 | selector: 15 | name: toktik-relation-api 16 | branch: dev 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /manifests-dev/service-toktik-user-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: toktik-user-api 6 | name: toktik-user-api 7 | namespace: toktik-service-bundle 8 | spec: 9 | ports: 10 | - name: grpc 11 | port: 40130 12 | protocol: TCP 13 | targetPort: 40130 14 | selector: 15 | name: toktik-user-api 16 | branch: dev 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /manifests-dev/service-toktik-wechat-api.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: toktik-wechat-api 6 | name: toktik-wechat-api 7 | namespace: toktik-service-bundle 8 | spec: 9 | ports: 10 | - name: grpc 11 | port: 40132 12 | protocol: TCP 13 | targetPort: 40132 14 | selector: 15 | name: toktik-wechat-api 16 | branch: dev 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /otel.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | 6 | exporters: 7 | prometheusremotewrite: 8 | endpoint: "http://victoriametrics:8428/api/v1/write" 9 | 10 | logging: 11 | 12 | jaeger: 13 | endpoint: jaeger-all-in-one:14250 14 | tls: 15 | insecure: true 16 | 17 | processors: 18 | batch: 19 | 20 | extensions: 21 | health_check: 22 | pprof: 23 | endpoint: :1888 24 | zpages: 25 | endpoint: :55679 26 | 27 | service: 28 | extensions: [ pprof, zpages, health_check ] 29 | pipelines: 30 | traces: 31 | receivers: [ otlp ] 32 | processors: [ batch ] 33 | exporters: [ logging, jaeger ] 34 | metrics: 35 | receivers: [ otlp ] 36 | processors: [ batch ] 37 | exporters: [ logging, prometheusremotewrite ] 38 | -------------------------------------------------------------------------------- /repo/gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package repo 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | 11 | "gorm.io/gorm" 12 | 13 | "gorm.io/gen" 14 | 15 | "gorm.io/plugin/dbresolver" 16 | ) 17 | 18 | var ( 19 | Q = new(Query) 20 | Comment *comment 21 | Favorite *favorite 22 | Relation *relation 23 | User *user 24 | UserToken *userToken 25 | Video *video 26 | ) 27 | 28 | func SetDefault(db *gorm.DB, opts ...gen.DOOption) { 29 | *Q = *Use(db, opts...) 30 | Comment = &Q.Comment 31 | Favorite = &Q.Favorite 32 | Relation = &Q.Relation 33 | User = &Q.User 34 | UserToken = &Q.UserToken 35 | Video = &Q.Video 36 | } 37 | 38 | func Use(db *gorm.DB, opts ...gen.DOOption) *Query { 39 | return &Query{ 40 | db: db, 41 | Comment: newComment(db, opts...), 42 | Favorite: newFavorite(db, opts...), 43 | Relation: newRelation(db, opts...), 44 | User: newUser(db, opts...), 45 | UserToken: newUserToken(db, opts...), 46 | Video: newVideo(db, opts...), 47 | } 48 | } 49 | 50 | type Query struct { 51 | db *gorm.DB 52 | 53 | Comment comment 54 | Favorite favorite 55 | Relation relation 56 | User user 57 | UserToken userToken 58 | Video video 59 | } 60 | 61 | func (q *Query) Available() bool { return q.db != nil } 62 | 63 | func (q *Query) clone(db *gorm.DB) *Query { 64 | return &Query{ 65 | db: db, 66 | Comment: q.Comment.clone(db), 67 | Favorite: q.Favorite.clone(db), 68 | Relation: q.Relation.clone(db), 69 | User: q.User.clone(db), 70 | UserToken: q.UserToken.clone(db), 71 | Video: q.Video.clone(db), 72 | } 73 | } 74 | 75 | func (q *Query) ReadDB() *Query { 76 | return q.ReplaceDB(q.db.Clauses(dbresolver.Read)) 77 | } 78 | 79 | func (q *Query) WriteDB() *Query { 80 | return q.ReplaceDB(q.db.Clauses(dbresolver.Write)) 81 | } 82 | 83 | func (q *Query) ReplaceDB(db *gorm.DB) *Query { 84 | return &Query{ 85 | db: db, 86 | Comment: q.Comment.replaceDB(db), 87 | Favorite: q.Favorite.replaceDB(db), 88 | Relation: q.Relation.replaceDB(db), 89 | User: q.User.replaceDB(db), 90 | UserToken: q.UserToken.replaceDB(db), 91 | Video: q.Video.replaceDB(db), 92 | } 93 | } 94 | 95 | type queryCtx struct { 96 | Comment ICommentDo 97 | Favorite IFavoriteDo 98 | Relation IRelationDo 99 | User IUserDo 100 | UserToken IUserTokenDo 101 | Video IVideoDo 102 | } 103 | 104 | func (q *Query) WithContext(ctx context.Context) *queryCtx { 105 | return &queryCtx{ 106 | Comment: q.Comment.WithContext(ctx), 107 | Favorite: q.Favorite.WithContext(ctx), 108 | Relation: q.Relation.WithContext(ctx), 109 | User: q.User.WithContext(ctx), 110 | UserToken: q.UserToken.WithContext(ctx), 111 | Video: q.Video.WithContext(ctx), 112 | } 113 | } 114 | 115 | func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) error { 116 | return q.db.Transaction(func(tx *gorm.DB) error { return fc(q.clone(tx)) }, opts...) 117 | } 118 | 119 | func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx { 120 | return &QueryTx{q.clone(q.db.Begin(opts...))} 121 | } 122 | 123 | type QueryTx struct{ *Query } 124 | 125 | func (q *QueryTx) Commit() error { 126 | return q.db.Commit().Error 127 | } 128 | 129 | func (q *QueryTx) Rollback() error { 130 | return q.db.Rollback().Error 131 | } 132 | 133 | func (q *QueryTx) SavePoint(name string) error { 134 | return q.db.SavePoint(name).Error 135 | } 136 | 137 | func (q *QueryTx) RollbackTo(name string) error { 138 | return q.db.RollbackTo(name).Error 139 | } 140 | -------------------------------------------------------------------------------- /repo/gen/gormGen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "toktik/constant/config" 5 | "toktik/repo/model" 6 | 7 | "gorm.io/driver/postgres" 8 | "gorm.io/gen" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | // Querier Dynamic SQL 13 | type Querier interface { 14 | // FilterWithNameAndRole SELECT * FROM @@table WHERE name = @name{{if role !=""}} AND role = @role{{end}} 15 | FilterWithNameAndRole(name, role string) ([]gen.T, error) 16 | } 17 | 18 | func main() { 19 | g := gen.NewGenerator(gen.Config{ 20 | OutPath: "repo", 21 | Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode 22 | }) 23 | 24 | gormdb, _ := gorm.Open(postgres.Open(config.EnvConfig.GetDSN())) 25 | g.UseDB(gormdb) // reuse your gorm db 26 | 27 | // Generate basic type-safe DAO API for struct `model.User` following conventions 28 | g.ApplyBasic(model.UserToken{}, model.Video{}, model.User{}, model.Comment{}, model.Relation{}, model.Favorite{}) 29 | 30 | // Generate Type Safe API with Dynamic SQL defined on Querier interface 31 | g.ApplyInterface(func(Querier) {}, model.UserToken{}, model.Video{}, model.User{}, model.Comment{}) 32 | 33 | // Generate the code 34 | g.Execute() 35 | } 36 | -------------------------------------------------------------------------------- /repo/init.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "time" 5 | "toktik/constant/config" 6 | 7 | "gorm.io/driver/postgres" 8 | "gorm.io/gorm" 9 | "gorm.io/gorm/logger" 10 | "gorm.io/plugin/opentelemetry/logging/logrus" 11 | "gorm.io/plugin/opentelemetry/tracing" 12 | ) 13 | 14 | var DB *gorm.DB 15 | 16 | func init() { 17 | var err error 18 | gormlogrus := logger.New( 19 | logrus.NewWriter(), 20 | logger.Config{ 21 | SlowThreshold: time.Millisecond, 22 | Colorful: false, 23 | LogLevel: logger.Info, 24 | }, 25 | ) 26 | DB, err = gorm.Open( 27 | postgres.Open(config.EnvConfig.GetDSN()), 28 | &gorm.Config{ 29 | PrepareStmt: true, 30 | Logger: gormlogrus, 31 | }, 32 | ) 33 | SetDefault(DB) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | if err := DB.Use(tracing.NewPlugin()); err != nil { 39 | panic(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /repo/migrate/migrate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "toktik/constant/config" 6 | "toktik/repo/model" 7 | 8 | "gorm.io/driver/postgres" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func main() { 13 | var err error 14 | db, err := gorm.Open( 15 | postgres.New( 16 | postgres.Config{ 17 | DSN: config.EnvConfig.GetDSN(), 18 | }), &gorm.Config{ 19 | DisableForeignKeyConstraintWhenMigrating: true, 20 | }) 21 | if err != nil { 22 | panic(fmt.Errorf("db connection failed: %v", err)) 23 | } 24 | err = db.AutoMigrate(&model.UserToken{}, &model.User{}, &model.Video{}, &model.Comment{}, &model.Relation{}, &model.Favorite{}) 25 | if err != nil { 26 | panic(fmt.Errorf("db migrate failed: %v", err)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /repo/model/comment.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Comment 评论表 /* 4 | type Comment struct { 5 | Model 6 | CommentId uint32 `json:"comment_id" column:"comment_id" gorm:"not null;index:comment_video"` 7 | VideoId uint32 `json:"video_id" column:"video_id" gorm:"not null;index:comment_video"` 8 | UserId uint32 `json:"user_id" column:"user_id" gorm:"not null"` 9 | Content string `json:"content" column:"content"` 10 | } 11 | -------------------------------------------------------------------------------- /repo/model/favorite.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Favorite 点赞表 /* 4 | type Favorite struct { 5 | Model 6 | UserId uint32 `gorm:"not null;uniqueIndex:user_video"` 7 | VideoId uint32 `gorm:"not null;uniqueIndex:user_video;index:video"` 8 | } 9 | -------------------------------------------------------------------------------- /repo/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type Model struct { 10 | ID uint32 `gorm:"primaryKey"` 11 | CreatedAt time.Time 12 | UpdatedAt time.Time 13 | DeletedAt gorm.DeletedAt `gorm:"index"` 14 | } 15 | -------------------------------------------------------------------------------- /repo/model/relation.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Relation 关系表 /* 4 | type Relation struct { 5 | Model // 基础模型 6 | UserId uint32 `gorm:"not null;uniqueIndex:relation_user_target"` // 用户ID 7 | TargetId uint32 `gorm:"not null;uniqueIndex:relation_user_target"` // 目标ID 8 | } 9 | -------------------------------------------------------------------------------- /repo/model/token.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // UserToken 用户令牌儿表 /* 4 | type UserToken struct { 5 | Token string `gorm:"not null;primaryKey"` 6 | Username string `gorm:"not null;unique;size: 32"` // 用户名 7 | UserID uint32 `gorm:"not null;index"` 8 | Role string `gorm:"not null;default:0"` // 用户角色 9 | } 10 | -------------------------------------------------------------------------------- /repo/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "strings" 11 | "toktik/constant/config" 12 | "toktik/logging" 13 | "toktik/rpc" 14 | ) 15 | 16 | // User 用户表 /* 17 | type User struct { 18 | Model // 基础模型 19 | Username string `gorm:"not null;unique;size: 32;index"` // 用户名 20 | Password *string `gorm:"not null;size: 32"` // 密码 21 | Avatar *string // 用户头像 22 | BackgroundImage *string // 背景图片 23 | Signature *string // 个人简介 24 | 25 | updated bool 26 | } 27 | 28 | func (u *User) IsUpdated() bool { 29 | return u.updated 30 | } 31 | 32 | func (u *User) isEmail() bool { 33 | parts := strings.Split(u.Username, "@") 34 | if len(parts) != 2 { 35 | return false 36 | } 37 | 38 | userPart := parts[0] 39 | if len(userPart) == 0 { 40 | return false 41 | } 42 | 43 | domainPart := parts[1] 44 | if len(domainPart) < 3 { 45 | return false 46 | } 47 | if !strings.Contains(domainPart, ".") { 48 | return false 49 | } 50 | if strings.HasPrefix(domainPart, ".") || strings.HasSuffix(domainPart, ".") { 51 | return false 52 | } 53 | 54 | return true 55 | } 56 | 57 | func getEmailMD5(email string) (md5String string) { 58 | lowerEmail := strings.ToLower(email) 59 | hasher := md5.New() 60 | hasher.Write([]byte(lowerEmail)) 61 | md5Bytes := hasher.Sum(nil) 62 | md5String = hex.EncodeToString(md5Bytes) 63 | return 64 | } 65 | 66 | type unsplashResponse []struct { 67 | Urls struct { 68 | Regular string `json:"regular"` 69 | } `json:"urls"` 70 | } 71 | 72 | func getImageFromUnsplash(query string) (url string, err error) { 73 | unsplashUrl := fmt.Sprintf("https://api.unsplash.com/photos/random?query=%s&count=1", query) 74 | 75 | resp, err := rpc.HttpRequest("GET", unsplashUrl, nil, rpc.WithAuthorizationHeader("Client-ID "+config.EnvConfig.UNSPLASH_ACCESS_KEY)) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | if resp.Body != nil { 81 | defer func(Body io.ReadCloser) { 82 | err := Body.Close() 83 | if err != nil { 84 | logging.Logger.Errorf("getImageFromUnsplash: %v", err) 85 | } 86 | }(resp.Body) 87 | } 88 | body, _ := io.ReadAll(resp.Body) 89 | 90 | var response unsplashResponse 91 | err = json.Unmarshal(body, &response) 92 | if err != nil { 93 | return "", err 94 | } 95 | 96 | url = response[0].Urls.Regular 97 | 98 | if url == "" { 99 | return "", fmt.Errorf("getImageFromUnsplash: url is empty") 100 | } 101 | return 102 | } 103 | 104 | func getCravatarUrl(email string) string { 105 | return `https://cravatar.cn/avatar/` + getEmailMD5(email) + `?d=` + "identicon" 106 | } 107 | 108 | func (u *User) GetBackgroundImage() (url string) { 109 | if u.BackgroundImage != nil && *u.BackgroundImage != "" { 110 | return *u.BackgroundImage 111 | } 112 | 113 | defer func() { 114 | u.BackgroundImage = &url 115 | u.updated = true 116 | }() 117 | 118 | unsplashURL, err := getImageFromUnsplash(u.Username) 119 | if err != nil { 120 | catURL, err := getImageFromUnsplash("cat") 121 | if err != nil { 122 | return getCravatarUrl(u.Username) 123 | } 124 | return catURL 125 | } 126 | return unsplashURL 127 | } 128 | 129 | func (u *User) GetUserAvatar() (url string) { 130 | if u.Avatar != nil && *u.Avatar != "" { 131 | return *u.Avatar 132 | } 133 | 134 | defer func() { 135 | u.Avatar = &url 136 | u.updated = true 137 | }() 138 | 139 | if u.isEmail() { 140 | return getCravatarUrl(u.Username) 141 | } 142 | 143 | unsplashURL, err := getImageFromUnsplash(u.Username) 144 | if err != nil || unsplashURL == "" { 145 | catURL, err := getImageFromUnsplash("cat") 146 | if err != nil || catURL == "" { 147 | return getCravatarUrl(u.Username) 148 | } 149 | return catURL 150 | } 151 | 152 | return unsplashURL 153 | } 154 | 155 | func (u *User) GetSignature() (signature string) { 156 | if u.Signature != nil && 157 | *u.Signature != "" && 158 | *u.Signature != u.Username /* For compatibility */ { 159 | return *u.Signature 160 | } 161 | 162 | defer func() { 163 | u.Signature = &signature 164 | u.updated = true 165 | }() 166 | 167 | resp, err := rpc.HttpRequest("GET", "https://v1.hitokoto.cn/?encode=text", nil) 168 | if err != nil { 169 | logging.Logger.Errorf("GetSignature: %v", err) 170 | signature = u.Username 171 | return 172 | } 173 | 174 | if resp.StatusCode != http.StatusOK { 175 | logging.Logger.Errorf("GetSignature: %v", err) 176 | signature = u.Username 177 | return 178 | } 179 | 180 | if resp.Body != nil { 181 | defer func(Body io.ReadCloser) { 182 | err := Body.Close() 183 | if err != nil { 184 | logging.Logger.Errorf("GetSignature: %v", err) 185 | } 186 | }(resp.Body) 187 | } 188 | 189 | body, err := io.ReadAll(resp.Body) 190 | if err != nil { 191 | logging.Logger.Errorf("GetSignature: %v", err) 192 | signature = u.Username 193 | return 194 | } 195 | 196 | signature = string(body) 197 | 198 | return 199 | } 200 | -------------------------------------------------------------------------------- /repo/model/user_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/cloudwego/hertz/pkg/common/test/assert" 8 | ) 9 | 10 | func TestUser_GetUserAvatar(t *testing.T) { 11 | exampleAvatar := "https://example.image" 12 | // should get default avatar 13 | var userHasAvatar = User{ 14 | Username: "nyanki", 15 | Avatar: &exampleAvatar, 16 | } 17 | avatar := userHasAvatar.GetUserAvatar() 18 | assert.Assert(t, avatar == exampleAvatar) 19 | assert.Assert(t, !userHasAvatar.updated) 20 | 21 | // should get cravatar url when username is email 22 | var userHasEmail = User{ 23 | Username: "example@email.com", 24 | } 25 | avatar = userHasEmail.GetUserAvatar() 26 | assert.Assert(t, avatar == getCravatarUrl(userHasEmail.Username)) 27 | assert.Assert(t, userHasEmail.updated) 28 | 29 | // should get unsplash url when username is not email 30 | var userHasUsername = User{ 31 | Username: "example", 32 | } 33 | avatar = userHasUsername.GetUserAvatar() 34 | assert.Assert(t, strings.HasPrefix(avatar, "https://")) 35 | assert.Assert(t, userHasUsername.updated) 36 | } 37 | -------------------------------------------------------------------------------- /repo/model/video.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // Video 视频表 /* 6 | type Video struct { 7 | Model 8 | UserId uint32 `json:"user_id" gorm:"not null;index"` 9 | Title string `json:"title"` 10 | FileName string `json:"play_name"` 11 | CoverName string `json:"cover_name"` 12 | CreatedAt time.Time `gorm:"index"` 13 | } 14 | -------------------------------------------------------------------------------- /rpc/http.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 8 | ) 9 | 10 | var ( 11 | client http.Client 12 | ) 13 | 14 | type HttpRequestOptClosure func(*http.Request) 15 | 16 | func WithAuthorizationHeader(authorization string) HttpRequestOptClosure { 17 | return func(req *http.Request) { 18 | req.Header.Add("Authorization", authorization) 19 | } 20 | } 21 | 22 | func HttpRequest(method, url string, body io.Reader, opt ...HttpRequestOptClosure) (*http.Response, error) { 23 | req, err := http.NewRequest(method, url, body) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | for _, item := range opt { 29 | item(req) 30 | } 31 | 32 | return client.Do(req) 33 | } 34 | 35 | func init() { 36 | client = http.Client{ 37 | Transport: otelhttp.NewTransport(http.DefaultTransport), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /service/auth/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="auth" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /service/auth/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "toktik/constant/biz" 6 | "toktik/kitex_gen/douyin/auth" 7 | "toktik/logging" 8 | "toktik/repo" 9 | commonModel "toktik/repo/model" 10 | 11 | "github.com/segmentio/ksuid" 12 | "github.com/sirupsen/logrus" 13 | "golang.org/x/crypto/bcrypt" 14 | ) 15 | 16 | func HashPassword(password string) (string, error) { 17 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) 18 | return string(bytes), err 19 | } 20 | 21 | func CheckPasswordHash(password, hash string) bool { 22 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 23 | return err == nil 24 | } 25 | 26 | // AuthServiceImpl implements the last service interface defined in the IDL. 27 | type AuthServiceImpl struct{} 28 | 29 | // Authenticate implements the AuthServiceImpl interface. 30 | func (s *AuthServiceImpl) Authenticate(ctx context.Context, req *auth.AuthenticateRequest) (resp *auth.AuthenticateResponse, err error) { 31 | logger := logging.Logger.WithFields(logrus.Fields{ 32 | "method": "Authenticate", 33 | "token": req.Token, 34 | }) 35 | logger.Debugf("Process start") 36 | if req == nil { 37 | logger.Warningf("request is nil") 38 | return &auth.AuthenticateResponse{ 39 | StatusCode: biz.RequestIsNil, 40 | StatusMsg: biz.InternalServerErrorStatusMsg, 41 | }, nil 42 | } 43 | userToken := repo.Q.UserToken 44 | first, err := userToken.WithContext(ctx).Where(userToken.Token.Eq(req.Token)).First() 45 | if err != nil { 46 | logger.Warningf(biz.TokenNotFoundMessage) 47 | return &auth.AuthenticateResponse{ 48 | StatusCode: biz.TokenNotFound, 49 | StatusMsg: biz.TokenNotFoundMessage, 50 | }, nil 51 | } 52 | resp = &auth.AuthenticateResponse{ 53 | StatusCode: 0, 54 | StatusMsg: "success", 55 | UserId: first.UserID, 56 | } 57 | return 58 | } 59 | 60 | // Register implements the AuthServiceImpl interface. 61 | func (s *AuthServiceImpl) Register(ctx context.Context, req *auth.RegisterRequest) (resp *auth.RegisterResponse, err error) { 62 | resp = &auth.RegisterResponse{} 63 | user := repo.Q.User 64 | userToken := repo.Q.UserToken 65 | dbUser, err := user.WithContext(ctx).Where(user.Username.Eq(req.Username)).Select().Find() 66 | if err != nil { 67 | resp = &auth.RegisterResponse{ 68 | StatusCode: biz.ServiceNotAvailable, 69 | StatusMsg: biz.InternalServerErrorStatusMsg, 70 | } 71 | return 72 | } 73 | if len(dbUser) > 0 { 74 | resp = &auth.RegisterResponse{ 75 | StatusCode: biz.UserNameExist, 76 | StatusMsg: "用户名已存在", 77 | } 78 | return 79 | } 80 | hashedPwd, err := HashPassword(req.Password) 81 | if err != nil { 82 | resp = &auth.RegisterResponse{ 83 | StatusCode: biz.ServiceNotAvailable, 84 | StatusMsg: biz.InternalServerErrorStatusMsg, 85 | } 86 | return 87 | } 88 | 89 | newUser := commonModel.User{ 90 | Username: req.Username, 91 | Password: &hashedPwd, 92 | } 93 | err = user.WithContext(ctx).Save( 94 | &newUser) 95 | if err != nil { 96 | resp = &auth.RegisterResponse{ 97 | StatusCode: biz.ServiceNotAvailable, 98 | StatusMsg: biz.InternalServerErrorStatusMsg, 99 | } 100 | return 101 | } 102 | token := ksuid.New().String() 103 | err = userToken.WithContext(ctx).Save(&commonModel.UserToken{ 104 | Token: token, 105 | Username: newUser.Username, 106 | UserID: newUser.ID, 107 | }) 108 | 109 | if err != nil { 110 | resp = &auth.RegisterResponse{ 111 | StatusCode: biz.ServiceNotAvailable, 112 | StatusMsg: biz.InternalServerErrorStatusMsg, 113 | } 114 | return 115 | } 116 | resp.Token = token 117 | resp.UserId = newUser.ID 118 | resp.StatusCode = 0 119 | resp.StatusMsg = "success" 120 | return 121 | } 122 | 123 | // Login implements the AuthServiceImpl interface. 124 | func (s *AuthServiceImpl) Login(ctx context.Context, req *auth.LoginRequest) (resp *auth.LoginResponse, err error) { 125 | resp = &auth.LoginResponse{} 126 | user := repo.Q.User 127 | userToken := repo.Q.UserToken 128 | dbUser, err := user.WithContext(ctx).Where(user.Username.Eq(req.Username)).Select().Find() 129 | if err != nil { 130 | resp = &auth.LoginResponse{ 131 | StatusCode: biz.ServiceNotAvailable, 132 | StatusMsg: biz.InternalServerErrorStatusMsg, 133 | } 134 | return 135 | } 136 | if len(dbUser) != 1 { 137 | resp = &auth.LoginResponse{ 138 | StatusCode: biz.UserNotFound, 139 | StatusMsg: "用户不存在", 140 | } 141 | return 142 | } 143 | if !CheckPasswordHash(req.Password, *dbUser[0].Password) { 144 | resp = &auth.LoginResponse{ 145 | StatusCode: biz.PasswordIncorrect, 146 | StatusMsg: "密码错误", 147 | } 148 | return 149 | } 150 | 151 | tokens, err := userToken.WithContext(ctx).Where(userToken.UserID.Eq(dbUser[0].ID)).Find() 152 | if len(tokens) == 1 { 153 | resp.Token = tokens[0].Token 154 | resp.UserId = dbUser[0].ID 155 | resp.StatusCode = 0 156 | resp.StatusMsg = "success" 157 | return 158 | } 159 | if err != nil { 160 | return nil, err 161 | } 162 | token := ksuid.New().String() 163 | err = userToken.WithContext(ctx).Save(&commonModel.UserToken{ 164 | Token: token, 165 | Username: dbUser[0].Username, 166 | UserID: dbUser[0].ID, 167 | }) 168 | 169 | if err != nil { 170 | resp = &auth.LoginResponse{ 171 | StatusCode: biz.ServiceNotAvailable, 172 | StatusMsg: biz.InternalServerErrorStatusMsg, 173 | } 174 | return 175 | } 176 | resp.Token = token 177 | resp.UserId = dbUser[0].ID 178 | resp.StatusCode = 0 179 | resp.StatusMsg = "success" 180 | return 181 | } 182 | -------------------------------------------------------------------------------- /service/auth/handler_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "reflect" 7 | "regexp" 8 | "testing" 9 | "toktik/kitex_gen/douyin/auth" 10 | "toktik/repo" 11 | 12 | "github.com/DATA-DOG/go-sqlmock" 13 | "gorm.io/driver/postgres" 14 | "gorm.io/gorm" 15 | ) 16 | 17 | var successArg = struct { 18 | ctx context.Context 19 | req *auth.AuthenticateRequest 20 | }{ctx: context.Background(), req: &auth.AuthenticateRequest{Token: "authenticated-token"}} 21 | 22 | var successResp = &auth.AuthenticateResponse{ 23 | StatusCode: 0, 24 | StatusMsg: "success", 25 | UserId: 114514, 26 | } 27 | 28 | func TestAuthServiceImpl_Authenticate(t *testing.T) { 29 | mock, db := NewDBMock(t) 30 | defer db.Close() 31 | mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_tokens" WHERE "user_tokens"."token" = $1 ORDER BY "user_tokens"."token" LIMIT 1`)). 32 | WithArgs(sqlmock.AnyArg()). 33 | WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "token", "created_at", "updated_at"}). 34 | AddRow(1, 114514, "authenticated-token", "2021-01-01 00:00:00", "2021-01-01 00:00:00")) 35 | 36 | type args struct { 37 | ctx context.Context 38 | req *auth.AuthenticateRequest 39 | } 40 | tests := []struct { 41 | name string 42 | args args 43 | wantResp *auth.AuthenticateResponse 44 | wantErr bool 45 | }{ 46 | {name: "should authenticate success", args: successArg, wantResp: successResp}, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | s := &AuthServiceImpl{} 51 | gotResp, err := s.Authenticate(tt.args.ctx, tt.args.req) 52 | if (err != nil) != tt.wantErr { 53 | t.Errorf("Authenticate() error = %v, wantErr %v", err, tt.wantErr) 54 | return 55 | } 56 | if !reflect.DeepEqual(gotResp, tt.wantResp) { 57 | t.Errorf("Authenticate() gotResp = %v, want %v", gotResp, tt.wantResp) 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func NewDBMock(t *testing.T) (sqlmock.Sqlmock, *sql.DB) { 64 | db, mock, err := sqlmock.New() 65 | DB, err := gorm.Open(postgres.New(postgres.Config{ 66 | DSN: "sqlmock_db_0", 67 | DriverName: "postgres", 68 | Conn: db, 69 | PreferSimpleProtocol: true, 70 | }), &gorm.Config{}) 71 | if err != nil { 72 | t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 73 | } 74 | repo.SetDefault(DB) 75 | return mock, db 76 | } 77 | -------------------------------------------------------------------------------- /service/auth/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'auth' 3 | ToolVersion: 'v0.4.4' 4 | 5 | -------------------------------------------------------------------------------- /service/auth/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "toktik/constant/config" 7 | auth "toktik/kitex_gen/douyin/auth/authservice" 8 | 9 | "github.com/cloudwego/kitex/pkg/rpcinfo" 10 | "github.com/cloudwego/kitex/server" 11 | "github.com/kitex-contrib/obs-opentelemetry/provider" 12 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 13 | consul "github.com/kitex-contrib/registry-consul" 14 | ) 15 | 16 | func main() { 17 | var err error 18 | 19 | r, err := consul.NewConsulRegister(config.EnvConfig.CONSUL_ADDR) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | addr, err := net.ResolveTCPAddr("tcp", config.AuthServiceAddr) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | provider.NewOpenTelemetryProvider( 30 | provider.WithServiceName(config.AuthServiceName), 31 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 32 | provider.WithInsecure(), 33 | ) 34 | 35 | srv := auth.NewServer( 36 | new(AuthServiceImpl), 37 | server.WithServiceAddr(addr), 38 | server.WithRegistry(r), 39 | server.WithSuite(tracing.NewServerSuite()), 40 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ 41 | ServiceName: config.AuthServiceName, 42 | }), 43 | ) 44 | err = srv.Run() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /service/auth/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/auth" 22 | 23 | -------------------------------------------------------------------------------- /service/comment/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="comment" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /service/comment/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'comment' 3 | ToolVersion: 'v0.4.4' 4 | 5 | -------------------------------------------------------------------------------- /service/comment/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "toktik/constant/config" 7 | comment "toktik/kitex_gen/douyin/comment/commentservice" 8 | 9 | "github.com/cloudwego/kitex/pkg/rpcinfo" 10 | "github.com/cloudwego/kitex/server" 11 | "github.com/kitex-contrib/obs-opentelemetry/provider" 12 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 13 | consul "github.com/kitex-contrib/registry-consul" 14 | ) 15 | 16 | func main() { 17 | var err error 18 | 19 | r, err := consul.NewConsulRegister(config.EnvConfig.CONSUL_ADDR) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | addr, err := net.ResolveTCPAddr("tcp", config.CommentServiceAddr) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | provider.NewOpenTelemetryProvider( 30 | provider.WithServiceName(config.CommentServiceName), 31 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 32 | provider.WithInsecure(), 33 | ) 34 | 35 | srv := comment.NewServer( 36 | new(CommentServiceImpl), 37 | server.WithServiceAddr(addr), 38 | server.WithRegistry(r), 39 | server.WithSuite(tracing.NewServerSuite()), 40 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ 41 | ServiceName: config.CommentServiceName, 42 | }), 43 | ) 44 | 45 | err = srv.Run() 46 | 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /service/comment/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/comment" 22 | 23 | -------------------------------------------------------------------------------- /service/favorite/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="favorite" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /service/favorite/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'favorite' 3 | ToolVersion: 'v0.4.4' 4 | 5 | -------------------------------------------------------------------------------- /service/favorite/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "toktik/constant/config" 7 | favoriteService "toktik/kitex_gen/douyin/favorite/favoriteservice" 8 | 9 | "github.com/cloudwego/kitex/pkg/rpcinfo" 10 | "github.com/cloudwego/kitex/server" 11 | "github.com/kitex-contrib/obs-opentelemetry/provider" 12 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 13 | consul "github.com/kitex-contrib/registry-consul" 14 | ) 15 | 16 | func main() { 17 | var err error 18 | 19 | r, err := consul.NewConsulRegister(config.EnvConfig.CONSUL_ADDR) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | addr, err := net.ResolveTCPAddr("tcp", config.FavoriteServiceAddr) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | provider.NewOpenTelemetryProvider( 30 | provider.WithServiceName(config.FavoriteServiceName), 31 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 32 | provider.WithInsecure(), 33 | ) 34 | 35 | srv := favoriteService.NewServer( 36 | new(FavoriteServiceImpl), 37 | server.WithServiceAddr(addr), 38 | server.WithRegistry(r), 39 | server.WithSuite(tracing.NewServerSuite()), 40 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ 41 | ServiceName: config.FavoriteServiceName, 42 | }), 43 | ) 44 | err = srv.Run() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /service/favorite/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/favorite" 22 | 23 | -------------------------------------------------------------------------------- /service/feed/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="feed" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /service/feed/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'feed' 3 | ToolVersion: 'v0.4.4' 4 | 5 | -------------------------------------------------------------------------------- /service/feed/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "toktik/constant/config" 7 | feed "toktik/kitex_gen/douyin/feed/feedservice" 8 | 9 | "github.com/cloudwego/kitex/pkg/rpcinfo" 10 | "github.com/cloudwego/kitex/server" 11 | "github.com/kitex-contrib/obs-opentelemetry/provider" 12 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 13 | consul "github.com/kitex-contrib/registry-consul" 14 | ) 15 | 16 | func main() { 17 | var err error 18 | 19 | r, err := consul.NewConsulRegister(config.EnvConfig.CONSUL_ADDR) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | addr, err := net.ResolveTCPAddr("tcp", config.FeedServiceAddr) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | provider.NewOpenTelemetryProvider( 30 | provider.WithServiceName(config.FeedServiceName), 31 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 32 | provider.WithInsecure(), 33 | ) 34 | 35 | srv := feed.NewServer( 36 | new(FeedServiceImpl), 37 | server.WithServiceAddr(addr), 38 | server.WithRegistry(r), 39 | server.WithSuite(tracing.NewServerSuite()), 40 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ 41 | ServiceName: config.FeedServiceName, 42 | }), 43 | ) 44 | 45 | err = srv.Run() 46 | 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /service/feed/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/feed" 22 | 23 | -------------------------------------------------------------------------------- /service/publish/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="publish" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /service/publish/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'publish' 3 | ToolVersion: 'v0.4.4' 4 | 5 | -------------------------------------------------------------------------------- /service/publish/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "toktik/constant/config" 7 | publish "toktik/kitex_gen/douyin/publish/publishservice" 8 | 9 | "github.com/kitex-contrib/obs-opentelemetry/provider" 10 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 11 | 12 | "github.com/cloudwego/kitex/server" 13 | 14 | "github.com/cloudwego/kitex/pkg/rpcinfo" 15 | consul "github.com/kitex-contrib/registry-consul" 16 | ) 17 | 18 | func main() { 19 | r, err := consul.NewConsulRegister(config.EnvConfig.CONSUL_ADDR) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | addr, err := net.ResolveTCPAddr("tcp", config.PublishServiceAddr) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | provider.NewOpenTelemetryProvider( 30 | provider.WithServiceName(config.PublishServiceName), 31 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 32 | provider.WithInsecure(), 33 | ) 34 | 35 | srv := publish.NewServer( 36 | new(PublishServiceImpl), 37 | server.WithServiceAddr(addr), 38 | server.WithRegistry(r), 39 | server.WithSuite(tracing.NewServerSuite()), 40 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ 41 | ServiceName: config.PublishServiceName, 42 | }), 43 | ) 44 | err = srv.Run() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /service/publish/resources/bear.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Toktik-Team/toktik/fd1c18ea896125d0a71b45d546a4eed5094d15d0/service/publish/resources/bear.mp4 -------------------------------------------------------------------------------- /service/publish/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/publish" 22 | 23 | -------------------------------------------------------------------------------- /service/relation/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="relation" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /service/relation/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'relation' 3 | ToolVersion: 'v0.4.4' 4 | 5 | -------------------------------------------------------------------------------- /service/relation/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "toktik/constant/config" 7 | relation "toktik/kitex_gen/douyin/relation/relationservice" 8 | 9 | "github.com/kitex-contrib/obs-opentelemetry/provider" 10 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 11 | 12 | "github.com/cloudwego/kitex/server" 13 | 14 | "github.com/cloudwego/kitex/pkg/rpcinfo" 15 | consul "github.com/kitex-contrib/registry-consul" 16 | ) 17 | 18 | func main() { 19 | r, err := consul.NewConsulRegister(config.EnvConfig.CONSUL_ADDR) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | addr, err := net.ResolveTCPAddr("tcp", config.RelationServiceAddr) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | provider.NewOpenTelemetryProvider( 30 | provider.WithServiceName(config.RelationServiceName), 31 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 32 | provider.WithInsecure(), 33 | ) 34 | 35 | srv := relation.NewServer( 36 | new(RelationServiceImpl), 37 | server.WithServiceAddr(addr), 38 | server.WithRegistry(r), 39 | server.WithSuite(tracing.NewServerSuite()), 40 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ 41 | ServiceName: config.RelationServiceName, 42 | }), 43 | ) 44 | err = srv.Run() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /service/relation/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/relation" 22 | 23 | -------------------------------------------------------------------------------- /service/user/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="user" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /service/user/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'user' 3 | ToolVersion: 'v0.4.4' 4 | 5 | -------------------------------------------------------------------------------- /service/user/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "toktik/constant/config" 7 | "toktik/kitex_gen/douyin/user/userservice" 8 | 9 | "github.com/cloudwego/kitex/pkg/rpcinfo" 10 | "github.com/cloudwego/kitex/server" 11 | "github.com/kitex-contrib/obs-opentelemetry/provider" 12 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 13 | consul "github.com/kitex-contrib/registry-consul" 14 | ) 15 | 16 | func main() { 17 | var err error 18 | 19 | r, err := consul.NewConsulRegister(config.EnvConfig.CONSUL_ADDR) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | addr, err := net.ResolveTCPAddr("tcp", config.UserServiceAddr) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | provider.NewOpenTelemetryProvider( 30 | provider.WithServiceName(config.UserServiceName), 31 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 32 | provider.WithInsecure(), 33 | ) 34 | 35 | srv := userservice.NewServer( 36 | new(UserServiceImpl), 37 | server.WithServiceAddr(addr), 38 | server.WithRegistry(r), 39 | server.WithSuite(tracing.NewServerSuite()), 40 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ 41 | ServiceName: config.UserServiceName, 42 | }), 43 | ) 44 | err = srv.Run() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /service/user/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/user" 22 | 23 | -------------------------------------------------------------------------------- /service/web/auth/handler.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "log" 6 | bizConstant "toktik/constant/biz" 7 | "toktik/constant/config" 8 | "toktik/kitex_gen/douyin/auth" 9 | authService "toktik/kitex_gen/douyin/auth/authservice" 10 | "toktik/logging" 11 | 12 | "github.com/kitex-contrib/obs-opentelemetry/provider" 13 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 14 | 15 | "github.com/cloudwego/hertz/pkg/app" 16 | httpStatus "github.com/cloudwego/hertz/pkg/protocol/consts" 17 | "github.com/cloudwego/kitex/client" 18 | consul "github.com/kitex-contrib/registry-consul" 19 | "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var Client authService.Client 23 | 24 | func init() { 25 | r, err := consul.NewConsulResolver(config.EnvConfig.CONSUL_ADDR) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | provider.NewOpenTelemetryProvider( 30 | provider.WithServiceName(config.AuthServiceName), 31 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 32 | provider.WithInsecure(), 33 | ) 34 | Client, err = authService.NewClient( 35 | config.AuthServiceName, 36 | client.WithResolver(r), 37 | client.WithSuite(tracing.NewClientSuite())) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | } 42 | 43 | func Register(ctx context.Context, c *app.RequestContext) { 44 | methodFields := logrus.Fields{ 45 | "method": "Register", 46 | } 47 | logger := logging.Logger.WithFields(methodFields) 48 | logger.Debugf("Process start") 49 | 50 | username, usernameExist := c.GetQuery("username") 51 | password, passwordExist := c.GetQuery("password") 52 | if !usernameExist || !passwordExist { 53 | bizConstant.NoUserNameOrPassWord.WithFields(&methodFields).LaunchError(c) 54 | return 55 | } 56 | logger.WithFields(logrus.Fields{ 57 | "username": username, 58 | "password": password, 59 | }).Debugf("Executing register") 60 | registerResponse, err := Client.Register(ctx, &auth.RegisterRequest{ 61 | Username: username, 62 | Password: password, 63 | }) 64 | if err != nil { 65 | bizConstant.RPCCallError.WithCause(err).WithFields(&methodFields).LaunchError(c) 66 | return 67 | } 68 | logger.WithFields(logrus.Fields{ 69 | "response": registerResponse, 70 | }).Debugf("Register success") 71 | c.JSON(httpStatus.StatusOK, registerResponse) 72 | } 73 | 74 | func Login(ctx context.Context, c *app.RequestContext) { 75 | methodFields := logrus.Fields{ 76 | "method": "Login", 77 | } 78 | logger := logging.Logger.WithFields(methodFields) 79 | logger.Debugf("Process start") 80 | 81 | username, usernameExist := c.GetQuery("username") 82 | password, passwordExist := c.GetQuery("password") 83 | if !usernameExist || !passwordExist { 84 | bizConstant.NoUserNameOrPassWord.WithFields(&methodFields).LaunchError(c) 85 | return 86 | } 87 | logger.WithFields(logrus.Fields{ 88 | "username": username, 89 | "password": password, 90 | }).Debugf("Executing login") 91 | loginResponse, err := Client.Login(ctx, &auth.LoginRequest{ 92 | Username: username, 93 | Password: password, 94 | }) 95 | if err != nil { 96 | bizConstant.RPCCallError.WithCause(err).WithFields(&methodFields).LaunchError(c) 97 | return 98 | } 99 | logger.WithFields(logrus.Fields{ 100 | "response": loginResponse, 101 | }).Debugf("Login success") 102 | c.JSON(httpStatus.StatusOK, loginResponse) 103 | } 104 | -------------------------------------------------------------------------------- /service/web/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="web" 3 | 4 | mkdir -p output/bin 5 | 6 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 7 | go build -o output/bin/${RUN_NAME} 8 | else 9 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 10 | fi 11 | -------------------------------------------------------------------------------- /service/web/comment/handler.go: -------------------------------------------------------------------------------- 1 | package comment 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "strconv" 9 | "toktik/constant/biz" 10 | "toktik/constant/config" 11 | "toktik/kitex_gen/douyin/comment" 12 | "toktik/kitex_gen/douyin/comment/commentservice" 13 | "toktik/logging" 14 | "toktik/service/web/mw" 15 | 16 | "github.com/kitex-contrib/obs-opentelemetry/provider" 17 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 18 | 19 | "github.com/cloudwego/hertz/pkg/app" 20 | "github.com/cloudwego/hertz/pkg/protocol/consts" 21 | "github.com/cloudwego/kitex/client" 22 | consul "github.com/kitex-contrib/registry-consul" 23 | "github.com/sirupsen/logrus" 24 | ) 25 | 26 | var commentClient commentservice.Client 27 | 28 | func init() { 29 | r, err := consul.NewConsulResolver(config.EnvConfig.CONSUL_ADDR) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | provider.NewOpenTelemetryProvider( 34 | provider.WithServiceName(config.CommentServiceName), 35 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 36 | provider.WithInsecure(), 37 | ) 38 | commentClient, err = commentservice.NewClient( 39 | config.CommentServiceName, 40 | client.WithResolver(r), 41 | client.WithSuite(tracing.NewClientSuite()), 42 | ) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | } 47 | 48 | func Action(ctx context.Context, c *app.RequestContext) { 49 | methodFields := logrus.Fields{ 50 | "method": "CommentAction", 51 | } 52 | logger := logging.Logger.WithFields(methodFields) 53 | logger.Debugf("Process start") 54 | 55 | actorIdPtr, ok := mw.Auth(c, mw.WithAuthRequired()) 56 | actorId := *actorIdPtr 57 | if !ok { 58 | return 59 | } 60 | 61 | videoId, videoIdExists := c.GetQuery("video_id") 62 | actionType, actionTypeExists := c.GetQuery("action_type") 63 | commentText, commentTextExists := c.GetQuery("comment_text") 64 | commentId, commentIdExists := c.GetQuery("comment_id") 65 | 66 | if !videoIdExists || !actionTypeExists { 67 | biz.BadRequestError.WithFields(&methodFields).LaunchError(c) 68 | return 69 | } 70 | 71 | pVideoId, err := strconv.ParseUint(videoId, 10, 32) 72 | pActionType, err := strconv.ParseInt(actionType, 10, 32) 73 | 74 | if err != nil { 75 | biz.BadRequestError.WithFields(&methodFields).WithCause(err).LaunchError(c) 76 | return 77 | } 78 | 79 | var rActionType = comment.ActionCommentType(pActionType) 80 | 81 | var ( 82 | rErr error 83 | ) 84 | 85 | switch rActionType { 86 | case comment.ActionCommentType_ACTION_COMMENT_TYPE_ADD: 87 | if !commentTextExists { 88 | rErr = errors.New("comment text is required") 89 | break 90 | } 91 | resp, err := commentClient.ActionComment(ctx, &comment.ActionCommentRequest{ 92 | ActorId: actorId, 93 | VideoId: uint32(pVideoId), 94 | ActionType: rActionType, 95 | Action: &comment.ActionCommentRequest_CommentText{CommentText: commentText}, 96 | }) 97 | if err != nil { 98 | rErr = err 99 | break 100 | } 101 | logger.WithFields(logrus.Fields{ 102 | "response": resp, 103 | }).Debugf("add comment success") 104 | c.JSON( 105 | consts.StatusOK, 106 | resp, 107 | ) 108 | return 109 | case comment.ActionCommentType_ACTION_COMMENT_TYPE_DELETE: 110 | if !commentIdExists { 111 | rErr = errors.New("comment id is required") 112 | break 113 | } 114 | pCommentId, err := strconv.ParseUint(commentId, 10, 32) 115 | if err != nil { 116 | rErr = err 117 | break 118 | } 119 | resp, err := commentClient.ActionComment(ctx, &comment.ActionCommentRequest{ 120 | ActorId: actorId, 121 | VideoId: uint32(pVideoId), 122 | ActionType: rActionType, 123 | Action: &comment.ActionCommentRequest_CommentId{CommentId: uint32(pCommentId)}, 124 | }) 125 | if err != nil { 126 | rErr = err 127 | break 128 | } 129 | logger.WithFields(logrus.Fields{ 130 | "response": resp, 131 | }).Debugf("delete comment success") 132 | c.JSON( 133 | consts.StatusOK, 134 | resp, 135 | ) 136 | return 137 | case comment.ActionCommentType_ACTION_COMMENT_TYPE_UNSPECIFIED: 138 | fallthrough 139 | default: 140 | rErr = errors.New(fmt.Sprintf("invalid action type: %d", rActionType)) 141 | } 142 | 143 | if rErr != nil { 144 | biz.InternalServerError.WithCause(rErr).WithFields(&methodFields).LaunchError(c) 145 | return 146 | } 147 | } 148 | 149 | func List(ctx context.Context, c *app.RequestContext) { 150 | methodFields := logrus.Fields{ 151 | "method": "CommentList", 152 | } 153 | logger := logging.Logger.WithFields(methodFields) 154 | logger.Debugf("Process start") 155 | 156 | actorIdPtr, ok := mw.Auth(c) 157 | actorId := *actorIdPtr 158 | if !ok { 159 | return 160 | } 161 | 162 | videoId, videoIdExists := c.GetQuery("video_id") 163 | 164 | if !videoIdExists { 165 | biz.BadRequestError.WithFields(&methodFields).LaunchError(c) 166 | return 167 | } 168 | 169 | pVideoId, err := strconv.ParseUint(videoId, 10, 32) 170 | 171 | if err != nil { 172 | biz.BadRequestError.WithFields(&methodFields).WithCause(err).LaunchError(c) 173 | return 174 | } 175 | 176 | resp, err := commentClient.ListComment(ctx, &comment.ListCommentRequest{ 177 | ActorId: actorId, 178 | VideoId: uint32(pVideoId), 179 | }) 180 | 181 | if err != nil { 182 | biz.InternalServerError.WithCause(err).WithFields(&methodFields).LaunchError(c) 183 | return 184 | } 185 | 186 | c.JSON( 187 | consts.StatusOK, 188 | resp, 189 | ) 190 | } 191 | -------------------------------------------------------------------------------- /service/web/favorite/handler.go: -------------------------------------------------------------------------------- 1 | package favorite 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strconv" 7 | "toktik/constant/biz" 8 | "toktik/constant/config" 9 | "toktik/kitex_gen/douyin/favorite" 10 | favoriteService "toktik/kitex_gen/douyin/favorite/favoriteservice" 11 | "toktik/logging" 12 | "toktik/service/web/mw" 13 | 14 | "github.com/cloudwego/hertz/pkg/app" 15 | "github.com/cloudwego/hertz/pkg/protocol/consts" 16 | "github.com/cloudwego/kitex/client" 17 | "github.com/kitex-contrib/obs-opentelemetry/provider" 18 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 19 | consul "github.com/kitex-contrib/registry-consul" 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | var Client favoriteService.Client 24 | 25 | func init() { 26 | r, err := consul.NewConsulResolver(config.EnvConfig.CONSUL_ADDR) 27 | if err != nil { 28 | panic(err) 29 | } 30 | provider.NewOpenTelemetryProvider( 31 | provider.WithServiceName(config.FavoriteServiceName), 32 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 33 | provider.WithInsecure(), 34 | ) 35 | Client, err = favoriteService.NewClient( 36 | config.FavoriteServiceName, 37 | client.WithResolver(r), 38 | client.WithSuite(tracing.NewClientSuite()), 39 | ) 40 | if err != nil { 41 | panic(err) 42 | } 43 | } 44 | 45 | var logger = logging.Logger 46 | 47 | // 用于解析 Action 函数所需参数 48 | func parseParameters(c *app.RequestContext) (videoId uint32, actionType uint32, isEnd bool) { 49 | field := logrus.Fields{ 50 | "method": "parseParameters", 51 | } 52 | 53 | isEnd = true 54 | // 获取参数 55 | qVideoId, videoIdExist := c.GetQuery("video_id") 56 | qActionType, actionTypeExist := c.GetQuery("action_type") 57 | if !videoIdExist || !actionTypeExist { 58 | biz.BadRequestError. 59 | WithFields(&field). 60 | LaunchError(c) 61 | return 62 | } 63 | // 解析 videoId 64 | temp, err := strconv.ParseUint(qVideoId, 10, 32) 65 | if err != nil { 66 | biz.BadRequestError. 67 | WithCause(err). 68 | WithFields(&field). 69 | LaunchError(c) 70 | return 71 | } 72 | videoId = uint32(temp) 73 | // 解析 actionType 74 | temp, err = strconv.ParseUint(qActionType, 10, 32) 75 | if err != nil { 76 | biz.BadRequestError. 77 | WithCause(err). 78 | WithFields(&field). 79 | LaunchError(c) 80 | return 81 | } 82 | actionType = uint32(temp) 83 | if actionType != 1 && actionType != 2 { 84 | biz.BadRequestError. 85 | WithFields(&field). 86 | LaunchError(c) 87 | return 88 | } 89 | 90 | isEnd = false 91 | return 92 | } 93 | 94 | // Action 处理点赞和取消点赞 95 | func Action(ctx context.Context, c *app.RequestContext) { 96 | field := logrus.Fields{ 97 | "method": "Action", 98 | } 99 | logger.WithFields(field).Debugf("Process start") 100 | 101 | actorIdPtr, ok := mw.Auth(c, mw.WithAuthRequired()) 102 | actorId := *actorIdPtr 103 | if !ok { 104 | return 105 | } 106 | 107 | videoId, actionType, isEnd := parseParameters(c) 108 | if isEnd { 109 | return 110 | } 111 | 112 | response, err := Client.FavoriteAction(ctx, &favorite.FavoriteRequest{ 113 | ActorId: actorId, 114 | VideoId: videoId, 115 | ActionType: actionType, 116 | }) 117 | 118 | if err != nil { 119 | biz.RPCCallError.WithCause(err).WithFields(&field).LaunchError(c) 120 | return 121 | } 122 | 123 | c.JSON( 124 | http.StatusOK, 125 | response, 126 | ) 127 | return 128 | } 129 | 130 | // List 列出用户所有点赞视频 131 | func List(ctx context.Context, c *app.RequestContext) { 132 | field := logrus.Fields{ 133 | "method": "List", 134 | } 135 | logger.WithFields(field).Info("Process start") 136 | 137 | actorIdPtr, ok := mw.Auth(c) 138 | actorId := *actorIdPtr 139 | if !ok { 140 | return 141 | } 142 | 143 | qUserId, userIdExist := c.GetQuery("user_id") 144 | if !userIdExist { 145 | biz.BadRequestError.WithFields(&field).LaunchError(c) 146 | return 147 | } 148 | 149 | userId, err := strconv.ParseUint(qUserId, 10, 32) 150 | if err != nil { 151 | biz.BadRequestError.WithCause(err).WithFields(&field).LaunchError(c) 152 | return 153 | } 154 | 155 | response, err := Client.FavoriteList(ctx, &favorite.FavoriteListRequest{ 156 | ActorId: actorId, 157 | UserId: uint32(userId), 158 | }) 159 | 160 | if err != nil { 161 | biz.RPCCallError.WithCause(err).WithFields(&field).LaunchError(c) 162 | return 163 | } 164 | 165 | c.JSON( 166 | consts.StatusOK, 167 | response, 168 | ) 169 | return 170 | } 171 | -------------------------------------------------------------------------------- /service/web/feed/handler.go: -------------------------------------------------------------------------------- 1 | package feed 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | bizConstant "toktik/constant/biz" 7 | "toktik/constant/config" 8 | "toktik/kitex_gen/douyin/feed" 9 | feedService "toktik/kitex_gen/douyin/feed/feedservice" 10 | "toktik/logging" 11 | "toktik/service/web/mw" 12 | 13 | "github.com/kitex-contrib/obs-opentelemetry/provider" 14 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 15 | 16 | "github.com/cloudwego/hertz/pkg/app" 17 | httpStatus "github.com/cloudwego/hertz/pkg/protocol/consts" 18 | "github.com/cloudwego/kitex/client" 19 | consul "github.com/kitex-contrib/registry-consul" 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | var feedClient feedService.Client 24 | 25 | func init() { 26 | r, err := consul.NewConsulResolver(config.EnvConfig.CONSUL_ADDR) 27 | if err != nil { 28 | logging.Logger.WithError(err).Fatal("init feed client failed") 29 | panic(err) 30 | } 31 | provider.NewOpenTelemetryProvider( 32 | provider.WithServiceName(config.FeedServiceName), 33 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 34 | provider.WithInsecure(), 35 | ) 36 | feedClient, err = feedService.NewClient( 37 | config.FeedServiceName, 38 | client.WithResolver(r), 39 | client.WithSuite(tracing.NewClientSuite()), 40 | ) 41 | if err != nil { 42 | logging.Logger.WithError(err).Fatal("init feed client failed") 43 | panic(err) 44 | } 45 | } 46 | func Action(ctx context.Context, c *app.RequestContext) { 47 | methodFields := logrus.Fields{ 48 | "method": "FeedAction", 49 | } 50 | logger := logging.Logger.WithFields(methodFields) 51 | logger.Debugf("Process start") 52 | 53 | latestTime := c.Query("latest_time") 54 | if _, err := strconv.Atoi(latestTime); latestTime != "" && err != nil { 55 | bizConstant.InvalidLatestTime. 56 | WithCause(err). 57 | WithFields(&methodFields). 58 | LaunchError(c) 59 | return 60 | } 61 | 62 | actorIdPtr, ok := mw.Auth(c) 63 | actorId := *actorIdPtr 64 | if !ok { 65 | return 66 | } 67 | 68 | logger.WithFields(logrus.Fields{ 69 | "latestTime": latestTime, 70 | "actorId": actorId, 71 | }).Debugf("Executing get feed") 72 | response, err := feedClient.ListVideos(ctx, &feed.ListFeedRequest{ 73 | LatestTime: &latestTime, 74 | ActorId: actorIdPtr, 75 | }) 76 | 77 | if err != nil { 78 | bizConstant.RPCCallError. 79 | WithCause(err). 80 | WithFields(&methodFields). 81 | LaunchError(c) 82 | return 83 | } 84 | 85 | logger.WithFields(logrus.Fields{ 86 | "response": response, 87 | }).Debugf("Getting feed success") 88 | c.JSON(httpStatus.StatusOK, response) 89 | } 90 | -------------------------------------------------------------------------------- /service/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "toktik/constant/config" 6 | "toktik/service/web/auth" 7 | "toktik/service/web/comment" 8 | "toktik/service/web/favorite" 9 | "toktik/service/web/feed" 10 | "toktik/service/web/mw" 11 | "toktik/service/web/publish" 12 | "toktik/service/web/relation" 13 | "toktik/service/web/user" 14 | "toktik/service/web/wechat" 15 | 16 | "github.com/cloudwego/hertz/pkg/app/server" 17 | "github.com/hertz-contrib/gzip" 18 | "github.com/hertz-contrib/obs-opentelemetry/provider" 19 | "github.com/hertz-contrib/obs-opentelemetry/tracing" 20 | "github.com/hertz-contrib/pprof" 21 | "github.com/hertz-contrib/swagger" 22 | swaggerFiles "github.com/swaggo/files" 23 | ) 24 | 25 | func main() { 26 | p := provider.NewOpenTelemetryProvider( 27 | provider.WithServiceName(config.WebServiceName), 28 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 29 | provider.WithInsecure(), 30 | ) 31 | defer p.Shutdown(context.Background()) 32 | 33 | tracer, cfg := tracing.NewServerTracer() 34 | h := server.Default( 35 | server.WithHostPorts(config.WebServiceAddr), 36 | server.WithMaxRequestBodySize(config.EnvConfig.MAX_REQUEST_BODY_SIZE), 37 | tracer, 38 | ) 39 | h.Use(gzip.Gzip(gzip.DefaultCompression)) 40 | h.Use(mw.AuthMiddleware()) 41 | h.Use(tracing.ServerMiddleware(cfg)) 42 | pprof.Register(h) 43 | 44 | douyin := h.Group("/douyin") 45 | 46 | // feed service 47 | douyin.GET("/feed", feed.Action) 48 | 49 | // user service 50 | userGroup := douyin.Group("/user") 51 | userGroup.POST("/register/", auth.Register) 52 | userGroup.POST("/login/", auth.Login) 53 | userGroup.GET("/", user.GetUserInfo) 54 | 55 | // publish service 56 | publishGroup := douyin.Group("/publish") 57 | publishGroup.POST("/action/", publish.Action) 58 | publishGroup.GET("/list", publish.List) 59 | 60 | // favorite service 61 | favoriteGroup := douyin.Group("/favorite") 62 | favoriteGroup.POST("/action/", favorite.Action) 63 | favoriteGroup.GET("/list/", favorite.List) 64 | 65 | // comment service 66 | commentGroup := douyin.Group("/comment") 67 | commentGroup.POST("/action/", comment.Action) 68 | commentGroup.GET("/list/", comment.List) 69 | 70 | // relation service 71 | relationGroup := douyin.Group("/relation") 72 | relationGroup.POST("/action/", relation.RelationAction) 73 | relationGroup.GET("/follow/list/", relation.GetFollowList) 74 | relationGroup.GET("/follower/list/", relation.GetFollowerList) 75 | relationGroup.GET("/friend/list/", relation.GetFriendList) 76 | 77 | // message service 78 | messageGroup := douyin.Group("/message") 79 | messageGroup.POST("/action/", wechat.MessageAction) 80 | messageGroup.GET("/chat/", wechat.MessageChat) 81 | 82 | url := swagger.URL("http://localhost:8080/swagger/doc.json") // The url pointing to API definition 83 | h.GET("/swagger/*any", swagger.WrapHandler(swaggerFiles.Handler, url)) 84 | 85 | h.Spin() 86 | } 87 | -------------------------------------------------------------------------------- /service/web/mw/auth.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "context" 5 | "toktik/constant/biz" 6 | "toktik/kitex_gen/douyin/auth" 7 | authHandler "toktik/service/web/auth" 8 | 9 | "github.com/cloudwego/hertz/pkg/app" 10 | "github.com/cloudwego/hertz/pkg/common/hlog" 11 | ) 12 | 13 | // AuthResult Authentication result enum 14 | const ( 15 | // authResultSuccess Authentication success 16 | authResultSuccess string = "success" 17 | // authResultNoToken Authentication failed due to no token 18 | authResultNoToken string = "no_token" 19 | // authResultUnknown Authentication failed due to unknown reason 20 | authResultUnknown string = "unknown" 21 | ) 22 | 23 | const ( 24 | authResultKey = "authentication_result" 25 | userIdKey = "user_id" 26 | ) 27 | 28 | func init() { 29 | hlog.Info("using auth") 30 | } 31 | 32 | func AuthMiddleware() app.HandlerFunc { 33 | return func(ctx context.Context, rc *app.RequestContext) { 34 | var token string 35 | formToken := string(rc.FormValue("token")) 36 | 37 | if formToken != "" { 38 | token = formToken 39 | } 40 | 41 | if token == "" { 42 | rc.Set(authResultKey, authResultNoToken) 43 | rc.Set(userIdKey, 0) 44 | rc.Next(ctx) 45 | return 46 | } 47 | 48 | authResp, err := authHandler.Client.Authenticate(ctx, &auth.AuthenticateRequest{Token: token}) 49 | if err != nil { 50 | rc.Set(authResultKey, authResultUnknown) 51 | rc.Set(userIdKey, 0) 52 | rc.Next(ctx) 53 | return 54 | } 55 | if authResp.StatusCode == 0 { 56 | rc.Set(authResultKey, authResultSuccess) 57 | rc.Set(userIdKey, authResp.UserId) 58 | } else { 59 | rc.Set(authResultKey, authResultUnknown) 60 | rc.Set(userIdKey, 0) 61 | } 62 | rc.Next(ctx) 63 | } 64 | } 65 | 66 | type config struct { 67 | authRequired bool 68 | } 69 | 70 | // Option opts for opentelemetry tracer provider 71 | type Option interface { 72 | apply(cfg *config) 73 | } 74 | 75 | type option func(cfg *config) 76 | 77 | func (fn option) apply(cfg *config) { 78 | fn(cfg) 79 | } 80 | 81 | func newConfig(opts []Option) *config { 82 | cfg := &config{} 83 | 84 | for _, opt := range opts { 85 | opt.apply(cfg) 86 | } 87 | 88 | return cfg 89 | } 90 | 91 | func WithAuthRequired() Option { 92 | return option(func(cfg *config) { 93 | cfg.authRequired = true 94 | }) 95 | } 96 | 97 | func Auth(c *app.RequestContext, opts ...Option) (actorIdPtr *uint32, ok bool) { 98 | cfg := newConfig(opts) 99 | actorIdPtr = new(uint32) 100 | switch c.GetString(authResultKey) { 101 | case authResultSuccess: 102 | *actorIdPtr = c.GetUint32(userIdKey) 103 | case authResultNoToken: 104 | // CHATGPT GENERATED CR COMMENT: 105 | // 这段代码中 fallthrough 和 break 的使用方法是正确的,不会产生 bug。 106 | // 107 | // 在 switch 语句中,使用 fallthrough 语句可以使程序继续执行下一个 case 语句,而不进行判断。在上述代码中,如果 108 | // authResultKey 的值为 AUTH_RESULT_NO_TOKEN,程序会先判断 !cfg.authRequired,如果为 true,则跳出 109 | // switch 语句,执行后续代码;如果为 false,则会继续执行下一个 case 语句,即 default,然后执行 110 | // biz.UnAuthorized.LaunchError(c),返回 nil 和 false。 111 | // 112 | // 而在 switch 语句中,使用 break 语句可以跳出 switch 语句,不再继续执行下一个 case 语句。在上述代码中,如果 113 | // authResultKey 的值为 AUTH_RESULT_SUCCESS,程序会执行 actorId = 114 | // c.GetUint32(userIdKey),然后直接跳出 switch 语句,返回 actorIdPtr 和 true。 115 | // 116 | // 因此,使用 fallthrough 和 break 的方式是正确的,不会产生 bug。 117 | if !cfg.authRequired { 118 | break 119 | } 120 | fallthrough 121 | default: 122 | biz.UnAuthorized.LaunchError(c) 123 | return nil, false 124 | } 125 | return actorIdPtr, true 126 | } 127 | -------------------------------------------------------------------------------- /service/web/mw/json.go: -------------------------------------------------------------------------------- 1 | package mw 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/cloudwego/hertz/pkg/app/server/render" 7 | "github.com/cloudwego/hertz/pkg/common/hlog" 8 | "google.golang.org/protobuf/encoding/protojson" 9 | "google.golang.org/protobuf/proto" 10 | ) 11 | 12 | var m = protojson.MarshalOptions{ 13 | EmitUnpopulated: true, 14 | UseProtoNames: true, 15 | } 16 | 17 | func init() { 18 | hlog.Info("using protojson") 19 | render.ResetJSONMarshal(marshal) 20 | } 21 | 22 | func marshal(v any) ([]byte, error) { 23 | switch v := v.(type) { 24 | case proto.Message: 25 | return m.Marshal(v) 26 | default: 27 | return json.Marshal(v) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /service/web/publish/handler.go: -------------------------------------------------------------------------------- 1 | package publish 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "mime/multipart" 7 | "strconv" 8 | bizConstant "toktik/constant/biz" 9 | "toktik/constant/config" 10 | "toktik/kitex_gen/douyin/publish" 11 | publishService "toktik/kitex_gen/douyin/publish/publishservice" 12 | "toktik/logging" 13 | "toktik/service/web/mw" 14 | 15 | "github.com/kitex-contrib/obs-opentelemetry/provider" 16 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 17 | 18 | "github.com/cloudwego/hertz/pkg/app" 19 | httpStatus "github.com/cloudwego/hertz/pkg/protocol/consts" 20 | "github.com/cloudwego/kitex/client" 21 | consul "github.com/kitex-contrib/registry-consul" 22 | "github.com/sirupsen/logrus" 23 | ) 24 | 25 | var publishClient publishService.Client 26 | 27 | func init() { 28 | r, err := consul.NewConsulResolver(config.EnvConfig.CONSUL_ADDR) 29 | if err != nil { 30 | panic(err) 31 | } 32 | provider.NewOpenTelemetryProvider( 33 | provider.WithServiceName(config.PublishServiceName), 34 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 35 | provider.WithInsecure(), 36 | ) 37 | publishClient, err = publishService.NewClient( 38 | config.PublishServiceName, 39 | client.WithResolver(r), 40 | client.WithSuite(tracing.NewClientSuite()), 41 | ) 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | 47 | func paramValidate(c *app.RequestContext) (err error) { 48 | var wrappedError error 49 | form, err := c.Request.MultipartForm() 50 | if err != nil { 51 | wrappedError = fmt.Errorf("invalid form: %w", err) 52 | } 53 | title := form.Value["title"] 54 | if len(title) <= 0 { 55 | wrappedError = fmt.Errorf("not title") 56 | } 57 | 58 | data := form.File["data"] 59 | if len(data) <= 0 { 60 | wrappedError = fmt.Errorf("not data") 61 | } 62 | if wrappedError != nil { 63 | return wrappedError 64 | } 65 | return nil 66 | } 67 | 68 | func Action(ctx context.Context, c *app.RequestContext) { 69 | methodFields := logrus.Fields{ 70 | "method": "PublishAction", 71 | } 72 | logger := logging.Logger.WithFields(methodFields) 73 | logger.Debugf("Process start") 74 | 75 | actorIdPtr, ok := mw.Auth(c, mw.WithAuthRequired()) 76 | actorId := *actorIdPtr 77 | if !ok { 78 | return 79 | } 80 | 81 | if err := paramValidate(c); err != nil { 82 | bizConstant.InvalidArguments.WithCause(err).WithFields(&methodFields).LaunchError(c) 83 | return 84 | } 85 | 86 | form, _ := c.MultipartForm() 87 | title := form.Value["title"][0] 88 | file := form.File["data"][0] 89 | opened, _ := file.Open() 90 | defer func(opened multipart.File) { 91 | err := opened.Close() 92 | if err != nil { 93 | logger.WithFields(logrus.Fields{ 94 | "error": err, 95 | }).Errorf("opened.Close() failed") 96 | } 97 | }(opened) 98 | var data = make([]byte, file.Size) 99 | readSize, err := opened.Read(data) 100 | if err != nil { 101 | bizConstant.OpenFileFailedError.WithCause(err).WithFields(&methodFields).LaunchError(c) 102 | return 103 | } 104 | if readSize != int(file.Size) { 105 | bizConstant.SizeNotMatchError.WithCause(err).WithFields(&methodFields).LaunchError(c) 106 | return 107 | } 108 | 109 | logger.WithFields(logrus.Fields{ 110 | "actorId": actorId, 111 | "title": title, 112 | "dataSize": len(data), 113 | }).Debugf("Executing create video") 114 | publishResp, err := publishClient.CreateVideo(ctx, &publish.CreateVideoRequest{ 115 | ActorId: actorId, 116 | Data: data, 117 | Title: title, 118 | }) 119 | if err != nil { 120 | bizConstant.RPCCallError.WithCause(err).WithFields(&methodFields).LaunchError(c) 121 | return 122 | } 123 | logger.WithFields(logrus.Fields{ 124 | "response": publishResp, 125 | }).Debugf("Create video success") 126 | c.JSON( 127 | httpStatus.StatusOK, 128 | publishResp, 129 | ) 130 | } 131 | 132 | func List(ctx context.Context, c *app.RequestContext) { 133 | methodFields := logrus.Fields{ 134 | "method": "CommentAction", 135 | } 136 | logger := logging.Logger.WithFields(methodFields) 137 | logger.Debugf("Process start") 138 | 139 | actorIdPtr, ok := mw.Auth(c) 140 | actorId := *actorIdPtr 141 | if !ok { 142 | return 143 | } 144 | 145 | userId, userIdExists := c.GetQuery("user_id") 146 | 147 | if !userIdExists { 148 | bizConstant.InvalidArguments.WithFields(&methodFields).LaunchError(c) 149 | } 150 | 151 | pUserId, err := strconv.ParseUint(userId, 10, 32) 152 | 153 | if err != nil { 154 | bizConstant.BadRequestError.WithFields(&methodFields).WithCause(err).LaunchError(c) 155 | return 156 | } 157 | 158 | resp, err := publishClient.ListVideo(ctx, &publish.ListVideoRequest{ 159 | UserId: uint32(pUserId), 160 | ActorId: actorId, 161 | }) 162 | 163 | if err != nil { 164 | bizConstant.InternalServerError.WithCause(err).WithFields(&methodFields).LaunchError(c) 165 | return 166 | } 167 | 168 | c.JSON( 169 | httpStatus.StatusOK, 170 | resp, 171 | ) 172 | } 173 | -------------------------------------------------------------------------------- /service/web/user/handler.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "toktik/constant/biz" 7 | "toktik/constant/config" 8 | "toktik/kitex_gen/douyin/user" 9 | userService "toktik/kitex_gen/douyin/user/userservice" 10 | "toktik/logging" 11 | "toktik/service/web/mw" 12 | 13 | "github.com/kitex-contrib/obs-opentelemetry/provider" 14 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 15 | 16 | "github.com/cloudwego/hertz/pkg/app" 17 | httpStatus "github.com/cloudwego/hertz/pkg/protocol/consts" 18 | "github.com/cloudwego/kitex/client" 19 | consul "github.com/kitex-contrib/registry-consul" 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | var userClient userService.Client 24 | 25 | func init() { 26 | r, err := consul.NewConsulResolver(config.EnvConfig.CONSUL_ADDR) 27 | if err != nil { 28 | panic(err) 29 | } 30 | provider.NewOpenTelemetryProvider( 31 | provider.WithServiceName(config.UserServiceName), 32 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 33 | provider.WithInsecure(), 34 | ) 35 | userClient, err = userService.NewClient( 36 | config.UserServiceName, 37 | client.WithResolver(r), 38 | client.WithSuite(tracing.NewClientSuite()), 39 | ) 40 | if err != nil { 41 | panic(err) 42 | } 43 | } 44 | 45 | func GetUserInfo(ctx context.Context, c *app.RequestContext) { 46 | methodFields := logrus.Fields{ 47 | "method": "GetUserInfo", 48 | } 49 | logger := logging.Logger 50 | logger.WithFields(methodFields).Info("Process start") 51 | 52 | actorIdPtr, ok := mw.Auth(c) 53 | actorId := *actorIdPtr 54 | if !ok { 55 | return 56 | } 57 | 58 | userId, idExist := c.GetQuery("user_id") 59 | id, err := strconv.Atoi(userId) 60 | 61 | if !idExist || err != nil { 62 | biz.InvalidUserID.WithCause(err).WithFields(&methodFields).LaunchError(c) 63 | return 64 | } 65 | 66 | logger.WithField("user_id", id).Debugf("Executing get user info") 67 | resp, err := userClient.GetUser(ctx, &user.UserRequest{ 68 | UserId: uint32(id), 69 | ActorId: actorId, 70 | }) 71 | 72 | if err != nil { 73 | biz.RPCCallError.WithCause(err).WithFields(&methodFields).LaunchError(c) 74 | return 75 | } 76 | 77 | logger.WithField("response", resp).Debugf("Get user info success") 78 | c.JSON( 79 | httpStatus.StatusOK, 80 | resp, 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /service/web/wechat/handler.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "strconv" 7 | bizConstant "toktik/constant/biz" 8 | bizConfig "toktik/constant/config" 9 | "toktik/kitex_gen/douyin/wechat" 10 | "toktik/kitex_gen/douyin/wechat/wechatservice" 11 | "toktik/logging" 12 | "toktik/service/web/mw" 13 | 14 | "github.com/cloudwego/hertz/pkg/app" 15 | httpStatus "github.com/cloudwego/hertz/pkg/protocol/consts" 16 | "github.com/cloudwego/kitex/client" 17 | "github.com/kitex-contrib/obs-opentelemetry/provider" 18 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 19 | consul "github.com/kitex-contrib/registry-consul" 20 | "github.com/sirupsen/logrus" 21 | ) 22 | 23 | var Client wechatservice.Client 24 | 25 | func init() { 26 | r, err := consul.NewConsulResolver(bizConfig.EnvConfig.CONSUL_ADDR) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | provider.NewOpenTelemetryProvider( 31 | provider.WithServiceName(bizConfig.WechatServiceName), 32 | provider.WithExportEndpoint(bizConfig.EnvConfig.EXPORT_ENDPOINT), 33 | provider.WithInsecure(), 34 | ) 35 | Client, err = wechatservice.NewClient( 36 | bizConfig.WechatServiceName, 37 | client.WithResolver(r), 38 | client.WithSuite(tracing.NewClientSuite()), 39 | ) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | } 44 | 45 | func MessageAction(ctx context.Context, c *app.RequestContext) { 46 | methodFields := logrus.Fields{ 47 | "method": "MessageAction", 48 | } 49 | logger := logging.Logger 50 | logger.WithFields(methodFields).Debugf("Process start") 51 | 52 | actorIdPtr, ok := mw.Auth(c, mw.WithAuthRequired()) 53 | actorId := *actorIdPtr 54 | if !ok { 55 | return 56 | } 57 | actionType, exist := c.GetQuery("action_type") 58 | if !exist { 59 | bizConstant.InvalidActionType.WithFields(&methodFields).LaunchError(c) 60 | return 61 | } 62 | if i, err := strconv.Atoi(actionType); err != nil || i != 1 { 63 | err2Launch := bizConstant.InvalidActionType.WithFields(&methodFields) 64 | if err != nil { 65 | err2Launch = err2Launch.WithCause(err) 66 | } 67 | err2Launch.LaunchError(c) 68 | return 69 | } 70 | 71 | receiverID, exist := c.GetQuery("to_user_id") 72 | if !exist { 73 | bizConstant.InvalidArguments.WithFields(&methodFields).LaunchError(c) 74 | return 75 | } 76 | receiverIDInt, err := strconv.ParseInt(receiverID, 10, 32) 77 | if err != nil { 78 | bizConstant.InvalidArguments.WithCause(err).WithFields(&methodFields).LaunchError(c) 79 | return 80 | } 81 | 82 | content, exist := c.GetQuery("content") 83 | if !exist { 84 | bizConstant.InvalidArguments.WithFields(&methodFields).LaunchError(c) 85 | return 86 | } 87 | 88 | logger.WithFields(logrus.Fields{ 89 | "action_type": actionType, 90 | "to_user_id": receiverIDInt, 91 | "content": content, 92 | }).Debugf("Executing message action") 93 | 94 | messageActionResponse, err := Client.WechatAction(ctx, &wechat.MessageActionRequest{ 95 | SenderId: actorId, 96 | ReceiverId: uint32(receiverIDInt), 97 | ActionType: 1, 98 | Content: content, 99 | }) 100 | 101 | if err != nil { 102 | bizConstant.RPCCallError.WithCause(err).WithFields(&methodFields).LaunchError(c) 103 | return 104 | } 105 | logger.WithFields(logrus.Fields{ 106 | "response": messageActionResponse, 107 | }).Debugf("Message action success") 108 | c.JSON(httpStatus.StatusOK, messageActionResponse) 109 | } 110 | 111 | func MessageChat(ctx context.Context, c *app.RequestContext) { 112 | methodFields := logrus.Fields{ 113 | "method": "MessageChat", 114 | } 115 | logger := logging.Logger 116 | logger.WithFields(methodFields).Debugf("Process start") 117 | 118 | actorIdPtr, ok := mw.Auth(c, mw.WithAuthRequired()) 119 | actorId := *actorIdPtr 120 | if !ok { 121 | return 122 | } 123 | 124 | receiverIdPtr, exist := c.GetQuery("to_user_id") 125 | if !exist { 126 | bizConstant.InvalidArguments.WithFields(&methodFields).LaunchError(c) 127 | return 128 | } 129 | receiverId, err := strconv.ParseInt(receiverIdPtr, 10, 32) 130 | if err != nil { 131 | bizConstant.InvalidArguments.WithCause(err).WithFields(&methodFields).LaunchError(c) 132 | return 133 | } 134 | 135 | preMsgTimeStr, exist := c.GetQuery("pre_msg_time") 136 | if !exist { 137 | preMsgTimeStr = "0" 138 | } 139 | preMsgTime, err := strconv.ParseInt(preMsgTimeStr, 10, 64) 140 | if err != nil { 141 | bizConstant.InvalidArguments.WithCause(err).WithFields(&methodFields).LaunchError(c) 142 | return 143 | } 144 | 145 | logger.WithFields(logrus.Fields{ 146 | "to_user_id": receiverId, 147 | "pre_msg_time": preMsgTimeStr, 148 | }).Debugf("Executing message chat") 149 | 150 | messageActionResponse, err := Client.WechatChat(ctx, &wechat.MessageChatRequest{ 151 | SenderId: actorId, 152 | ReceiverId: uint32(receiverId), 153 | PreMsgTime: preMsgTime, 154 | }) 155 | 156 | if err != nil { 157 | bizConstant.RPCCallError.WithCause(err).WithFields(&methodFields).LaunchError(c) 158 | return 159 | } 160 | logger.WithFields(logrus.Fields{ 161 | "response": messageActionResponse, 162 | }).Debugf("Message chat success") 163 | c.JSON(httpStatus.StatusOK, messageActionResponse) 164 | } 165 | -------------------------------------------------------------------------------- /service/wechat/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | RUN_NAME="wechat" 3 | 4 | mkdir -p output/bin 5 | cp script/* output/ 6 | chmod +x output/bootstrap.sh 7 | 8 | if [ "$IS_SYSTEM_TEST_ENV" != "1" ]; then 9 | go build -o output/bin/${RUN_NAME} 10 | else 11 | go test -c -covermode=set -o output/bin/${RUN_NAME} -coverpkg=./... 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /service/wechat/db/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | protoc --go_out=../../../../ msg.proto 4 | -------------------------------------------------------------------------------- /service/wechat/db/msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package toktik.service.wechat.db; 3 | option go_package = "toktik/service/wechat/db"; 4 | 5 | message ChatMessage { 6 | uint32 from = 1; 7 | uint32 to = 2; 8 | string msg = 3; 9 | int64 time = 4; 10 | } 11 | 12 | message ChatGPTMessage { 13 | uint32 sender_id = 1; 14 | string msg = 2; 15 | bool reset_session = 3; 16 | int64 time = 4; 17 | } 18 | -------------------------------------------------------------------------------- /service/wechat/handler_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | "toktik/kitex_gen/douyin/wechat" 9 | ) 10 | 11 | func TestWechatServiceImpl_WechatAction(t *testing.T) { 12 | type args struct { 13 | ctx context.Context 14 | req *wechat.MessageActionRequest 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | wantResp *wechat.MessageActionResponse 20 | wantErr bool 21 | }{ 22 | // TODO: Use testcontainers to test redis 23 | //{"should pass", args{context.Background(), &wechat.MessageActionRequest{SenderId: 114, ReceiverId: 514, ActionType: 1, Content: `Hi, nice 2 meet u.`}}, &wechat.MessageActionResponse{StatusCode: 0, StatusMsg: "success"}, false}, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | s := &WechatServiceImpl{} 28 | gotResp, err := s.WechatAction(tt.args.ctx, tt.args.req) 29 | if (err != nil) != tt.wantErr { 30 | t.Errorf("WechatAction() error = %v, wantErr %v", err, tt.wantErr) 31 | return 32 | } 33 | if !reflect.DeepEqual(gotResp, tt.wantResp) { 34 | t.Errorf("WechatAction() gotResp = %v, want %v", gotResp, tt.wantResp) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | func TestWechatServiceImpl_WechatChat(t *testing.T) { 41 | type args struct { 42 | ctx context.Context 43 | req *wechat.MessageChatRequest 44 | } 45 | tests := []struct { 46 | name string 47 | args args 48 | wantResp *wechat.MessageChatResponse 49 | wantErr bool 50 | }{ 51 | // TODO: Use testcontainers to test redis 52 | //{"should pass", args{context.Background(), &wechat.MessageChatRequest{SenderId: 114, ReceiverId: 514}}, &wechat.MessageChatResponse{StatusCode: 0, StatusMsg: "success"}, false}, 53 | } 54 | for _, tt := range tests { 55 | t.Run(tt.name, func(t *testing.T) { 56 | s := &WechatServiceImpl{} 57 | gotResp, err := s.WechatChat(tt.args.ctx, tt.args.req) 58 | if (err != nil) != tt.wantErr { 59 | t.Errorf("WechatChat() error = %v, wantErr %v", err, tt.wantErr) 60 | return 61 | } 62 | if !reflect.DeepEqual(gotResp, tt.wantResp) { 63 | t.Errorf("WechatChat() gotResp = %v, want %v", gotResp, tt.wantResp) 64 | } 65 | }) 66 | } 67 | } 68 | 69 | func TestWechatServiceImpl_generateKey(t *testing.T) { 70 | test1Result := "chat:0:0" 71 | test2Result := "chat:1:2" 72 | test3Result := "chat:1:2" 73 | test4Result := fmt.Sprintf("chat:%d:%d", 1<<32-1, 1<<32-1) 74 | test5Result := fmt.Sprintf("chat:%d:%d", 1<<32-2, 1<<32-1) 75 | 76 | getUint32Ptr := func(i uint32) *uint32 { 77 | return &i 78 | } 79 | 80 | type args struct { 81 | uid1 *uint32 82 | uid2 *uint32 83 | } 84 | tests := []struct { 85 | name string 86 | args args 87 | want func(s *string) bool 88 | }{ 89 | {"test1", args{uid1: new(uint32), uid2: new(uint32)}, func(s *string) bool { 90 | return *s == test1Result 91 | }}, 92 | {"test2", args{uid1: getUint32Ptr(1), uid2: getUint32Ptr(2)}, func(s *string) bool { 93 | return *s == test2Result 94 | }}, 95 | {"test3", args{uid1: getUint32Ptr(2), uid2: getUint32Ptr(1)}, func(s *string) bool { 96 | return *s == test3Result 97 | }}, 98 | {"test4", args{uid1: getUint32Ptr(1<<32 - 1), uid2: getUint32Ptr(1<<32 - 1)}, func(s *string) bool { 99 | return *s == test4Result 100 | }}, 101 | {"test5", args{uid1: getUint32Ptr(1<<32 - 2), uid2: getUint32Ptr(1<<32 - 1)}, func(s *string) bool { 102 | return *s == test5Result 103 | }}, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | s := &WechatServiceImpl{} 108 | if got := s.generateKey(tt.args.uid1, tt.args.uid2); !tt.want(got) { 109 | t.Errorf("generateKey() = %v", got) 110 | } 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /service/wechat/kitex.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'wechat' 3 | ToolVersion: 'v0.4.4' 4 | 5 | -------------------------------------------------------------------------------- /service/wechat/kitex_info.yaml: -------------------------------------------------------------------------------- 1 | kitexinfo: 2 | ServiceName: 'wechat' 3 | ToolVersion: 'v0.4.4' 4 | 5 | -------------------------------------------------------------------------------- /service/wechat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "toktik/constant/config" 7 | "toktik/kitex_gen/douyin/wechat/wechatservice" 8 | 9 | "github.com/cloudwego/kitex/pkg/rpcinfo" 10 | "github.com/cloudwego/kitex/server" 11 | "github.com/kitex-contrib/obs-opentelemetry/provider" 12 | "github.com/kitex-contrib/obs-opentelemetry/tracing" 13 | consul "github.com/kitex-contrib/registry-consul" 14 | ) 15 | 16 | func main() { 17 | var err error 18 | 19 | r, err := consul.NewConsulRegister(config.EnvConfig.CONSUL_ADDR) 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | addr, err := net.ResolveTCPAddr("tcp", config.WechatServiceAddr) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | provider.NewOpenTelemetryProvider( 30 | provider.WithServiceName(config.WechatServiceName), 31 | provider.WithExportEndpoint(config.EnvConfig.EXPORT_ENDPOINT), 32 | provider.WithInsecure(), 33 | ) 34 | 35 | srv := wechatservice.NewServer( 36 | new(WechatServiceImpl), 37 | server.WithServiceAddr(addr), 38 | server.WithRegistry(r), 39 | server.WithSuite(tracing.NewServerSuite()), 40 | server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ 41 | ServiceName: config.WechatServiceName, 42 | }), 43 | ) 44 | err = srv.Run() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /service/wechat/script/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | CURDIR=$(cd $(dirname $0); pwd) 3 | 4 | if [ "X$1" != "X" ]; then 5 | RUNTIME_ROOT=$1 6 | else 7 | RUNTIME_ROOT=${CURDIR} 8 | fi 9 | 10 | export KITEX_RUNTIME_ROOT=$RUNTIME_ROOT 11 | export KITEX_LOG_DIR="$RUNTIME_ROOT/log" 12 | 13 | if [ ! -d "$KITEX_LOG_DIR/app" ]; then 14 | mkdir -p "$KITEX_LOG_DIR/app" 15 | fi 16 | 17 | if [ ! -d "$KITEX_LOG_DIR/rpc" ]; then 18 | mkdir -p "$KITEX_LOG_DIR/rpc" 19 | fi 20 | 21 | exec "$CURDIR/bin/wechat" 22 | 23 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | output_dir="output" 4 | services_dir="service/" 5 | services=$(ls "$services_dir") 6 | 7 | while [[ $# -gt 0 ]] 8 | do 9 | case "$1" in 10 | --service) 11 | service_name="$2" 12 | shift 2 13 | ;; 14 | *) 15 | echo "Unknown option: $1" 16 | exit 1 17 | ;; 18 | esac 19 | done 20 | 21 | if [ -z "$service_name" ]; then 22 | echo "Error: --service option is required." 23 | exit 1 24 | fi 25 | 26 | found=false 27 | for s in $services; do 28 | if [ "$s" == "$service_name" ]; then 29 | found=true 30 | break 31 | fi 32 | done 33 | 34 | if [ "$found" = false ]; then 35 | echo "Error: Unrecognized service name: $service_name" 36 | printf 'Available service names:\n%s\n' "$services" 37 | exit 1 38 | fi 39 | 40 | command="$output_dir/bin/$service_name" 41 | 42 | # Check if the bootstrap.sh file exists 43 | if [ -f output/bootstrap-"${service_name}".sh ]; then 44 | command="$output_dir/bootstrap-${service_name}.sh" 45 | fi 46 | 47 | if [ ! -f "$command" ]; then 48 | echo "Error: Service binary not found: $command" 49 | exit 1 50 | fi 51 | 52 | "$command" -------------------------------------------------------------------------------- /storage/fs.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io" 5 | "net/url" 6 | "os" 7 | "path" 8 | "toktik/constant/config" 9 | "toktik/logging" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type FSStorage struct { 15 | } 16 | 17 | func (f FSStorage) Upload(fileName string, content io.Reader) (output *PutObjectOutput, err error) { 18 | methodFields := logrus.Fields{ 19 | "function": "FSStorage.Upload", 20 | "file_name": fileName, 21 | } 22 | logger := logging.Logger.WithFields(methodFields) 23 | logger.Debug("Process start") 24 | 25 | all, err := io.ReadAll(content) 26 | if err != nil { 27 | logger.WithFields(logrus.Fields{ 28 | "err": err, 29 | }).Debug("failed reading content") 30 | return nil, err 31 | } 32 | filePath := path.Join(config.EnvConfig.LOCAL_FS_LOCATION, fileName) 33 | dir := path.Dir(filePath) 34 | err = os.MkdirAll(dir, os.FileMode(0755)) 35 | if err != nil { 36 | logger.WithFields(logrus.Fields{ 37 | "err": err, 38 | }).Debug("failed writing creating directory before writing file") 39 | return nil, err 40 | } 41 | err = os.WriteFile(filePath, all, os.FileMode(0755)) 42 | if err != nil { 43 | logger.WithFields(logrus.Fields{ 44 | "err": err, 45 | }).Debug("failed writing content to file") 46 | return nil, err 47 | } 48 | return &PutObjectOutput{}, nil 49 | } 50 | 51 | func (f FSStorage) GetLink(fileName string) (string, error) { 52 | return url.JoinPath(config.EnvConfig.LOCAL_FS_BASEURL, fileName) 53 | } 54 | -------------------------------------------------------------------------------- /storage/provider.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io" 5 | "toktik/constant/config" 6 | "toktik/logging" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | var Instance storageProvider = S3Storage{} 12 | 13 | func init() { 14 | if config.EnvConfig.STORAGE_TYPE == "fs" { 15 | Instance = FSStorage{} 16 | } 17 | logging.Logger.WithFields(logrus.Fields{ 18 | "storage_type": config.EnvConfig.STORAGE_TYPE, 19 | }).Info("storage init") 20 | } 21 | 22 | type PutObjectOutput struct { 23 | } 24 | 25 | type storageProvider interface { 26 | Upload(fileName string, content io.Reader) (*PutObjectOutput, error) 27 | GetLink(fileName string) (string, error) 28 | } 29 | 30 | // Upload to the s3 storage using given fileName 31 | func Upload(fileName string, content io.Reader) (*PutObjectOutput, error) { 32 | return Instance.Upload(fileName, content) 33 | } 34 | 35 | func GetLink(fileName string) (string, error) { 36 | return Instance.GetLink(fileName) 37 | } 38 | -------------------------------------------------------------------------------- /storage/s3.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net/url" 8 | "strconv" 9 | appcfg "toktik/constant/config" 10 | 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | "github.com/aws/aws-sdk-go-v2/config" 13 | "github.com/aws/aws-sdk-go-v2/credentials" 14 | "github.com/aws/aws-sdk-go-v2/service/s3" 15 | ) 16 | 17 | var client *s3.Client 18 | 19 | func init() { 20 | r2Resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...any) (aws.Endpoint, error) { 21 | return aws.Endpoint{ 22 | URL: appcfg.EnvConfig.S3_ENDPOINT_URL, 23 | }, nil 24 | }) 25 | 26 | cfg, err := config.LoadDefaultConfig(context.TODO(), 27 | config.WithEndpointResolverWithOptions(r2Resolver), 28 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(appcfg.EnvConfig.S3_SECRET_ID, appcfg.EnvConfig.S3_SECRET_KEY, "")), 29 | ) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | client = s3.NewFromConfig(cfg, func(o *s3.Options) { 35 | // Required when using minio 36 | o.UsePathStyle, _ = strconv.ParseBool(appcfg.EnvConfig.S3_PATH_STYLE) 37 | }) 38 | } 39 | 40 | type S3Storage struct { 41 | } 42 | 43 | func (s S3Storage) Upload(fileName string, content io.Reader) (*PutObjectOutput, error) { 44 | _, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ 45 | Bucket: &appcfg.EnvConfig.S3_BUCKET, 46 | Key: &fileName, 47 | Body: content, 48 | }) 49 | 50 | return &PutObjectOutput{}, err 51 | } 52 | 53 | func (s S3Storage) GetLink(fileName string) (string, error) { 54 | return url.JoinPath(appcfg.EnvConfig.S3_PUBLIC_URL, fileName) 55 | } 56 | -------------------------------------------------------------------------------- /test/e2e/base_api_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | // Progress: 4 | // All tests are synced with 2023-2-20 version of the API. Not sure if the API will change in the future. 5 | 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "net/http" 11 | "strconv" 12 | "testing" 13 | 14 | "github.com/segmentio/ksuid" 15 | ) 16 | 17 | // TestFeed tests the procedure of unauthenticated users getting feed. 18 | func TestFeed(t *testing.T) { 19 | e := newExpect(t) 20 | feedResp := e.GET("/douyin/feed/").Expect().Status(http.StatusOK).JSON().Object() 21 | feedResp.Value("status_code").Number().Equal(0) 22 | feedResp.Value("status_msg").String().NotEmpty() 23 | feedResp.Value("video_list").Array().Length().Gt(0) 24 | nextTime := feedResp.Value("next_time").String() 25 | nextTimeInt, err := strconv.ParseInt((*nextTime).Raw(), 10, 64) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | // check if nextTimeInt is a valid timestamp later than 2023-01-01 00:00:00 30 | if nextTimeInt < 1672502400000 { 31 | t.Error("next_time is not a valid timestamp") 32 | } 33 | 34 | for _, element := range feedResp.Value("video_list").Array().Iter() { 35 | video := element.Object() 36 | video.ContainsKey("id") 37 | author := video.Value("author").Object() 38 | 39 | ValidateUser(author) 40 | 41 | video.Value("play_url").String().NotEmpty() 42 | video.Value("cover_url").String().NotEmpty() 43 | video.ContainsKey("favorite_count") 44 | video.ContainsKey("comment_count") 45 | video.ContainsKey("is_favorite") 46 | video.Value("title").String().NotEmpty() 47 | } 48 | } 49 | 50 | // TestUserAction tests a whole user register & login & fetch user info procedure. 51 | func TestUserAction(t *testing.T) { 52 | e := newExpect(t) 53 | 54 | registerValue := fmt.Sprintf("douyin_test_%s", ksuid.New().String()) 55 | 56 | registerResp := e.POST("/douyin/user/register/"). 57 | WithQuery("username", registerValue).WithQuery("password", registerValue). 58 | WithFormField("username", registerValue).WithFormField("password", registerValue). 59 | Expect(). 60 | Status(http.StatusOK). 61 | JSON().Object() 62 | registerResp.Value("status_code").Number().Equal(0) 63 | registerResp.Value("status_msg").String().NotEmpty() 64 | registerResp.Value("user_id").Number().Gt(0) 65 | registerResp.Value("token").String().NotEmpty() 66 | 67 | loginResp := e.POST("/douyin/user/login/"). 68 | WithQuery("username", registerValue).WithQuery("password", registerValue). 69 | WithFormField("username", registerValue).WithFormField("password", registerValue). 70 | Expect(). 71 | Status(http.StatusOK). 72 | JSON().Object() 73 | loginResp.Value("status_code").Number().Equal(0) 74 | registerResp.Value("status_msg").String().NotEmpty() 75 | loginResp.Value("user_id").Number().Gt(0) 76 | loginResp.Value("token").String().NotEmpty() 77 | 78 | token := loginResp.Value("token").String().Raw() 79 | userId := loginResp.Value("user_id").Number().Raw() 80 | 81 | userResp := e.GET("/douyin/user/"). 82 | WithQuery("user_id", userId). 83 | WithQuery("token", token). 84 | Expect(). 85 | Status(http.StatusOK). 86 | JSON().Object() 87 | userResp.Value("status_code").Number().Equal(0) 88 | userResp.Value("status_msg").String().NotEmpty() 89 | userInfo := userResp.Value("user").Object() 90 | ValidateUser(userInfo) 91 | } 92 | 93 | // TestPublish tests the procedure of a publish & check the publish list of the user. 94 | func TestPublish(t *testing.T) { 95 | e := newExpect(t) 96 | 97 | userId, token := getTestUserToken(testUserA, e) 98 | 99 | publishResp := e.POST("/douyin/publish/action/"). 100 | WithMultipart(). 101 | WithFile("data", "../../service/publish/resources/bear.mp4"). 102 | WithFormField("token", token). 103 | WithFormField("title", "Bear"). 104 | Expect(). 105 | Status(http.StatusOK). 106 | JSON().Object() 107 | publishResp.Value("status_code").Number().Equal(0) 108 | publishResp.Value("status_msg").String().NotEmpty() 109 | 110 | publishListResp := e.GET("/douyin/publish/list/"). 111 | WithQuery("user_id", userId).WithQuery("token", token). 112 | Expect(). 113 | Status(http.StatusOK). 114 | JSON().Object() 115 | publishListResp.Value("status_code").Number().Equal(0) 116 | publishListResp.Value("video_list").Array().Length().Gt(0) 117 | 118 | for _, element := range publishListResp.Value("video_list").Array().Iter() { 119 | video := element.Object() 120 | video.ContainsKey("id") 121 | video.ContainsKey("author") 122 | video.Value("play_url").String().NotEmpty() 123 | video.Value("cover_url").String().NotEmpty() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/e2e/common.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | package main 4 | 5 | import ( 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/gavv/httpexpect/v2" 10 | ) 11 | 12 | var serverAddr = "https://toktik.xctra.cn" 13 | 14 | // TODO: use safer way to store test user credentials 15 | var testUserA = "douyinTestUserA" 16 | var testUserB = "douyinTestUserB" 17 | 18 | func newExpect(t *testing.T) *httpexpect.Expect { 19 | return httpexpect.WithConfig(httpexpect.Config{ 20 | Client: http.DefaultClient, 21 | BaseURL: serverAddr, 22 | Reporter: httpexpect.NewAssertReporter(t), 23 | Printers: []httpexpect.Printer{ 24 | httpexpect.NewDebugPrinter(t, true), 25 | }, 26 | }) 27 | } 28 | 29 | func getTestUserToken(user string, e *httpexpect.Expect) (int, string) { 30 | registerResp := e.POST("/douyin/user/register/"). 31 | WithQuery("username", user).WithQuery("password", user). 32 | WithFormField("username", user).WithFormField("password", user). 33 | Expect(). 34 | Status(http.StatusOK). 35 | JSON().Object() 36 | 37 | userId := 0 38 | token := registerResp.Value("token").String().Raw() 39 | if len(token) == 0 { 40 | loginResp := e.POST("/douyin/user/login/"). 41 | WithQuery("username", user).WithQuery("password", user). 42 | WithFormField("username", user).WithFormField("password", user). 43 | Expect(). 44 | Status(http.StatusOK). 45 | JSON().Object() 46 | loginToken := loginResp.Value("token").String() 47 | loginToken.Length().Gt(0) 48 | token = loginToken.Raw() 49 | userId = int(loginResp.Value("user_id").Number().Raw()) 50 | } else { 51 | userId = int(registerResp.Value("user_id").Number().Raw()) 52 | } 53 | return userId, token 54 | } 55 | 56 | func ValidateUser(user *httpexpect.Object) { 57 | user.ContainsKey("id") 58 | user.Value("name").String().NotEmpty() 59 | user.ContainsKey("follow_count") 60 | user.ContainsKey("follower_count") 61 | user.ContainsKey("is_follow") 62 | user.Value("avatar").String().NotEmpty() 63 | user.Value("background_image").String().NotEmpty() 64 | user.Value("signature").String().NotEmpty() 65 | user.ContainsKey("total_favorited") // TODO: determine if this field should be string or int 66 | user.ContainsKey("work_count") 67 | user.ContainsKey("favorite_count") 68 | } 69 | -------------------------------------------------------------------------------- /test/e2e/interact_api_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | // Progress: 4 | // All tests are synced with 2023-2-20 version of the API. Not sure if the API will change in the future. 5 | 6 | package main 7 | 8 | import ( 9 | "net/http" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | // TestFavorite tests the procedure of like & unlike 16 | func TestFavorite(t *testing.T) { 17 | e := newExpect(t) 18 | 19 | feedResp := e.GET("/douyin/feed/").Expect().Status(http.StatusOK).JSON().Object() 20 | feedResp.Value("status_code").Number().Equal(0) 21 | feedResp.Value("video_list").Array().Length().Gt(0) 22 | firstVideo := feedResp.Value("video_list").Array().First().Object() 23 | videoId := firstVideo.Value("id").Number().Raw() 24 | 25 | userId, token := getTestUserToken(testUserA, e) 26 | 27 | favoriteResp := e.POST("/douyin/favorite/action/"). 28 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 1). 29 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 1). 30 | Expect(). 31 | Status(http.StatusOK). 32 | JSON().Object() 33 | favoriteResp.Value("status_code").Number().Equal(0) 34 | favoriteResp.Value("status_msg").String().NotEmpty() 35 | 36 | favoriteListResp := e.GET("/douyin/favorite/list/"). 37 | WithQuery("token", token).WithQuery("user_id", userId). 38 | WithFormField("token", token).WithFormField("user_id", userId). 39 | Expect(). 40 | Status(http.StatusOK). 41 | JSON().Object() 42 | favoriteListResp.Value("status_code").Number().Equal(0) 43 | favoriteListResp.Value("status_msg").String().NotEmpty() 44 | for _, element := range favoriteListResp.Value("video_list").Array().Iter() { 45 | video := element.Object() 46 | video.ContainsKey("id") 47 | ValidateUser(video.Value("author").Object()) 48 | video.Value("play_url").String().NotEmpty() 49 | video.Value("cover_url").String().NotEmpty() 50 | video.ContainsKey("favorite_count") 51 | video.ContainsKey("comment_count") 52 | video.ContainsKey("is_favorite") 53 | video.Value("title").String().NotEmpty() 54 | } 55 | 56 | unlikeResp := e.POST("/douyin/favorite/action/"). 57 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 2). 58 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 2). 59 | Expect(). 60 | Status(http.StatusOK). 61 | JSON().Object() 62 | unlikeResp.Value("status_code").Number().Equal(0) 63 | unlikeResp.Value("status_msg").String().NotEmpty() 64 | } 65 | 66 | // TestComment tests the procedure of add, list & delete a comment. 67 | func TestComment(t *testing.T) { 68 | e := newExpect(t) 69 | 70 | feedResp := e.GET("/douyin/feed/").Expect().Status(http.StatusOK).JSON().Object() 71 | feedResp.Value("status_code").Number().Equal(0) 72 | feedResp.Value("video_list").Array().Length().Gt(0) 73 | firstVideo := feedResp.Value("video_list").Array().First().Object() 74 | videoId := firstVideo.Value("id").Number().Raw() 75 | 76 | _, token := getTestUserToken(testUserA, e) 77 | 78 | addCommentResp := e.POST("/douyin/comment/action/"). 79 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 1).WithQuery("comment_text", "测试评论"). 80 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 1).WithFormField("comment_text", "测试评论"). 81 | Expect(). 82 | Status(http.StatusOK). 83 | JSON().Object() 84 | addCommentResp.Value("status_code").Number().Equal(0) 85 | addCommentResp.Value("status_msg").String().NotEmpty() 86 | addCommentResp.Value("comment").Object().Value("id").Number().Gt(0) 87 | ValidateUser(addCommentResp.Value("comment").Object().Value("user").Object()) 88 | commentId := int(addCommentResp.Value("comment").Object().Value("id").Number().Raw()) 89 | 90 | commentListResp := e.GET("/douyin/comment/list/"). 91 | WithQuery("token", token).WithQuery("video_id", videoId). 92 | WithFormField("token", token).WithFormField("video_id", videoId). 93 | Expect(). 94 | Status(http.StatusOK). 95 | JSON().Object() 96 | commentListResp.Value("status_code").Number().Equal(0) 97 | commentListResp.Value("status_msg").String().NotEmpty() 98 | 99 | containTestComment := false 100 | for _, element := range commentListResp.Value("comment_list").Array().Iter() { 101 | comment := element.Object() 102 | comment.ContainsKey("id") 103 | ValidateUser(comment.Value("user").Object()) 104 | comment.Value("content").String().NotEmpty() 105 | comment.Value("create_date").String().NotEmpty() 106 | if int(comment.Value("id").Number().Raw()) == commentId { 107 | containTestComment = true 108 | } 109 | } 110 | 111 | assert.True(t, containTestComment, "Can't find test comment in list") 112 | 113 | delCommentResp := e.POST("/douyin/comment/action/"). 114 | WithQuery("token", token).WithQuery("video_id", videoId).WithQuery("action_type", 2).WithQuery("comment_id", commentId). 115 | WithFormField("token", token).WithFormField("video_id", videoId).WithFormField("action_type", 2).WithFormField("comment_id", commentId). 116 | Expect(). 117 | Status(http.StatusOK). 118 | JSON().Object() 119 | delCommentResp.Value("status_code").Number().Equal(0) 120 | } 121 | -------------------------------------------------------------------------------- /test/e2e/social_api_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | // Progress: 4 | // All tests are synced with 2023-2-20 version of the API. Not sure if the API will change in the future. 5 | 6 | package main 7 | 8 | import ( 9 | "net/http" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | // TestRelation tests the procedure of follow, list users. 16 | func TestRelation(t *testing.T) { 17 | e := newExpect(t) 18 | 19 | userIdA, tokenA := getTestUserToken(testUserA, e) 20 | userIdB, tokenB := getTestUserToken(testUserB, e) 21 | 22 | relationResp := e.POST("/douyin/relation/action/"). 23 | WithQuery("token", tokenA).WithQuery("to_user_id", userIdB).WithQuery("action_type", 1). 24 | WithFormField("token", tokenA).WithFormField("to_user_id", userIdB).WithFormField("action_type", 1). 25 | Expect(). 26 | Status(http.StatusOK). 27 | JSON().Object() 28 | relationResp.Value("status_code").Number().Equal(0) 29 | relationResp.Value("status_msg").String().NotEmpty() 30 | 31 | followListResp := e.GET("/douyin/relation/follow/list/"). 32 | WithQuery("token", tokenA).WithQuery("user_id", userIdA). 33 | WithFormField("token", tokenA).WithFormField("user_id", userIdA). 34 | Expect(). 35 | Status(http.StatusOK). 36 | JSON().Object() 37 | followListResp.Value("status_code").Number().Equal(0) 38 | followListResp.Value("user_list").Array().NotEmpty() 39 | 40 | containTestUserB := false 41 | for _, element := range followListResp.Value("user_list").Array().Iter() { 42 | user := element.Object() 43 | user.ContainsKey("id") 44 | ValidateUser(user) 45 | if int(user.Value("id").Number().Raw()) == userIdB { 46 | containTestUserB = true 47 | } 48 | } 49 | assert.True(t, containTestUserB, "Follow test user failed") 50 | 51 | followerListResp := e.GET("/douyin/relation/follower/list/"). 52 | WithQuery("token", tokenB).WithQuery("user_id", userIdB). 53 | WithFormField("token", tokenB).WithFormField("user_id", userIdB). 54 | Expect(). 55 | Status(http.StatusOK). 56 | JSON().Object() 57 | followerListResp.Value("status_code").Number().Equal(0) 58 | followerListResp.Value("user_list").Array().NotEmpty() 59 | 60 | containTestUserA := false 61 | for _, element := range followerListResp.Value("user_list").Array().Iter() { 62 | user := element.Object() 63 | user.ContainsKey("id") 64 | ValidateUser(user) 65 | if int(user.Value("id").Number().Raw()) == userIdA { 66 | containTestUserA = true 67 | } 68 | } 69 | assert.True(t, containTestUserA, "Follower test user failed") 70 | } 71 | 72 | // TestChat tests the procedure of sending and receiving messages. 73 | func TestChat(t *testing.T) { 74 | e := newExpect(t) 75 | 76 | userIdA, tokenA := getTestUserToken(testUserA, e) 77 | userIdB, tokenB := getTestUserToken(testUserB, e) 78 | 79 | messageResp := e.POST("/douyin/message/action/"). 80 | WithQuery("token", tokenA).WithQuery("to_user_id", userIdB).WithQuery("action_type", 1).WithQuery("content", "Send to UserB"). 81 | WithFormField("token", tokenA).WithFormField("to_user_id", userIdB).WithFormField("action_type", 1).WithQuery("content", "Send to UserB"). 82 | Expect(). 83 | Status(http.StatusOK). 84 | JSON().Object() 85 | messageResp.Value("status_code").Number().Equal(0) 86 | messageResp.Value("status_msg").String().NotEmpty() 87 | 88 | chatResp := e.GET("/douyin/message/chat/"). 89 | WithQuery("token", tokenA).WithQuery("to_user_id", userIdB). 90 | WithFormField("token", tokenA).WithFormField("to_user_id", userIdB). 91 | Expect(). 92 | Status(http.StatusOK). 93 | JSON().Object() 94 | chatResp.Value("status_code").Number().Equal(0) 95 | chatResp.Value("status_msg").String().NotEmpty() 96 | chatResp.Value("message_list").Array().Length().Gt(0) 97 | chatResp.Value("message_list").Array().First().Object().Value("content").String().Equal("Send to UserB") 98 | chatResp.Value("message_list").Array().First().Object().Value("create_time").String().NotEmpty() 99 | 100 | chatResp = e.GET("/douyin/message/chat/"). 101 | WithQuery("token", tokenB).WithQuery("to_user_id", userIdA). 102 | WithFormField("token", tokenB).WithFormField("to_user_id", userIdA). 103 | Expect(). 104 | Status(http.StatusOK). 105 | JSON().Object() 106 | chatResp.Value("status_code").Number().Equal(0) 107 | chatResp.Value("status_msg").String().NotEmpty() 108 | chatResp.Value("message_list").Array().Length().Gt(0) 109 | chatResp.Value("message_list").Array().First().Object().Value("content").String().Equal("Send to UserB") 110 | chatResp.Value("message_list").Array().First().Object().Value("create_time").String().NotEmpty() 111 | } 112 | -------------------------------------------------------------------------------- /test/mock/db.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "toktik/repo" 7 | 8 | "github.com/DATA-DOG/go-sqlmock" 9 | "gorm.io/driver/postgres" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | var DBMock sqlmock.Sqlmock 14 | var Conn *sql.DB 15 | 16 | func init() { 17 | var err error 18 | Conn, DBMock, err = sqlmock.New() 19 | db, err := gorm.Open(postgres.New(postgres.Config{ 20 | DSN: "sqlmock_db_0", 21 | DriverName: "postgres", 22 | Conn: Conn, 23 | PreferSimpleProtocol: true, 24 | }), &gorm.Config{}) 25 | if err != nil { 26 | log.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 27 | } 28 | repo.SetDefault(db) 29 | } 30 | -------------------------------------------------------------------------------- /unit-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for dir in service/*/; do 4 | pushd "$dir" || exit 5 | go test -gcflags=-l -v -cover ./... 6 | popd || exit 7 | done 8 | --------------------------------------------------------------------------------